源码
目录构造
Application
application.js
次要是对 App 做的一些操作,包含创立服务、在 ctx 对象上挂载 request、response 对象,以及解决异样等操作。接下来将对这些实现进行具体论述。
Koa 创立服务的原理
- Node 原生创立服务
const http = require("http");const server = http.createServer((req, res) => { res.writeHead(200); res.end("hello world");});server.listen(4000, () => { console.log("server start at 4000");});
module.exports = class Application extends Emitter { /** * Shorthand for: * * http.createServer(app.callback()).listen(...) * * @param {Mixed} ... * @return {Server} * @api public */ listen(...args) { debug("listen"); const server = http.createServer(this.callback()); return server.listen(...args); } /** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */ 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 } /** * Handle request in callback. * * @api private */ 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) }};
中间件实现原理
中间件应用例子
const Koa = require("koa")const app = new Koa()app.use(async (ctx, next) => { console.log('---1--->') await next() console.log('===6===>')})app.use(async (ctx, next) => { console.log('---2--->') await next() console.log('===5===>')})app.use(async (ctx, next) => { console.log('---3--->') await next() console.log('===4===>')})app.listen(4000, () => { console.log('server is running, port is 4000')})
注册中间件
Koa注册中间件是用app.use()
办法实现的
module.exports = class Application extends Emitter { constructor (options) { this.middleware = [] } /** * Use the given middleware `fn`. * * Old-style middleware will be converted. * * @param {Function} fn * @return {Application} self * @api public */ use (fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!') debug('use %s', fn._name || fn.name || '-') this.middleware.push(fn) return this }}
Application类的构造函数中申明了一个名为middleware的数组,当执行use()办法时,会始终往middleware中的push()办法传入函数。其实,这就是Koa注册中间件的原理,middleware就是一个队列,注册一个中间件,就进行入队操作。
koa-compose
中间件注册后,当申请进来的时候,开始执行中间件外面的逻辑,因为有next的宰割,一个中间件会分为两局部执行。
midddleware队列是如何执行?
const compose = require('koa-compose')module.exports = class Application extends Emitter { 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 }}
探索下koa-compose
的外围源码实现:
/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */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 this.callback的函数后续肯定会应用这个闭包传入过滤的上下文 return function (context, next) { // last called middleware # // 初始化中间件函数数组执行下标值 let index = -1 // 返回递归执行的Promise.resolve去执行整个中间件数组 // 从第一个开始 return dispatch(0) function dispatch (i) { // 测验上次执行的下标索引不能大于本次执行的下标索引i,如果大于,可能是下个中间件屡次执行导致的 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 以后执行的中间件函数 let fn = middleware[i] // 如果以后执行下标等于中间件数组长度,放回Promise.resolve()即可 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) } } }}
如何封装ctx
module.exports = class Application extends Emitter { // 3个属性,Object.create别离继承 constructor (options) { this.context = Object.create(context) this.request = Object.create(request) this.response = Object.create(response) } callback () { const fn = compose(this.middleware) if (!this.listenerCount('error')) this.on('error', this.onerror) const handleRequest = (req, res) => { // 创立context对象 const ctx = this.createContext(req, res) return this.handleRequest(ctx, fn) } return handleRequest } 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 }}
中间件中的ctx
对象通过createContext()
办法进行了封装,其实ctx
是通过Object.create()
办法继承了this.context
,而this.context
又继承了lib/context.js
中导出的对象。最终将http.IncomingMessage
类和http.ServerResponse
类都挂载到了context.req
和context.res
属性上,这样是为了不便用户从ctx
对象上获取须要的信息。
繁多上下文准则: 是指创立一个context对象并共享给所有的全局中间件应用。也就是说,每个申请中的context对象都是惟一的,并且所有对于申请和响应的信息都放在context对象外面。
function respond (ctx) { // allow bypassing koa if (ctx.respond === false) 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 (ctx.method === 'HEAD') { 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 (body == null) { if (ctx.response._explicitNullBody) { ctx.response.remove('Content-Type') ctx.response.remove('Transfer-Encoding') ctx.length = 0 return res.end() } 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 (typeof body === 'string') 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)}
错误处理
onerror (err) { // When dealing with cross-globals a normal `instanceof` check doesn't work properly. // See https://github.com/koajs/koa/issues/1466 // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549. const isNativeError = Object.prototype.toString.call(err) === '[object Error]' || err instanceof Error if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err)) if (err.status === 404 || err.expose) return if (this.silent) return const msg = err.stack || err.toString() console.error(`\n${msg.replace(/^/gm, ' ')}\n`)}
Context外围实现(TODO)
request具体实现 (TODO)
response具体实现 (TODO)
总结
参考文章
- Koa2第三篇:koa-compose