koa源码

源码

目录结构

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)

总结

参考文章

你可能感兴趣的:(koa源码分析)