乐趣区

关于源码学习:源码学习思绪

源码学习思路

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 服务器监听连接
}
复制代码

  1. 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;
}
复制代码

  1. 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() 的执行后果。这里可能拆散洋葱模型去理解。

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

已知从 context.js、request.js、response.js 引入对象 context、request 和 response,并根据这三个对象通过 Object.create() 生成新的 context、request 和 response 对象,防止引入的原始对象被净化;

通过 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;

通过一系列的赋值操作,将原始的 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
}
复制代码

  1. 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)
}
复制代码

  1. 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()
      }
      复制代码
  2. 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) {
    // 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)
    }
    复制代码
    二、context.js

  3. cookie
    context.js 中通过 get 和 set 方法做了 cookie 的设置和读取操作。

// 获取 cookie
get cookies () {
if (!this[COOKIES]) {

this[COOKIES] = new Cookies(this.req, this.res, {
  keys: this.app.keys,
  secure: this.request.secure
})

}
return this[COOKIES]
},
// 设置 cookie
set cookies (_cookies) {
this[COOKIES] = _cookies
}
复制代码

  1. delegate
    context.js 中有大量的 delegate 操作。

通过 delegate,可能让 ctx 能够间接拜访其下面 response 和 request 中的属性和方法,即可能通过 ctx.xxx 获取到 ctx.request.xxx 或 ctx.response.xxx。

delegate 是通过 delegates 这个库实现的,通过 proto.defineGetter 和 proto.defineSetter 去代理对象上面节点的属性和方法等。(proto.defineGetter 和 proto.defineSetter 现已被 mdn 废除,改用 Object.defineProperty())

const delegate = require('delegates')
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
// ...
delegate(proto, 'request')
.method('acceptsLanguages')
.getter('ip');
// ...

退出移动版