Request
,Context
,Response 在代码运行之前就已经存在的
Request和Response自身的方法会委托到Context中。
Context源码片段
var delegate = require('delegates'); var proto = module.exports = {}; // 一些自身方法,被我删了 /** * Response delegation. */ delegate(proto, 'response') .method('attachment') .method('redirect')
可以看到, context 把 response 的方法加到 context自己的原型中来, 方便用户直接通过context调用 response request的方法
delegate实现过程, 先在构造函数中存储context的原型 proto, 再method中更加name 把 response原型的方法追加到 context中
function Delegator(proto, target) { if (!(this instanceof Delegator)) return new Delegator(proto, target); this.proto = proto; this.target = target; this.methods = []; this.getters = []; this.setters = []; this.fluents = []; } Delegator.prototype.method = function(name){ var proto = this.proto; var target = this.target; this.methods.push(name); proto[name] = function(){
//Response[name].apply(Response, arguments); 相当下面 return this[target][name].apply(this[target], arguments); }; return this; };
上面是委托了 Response Request的方法, 其实context还委托了 Response request的属性名称,
可以用context直接访问他们的属性的值, 用context设置属性的值的时候, Response request的属性值也会改变
分别是用getter setter实现的
Delegator.prototype.getter = function(name){ var proto = this.proto; var target = this.target; this.getters.push(name); proto.__defineGetter__(name, function(){
//Response[name] return this[target][name]; }); return this; };
我们在来看看access
Delegator.prototype.access = function(name){
return this.getter(name).setter(name);
};
可以看到,这个方法是getter+setter
应用启动前的内容到现在就说完了
接下来我们看看使用 koa来启动一个app的时候,koa内部会发生什么呢?
启动server
会经过两步
第一步是 new 一个 koa 对象app,
第二步是用 app对象 监听端口号,
// 第一步 - 初始化app对象
var koa = require('koa');
var app = koa();
// 第二步 - 监听端口
app.listen(1995);
可以看一下源码 koa() 这里发生了什么
module.exports = Application;
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.proxy = false;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
可以看到, 这里只是定义了一个空的中间件对象, 并把context request等的实例挂载到application中
这里应为实例化了context, 所以会执行delegate方法,所以这一步, 也是 正式把request response委托到context上
到了这里并没启动server, 直到监听的时候
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
可以看到,在执行app.listen(1995)
的时候,启动了一个server,并且监听端口。
熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,
这个函数会传入两个参数(request和response,简称req和res),那么现在重点在this.callback
这个方法上。
比如 http.createServer( funciton(res, req){ req.send('text') } //callback),
koa利用 listen 包装了这个函数, 使用listen直接启动服务, 并把callback包装一下 ,注入自己的东西, 这里其实就是初始化了一些中间件
为什么要初始化呢, 我们是使用app.use(log)添加一个日志中间件, 这里只是push一个中间件, 而初始化就是使这些中间件联系起来
app.callback 源码
app.callback = function(){
//我们可以指定 app.experimental = true, 这样就开启了es7的一些功能, 比如我们可以使用 async await 了 if (this.experimental) { console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.') }
//这里分析的时候简化为 co.wrap(compose(this.middleware)); 因为两个分支的核心思想是一样的, es7更简化了而已 var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));
var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function(req, res){
res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror);
fn.call(ctx).then(function () { respond.call(ctx); }).catch(ctx.onerror);
}
};
var fn = co.wrap(compose(this.middleware));
虽然只剩下一行代码,但不要小瞧它哦~~
我们先看compose(this.middleware)
这部分,compose
的全名叫koa-compose
,他的作用是把一个个不相干的中间件串联在一起。
// 有3个中间件
this.middlewares = [function *m1() {}, function *m2() {}, function *m3() {}];
// 通过compose转换
var middleware = compose(this.middlewares);
// 转换后得到的middleware是这个样子的
function *() {
yield *m1(m2(m3(noop())))
}
类似:
function a(s){ console.log(s) }
function b(s){console.log(s); return s+1}
function c(s){console.log(s); return s+2}
a( b(c(0)) );
上面这个函数执行的时候遇到 yield, 所以 *m1不会执行, 当调用next后, 执行顺序 noop() => m3() => m2( ) => m1()
yield *m1(m2(m3(noop())))
但m3是一个generator函数, 所以它第一次执行, 实际上里面的代码是不会执行的, 直到你调用 next,
所以当你把 m3() 执行后返回的一个对象作为m2的参数时, 这个参数暂时命名为 p1, 使得有条件在m2中调用 p1.next
来真正执行m3里面的代码 , 没错,这就是大名鼎鼎的 中间件 next
我们是这样利用中间件的
var app = require('koa')(),
router = require('koa-router')();
app.use(function *(next){
console.log(1);
yield next;
console.log(5);
});
app.use(function *(next){
console.log(2);
//实际上这个next 就是m3第一次执行完以后的generator对象, 当console.log(2)执行完后, 再koa会调用app.middleware.next方法, 则把yield后面的 next执行了
yield next;
console.log(4);
});
app.use(function *(){
console.log(3);
});
app.listen(3000);
当一个请求到达的时候,控制台会依次输出1 2 3 4 5
,这就是koa中强大的middleware特性
可以中断当前代码的执行, 去执行下一个中间件B的代码, 等下一个中间件B的代码执行完成, 再去执行剩余的,
同时B也可以中断其执行下一个中间件C, 从而使得这些中间件可以窜起来使用, 一个中间件内部直接利用另一个中间件的结果,
问题:
如果另一个中间件是异步? 估计也会内部增加一个yield, 多调用一次next拿到异步数据, + 哦, 后面会解释道, 好像是利用promise 保证异步完成, 才调用返回上一个中间件的.
执行以下代码, 可以更了解 yield特性
function* numbers() { console.log('function start.'); var v1 = yield 0; console.log('v1 = ' + v1); var v2 = yield 1; console.log('v2 = ' + v2); return 5; } var nums = numbers();
//nums.next(); nums.next(); nums.next();
compose 实现过程/** * Expose compositor. */
module.exports = compose; /** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */ function compose(middleware){
return function *(next){
if (!next) next = noop(); var i = middleware.length;
//先把中间件从后往前依次执行
while (i--) {
//把每一个中间件执行后得到的 generator对象 赋值给变量next next = middleware[i].call(this, next); } return yield *next; } } /** * Noop. * * @api private */ function *noop(){}
最后,有一个非常巧妙的地方,就是最后一行return yield *next;
这行代码可以实现把compose
执行后 return的函数变成第一个中间件,(因为第一个中间件其实在while循环中执行了一次)
也就是说,执行compose
之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,
这主要依赖了generator函数的yield *语句的特性。
我们接着说刚才没说完的
var fn = co.wrap(compose(this.middleware));
上面这段代码现在就可以理解成下面这样
var fn = co.wrap(function *() {yield *m1(m2(m3(noop())))});
co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。
看下源码
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,
返回一个函数createPromise,当执行createPromise这个函数的时候,
调用co并传入一个参数,这个参数 fn 是中间件函数执行后生成的 Generator对象 。
这意味着,返回的这个函数 createPromise 是 触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑
回到callback代码
app.callback = function(){
if (this.experimental) { console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.') }
var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror);
//最后的等待函数 return function(req, res){
res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror);
//执行这里 实际上就是执行了createPromise函数, 也就是执行了第一个中间件 fn.call(ctx).then(function () { respond.call(ctx); }).catch(ctx.onerror); } };
从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~
到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。
东风就是request请求~~
接收请求
前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,
koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。
上面我们说this.callback
这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。
简单回顾下
// 创建server并监听端口 app.listen = function(){ debug('listen'); var server = http.createServer(this.callback());
return server.listen.apply(server, arguments); }; // 这个方法返回的函数会被传递到http.createServer中,
// http.createServer这个方法的作用是 : 每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和response
// listen 服务器是怎么处理的, 如何传resp resq 得深入到底层了
app.callback = function(){ if (this.experimental) { console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.') } var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function(req, res){
res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror);
fn.call(ctx).then(function () { respond.call(ctx); }).catch(ctx.onerror);
} };
所以第二部分的重点就是下面段代码啦~
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
}
我们先看这段代码
var ctx = self.createContext(req, res);
不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???
这行代码就是创建一个最终可用版的context。
从上图中,可以看到分别有五个箭头指向ctx,表示ctx上包含5个属性,分别是request,response,req,res,app。
request和response也分别有5个箭头 指向它们,所以也是 同样的逻辑。
这里需要说明下
- request - request继承于Request静态类,包含操作request的一些常用方法 (处于上层)
- response - response继承于Response静态类,包含操作response的一些常用方法
- req - nodejs原生的request对象 (处于底层)
- res - nodejs原生的response对象
- app - koa的原型对象
var ctx = self.createContext(req, res);
不多说,咱们观摩下代码
app.createContext = function(req, res){ // 继承 var context = Object.create(this.context); var request = context.request = Object.create(this.request); var response = context.response = Object.create(this.response); // 往context,request,response身上挂载属性 实现上图 的关系 效果 context.app = request.app = response.app = this; context.req = request.req = response.req = req; context.res = request.res = response.res = res;
request.ctx = response.ctx = context; request.response = response; response.request = request;
context.onerror = context.onerror.bind(context); context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure });
context.accept = request.accept = accepts(req); context.state = {}; // 最后返回完整版context return context;
};
讲到这里其实我可以很明确的告诉大家,,,koa中的this
其实就是app.createContext
方法返回的完整版context
又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文context
上下文到这里我们就说完啦。我们接着往下说,看下一行代码
onFinished(res, ctx.onerror);
这行代码其实很简单,就是监听response,如果response有错误,
会执行ctx.onerror
中的逻辑,设置response类型,状态码和错误信息等。
源码如下:
onerror: function(err){
// don't do anything if there is no error.
// this allows you to pass `this.onerror`
// to node-style callbacks.
if (null == err) return;
if (!(err instanceof Error)) err = new Error('non-error thrown: ' + err);
// delegate
this.app.emit('error', err, this);
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (this.headerSent || !this.writable) {
err.headerSent = true;
return;
}
// unset all headers
this.res._headers = {};
// force text/plain
this.type = 'text';
// ENOENT support
if ('ENOENT' == err.code) err.status = 404;
// default to 500
if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;
// respond
var code = statuses[err.status];
var msg = err.expose ? err.message : code;
this.status = err.status;
this.length = Buffer.byteLength(msg);
this.res.end(msg);
}
我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。
我们先看代码
我们先看代码
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
- fn - 我们上面讲的
co.wrap
返回的那个函数 - ctx - app.createContext执行后返回的完整版context对象
总体上来说,执行fn.call(ctx)
会返回promise,koa会监听执行的成功和失败,成功则执行respond.call(ctx);
,失败则执行ctx.onerror
,
失败的回调函数刚刚已经讲过。这里先说说respond.call(ctx);
。
我们在写koa的时候,会发现所有的response操作都是
this.body = xxx;
this.status = xxxx;
这样的语法,但如果对原生nodejs有了解的童鞋知道,
nodejs的response只有一个api那就是res.end();
,而设置status状态码什么的都有不同的api,
那么koa是如何做到通过this.xxx = xxx
来设置response的呢?
先看一张图,,我盗的图
从图中看到,request请求是以respond结束的。
是滴,所有的request请求都是以respond这个函数结束的,
这个函数会 读取this.body中的值 根据不同的类型来决定以什么类型响应请求
我们来欣赏一下源码
function respond() {
// allow bypassing koa
if (false === this.respond) return;
var res = this.res;
if (res.headersSent || !this.writable) return;
var body = this.body;
var code = this.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
this.body = null;
return res.end();
}
if ('HEAD' == this.method) {
if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
return res.end();
}
// status body
if (null == body) {
this.type = 'text';
body = this.message || String(code);
this.length = Buffer.byteLength(body);
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
this.length = Buffer.byteLength(body);
res.end(body);
}
仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?
这不科学啊。我分明记得状态码是rs.statusCode = 400
这样设置的,为啥代码中没有??
这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?
就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了
set status(code) {
assert('number' == typeof code, 'status code must be a number');
assert(statuses[code], 'invalid status code: ' + code);
this._explicitStatus = true;
this.res.statusCode = code;
this.res.statusMessage = statuses[code];
if (this.body && statuses.empty[code]) this.body = null;
},
主要是this.res.statusCode = code;
this.res.statusMessage = statuses[code];
这两句,
statusCode
和statusMessage
都是nodejs原生api。有兴趣可以自行查看~
接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?
我们看这段代码
fn.call(ctx)
fn刚刚上面说过,就是co.wrap
返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.call
把ctx
设为上下文,也就是this。
那中间件逻辑是什么样的呢。我们先看一下源码:
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,
中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。
而执行co所传递的那个参数,我们给它起个名,就叫中间件函数
吧,中间件函数也是一个generator函数,
因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧
那我们看co的源码:
function co(gen) {
var ctx = this;
//获取 gen 后面的参数
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
//如果一个中间件已经完成所有的 yield, 回到上一个中间件继续执行
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
//如果还有yield 则继续调用 then方法, 其实会调用next方法, 执行剩余的 yield
//if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); <==> tomPromise.call
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
可以看到,代码并不是很多。
首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。
我们在看这段代码
function onFulfilled(res) {
var ret;
try {
//按顺序执行, 从第一个中间件开始, 遇到 yield next 才执行下一个
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
这个函数最重要的作用是运行gen.next
来 执行 中间件中的 业务逻辑。
通常在开发中间件的时候会这样写
yield next;
所以ret中包含下一个中间件对象
(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)
然后把下一个中间件对象传到了next(ret)
这个函数里,next函数是干什么的?我们看看
function next(ret) {
//如果中间件已经结束(没有yield了),那么调用promise的resolve。
if (ret.done) return resolve(ret.value);
//否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);
var value = toPromise.call(ctx, ret.value); //if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected( new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
上面是toPromise中的一段代码
既然是用co又执行了一遍,那么co是返回promise的。所以返回的 这个value就分别被监听了 成功和失败 的不同处理。
value.then(onFulfilled, onRejected);
所以我们可以看到,如果第二个中间件里依然有yield next
这样的语句,那么第三个中间件依然会被co包裹一层并运行.next方法,依次列推,这是一个递归的操作
所以我们可以肯定的是,每一个中间件都被promise包裹着,直到有一天中间件中的逻辑运行完成了,那么会调用promise的resolve来告诉程序这个中间件执行完了。
那么中间件执行完了之后,会触发onFulfilled
,这个函数会执行.next方法。
前方高能预警!!!
比如有3个中间件,当系统接收到请求的时候,会执行co,co会立刻执行onFulfilled来调用.next往下执行,
将得到的返回结果(第二个中间件的generator对象,上面我们分析过)传到co中在执行一遍。以此类推,一直运行到最后一个yield,
这个时候系统会等待中间件的执行结束,一旦最后一个中间件执行完毕,会立刻调用promise的resolve方法表示结束。
(这个时候onFulfilled函数的第二个执行时机到了,这样就会出现一个现象,
一个generator对象的yield只能被next一次,下次执行.next的时候从上一次停顿的yield处继续执行,
所以现在当有一个中间件执行完毕后,在执行.next就会在前一个中间件的yield处继续执行)
当最后一个中间件执行完毕后,触发promise的resolve,而别忘了,第二个中间件可是用then监听了成功和失败的不同处理方法,
一旦第三个中间件触发成功,第二个中间件会立刻调用onFulfilled来执行.next,
继续从第二个中间件上一次yield停顿处开始执行下面的代码,而第二个中间件的逻辑执行完毕后,
同样会执行resolve表示成功,而这个时候第一个中间件正好也通过.then方法监听了第二个中间件的promise,
也会立刻调用onFulfilled函数来执行.next方法,这样就会继续从第一个中间件上一次yield的停顿处继续执行下面的逻辑,以此类推。
这样就实现了中间件的回逆,通过递归从外到里执行一遍中间件,然后在通过promise+generator从里往外跳。
所以有一个非常重要的一点需要注意,onFulfilled
这个函数非常重要,重要在哪里???重要在它执行的时间上。
onFulfilled
这个函数只在两种情况下被调用,一种是调用co的时候执行,还有一种是当前promise中的所有逻辑都执行完毕后执行
其实就这一句话就能说明koa的中间件为什么会回逆。
所以如果我们在一个中间件中写好多yield,就可以看出关键所在,
先通过递归从外往里(从第一个中间件运行到最后一个中间件)每次遇到yield next就会进入到下一个中间件执行,
当运行到最后发现没有yield的时候,会跳回上一个中间件继续执行yield后面的,结果发现又有一个yield next,
它会再次进入到下一个中间件,进入到下一个中间件后发现什么都没有,
因为yield的特性(一个generator对象的yield只能被next一次,下次执行.next的时候从上一次停顿的yield处继续执行),
所以便又一次跳入上一个中间件来执行。以此类推。
我们试一下:
var koa = require('koa');
var app = koa();
app.use(function* f1(next) {
console.log('f1: pre next');
yield next;
console.log('f1: post next');
yield next;
console.log('f1: fuck');
});
app.use(function* f2(next) {
console.log(' f2: pre next');
yield next;
console.log(' f2: post next');
yield next;
console.log(' f2: fuck');
});
app.use(function* f3(next) {
console.log(' f3: pre next');
yield next;
console.log(' f3: post next');
yield next;
console.log(' f3: fuck');
});
app.use(function* (next) {
console.log('hello world')
this.body = 'hello world';
});
app.listen(3000);
上面的代码打印的log是下面这样的
f1: pre next
f2: pre next
f3: pre next
hello world
f3: post next
f3: fuck
f2: post next
f2: fuck
f1: post next
f1: fuck
那么我用白话文来说一下中间件的逻辑,大概是这样的, 就是前面说的
第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,
然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,
第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束
用一张图表示大概是这样的。
为了方便理解,伪代码大概是下面这样
new Promise(function(resolve, reject) {
// 我是中间件1
yield new Promise(function(resolve, reject) {
// 我是中间件2
yield new Promise(function(resolve, reject) {
// 我是中间件3
yield new Promise(function(resolve, reject) {
// 我是body
});
// 我是中间件3
});
// 我是中间件2
});
// 我是中间件1
});
这就是最核心的思想!!
最后这句话解释的思想好像文章的前半部分都可以看出来了, 我觉得 难点在于如果后面的中间件也是异步的,调用了多个 yield ... 实现异步操作,
如何处理 .. 也是是利用了promise把generator进一步封装... 等下一个中间件把yield执行完, 在调用resolve 去回到上一个中间件去执行剩余的代码
总结
简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。
其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。
比如有几个中间件,mw1,mw2,mw3,mwn...
站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。
到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。
两个重要技术点
最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念
- Middleware - 中间件
- Context - 上下文