Node.js写了两三年了。刚开始学习Node的时候,helloworld创建了一个HttpServer。后来在工作中也体验了Express、Koa1.x、Koa2。.x和最近研究的路由控制器与TypeScript结合(驱动程序仍然是Express和Koa)。Koa版本用的比较多,我也对它的洋葱模型比较感兴趣,所以最近抽空看了下它的源码。刚好最近一个Express项目可能会重构为koa2.x版本,所以阅读它的源码对于重构也是一个有效的帮助。Koa是怎么来的?首先,我们需要确定Koa是什么。任何框架的出现都是为了解决问题,而Koa的出现就是为了方便构建http服务。可以简单理解为HTTP服务的中间件框架。使用http模块创建http服务。相信大家在学习Node的时候,应该写过类似这样的代码:consthttp=require('http')constserverHandler=(request,response)=>{response.end('HelloWorld')//returndata}http.createServer(serverHandler).listen(8888,_=>console.log('Serverrunashttp://127.0.0.1:8888'))一个简单的例子,脚本运行后访问http://127.0.0.1:8888以查看HelloWorld字符串。但这只是一个简单的例子,因为无论访问什么地址(甚至修改请求的方法),我们总是会得到这个字符串:>curlhttp://127.0.0.1:8888>curlhttp://127.0.0.1:8888/sub>curl-XPOSThttp://127.0.0.1:8888所以我们可以在回调中加入逻辑,根据路径和Method返回相应的数据给用户:constserverHandler=(request,response)=>{//默认让responseData='404'if(request.url==='/'){if(request.method==='GET'){responseData='HelloWorld'}elseif(request.method==='POST'){responseData='HelloWorldWithPOST'}}elseif(request.url==='/sub'){responseData='subpage'}response.end(responseData)//returndata}和Express的实现类似,但是这种写法会带来另外一个问题。如果是大项目,接口就不止N个。如果都写在这个handler里面,维护起来就太难了。该示例只是简单地为变量赋值,但实际项目没有这么简单的逻辑。因此,我们对handler做一个抽象,方便我们管理路由:classApp{constructor(){this.handlers={}this.get=this.route.bind(this,'GET')post=this.route.bind(this,'POST')}route(method,path,handler){letpathInfo=(this.handlers[path]=this.handlers[path]||{})//注册处理器pathInfo[method]=handler}callback(){return(request,response)=>{let{url:path,method}=requestthis.handlers[path]&&this.handlers[path][method]?this.handlers[path][method](request,response):response.end('404')}}}然后通过实例化一个Router对象注册对应的路径,最后启动服务:constapp=newApp()app.get('/',function(request,response){response.end('HelloWorld')})app.post('/',function(request,response){response.end('HelloWorldWithPOST')})app.get('/sub',function(request,response){response.end('subpage')})http.createServer(app.callback()).listen(8888,_=>console.log('服务器运行为http://127.0.0.1:8888'))这样,Express中的中间件实现了一个HttpServer,代码比较整洁,但是功能还是很简单的。如果我们现在有一个需求,肯定是在一些请求的前面加上一些参数的生成,比如一个请求的唯一ID。在我们的处理程序中重复编写代码是绝对不可取的。所以我们需要优化路由的处理,使其支持传入多个handler:route(method,path,...handler){letpathInfo=(this.handlers[path]=this.handlers[path]||{})//注册处理程序pathInfo[method]=handler}callback(){return(request,response)=>{let{url:path,method}=requestlethandlers=this.handlers[path]&&this.handlers[路径][方法]if(handlers){letcontext={}functionnext(handlers,index=0){handlers[index]&&handlers[index].call(context,request,response,()=>next(handlers,index+1))}next(handlers)}else{response.end('404')}}}然后为上面的路径监听添加另外一个handler:functiongeneratorId(request,response,next){this.id=123next()}app.get('/',generatorId,function(request,response){response.end(`HelloWorld${this.id}`)})这样在访问接口的时候,就可以请参阅HelloWorld123字样。这可以简单地认为是在Express中实现的中间件。中间件是Express和Koa的核心,所有的依赖都是通过中间件来加载的。更灵活的中间件方案——洋葱模型上面的方案确实可以让人非常方便的使用一些中间件。在流程控制中调用next()进入下一个环节,整个流程变得非常清晰。但仍有一些限制。比如我们需要对一些接口进行耗时统计,Express中有几种??可能的解决方案://解决方案1.修改原handler的处理逻辑,做耗时统计,然后结束发送数据app.get('/a',beforeRequest,function(request,response){//请求耗时统计console.log(`${request.url}duration:${newDate().valueOf()-this.requestTime}`)response.end('XXX')})//解决方案2.移动输出逻辑数据到一个post中间件中functionafterRequest(request,response,next){//请求耗时统计console.log(`${request.url}duration:${newDate().valueOf()-this.requestTime}`)response.end(this.body)}app.get('/b',beforeRequest,function(request,response,next){this.body='XXX'next()//记得调用,否则themiddlewareishere},afterRequest)无论哪种方案,都是对原代码的破坏性修改,不可取。因为Express使用response.end()方法向接口请求者返回数据,调用后会终止后续代码的执行。又因为当时在某个中间件中等待某个异步函数的执行还没有很好的解决方案。functiona(_,_,next){console.log('beforea')letresults=next()console.log('aftera')}functionb(_,_,next){console.log('beforeb')setTimeout(_=>{this.body=123456next()},1000)}functionc(_,response){console.log('beforec')response.end(this.body)}app.get('/',a,b,c)就像上面的例子。其实日志的输出顺序是:beforebeforeafterafterbeforec,这显然不符合我们的预期,所以获取next()的returninExpress值是没有意义的。于是就有了Koa带来的洋葱模型。Koa1.x出现的时候,恰好赶上了Node对新语法、生成器函数和Promise定义的支持。所以才会有co这样一个神奇的库,而当我们的中间件使用Promise的时候,后面的代码执行完成后,前面的中间件可以很轻松的处理自己的事务。但是Generator本身的作用并不是帮助我们更方便的使用Promise来控制异步过程。所以随着Node7.6的发布,支持了async和await语法,社区也推出了Koa2.x,用async语法来替代之前的co+Generator。koa也去掉了co的依赖(2.x版本使用koa-convert将Generator函数转换为promises,3.x版本不会直接支持Generator)ref:removegeneratorsupportsduetoKoa'sfunctionalityanduse没有区别两个版本,顶多有一些语法上的调整,所以我会略过一些Koa1.x相关的东西,直奔主题。在Koa中,可以通过以下方式定义和使用中间件:asyncfunctionlog(ctx,next){letrequestTime=newDate().valueOf()awaitnext()console.log(`${ctx.url}duration:${newDate().valueOf()-requestTime}`)}router.get('/',log,ctx=>{//dosomething...})由于存在一些语法糖,覆盖真正运行代码的过程,所以我们使用Promise来还原上面的代码:then(_=>{console.log(`${ctx.url}duration:${newDate().valueOf()-requestTime}`)}).then(resolve)})}大致代码是这样的,换句话说,调用next会返回给我们一个Promise对象,而Promise什么时候resolve就是Koa内部做的事情。可以简单实现(关于上面实现的App类,只需要修改callback):handlers[path]&&this.handlers[path][method]if(handlers){letcontext={url:request.url}functionnext(handlers,index=0){returnnewPromise((resolve,reject)=>{if(!handlers[index])returnresolve()handlers[index](context,()=>next(handlers,index+1)).then(resolve,reject)})}next(handlers).then(_=>{//结束请求response.end(context.body||'404')})}else{response.end('404')}}}每次调用中间件时监听然后发送当前Promise解析和拒绝过程被传递到Promise回调中。即第一个中间件的then回调只有在调用第二个中间件的resolve时才会执行。这实现了一个洋葱模型。就像我们日志中间件的执行流程:获取当前时间戳requestTime,调用next()执行后续中间件,监听其回调。第二个中间件可能会调用第三个、第四个、第五个这个,但这不是log关心的。Log只关心第二个中间件什么时候解析,第二个中间件的解析依赖于它后面的中间件的解析。等到第二个中间件resolve,也就是没有其他中间件执行(都resolved),然后log会继续执行后面的代码,所以像洋葱一样一层层包裹,最后外层最大,先执行,后执行。(在一个完整的请求中,它在下一个之前执行,在下一个之后执行)。小吉最近抽空翻了一下Koa相关的源码。我很兴奋,想把它们记录下来。它应该分成几个段落。我不会写一篇完整的文章。上次写了一个装饰器。太长了,看完有点困了。先占几个坑:核心模块koa和koa-compose流行中间件koa-router和koa-views杂轮koa-bodyparser/multer/better-body/static示例代码仓库地址源码阅读仓库地址
