阅读 137

大数据高级开发工程师-开课吧

### download:大数据高级开发工程师

源码学习思绪

Koa的中心文件一共有四个:application.js、context.js、request.js、response.js。一切的代码加起来不到 2000 行,非常笨重,而且大量代码集中在 request.js 和 response.js 关于恳求头和响应头的处置,真正的中心代码只要几百行。


另外,为了更直观地梳理 koa 的运转原理和逻辑,还是经过调试来走一遍流程,本文将分离调试源码停止剖析。

以下面代码调试为例:

const Koa = require('koa')
const app = new Koa()
const convert = require('koa-convert');
// 日志中间件
app.use(async(ctx, next) => {
  console.log('middleware before await');
  const start = new Date()
  await next();
  console.log('middleware after await');
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
app.use(async(ctx, next) => {
  console.log('response');
  ctx.body = "response"
})
app.listen(3000);复制代码

node 的调试方式比拟多,可参考 Node.js 调试大法稍做理解。


一、application

application.js 是 koa 的入口文件,里面导出了 koa 的结构函数,结构函数中包含了 koa 的主要功用完成。

导出一个结构函数 Application,这个结构函数对外提供功用 API 办法,从主要 API 办法动手剖析功用完成。

1. listen

application 结构函数经过 node 中 http 模块,完成了 listen 功用:

/**
   * Shorthand for:
   *
   *  http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
*/
listen (...args) {
  debug('listen')
  const server = http.createServer(this.callback()) // 返回 http.Server 类的新实例,并运用this.callback()回调处置每个单独恳求
  return server.listen(...args) // 启动 HTTP 效劳器监听衔接 完成 KOA 效劳器监听衔接
}复制代码

2. use

use 办法将接纳到的中间件函数,全部添加到了 this.middleware ,以便后面按次第调用各个中间件,假如该办法接纳了非函数类型将会报错 'middleware must be a function!'。

/**
   * 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!');
  // 关于 generator 类型的中间件函数,经过 koa-convert 库将其停止转换,以兼容 koa2 中的koa的递归调用。
  if (isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3. ' +
              'See the documentation for examples of how to convert old middleware ' +
              'https://github.com/koajs/koa/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}复制代码

3. callback

上面 listen 函数在效劳启动时,createServer 函数会返回 callback 函数的执行结果。

在效劳启动时,callback函数执行将会完成中间件的兼并以及监听框架层的错误恳求等功用。

然后返回了 handleRequest 的办法,它接纳 req 和 res 两个参数,每次效劳端收到恳求时,会依据 node http 原生的 req 和 res,创立一个新的 koa 的上下文 ctx。

/**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
*/
callback () {
  const fn = compose(this.middleware) // 经过 compose 兼并中间件,后面分离koa-compose源码剖析
  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
}复制代码

在 application.js 中,经过 compose 将中间件停止了兼并,是 koa 的一个中心完成。

能够看到 koa-compose 的源码,完成十分简单,只要几十行:

/** 
 * @param {Array} middleware 参数为 middleware 中间件函数数组, 数组中是一个个的中间件函数
 * @return {Function}
 * @api public
 */
function compose (middleware) { // compose函数需求传入一个函数数组队列 [fn,fn,fn...]
  // 假如传入的不是数组,则抛出错误
  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
   */
  return function (context, next) {
    // 初始下标为-1
    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 接纳一个中间件函数的数组,返回了一个闭包函数,闭包中维护了一个 index 去记载当前调用的中间件。

里面创立了一个 dispatch 函数,dispatch(i) 会经过 Promise.resolve() 返回 middleware 中的第 i 项函数执行结果,即第 i + 1 个 app.use() 传入的函数。 app.use() 回调的第二个参数是 next,所以当 app.use() 中的代码执行到 next() 时,便会执行 dispatch.bind(null, i + 1)),即执行下一个 app.use() 的回调。

依次类推,便将一个个 app.use() 的回调给串联了起来,直至没有下一个 next,边会按次第返回执行每个 app.use() 的 next() 后面的逻辑。最终经过 Promise.resolve() 返回第一个 app.use() 的执行结果。这里能够分离洋葱模型去了解。

4. createContext

再来看 createContext 函数,一大串的赋值骚操作,我们细细解读一下:

  1. 已知从 context.js、request.js、response.js 引入对象context、request和response,并依据这三个对象经过 Object.create() 生成新的context、request和response对象,避免引入的原始对象被污染;

  2. 经过 context.request = Object.create(this.request) 和 context.response = Object.create(this.response) 将 request 和 response 对象挂载到了 context 对象上。这局部对应了 context.js 中delegate 的拜托局部(有关 delegate 可见后面 koa 中心库局部的解读),能让 ctx 直接经过 ctx.xxx 去访问到 ctx.request.xxx 和 ctx.response.xxx;

  3. 经过一系列的赋值操作,将原始的 http 恳求的 res 和 req,以及 Koa 实例app 等等分别挂载到了 context、request 和 response 对象中,以便于在 context.js、request.js 和response.js 中针对原始的恳求、相应参数等做一些系列的处置访问,便于用户运用。

const response = require('./response')
const context = require('./context')
const request = require('./request')
/**
   * Initialize a new context.
   *
   * @api private
*/
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
}复制代码

5. handleRequest

callback 中执行完 createContext 后,会将创立好的 ctx 以及兼并中间件后生成的次第执行函数传给 handleRequest 并执行该函数。

handleRequest 中会经过 onFinished 这个办法监听 res,当 res 完成、关闭或者出错时,便会执行 onerror 回调。 之后返回中间件执行的结果,当中间件全部执行完之后,执行 respond 停止数据返回操作。

/**
   * 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)
}复制代码

6. toJSON

 /**
   * 返回JSON表示
   * only() - only办法返回对象的白名单属性
   * @return {Object}
   * @api public
 */
  toJSON () {
    return only(this, [
      'subdomainOffset',
      'proxy',
      'env'
    ])
  }
  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
 */
inspect () {
  return this.toJSON()
}复制代码

7. respond

/**
 * Response helper.
 */
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)
}复制代码


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐