源码

目录构造

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.reqcontext.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