逐行分析Koa源码

Koa 简介

官网介绍:Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

其中有几个关键字:更小、更富有表现力、更健壮、错误处理、中间件、快速。

用过 Express,再来用 Koa,肯定会有以上几点感受。

优秀的作品,总是忍不住想通过源码看看它是怎么实现的。

从 require('koa') 开始看 Koa 源码

const Koa = require('koa');

在 Node.js 里导入是通过 require 函数调用进行的。 Node.js 会根据 require 的是相对路径还是非相对路径做出不同的行为。

require('koa') 是非相对路径,Node.js 会在一个特殊的文件夹 node_modules 里查找你的模块。node_modules 可能与当前文件在同一级目录下,或者在上层目录里。 Node.js 会向上级目录遍历,查找每个 node_modules 直到它找到要加载的模块。写 require 的时候,vscode 会有提示。

在项目的 node_modules 目录下找到 koa 目录,首先看 package.json,找到 main 字段:

{
  "main": "lib/application.js"
}

main 字段对应的是 Koa 的入口文件。lib 目录下只有4个文件,这也是 Koa 的所有源码。相比 Express 『更小』。

找到入口文件就好办了。

new Koa() 发生了什么

实例化一个 Koa 对象来使用 Koa 对外提供的各种 API。

const app = new Koa();

在 application.js 中找到 Application 类,看其 constructor 构造函数,了解其初始化过程。

// ... 省略一些 require
const Emitter = require('events');

// Application 继承了 events,也就有了事件发布订阅的功能
// Koa 错误处理功能就是以此为基础
module.exports = class Application extends Emitter {
  constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;

    // 很重要:初始化 middleware 中间件数组
    this.middleware = [];

    // 很重要:初始化 context、request、response 三个属性,它们与 middleware 共同组成了 Koa 最核心的部分
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);

    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
}

Koa 实例化完成之后,我们会使用以下方法正式创建一个应用,让它具有处理请求和响应的能力。

app.use(async function (ctx, next) {
  // ...
  await next();
  // ...
});

// ... 或许还需要使用很多的 use 方法

app.listen(3000, '127.0.0.1', error => {
  console.log('app started at port 3000...');
});

use 方法:

module.exports = class Application extends Emitter {
  use(fn) {
    // 规定 use 方法的参数必须是一个 function
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');

    // Koa1.x 是用 generator 函数来操作异步流程的,Koa2在这里做了兼容,并将在 V3 版本中彻底弃用。
    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');
      // 将 generator 形式的函数转换成 async 形式。
      // https://www.npmjs.com/package/koa-convert
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    // 将回调函数添加到中间件队列中
    this.middleware.push(fn);
    return this;
  }
}

listen 方法:

module.exports = class Application extends Emitter {
  listen(...args) {
    debug('listen');
    // 下面两段代码很熟悉了,使用 Node.js 创建一个 HTTP 服务
    // this.callback 方法就是每次接收到 HTTP 请求之后的具体操作
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

callback 方法:

module.exports = class Application extends Emitter {
  callback() {
    // 使用 compose 方法预处理 middleware:https://www.npmjs.com/package/koa-compose
    // 首先判断 compose 方法的入参是不是数组,如果不是,则 throw new TypeError('Middleware stack must be an array!')
    // 接着使用 for of 循环,判断数组的每一项是不是 function,如果不是,则 throw new TypeError('Middleware must be composed of functions!')
    // 最后返回一个 function,这个 function 是 Koa 中间件执行流程的核心,后面会记录。
    const fn = compose(this.middleware);

    // 因为 Application 类继承了 events,所以也有 listenerCount 方法
    // 用来判断开发者是否有监听 error,如果没有的话,Koa 会内置一个,并执行自定义的 onerror 方法
    // 当错误发生时,console.log 一些信息
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    // http.createServer 的入参是一个方法,有两个入参,即 req、res
    const handleRequest = (req, res) => {
      // 使用 createContext 将 req 和 res 包装成一个 ctx 上下文对象
      const ctx = this.createContext(req, res);
      // 正式处理接收到的 HTTP 请求
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
}

先来看下 createContext 做了哪些事情?

module.exports = class Application extends Emitter {
  createContext(req, res) {
    // 代码很清晰,就是在 context 对象上挂载了一些属性,然后返回
    // context 的初始内容,可参考 lib/context.js 文件
    // 做一个 demo,收到请求后,把 context 打印出来,结合代码看,就都清楚了
    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;
  }
}

再来看 handleRequest 方法:

module.exports = class Application extends Emitter {
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    // onFinish 方法是通过引入 on-finished 包得到的
    // 主要作用是:当 HTTP 请求 closes、finishes 或 errors 时执行回调
    // https://www.npmjs.com/package/on-finished
    // 执行的 onerror 方法,可参考 context.js 文件中的 onerror 方法,主要通过 emit 触发 error 事件,最后 res.end(msg) 返回错误信息
    onFinished(res, onerror);
    // fnMiddleware 就是上面通过 compose 组合之后的一系列 middleware,下面重点叙述
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}

compose 方法:

// https://github.com/koajs/compose/blob/master/index.js
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!')
  }

  // compose 最后返回的方法,即 handleRequest 中执行的 fnMiddleware 方法。
  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
      // 每个中间件是一个 async 函数,被上一个中间件的 next 方法调用(首个除外)
      if (!fn) return Promise.resolve()
      try {
        // 执行 next 方法,起始就是再次执行 dispatch,只是传入的 index + 1,表示执行下一个中间件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Koa 使用 compose 组合之后的一系列中间件来处理 HTTP 的 request 和 response,而 compose 的实现原理很像是一个『洋葱模型』:


koa.png

具体过程是:

  • 一个请求到一旦到后端,就开始接触洋葱的最外层。

  • 遇到一个 next(),就进入下一层。不过值得提醒的是,异步函数的 next() 与同步函数的 next(),不是在同一个空间的,我们可以假想一个“异步空间栈”,后入先出。

  • 什么时候到洋葱中心?就是遇到的第一个没有next的中间件,或者遇到一个中间件报错,就会把这个中间件当成中心,因为遇到错误了,不会再继续往里面走。这个时候,就开始向洋葱的外层开始走了。如果第一个中间件就没有 next,直接返回的。那么就不存在洋葱模型。

  • 一层一层外面走的时候,就先走位所有的同步中间件,再依次走“异步空间栈”的中间件。

有没有一种『递归』的感觉。

之前模拟实现了上述 compose 方法,可参考:https://github.com/zymfe/test-code/blob/master/test93.js

整体流程就是这样,Koa 的核心就是提供了一套简单的中间件(一些自定义方法)使用方法,可以拦截、处理 HTTP 请求和响应,方便我们处理业务。

request.js 和 response.js 没有重点说,它们是在 createContext 方法被添加了一些新的属性,原有属性和方法可以参考对应文件中的代码,就是一些设置、读取请求头、请求体的方法和属性等。

你可能感兴趣的:(逐行分析Koa源码)