Koa 本体项目比较简单,可以看作 node http 模块的封装。
分为 application.js、context.js、request.js、response.js
四个文件。
项目入口是 application.js
application.js
该文件定义和导出了 application 类。
首先我们来看看 listen
方法,该函数可以创建一个 Http 服务。可以理解为 http.createServer
的语法糖。 源码:
listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args);}复制代码
app.callback
是 createServer 的回调函数。
callback() { // 中间件合成一个函数,处理中间件的数据流动 const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { // 创建 ctx 对象 const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest;}复制代码
middleware
是一个中间件函数构成的数组,中间件通过 app.use
被压入这个数组中,
use(fn) { this.middleware.push(fn); return this;}复制代码
handleRequest:
handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; // 默认返回 404 const onerror = err => ctx.onerror(err); // 出错时调用 ctx.onerror 函数 const handleResponse = () => respond(ctx); // 处理完中间件后,调用 respond 函数 onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror);}复制代码
respond:
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(); } // 处理 HEAD 请求 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); } // 处理 body 是 buffer 或者 string 或者 stream 的情况 // 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); // 发送 json // body: json body = JSON.stringify(body); if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body);}复制代码
createContext:
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;}复制代码
compose 是 koa 的核心,在这里 compose 则实现了 koa 的中间件模型。洋葱模型...详情见 google。
function compose (middleware) { 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) } } }}复制代码
通过测试用例来理解这个函数的作用
const ctx = {}const fn0 = async (ctx, next) => { console.log(1); await next(); console.log(6);}const fn1 = async (ctx, next) => { console.log(2); await next(); console.log(5);}const fn2 = async (ctx, next) => { console.log(3); await next(); console.log(4);}const stack = [fn0, fn1, fn2];return compose(stack)(ctx);复制代码
函数运行时将经历的步骤是:
- 首先运行 fn0(ctx, dispatch.bind(null, 1))
- 运行到 fn0 中 await next() 的时候,即等待 dispatch(1) = fn1(ctx, dispatch.bind(null, 2)) 执行完成
- 运行到 fn1 中 await next() 的时候,即等待 dispatch(2) = fn2(ctx, dispatch.bind(null, 3)) 执行完成
- 运行到 fn2 中 await next() 的时候,即等待 dispatch(3) 执行完成。由于不存在 fn3,所以 dispatch(3) = Promise.resolve()。此时因为立即 resolve 了,所以 fn2 会继续执行。
- 当 fn2 执行完成,dispatch(2) 被 resolve 了,所以 fn1 会继续执行至完成,以此类推直到 fn0 执行完成。
如果 fn1 中,有两个 next
const fn1 = async (ctx, next) => { console.log(2); await next(); await next(); console.log(5);}复制代码
执行第二个 next() 的时候,相当于执行 dispatch(2),此时 i = 2,而 index 相当于一个全局的 i 的值,此时等于 3。判断 i < index 抛出错误。
如果传入 next 函数:
compose(stack)(ctx, async (ctx) => console.log(called))复制代码
在第 4 步,执行 dispatch(3) 的时候 if (i === middleware.length) fn = next
由于 i 为 middleware.length, next 不为 undefined。 所以将会返回 next(context, dispatch.bind(null, 4))。如果 next(context, dispatch.bind(null, 4)) 返回一个 promise,则会等待 next 返回结果,fn1 才会继续执行。
context.js
context 有一个 proto 对象,它里面里面有一些辅助的函数,并且 proto 代理了它的 response 和 request 属性中的一些属性和方法,以便快捷访问。在调用 app.createContext
函数的时候,会通过 Object.create(proto) 创建一个实例。然后给这个 request response state 之类的属性。
request.js 和 response.js
request 对象和 response 对象。改善了原生的 req 和 res 对象。 增加了易用性。