背景知识
掌握这些背景知识对理解koa1源码有帮助:
- 原型链查找 闭包 高阶函数(返回函数的函数) getter/setter call/apply
- 代理委托
- promise 异步函数 迭代器 委托生成器(yield *)
- co模块处理
- node http模块
koa使用
先从koa1开始
|
|
上面代码加了两个中间件,执行过程是:
每一个请求到达服务器后初始化请求上下文对象,然后按照中间件添加的顺序,一个一个执行:
- 执行中间件1,next之前的逻辑
next
- 执行中间2的逻辑
执行中间件1,next之后的逻辑
其他的过程都是模拟上面的流程。
koa的方法
koa有公共方法use/listen,私有方法callback等;
其中use是添加中间件,listen是创建服务、监听端口,callback初始化中间件形成闭包并返回网络请求到达时的回调函数。
|
|
koa实现原理
其实koa中间件调用过程实现原理就是根据之前写过一篇文章JS之异步1中讲述的异步生成器。
但是koa实现有所不同,koa采用的是根据promise来实现;一步一步看看上面关键callback
做了什么工作;
co.wrap
上面关键代码var fn = co.wrap(compose(this.middleware))
实现中间件的封装,其中co.warp
其实就是返回一个函数,这个返回函数的函数体是把fn执行得到的结果作为参数传入co并执行:
|
|
compose
其中compose(this.middleware)
实现的是一个迭代器的嵌套调用(这里有个背景知识,生成器函数执行是生成一个迭代器,也就是就是middleware[i].call
的结果):
|
|
执行compose实际生成的可以看作返回了一个异步函数:
|
|
这个异步函数其实就是传入到co.wrap中的参数fn
再看最后一个关键函数co
co其实就是实现koa’剥洋葱’执行流程的执行体;
|
|
当co执行的时候,迭代器gen被传入,gen的值为上面compose生成的异步函数执行得到的迭代器,为啥是迭代器,因为co执行的时候,传入的是fn.call(…);
再回顾一下compose生成的异步函数:
|
|
co执行过程:
- 返回一个promise,promise中包括的执行流程是:
- 如果gen是普通函数,直接执行;如果gen是不为空的值并且gen.next不为function也就是说gen不是迭代器,直接返回resolve(), 这个实际是co返回promise的resolve,也就是总的出口;
- gen是迭代器,执行gen.next(),也就是会执行
yield * itor1(itor2(noop_itor))
,yield *实际上是委托生成器; 会直接执行到itor1第一个yield next处;并且此时gen.next()得到ret;并且ret.value就是next的值,也就是itor1的传入的实参itor2(itor3); - 我们快要接近剥洋葱的流程了,只要itor2重复前面的流程,怎么实现呢,实际就是toPromise的工作了,且看next(ret);
- next函数中:如果ret.done === true, 直接退出, 这也是一个出口;否则把ret.value转换为promise对象,当ret.value是一个迭代器的时候,toPromise主要工作是重复co.call(this, ret.value); 这样就重复的执行每个中间件的yield next前面的代码,直到执行到noop并层层返回;这实际有点类似二叉树的先序遍历过程;
- 最后会回到第一次返回promise的函数中,并且resolve,这样就可以执行
fn.call(ctx).then(function handleResponse() {respond.call(ctx);}).catch(ctx.onerror);
的then处理了,否则catch处理; - 自此一个网络请求后的中间件处理完毕;
总执行过程
- 请求到来
- …
- 执行fn.call(ctx),实际上就是执行
co.wrap(compose(this.middleware)).call(ctx)
, 而co.wrap(...).call(ctx)
实际就是上面的co函数的执行过程; - 中间件处理完毕, 然后执行respond.call(ctx);
koa的上下文
koa服务每当一个新的网络请求来临的时候会生成一个新的上下文,也就是代码中的多次出现的ctx;
|
|
这个context是以koa的实例服务app.context为原型,并且对ctx(即context)的属性request、response也设计了原型,这有什么用处呢?
实际上我们获取ctx.header、ctx.method或者设置ctx.method等的值,就是通过这里来的;举一个例子,获取ctx.header
- 1 ctx中没有,从原型app.context中寻找
2 app.context从静态类context中寻找
1234567function Application() {if (!(this instanceof Application)) return new Application;...**this.context = Object.create(context);**this.request = Object.create(request);this.response = Object.create(response);}3 context中的header代理了request静态类的属性header;
1234567891011121314151617delegate(proto, 'request').method('acceptsLanguages')... // 省略.getter('header')....getter('ip');Delegator.prototype.getter = function(name){var proto = this.proto;var target = this.target;this.getters.push(name);proto.__defineGetter__(name, function(){return this[target][name];});return this;};4 也就是ctx[‘request’][‘header’]
5 寻找ctx[‘request’][‘header’]也就是从ctx[‘request’]的原型app.request寻找,接着到静态类request中寻找
123get header() {return this.req.headers;},6 也就是return ctx.request.req.headers, 其中ctx.request.req实际就是网络请求传入的req对象
- 7 获取header成功
koa利用原型链查找、委托代理等手段,设置了很多方面快捷的属性获取、属性设置和方法调用;
总结
本文讲述了下面几方面内容:
- koa简单使用
- koa中间件执行过程
- koa中间件执行原理
- koa上下文ctx
- koa快捷过程
不过koa也有不足之处,如果上层中间件设置了多次yield next,虽然后面的迭代器已经执行到done:true
,但每次重新到下层中间件也是一种不必要的开销;koa1的代码也比较繁杂,理解起来比较困难;不过这些koa2都有所改善,koa2见。