const Koa = require('koa'); const app = new Koa(); // response app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(3000);
application.js
: koa
的导出文件夹,也就是定义 koa 的地方context.js
: 上下文,也就是常见的 ctx
。每个 app
都会有一份 context
,经过继承这个 context.js
中定义的 context
。同时每个请求中,都会有一份单独的 ctx
, 经过继承 app
中定义的 context。说白了就是复用。request.js
: 同前面的 context.js
说明。response.js
: 同前面的 context.js
说明。说明:在 koa
中 nodejs
原生对应的是 req
和 res
node
包的功能能够参考截图中给出的注释json
这个就是 koa
的定义的类api
New 会执行构造函数。
因此在实例化的时候,能够传入这些 options
。数组
在这里会检查 middleware
的类型,若是是老的 middleware
会转换一下,最后直接放到 middleware
这个数组中。数组中的中间件,会在每个请求中去挨个执行一遍。cookie
listen
的时候,才会去建立 server
。
对于每个请求,都会走到 callback
中去,因此 callback
是用于处理实际请求的。通常不要去重写这个 callback
。app
接下来去看看 callback
作了什么:
这里涉及到几个大的点:dom
createContext
都干了什么Compose
是如何实现洋葱模型的。this.handleRequest(ctx, fn)
干了什么这几个点分红两个大块来说,二、3 两点放到一块儿讲。koa
这里作了三件重要的事情socket
app
都有其对应的 context、request、response
实例,每个请求,都会基于这些实例去建立本身的实例。在这里就是建立了 context、request、response
node
原生的 res、req
以及 this
挂载到 context、request、response
上面。还有一些其余为了方便访问作得一些挂载,不过前面三个的挂载是必须的。context
返回,传给全部中间件的第一个 ctx
参数,做为这个请求的上下文下面着重解释一下第二点中,为何要把这些属性挂载上去。由于全部的访问都是代理,最终都是访问的 req、res
上面的东西,context
访问的是 request、response
上面的东西,可是他们上面的东西又是访问的是 req、res
上面的。
例如访问 ctx.method
,context
会去 request
上面早,而后 request
会返回 req.method
。后面分析其余文件时会讲到这种代理结构。ide
在第三步中最后讲到的 callback
中,middleware
所有经过 koa-compose
这个包包装,返回了一个可执行的方法,在请求阶段会去执行这个方法,从而执行每个中间件。先本身来手撸一个 compose
的🌰
function compose(middleware) { return function (ctx, next) { function dispatch(i) { if (i >= middleware.length) { return Promise.resolve() } let curMiddleware = middleware[i] return curMiddleware(ctx, dispatch.bind(null, i + 1)) } return dispatch(0) } } function mid1(ctx, next) { console.log('mid1 before') next() console.log('mid1 after') } function mid2(ctx, next) { console.log('mid2 before') next() console.log('mid2 after') } function mid3(ctx, next) { console.log('mid3 before') next() console.log('mid3 after') } const fn = compose([mid1, mid2, mid3]) fn({}) --------------------------------------------------------------------打印结果 mid1 before mid2 before mid3 before mid3 after mid2 after mid1 after
在 compose
中会根据 i
去挨个执行中间件,而且有一个回溯的过程。官方代码以下。
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!') } 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) } } } }
因此总结一下,洋葱模型本质是经过递归去实现的。
讲了 compose
的原理以后,回到第三步中最后的 this.handleRequest(ctx, fn);
fn
就是compose
返回的包装过 middleware
的函数。下面进入 handleRequest
能够看到当一个请求来到的时候,最后会去执行包装过的中间件函数,也就是这里的最后一行,并在中间件执行完毕以后,到 handleResponse
中去处理响应。在 handleResponse
中最终执行的是 respond
function respond(ctx) { // allow bypassing koa if (false === ctx.respond) return; if (!ctx.writable) return; const res = ctx.res; 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 && !ctx.response.has('Content-Length')) { const { length } = ctx.response; if (Number.isInteger(length)) ctx.length = length; } return res.end(); } // status body if (null == body) { if (ctx.req.httpVersionMajor >= 2) { body = String(code); } else { 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); }
主要是将 ctx
上挂载的 body
经过 res.end
返回响应。
module.exports = { /** * Return request header. * * @return {Object} * @api public */ get header() { return this.req.headers; }, /** * Set request header. * * @api public */ set header(val) { this.req.headers = val; }, .............. }
可见前面在 createContext
的时候在 request
上面去挂载 req、res
的缘由就在这里。
module.exports = { /** * Return the request socket. * * @return {Connection} * @api public */ get socket() { return this.res.socket; }, /** * Get response status code. * * @return {Number} * @api public */ get status() { return this.res.statusCode; }, ....................... }
const proto = module.exports = { ............. get cookies() { if (!this[COOKIES]) { this[COOKIES] = new Cookies(this.req, this.res, { keys: this.app.keys, secure: this.request.secure }); } return this[COOKIES]; }, set cookies(_cookies) { this[COOKIES] = _cookies; } ............ } /** * Response delegation. */ delegate(proto, 'response') .method('attachment') .method('redirect') .method('remove') .method('vary') .method('has') .method('set') .method('append') .method('flushHeaders') .access('status') .access('message') .access('body') .access('length') .access('type') .access('lastModified') .access('etag') .getter('headerSent') .getter('writable'); /** * Request delegation. */ delegate(proto, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') .method('accepts') .method('get') .method('is') .access('querystring') .access('idempotent') .access('socket') .access('search') .access('method') .access('query') .access('path') .access('url') .access('accept') .getter('origin') .getter('href') .getter('subdomains') .getter('protocol') .getter('host') .getter('hostname') .getter('URL') .getter('header') .getter('headers') .getter('secure') .getter('stale') .getter('fresh') .getter('ips') .getter('ip');
这里的 proto
就是 context
,在自身定义了一个经常使用的方法,可经过 ctx.method
去访问,还有后面使用 delegate
,这个函数会把自 context
上面的 request、response
上面的一些属性定义到 proto
也就是 context
上面去,可是当使用 ctx.xxx
去访问的时候,实际上是访问 request、response
上面的属性,这也是为何须要将 request、response
挂载到 context
上面去。