Koa2源码简读
koa2 源码非常精简,只有四个文件:
- application.js: 框架入口,负责管理中间件,以及处理请求
- context.js: context 对象的原型,代理 request 和 response 对象上的方法和属性
- request.js: request 对象的原型,提供请求相关的方法
- response.js: response 对象的原型,提供响应相关的方法和属性
application.js
先从入口开始看,koa有几个关键的点:
- 导出的 koa 可以new
- use 方法
- listen 方法
- 错误抛出 error
koa可以new,说明koa.js返回是是一个类,可以被监听(on),说明也是一个events,所以返回的这个类是继承过event的。
const Emitter = require('events');
module.exports = class Application extends Emitter {
constructor(options) {
super();
options = options || {};
// 是否信任proxy header参数,默认为 false
this.proxy = options.proxy || false;
// 子域偏移量,默认为 2
this.subdomainOffset = options.subdomainOffset || 2;
// 代理ip标头,默认为 X-Forwarded-For
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
// 从代理ip标头读取的最大ip数,默认为 0
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
this.middleware = [];
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// util.inspect.custom support for node 6+
/* istanbul ignore else */
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
};
需要注意,koa1.x 用的是构造函数方式,koa2.x 用的是类。所以调用的时候两者有差别。
const koa = require('koa');
// koa 1.x
const app = koa();
// koa 2.x
const app = new koa();
use
初始化koa实例化,我们需要调用app.use() 去挂载中间件。
use 做的事很简单,注册一个中间fn,将fn放入middleware数组中。
中间件可以在请求前(request) 和 响应后(response) 进行拦截修改context的request 和 response 。
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 || '-');
// 将中间件函数放入middleware数组中
this.middleware.push(fn);
return this;
}
listen
listen方法,与node的listen如出一辙。 这里的http.createServer就是node原生启动http服务的方法。此方法接受两个参数:
- options?: Object, 可选属性 [IncomingMessage, ServeResponse, insecureHTTPParser, maxHeaderSize]
- requestListener?: Function, 接收req 和 res
this.callback方法会返回一个函数作为http.createSever的回调函数,然后进行监听。
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback
callback函数是koa的开始,中间件与request 和 response 调和的入口。
- compose, 这个方法会将之前use过的中间件全部组合起来,形成洋葱模型等待调用。
- createContext, 创建上下文,会接收原生nodejs http类提供的的req与res参数,生成ctx对象,使其贯穿整个koa
- handleRequest, 用来调和request、response和中间件。
callback() {
// 用于生成koa洋葱模型
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);
};
// node http.createServer 接收的就是这个函数
return handleRequest;
}
compose
compose 方法主要负责生成洋葱模型,通过 koa-compose 包实现。
是一个高阶函数,会返回一个匿名函数。
module.exports = compose;
function compose (middleware) {
// 传入的middleware 必须是一个数组
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
// 传入的middleware每一个value必须是函数
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
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]
// 处理最后一个中间件还有next的情况,没有next就直接resolve, 从下往上执行回去
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 调用中间函数,第二个参数表示调用下一个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
这段代码重点就是 fn(context, dispatch.bind(null, i + 1)), 递归。
dispatch.bind(null, i + 1) 就是我们通常写中间件的第二个参数next。
我们执行这个next() 方法实际上得到的是下一个中间件的执行。
var middleware = [];
function next() {
return console.log('最外层的函数');
}
function createNext(middleware, next) {
return function(){
middleware(next);
}
}
middleware.push(function(next){
console.log(1);
next();
console.log(6);
})
middleware.push(function(next){
console.log(2);
next();
console.log(5);
})
middleware.push(function(next){
console.log(3);
next();
console.log(4);
})
var len = middleware.length;
for(var i = len-1;i>=0;i--) {
var currentMiddleware = middleware[i];
next = createNext(currentMiddleware, next)
}
next();
// 123456
createContext
生成koa的上下文,主要是用来引入request.js和response.js。
这里主要将req, res, this.request, this.response 挂载到context上。并通过赋值理清了循环引用层级关系,为使用者提供方便。
// const ctx = this.createContext(req, res);
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;
}
handleRequest
handleRequest方法用来处理请求与响应。
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 默认设置响应状态码为 404
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
// 监听http response, 当请求结束时执行回调。传入的回调是 ctx.onerror(),即当发生错误时才执行
onFinished(res, onerror);
// 执行洋葱模型
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
respond
用来处理响应,首先会判断 res 的 code是否为空,method 是否为 HEAD,body是否为空。以上条件都没命中,则会开始判断body的类型,是 Buffer, String , Stream 还是 JSON,做对应的返回。
function respond(ctx) {
// 没有响应体直接返回
if (false === ctx.respond) return;
// 上下文不让写,返回
if (!ctx.writable) return;
const res = ctx.res;
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 && !ctx.response.has('Content-Length')) {
const { length } = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
return res.end();
}
// status body
if (null == body) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type');
ctx.response.remove('Transfer-Encoding');
return res.end();
}
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
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);
}
onerror
需要区分清楚。这里有两个onerror,ctx.onerror 和 koa.onerror。
在handleRequest时处理错误的是ctx.onerror,在callback时绑定的错误处理是koa.onerror。
// context.js
onerror(err) {
if (null == err) return;
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
this.app.emit('error', err, this);
if (headerSent) {
return;
}
const { res } = this;
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
this.set(err.headers);
// force text/plain
this.type = 'text';
let statusCode = err.status || err.statusCode;
// ENOENT support
if ('ENOENT' === err.code) statusCode = 404;
// default to 500
if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;
// respond
const code = statuses[statusCode];
const msg = err.expose ? err.message : code;
this.status = err.status = statusCode;
this.length = Buffer.byteLength(msg);
res.end(msg);
}
// koa.js
onerror(err) {
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) 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(`\n${msg.replace(/^/gm, ' ')}\n`);
}
content.js
content.js 主要的功能提供了对request和response对象的方法与属性便捷访问能力。 其中使用了node-delegates, 将context.request与context.response上的方法与属性代理到context上。
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
request.js && response.js
request.js || response.js 封装了请求 || 响应 相关的属性以及方法。
通过application.js 中的 createContext 方法引入,挂载在koa ctx上。
最后
来梳理一下运行过程:
koa在new Application 时, 会初始化 context, request, response, middleware 及其他属性。
其中context, request, response 都有自己初始化属性,并且有单独的把代码抽出来放在对应文件中。通过use对koa添加中间件,将中间件 push 到 中间件数组中。
最后通过 listen 方法,激活整个application,跟node原生启动服务一致。
由于http.createServer(this.callback()), 有个callback方法,作为http.createServer的回调函数。callback中有四条主线,分别是 compose, onerror, handleRequest, createContext
compose, 将use使用的中间件进行拼接,形成一个洋葱模型,以await next() 上下分割,作为先后执行的顺序。
onerror ,因为 Application 继承了 Events类,所以拥有监听事件的on方法。用于监听koa自身抛出的错误。
callback函数里的handleRequest, 顾名思义是个请求处理器,主要的功能有两个。
- 接收node原生http模块的req, res 对象,创建请求响应的上下文
- 传入ctx与中间件,调和中间件、上下文。
createContext, 用Object.create 重新初始化 context、request、req 三个对象,并将它们塞到原型链(__proto__)上。最后返回context,供外部使用。
handleRequest 主要是处理中间件, 再处理then 的handleResponse,catch错误。
到这里koa源码大致就完成了 🎉