概述
Koa和Express框架都是出自同一团队的,我们常常拿它们作比较。
Koa比Express更加轻便,Koa没有集成路由功能、模板引擎,仅保留了中间件功能。
而且通过阅读源码,你会发现Koa比Express更易读,Koa使用了比较新的ES 6的语法,有class、extend关键字,也有promise异步风格等等。
整体思路
把Koa app分为两个阶段:
-
服务器准备阶段,这个阶段主要是注册中间件和整合中间件
-
服务器响应请求阶段,这个阶段主要是代理req对象和res对象,遍历并执行中间件函数
服务器准备阶段
这一阶段会进行初始化Koa实例的操作,比如装载中间件的middleware数组,增强了的req对象和res对象,以及新增的上下文对象context,可以从下面的构造函数看出来:
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;
}
}
复制代码
1. 注册中间件
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;
}
复制代码
2. 整合中间件
这里依赖的是koa-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!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
从中可以看出,整合中间件的原理是,通过闭包维护中间件数组,然后返回一个接收context上下文对象的函数,该context会被传入所有中间件。
或许你也注意到了,所有中间件执行的结果都被传入Promise.resolve
,所以我们的中间件函数是可以返回普通对象,也可以返回Promise实例的。这也决定了,使用Koa的用户可以使用最新的ES 2017的语法糖:async / await(当然也可以通过babel转译)。
在中间件函数被执行的时候,不仅传入了context对象,还传入了下一个中间件的引用,所以中间件可以暂时把执行控制权交给下一个中间件,这一点跟Express是一样的。不同的地方在于,Express的中间件不能处理异步操作,而Koa可以,因为Koa的中间件可以返回一个Promise实例。
服务器响应请求阶段
跟普通的NodeJS服务器一样,当服务器接收到客户端请求,也会触发request事件,这时Koa会把request handler传入的req和res整合并加强成上下文对象context,然后遍历所有的中间件函数并传入context对象。
1. 整合并加强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;
}
复制代码
2. 遍历中间件
先看下面的代码:
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);
}
复制代码
传入的fnMiddleware
函数其实是在准备阶段整合过的中间件,它会安装顺序执行或者通过next把执行权提前交给下一个中间件,直到执行玩所有中间件,最后返回一个Promise实例。
从中我们可以看到fnMiddleware
返回Promise实例后,会执行handleResponse
,它的作用就是把经过所有中间件处理过的res对象最后输出给客户端,它的主要源码如下:
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);
}
复制代码
看到这里,你应该明白为什么官方文档有一段话是这样的:
绕过 Koa 的 response 处理是 不被支持的. 应避免使用以下 node 属性:
- res.statusCode
- res.writeHead()
- res.write()
- res.end()
因为res的最后处理并返回给客户端是Koa框架来完成的,这一点跟Express框架不一样,在Express框架里,在中间件函数里调用res.end()
后,排在后面的中间件的对res对象的处理将无效。
推荐的插件
因为Koa框架自身很轻,所以我们可以自主选择一些插件来增强它,下面就是推荐的一些插件:
- koa-router
- koa-views
- koa-bodyparser
- koa-static