阅读学习Koa Core源码,基于koa v2.5.2。
Koa的核心代码很少,就四个文件application
, context
, request
, response
,算上注释和空行目前也还没过2000行代码。
这一篇针对application
的源码进行阅读学习。
# Koa Core
lib/application
lib/context
lib/request
lib/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