Koa2 的源码 相当的简短,因为抽离了路由、模板引擎等模块,只提供一个架子,而几乎所有的功能都需要由第三方中间件完成。阅读时建议对照源码。
1 2 3 4 5
| . lib ├── application.js ---------- 构造 app 对象 ├── context.js -------------- 构造 ctx 对象 ├── request.js --------------- 构造 req 对象 └── response.js ------------- 构造 res 对象
|
底层原理同 express,即代理http.createServer(requestHandler)
中的requestHandler
,使用中间件来处理请求。
代码梳理
application.js
中构造了 app 对象,初始化 context 属性 ,定义了 listen、use 等方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| const Emitter = require("events"); const onFinished = require("on-finished"); const response = require("./response"); const compose = require("koa-compose"); const context = require("./context"); const request = require("./request");
class Application extends Emitter { constructor() { super(); this.middleware = []; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } 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; } 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); } }
|
compose
compose
接受一个包含中间件的数组作为参数,返回一个函数,且该函数会返回一个 Promise 。compose
是 koa 洋葱模型的关键 ,来看一下 koa-compose 的源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function compose(middleware) { 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]; 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); } } }; }
|
中间件内去调用 next 方法就是调用dispatch(i + 1)
,即执行下一个中间件(入栈),以此类推执行到最后一个中间件(所有中间件入栈)。当最后一个中间件执行完毕,出栈,然后执行前一个中间件的处理函数,以此类推执行到第一个中间件出栈。
其实就是执行层层嵌套的函数,这个和 express 一样,但是 koa 的中间件模式为洋葱型,express 为直线型。这因为考虑异步中间件的情况,koa 的 next 方法会返回一个 promise 实例,因此使用await next()
可以用同步的写法处理异步中间件。express 的 next 方法只是遍历 stack 找出并执行匹配的中间件,而没有返回 promise 实例,所以无法使用async/await
特性,在处理异步中间件时,只能是线性的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const Koa = require("koa"); const app = new Koa();
app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set("X-Response-Time", `${ms}ms`); });
app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}`); });
app.use(async ctx => { ctx.body = "Hello World"; });
app.listen(3000);
|
案例
结合具体案例来看一下,compose
函数的作用。
1 2 3 4 5 6 7 8 9 10 11 12 13
| var middleware = [ function foo(ctx, next) { console.log("foo1"); next(); console.log("foo2"); }, function bar(ctx, next) { console.log("bar1"); next(); console.log("bar2"); } ]; var fn = compose(middleware);
|
此时 fn 可以视为下面的函数:
1 2 3 4 5 6 7
| function (context) { return function () { return Promise.resolve(foo(context, bar(context, () => { return Promise.resolve() }))); } }
|
所以当执行fn(ctx).then(handleResponse).catch(onerror);
时,输出如下: