// 第一步 - 初始化app對(duì)象
var koa = require('koa');
var app = koa();
// 第二步 - 監(jiān)聽端口
app.listen(1995);
初始化
執(zhí)行koa()的時(shí)候初始化了一些很有用的東西,包括初始化一個(gè)空的中間件集合,基于Request,Response,Context為原型,生成實(shí)例等操作。Request和Response的屬性和方法委托到Context中也是在這一步進(jìn)行的。在這一步并沒有啟動(dòng)Server。
module.exports = class Application extends Emitter {
/**
* Initialize a new 'Application'.
*
* @api public
*/
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
...
幾項(xiàng)配置的含義:
env 環(huán)境,默認(rèn)為 NODE_ENV 或者 development
proxy 如果為 true,則解析 "Host" 的 header 域,并支持 X-Forwarded-Host
subdomainOffset 默認(rèn)為2,表示 .subdomains 所忽略的字符偏移量。
啟動(dòng)Server
app.listen = function () {
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
在執(zhí)行app.listen(1995)的時(shí)候,啟動(dòng)了一個(gè)server,并且監(jiān)聽端口。
http.createServer接收一個(gè)函數(shù)作為參數(shù),每次服務(wù)器接收到請(qǐng)求都會(huì)執(zhí)行這個(gè)函數(shù),并傳入兩個(gè)參數(shù)(request和response,簡(jiǎn)稱req和res),那么現(xiàn)在重點(diǎn)在this.callback這個(gè)方法上。
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);
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
}
};
上述代碼完成了兩件事情:初始化中間件,接收處理請(qǐng)求。
初始化中間件
其中,compose的全名叫koa-compose,他的作用是把一個(gè)個(gè)不相干的中間件串聯(lián)在一起。
// 有3個(gè)中間件
this.middlewares = [function *m1() {}, function *m2() {}, function *m3() {}];
// 通過compose轉(zhuǎn)換
var middleware = compose(this.middlewares);
// 轉(zhuǎn)換后得到的middleware是這個(gè)樣子的
function *() {
yield *m1(m2(m3(noop())))
}
上述是V1的代碼,跟V2意思差不多。generator函數(shù)的特性是,第一次執(zhí)行并不會(huì)執(zhí)行函數(shù)里的代碼,而是生成一個(gè)generator對(duì)象,這個(gè)對(duì)象有next,throw等方法。
這就造成了一個(gè)現(xiàn)象,每個(gè)中間件都會(huì)有一個(gè)參數(shù),這個(gè)參數(shù)就是下一個(gè)中間件執(zhí)行后,生成出來的generator對(duì)象,沒錯(cuò),這就是大名鼎鼎的 next。
那compose是如何實(shí)現(xiàn)這樣的功能的呢?我們看一下代碼:
module.exports = compose
function compose(middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
這里的邏輯就是:先把中間件從后往前依次執(zhí)行,并把每一個(gè)中間件執(zhí)行后得到的值賦值給變量next,當(dāng)下一次執(zhí)行中間件的時(shí)候(也就是執(zhí)行前一個(gè)中間件的時(shí)候),把next傳給第二個(gè)參數(shù)。這樣就保證前一個(gè)中間件的參數(shù)是下一個(gè)中間件生成的值,第一次執(zhí)行的時(shí)候next為underfined。

compose(this.middleware)() = this.middleware[0](context, this.middleware[1](context, this.middleware[2](context, null)))
有一個(gè)問題,什么時(shí)候會(huì)出現(xiàn)i <= index的情況?答案是當(dāng)一個(gè)中間件中兩次調(diào)用next時(shí)。比如,當(dāng)?shù)诙€(gè)中間件里兩次調(diào)用next,那執(zhí)行結(jié)果就變成了這樣。

可以看出,當(dāng)?shù)诙螆?zhí)行next時(shí),index===i,就會(huì)拋出異常。
注意:一個(gè)中間件里是不能多次調(diào)用next的。
接收請(qǐng)求
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);
}
創(chuàng)建上下文
var ctx = self.createContext(req, res);
對(duì)應(yīng)的源碼是:
/**
* Initialize a new context.
*
* @api private
*/
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.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.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || '';
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}
koa中的this其實(shí)就是app.createContext方法返回的完整版context。
又由于這段代碼的執(zhí)行時(shí)間是接受請(qǐng)求的時(shí)候,所以表明:
每一次接受到請(qǐng)求,都會(huì)為該請(qǐng)求生成一個(gè)新的上下文。
可以看看,經(jīng)過這一步處理后他們之間的關(guān)系是怎樣的,可以用這個(gè)圖來表示:

從上圖中,可以看到分別有五個(gè)箭頭指向ctx,表示ctx上包含5個(gè)屬性,分別是request,response,req,res,app。request和response也分別有5個(gè)箭頭指向它們,所以也是同樣的邏輯。
介紹一下這幾個(gè)概念:
ctx,就是上下文,context 在每個(gè) request 請(qǐng)求中被創(chuàng)建,在中間件中作為接收器(receiver)來引用,或者通過 this 標(biāo)識(shí)符來引用:
app.use(function *(){
this; // is the Context
this.request; // is a koa Request
this.response; // is a koa Response
});
node里有request和response兩個(gè)對(duì)象,分別有處理請(qǐng)求和響應(yīng)的API。在koa里,將這兩個(gè)對(duì)象封裝在了ctx里,可以通過ctx.req(=noderequest)和ctx.res(=node request)來使用。
Koa Request 對(duì)象(=ctx.request)是對(duì) node 的 request 進(jìn)一步抽象和封裝,提供了日常 HTTP 服務(wù)器開發(fā)中一些有用的功能。
Koa Response 對(duì)象(=ctx.response)是對(duì) node 的 response 進(jìn)一步抽象和封裝,提供了日常 HTTP 服務(wù)器開發(fā)中一些有用的功能。
許多 context 的訪問器和方法為了便于訪問和調(diào)用,簡(jiǎn)單的委托給他們的 ctx.request 和 ctx.response 所對(duì)應(yīng)的等價(jià)方法,比如說 ctx.type 和 ctx.length 代理了 response 對(duì)象中對(duì)應(yīng)的方法,ctx.path 和 ctx.method 代理了 request 對(duì)象中對(duì)應(yīng)的方法。
app是應(yīng)用實(shí)例引用。
具體API看http://koa.bootcss.com/
錯(cuò)誤監(jiān)視
onFinished(res, ctx.onerror);
這行代碼的作用是監(jiān)聽response,如果response有錯(cuò)誤,會(huì)執(zhí)行ctx.onerror中的邏輯,設(shè)置response類型,狀態(tài)碼和錯(cuò)誤信息等。
執(zhí)行中間件
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
fn是compose(初始化中間件)執(zhí)行后的結(jié)果,fn.call(ctx)執(zhí)行中間件邏輯,執(zhí)行成功則接著執(zhí)行response.call(ctx),否則進(jìn)行錯(cuò)誤處理。
請(qǐng)求的時(shí)候會(huì)經(jīng)過一次中間件,響應(yīng)的時(shí)候又會(huì)經(jīng)過一次中間件。

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);
打印如下:
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
用一張圖表示如下:

由于每次接收請(qǐng)求,都會(huì)執(zhí)行callback,所以每次接收請(qǐng)求以下操作都會(huì)被執(zhí)行:
- 初始化中間件
- 生成新的上下文
- 執(zhí)行中間件邏輯
- 響應(yīng)請(qǐng)求或錯(cuò)誤處理