Koa2 源碼解析
其實本來不想寫這個系列文章的,因為Koa本身很精簡,一共就4個文件,千十來行代碼。
但是因為想寫 egg[1] 的源碼解析,而egg是基於Koa2的,所以就先寫個Koa2的吧,用作承上啟下。
[1] egg 是阿里巴巴團隊開源的企業級web開發框架
面向讀者
我們假定讀者具備javascript基礎知識,簡單了解promise、generator和async。
入口
我們以 koajs中文官網 的例子作為入口。
const Koa = require('koa');
const app = new Koa();
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
這樣就啟動起來了一個Koa2網站,可以看到只做了3件事: Koa的構造函數、app實例的use函數、app實例的listen函數。
查看Koa源碼的package.json文件得知,默認入口是 application.js文件,也就是上面代碼的Koa,那么讓我們來看看里面是什么?
Application.js
module.exports = class Application extends Emitter {
我們可以看到Application是繼承自 Event 模塊的事件監聽器,並且 Koa2 已經使用了 ES6 的 class 語法。
構造函數
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);
}
下面來看看構造函數,也沒什么稀奇的。
調用父類的構造函數,然后4個屬性的初始化,我們先不管它們都是干什么的。
接下來是創建3個對象context、request和response,其實這就是koa2的核心了,構造出的context表示本次請求的上下文,request和response這個大家都知道。
use函數
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x---deprecated');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
use函數更簡單:判斷是不是function,判斷是不是generator,如果是generator那么轉換一下,將fn放入middleware數組。
listen函數
listen() {
debug('listen');
const server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
listen看似也沒什么,其實不然,獲取createServer的callback函數是個核心的東西,我們來看下
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
return (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() => respond(ctx)).catch(onerror);
};
}
首先,把所有middleware進行了組合,使用了koa-compose,我們也不用去管他的內部實現,簡單來說就是返回了一個promise數組的遞歸調用。
然后,我們看看這個匿名函數,把http code默認設置為404,接着利用createContext函數把node返回的req和res進行了組合創建出context,可以看看createContext函數
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;
}
這里面都是一堆的組合和賦值,例如把req掛到context下面啦、把request掛到reponse下面啦、獲取下accept啦、ip啦。
回頭來看看創建完context又干了些什么
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() => respond(ctx)).catch(onerror);
前兩行沒什么,給res注冊了一個onerror事件,然后第三行就是具體所有middleware的執行了,所有middleware都執行完后,調用respond(ctx)函數
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
const res = ctx.res;
if (!ctx.writable) return;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' == ctx.method) {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
body = ctx.message || String(code);
if (!res.headersSent) {
ctx.type = 'text';
ctx.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);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
這個respond函數里面也不過是一些收尾工作,例如判斷http code為空如何輸出啦,http method是head如何輸出啦,body返回是流或json時如何輸出啦。
完事,你看koa是不是很簡單,只不過是把res和req組合成了context,並提供了一些便利的函數,以及最重要的把middleware promise化了,寫異步更爽了,加上es2017的async/await語法,跟C#也很像了。
接下來我們還會對context、request和response對象進行一番解析,敬請期待。
