阅读学习Koa Core源码,基于koa v2.5.2。
Koa的核心代码很少,就四个文件application, context, request, response,算上注释和空行目前也还没过2000行代码。
这一篇针对application的源码进行阅读学习。
# Koa Core
lib/applicationlib/contextlib/requestlib/response
主要这四个文件,当然也还依赖了很多外部库,以及koa的其他仓库。这一篇看第一部分lib/application。
# Application
Koa 的 Hello, world 是这样:
const Koa = require('koa')
const app = new Koa()
app.use(ctx => {
ctx.body = 'Hello Koa'
})
app.listen(3000)
第一个const Koa = require('koa')就是引入koa的Application 类,即源码中的 lib/application.js。
// package.json
{
// ...
"main": "lib/application.js",
// ...
}
# constructor()
// @see https://nodejs.org/dist/latest-v10.x/docs/api/events.html#events_class_eventemitter
const Emitter = require('events');
module.exports = class Application extends Emitter {
// ...
}
整个Application类继承自node的EventEmitter。也就是说,Application本身会带有on(), off(), emit()等事件相关的方法。
const context = require('./context');
const request = require('./request');
const response = require('./response');
const util = require('util');
// ...
/**
* 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);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
// ...
构造函数,初始化Application的属性。
其中的context, request, response就是在另外三个文件中实现的了。
node v6.6.0+ 增加了自定义inspect函数,deprecate了原本的inspect()方法。为了支持旧版本node,还保留了this.inspect。util.inspect.custom将返回一个Symbol,专门用于对应inspect方法。预计不再支持旧版node后,会把这里移除,直接用[util.inspect.custom]。
# use()
// @see https://github.com/visionmedia/debug
const debug = require('debug')('koa:application');
const isGeneratorFunction = require('is-generator-function');
const deprecate = require('depd')('koa');
const convert = require('koa-convert');
// ...
/**
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/
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/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
// ...
koa默认支持通过debug进行调试,只需要启动时增加环境变量DEBUG=koa*,即可调试koa相关的组件代码 - 参考文档。
这里注册了koa:application为模块名,所以在当前文件中的debug都会属于该模块。后面出现debug的地方不再特别说明。
简单说来,use()方法就是往this.middileware数组中push新的middleware。由于koa的middleware洋葱模型是有顺序的,所以this.middileware数组中的顺序就是从外到内的顺序。
旧版1.x的middleware还不是async函数,而是generator函数。为了支持旧版middleware,对generator函数进行了判断,通过koa-convert转换成async版的middleware,并通过deprecate提示旧版的middleware已经被废弃。预计3.x就不再支持旧版middleware了。
use()支持链式调用,所以最后返回的是this。
# listen()
const http = require('http');
// ...
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
// ...
listen()方法就是调用了http模块并创建了http.Server实例进行listen。
this.callback()作为requestHandler传入,监听request事件 - 文档。
# callback()
const compose = require('koa-compose');
// ...
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
// ...
callback()就是传入http模块的requestHandler,通过koa-compose将所有通过use()注册的middlewares打包成一个,然后通过handleRequest()去真正处理请求。
listenerCount()是node的EventEmitter的方法。如果还没有给Application单独注册过error事件的监听器,则默认使用onerror来处理error事件。
通过this.createContext()方法新建上下文,作为后续整个middleware链中的ctx。因为每个请求都要有独立的context,所以每次处理请求时都要新创建一个。
# handleRequest()
const onFinished = require('on-finished');
// ...
/**
* Handle request in callback.
*
* @api private
*/
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
// ...
执行middlerware就是在这里了,将该请求的ctx传入之前通过koa-compose打包好的fnMiddleware中,执行完成后通过respond生成最终的响应返回给客户端。
如果res.statusCode没有被改过,说明没进入middleware,资源未找到,并且也没有报错,所以默认是设置了404。后面如果设置了body就会改成200,如果有别的错误码就会改成别的。
onFinished()的话,简单去看了看on-finished的源码,就是在HTTP请求/响应关闭(closes)、完成(finishes)或者出错(errors)时,执行一个回调函数。那么这里就是在响应res出错的时候,执行context.onerror()方法。应该是因为响应出错的时候,有可能不会被后面的catch捕捉到,所以这里才额外写了一个方法。
fnMiddleware(ctx)传入ctx,将所有注册的middleware执行一遍,最后最处理完成的ctx执行respond方法,生成响应返回给客户端。
# respond()
其实这个respond()方法并不是Application类的方法,是写在类外的一个helper函数。
const isJSON = require('koa-is-json');
const Stream = require('stream');
// ...
/**
* Response helper.
*/
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);
}
如果设置了ctx.respond = false,则跳过koa默认的响应方法。如果!ctx.writable(即对应到res.socket.writable,或者res.isfinished),也跳过默认响应方法。
后面则是对响应的body进行判断和处理,如果不是string, Buffer, Stream等类型,则默认JSON.stringify处理后返回。
# createContext()
/**
* 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.state = {};
return context;
}
每个请求初始化一个新的context,把ctx, res, req互相挂在一起。
TIP
其实这里有点不太明白,在构造函数里面:
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
已经是继承了一层了,在这里为什么还要再多套一层,又跑一次Object.create()。
直接在createContext的时候这么写会有什么问题吗:
createContext(req, res) {
const context = Object.create(context);
const request = context.request = Object.create(request);
const response = context.response = Object.create(response);
// ...
}
# onerror()
// ...
/**
* Default error handler.
*
* @param {Error} err
* @api private
*/
onerror(err) {
if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
// ...
默认的错误处理方法,注意是在callback()里面this.on('error')注册的,是对Application上的error进行处理的方法,不是响应的error。
handleRequest()里面对应的响应的onerror是在context下实现的。
# toJSON(), inspect()
const only = require('only');
// ...
/**
* Return JSON representation.
* We only bother showing settings.
*
* @return {Object}
* @api public
*/
toJSON() {
return only(this, [
'subdomainOffset',
'proxy',
'env'
]);
}
/**
* Inspect implementation.
*
* @return {Object}
* @api public
*/
inspect() {
return this.toJSON();
}
// ...
打印app时的方法。
only就是通过白名单的方式,过滤掉对象中的多余属性,返回一个只包含相应属性的新对象。
# References
# Related posts
Koa源码阅读:
Koa Core - 源码阅读 2 - Context
Koa Core - 源码阅读 3 - Request & Response