likes
comments
collection
share

Koa 源码解析

作者站长头像
站长
· 阅读数 13

一个简单的 koa 程序

const Koa = require('koa');

const app = new Koa();

 // response

app.use(ctx => {

  ctx.body = 'Hello Koa';

});



app.listen(3000);

源码解析- koa 源码目录

Koa 源码解析

  • application.js: koa 的导出文件夹,也就是定义 koa 的地方
  • context.js: 上下文,也就是常见的 ctx。每一个 app 都会有一份 context,通过继承这个 context.js 中定义的 context。同时每一个请求中,都会有一份单独的 ctx, 每次通过继承 app 中定义的 context。说白了就是复用。
  • request.js: 同前面的 context.js 说明。
  • response.js: 同前面的 context.js 说明。

说明:在 koa 中 nodejs 原生对应的是 req 和 res

源码解析- application.js

Koa 源码解析 包的功能可以参考截图中给出的注释

Application

这个就是 koa 的定义的类

Koa 源码解析

第一步 - new koa()

New 会执行构造函数。

Koa 源码解析

Koa 源码解析

所以在实例化的时候,可以传入这些 options

第二步 - app.use

所以在实例化的时候,可以传入这些 options

第二步 - app.use

Koa 源码解析 在这里会检查 middleware 的类型,如果是老的 middleware 会转换一下,最后直接放到 middleware 这个数组中。数组中的中间件,会在每一个请求中去挨个执行一遍。

第三步 - app.listen

listen 的时候,才会去创建 server

Koa 源码解析 对于每一个请求,都会走到 callback 中去,所以 callback 是用于处理实际请求的。一般不要去重写这个 callback

接下来去看看 callback 做了什么:

Koa 源码解析

这里涉及到几个大的点:

  1. createContext 都干了什么
  2. Compose 是如何实现洋葱模型的。
  3. this.handleRequest(ctx, fn) 干了什么

这几个点分成两个大块来讲,2、3 两点放到一起讲。

createContext 干了什么

Koa 源码解析 这里做了三件重要的事情

  1. 每一个 app 都有其对应的 context、request、response 实例,每一个请求,都会基于这些实例去创建自己的实例。在这里就是创建了 context、request、response
  2. node 原生的 res、req 以及 this 挂载到 context、request、response 上面。还有一些其他为了方便访问做得一些挂载,不过前面三个的挂载是必须的。
  3. 将创建的 context 返回,传给所有中间件的第一个 ctx 参数,作为这个请求的上下文

下面着重解释一下第二点中,为什么要把这些属性挂载上去。因为所有的访问都是代理,最终都是访问的 req、res 上面的属性,context 访问的是 request、response 上面的属性,但是他们上面的属性又是访问的是 req、res 上面的。

Koa 源码解析

compose 如何实现的洋葱模型

Koa 源码解析 在第三步中最后讲到的 callback 中,middleware 全部通过 koa-compose 这个包包装,返回了一个可执行的方法,在请求阶段会去执行这个方法,从而执行每一个中间件。先自己来手撸一个 compose 的🌰

function compose(middleware) {

    return function (ctx, next) {

        function dispatch(i) {

            if (i >= middleware.length) {

                return Promise.resolve()

            }

            let curMiddleware = middleware[i]

            return curMiddleware(ctx, dispatch.bind(null, i + 1))

        }

        return dispatch(0)

    }

}



function mid1(ctx, next) {

    console.log('mid1 before')

    next()

    console.log('mid1 after')

}

function mid2(ctx, next) {

    console.log('mid2 before')

    next()

    console.log('mid2 after')

}

function mid3(ctx, next) {

    console.log('mid3 before')

    next()

    console.log('mid3 after')

}



const fn = compose([mid1, mid2, mid3])

fn({})

--------------------------------------------------------------------打印结果

mid1 before

mid2 before

mid3 before

mid3 after

mid2 after

mid1 after

compose 中会根据 i 去挨个执行中间件,并且有一个回溯的过程。官方代码如下。

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!')

  }



  return function (context, next) {

    // last called middleware #

    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 的原理之后,回到第三步中最后的 this.handleRequest(ctx, fn); fn 就是compose 返回的包装过 middleware 的函数。下面进入 handleRequest

Koa 源码解析 可以看到当一个请求来到的时候,最后会去执行包装过的中间件函数,也就是这里的最后一行,并在中间件执行完毕之后,到 handleResponse 中去处理响应。在 handleResponse 中最终执行的是 respond

respond

function respond(ctx) {

  // allow bypassing koa

  if (false === ctx.respond) 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 ('HEAD' === ctx.method) {

    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 (null == body) {

    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 ('string' == typeof body) 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);

}

主要是将 ctx 上挂载的 body 通过 res.end 返回响应。

源码解析 - request.js

module.exports = {

   /**

 * Return request header.

 *

 * @return {Object}

 * @api public

 */

  get header() {

    return this.req.headers;

  },



  /**

 * Set request header.

 *

 * @api public

 */



  set header(val) {

    this.req.headers = val;

  },

 ..............

}

可见前面在 createContext 的时候在 request 上面去挂载 req、res 的原因就在这里。

源码解析 - response.js

module.exports = {

    /**

 * Return the request socket.

 *

 * @return {Connection}

 * @api public

 */



  get socket() {

    return this.res.socket;

  },

    /**

 * Get response status code.

 *

 * @return {Number}

 * @api public

 */

  get status() {

    return this.res.statusCode;

  },

.......................

}

源码解析 - context.js

const proto = module.exports = {

.............

  get cookies() {

    if (!this[COOKIES]) {

      this[COOKIES] = new Cookies(this.req, this.res, {

        keys: this.app.keys,

        secure: this.request.secure

      });

    }

    return this[COOKIES];

  },



  set cookies( _cookies) {

    this[COOKIES] = _cookies;

  }

............

}



 /**

 * Response delegation.

 */



delegate(proto, 'response')

  .method('attachment')

  .method('redirect')

  .method('remove')

  .method('vary')

  .method('has')

  .method('set')

  .method('append')

  .method('flushHeaders')

  .access('status')

  .access('message')

  .access('body')

  .access('length')

  .access('type')

  .access('lastModified')

  .access('etag')

  .getter('headerSent')

  .getter('writable');



 /**

 * Request delegation.

 */



delegate(proto, 'request')

  .method('acceptsLanguages')

  .method('acceptsEncodings')

  .method('acceptsCharsets')

  .method('accepts')

  .method('get')

  .method('is')

  .access('querystring')

  .access('idempotent')

  .access('socket')

  .access('search')

  .access('method')

  .access('query')

  .access('path')

  .access('url')

  .access('accept')

  .getter('origin')

  .getter('href')

  .getter('subdomains')

  .getter('protocol')

  .getter('host')

  .getter('hostname')

  .getter('URL')

  .getter('header')

  .getter('headers')

  .getter('secure')

  .getter('stale')

  .getter('fresh')

  .getter('ips')

  .getter('ip');

这里的 proto 就是 context,在自身定义了一些常用的方法,可通过 ctx.method 去访问,还有后面使用 delegate ,这个函数会把自 context 上面的 request、response 上面的一些属性定义到 proto 也就是 context 上面去,但是当使用 ctx.xxx 去访问的时候,其实是访问 request、response 上面的属性,这也是为什么需要将 request、response 挂载到 context 上面去。

转载自:https://juejin.cn/post/7179640478386618405
评论
请登录