HomePostsWorks

Koa 源码解析

这篇文章介绍一个应用服务器框架的主要两个过程:app init 过程和 request handle 过程。一些有趣的细节问题看看以后再写,包括 context, request, response 三个对象,错误处理,egg.js 等等。


init 过程

通过一个简单的 demo(实际上就是官网的例子)来讲解 app init 过程。对于 Koa 来说,init 过程是比较简单的。

const Koa = require('koa') const app = new Koa() // Koa 对象实例化 // use 增加 middleware app.use(async (ctx, next) => { await next() const rt = ctx.response.get('X-Response-Time') console.log(`${ctx.method} ${ctx.url} - ${rt}`) }) 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) => { ctx.body = 'Hello World' }) app.listen(3000) // 监听端口

Koa 对象实例化

lib/application.js

constructor() { super(); this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; // 在应用程序实例上绑定 context request repsonse 的原型,实际上这三个对象都没有任何属性 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; } }

use 增加 middleware

use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); // 如果是一个生成器函数要转换成 async 函数,细节问题 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 当中 this.middleware.push(fn); return this; // 通过返回自己可以进行链式调用 }

监听端口

listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); // 调用 Node.js 原生的方法监听端口 }

this.callback() 方法返回一个回调函数,它符合 Node.js 原生 http.createServer 的要求,被当作 request handler.

callback() { // 将自己绑定的中间件封装起来 const fn = compose(this.middleware); // 进行错误处理的回调函数 // 由于 Koa 继承了 Emitter, 所以用户可以在上面绑定 error 方法,如果用户没用绑定,就绑定自带的 onerror 方法 // 错误处理暂时不讲 if (!this.listenerCount('error')) this.on('error', this.onerror); // http server 的回调函数 const handleRequest = (req, res) => { // 将 request response 对象封装为 context 对象,然后开始对 request 的处理过程,这个放到第二节再讲 const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); // 返回对 request 的处理结果 }; return handleRequest; }

compose

这是个很重要的方法,其返回的 fn, 将会在请求到达的时候实际负责 context 在 middleware 中的传递。

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 */ // 这个函数签名就是 koa middleware 常见的函数签名 // 它作为 this.handleRequest 的参数,this.handleRequest 会调用它 // 注意! 下面的代码及注释请在阅读 request handler 的过程阅读 return function (context, next) { // last called middleware # // 指示 context 在 middleware 链上的位置 // context 刚来的时候没有进入链,所以 index === -1 let index = -1 // 从第 1 个 middleware 开始 context 之旅,index === 0 return dispatch(0) // 这个 dispatch 串接 context 在 middleware 中的流动 function dispatch(i) { // 如果 i 到了起点之前,说明 next 被用了太多次 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 压栈阶段,记录自己所在的位置 let fn = middleware[i] if (i === middleware.length) fn = next // 如果走到了 middleware 的最后一站,那么就用传入的 next 当作 next if (!fn) return Promise.resolve() // 如果都没有 middleware, 直接返回,然后层层 resolve 返回 try { // 进入 middleware 函数的执行过程,middleware 中访问的 next 被定义在这里, // 而当这个 middleware 调用 next 的时候,就等于调用 dispatch, 同时进入 middleware 的下一层 // 如果当前 middleware 是最后一个,上面的 if (i === middleware.length) fn = next 逻辑就会被激活,顶层调用 next // 可以看到我们 await 的东西就是一个 resolved 的 Promise! // 根据 async 函数的定义,默认返回的就是一个 resolved 的 Promise<undefined> return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) } catch (err) { // 如果抛出了异常,就会被捕获,层层 reject 回来 return Promise.reject(err) } } } }

request handle 过程

还是用上面的例子来讲解 request handle 过程。

当有 http 请求过来的时候,如下的方法最先被调用:

const handleRequest = (req, res) => { const ctx = this.createContext(req, res) // 创建 context return this.handleRequest(ctx, fn) // 过程处理 === context 在 middleware 中的传递 }

创建 context

createContext(req, res) { // 创建三个对象,将它们的 prototype 分别指向 this.context, this.request, this.repsonse, 实际上这三个对象 hasOwnProperties 为空 // 然后就是各种引用,比较简单 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; }

处理过程

对 request 的实际处理过程。

handleRequest(ctx, fnMiddleware) { // 这里的 fnMiddleware 即是 compose() 返回的 fn 的函数,可以看到并没有给第二参数传递值,所以在那里 next === undefined, 直接 resolve const res = ctx.res; res.statusCode = 404; // 准备两个 Promise 的回调函数 const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); // 开始 middleware 的传递过程 // 如果执行过程成功,并没有从 Promise 里拿任何的参数,是利用闭包访问的 ctx 来生成响应的 // 但执行失败则要从 Promise 链条里拿到错误信息 return fnMiddleware(ctx).then(handleResponse).catch(onerror); }

context 在 middleware 中间的传递

fnMiddleware 被调用的时候,即这个函数被调用:

function (context, next) { // last called middleware # // 指示 context 在 middleware 链上的位置 // context 刚来的时候没有进入链,所以 index === -1 let index = -1 // 从第 1 个 middleware 开始 context 之旅,index === 0 return dispatch(0) // 这个 dispatch 串接 context 在 middleware 中的流动 // 注意! 下面的代码及注释在阅读 request handler 的过程阅读 function dispatch (i) { // 如果 i 到了起点之前,说明 next 被用了太多次 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 压栈阶段,记录自己所在的位置 let fn = middleware[i] if (i === middleware.length) fn = next // 如果走到了 middleware 的最后一站,那么就用传入的 next 当作 next if (!fn) return Promise.resolve() // 如果都没有 middleware, 直接返回,然后层层 resolve 返回 try { // 进入 middleware 函数的执行过程,middleware 中访问的 next 被定义在这里, // 而当这个 middleware 调用 next 的时候,就等于调用 dispatch, 同时进入 middleware 的下一层 // 如果当前 middleware 是最后一个,上面的 if (i === middleware.length) fn = next 逻辑就会被激活,顶层调用 next // 可以看到我们 await 的东西就是一个 resolved 的 Promise! // 根据 async 函数的定义,默认返回的就是一个 resolved 的 Promise<undefined> return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { // 如果抛出了异常,就会被捕获,层层 reject 回来 return Promise.reject(err) } } }

可以看到这个函数是递归的:

用我们的例子:

  1. 执行 dispatch(0), 我们注册的第一个异步函数被当成 fn, 然后 fn(context, dispatch.bind(null, 1)) 调用了这第一个异步函数
  2. 第一个异步函数执行 next(), 实际上执行了 dispatch(1), 然后调用了第二个异步函数。
  3. 同理,调用了第三个异步函数,middleware 到这里已经全部执行过了
  4. 第三个函数执行的时候没用再调用 next(), 所以异步函数返回了状态为 resolved 的 Promise<undefined>
  5. return Promise.resolve() 把异步函数返回的 Promise 接着 resolved 下去
  6. 直到 dispatch(0) 中的 resolved 的 Promise 被 return 出去
  7. handleRequest 进入 fnMiddleware(ctx).then(handleResponse), 执行 handleResponse
例外情形
如果最后一个中间件也调用了 next

此时 fn === undefined, 并且 next === undefined, 所以就会直接返回已 resolved 的 Promise, 开始回溯。

如果有一个中间件调用了两次 next

我们已经知道每次调用 next 实际是调用了一次 dispatch(i), 如果我们调用了同一个 next 两次,那么第二次调用的时候,i === index 的条件就会成立。我们说过 index 是指示 context 在 middleware 中的位置的。

创建响应

function respond(ctx) { // allow bypassing koa // 允许 bypass 直通 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 的各种可能情况,然后调用 http 模块 res 的方法返回响应。

CC BY-NC 4.0 2025 © Evan HuRSS