概述
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 实例.
服务器响应请求阶段
跟普通的 Node.JS 服务器一样, 当服务器接收到客户端请求, 也会触发 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
来源: https://juejin.im/post/5bb84105f265da0aa74f3dd3