Koa2 核心源码解析

559 阅读4分钟

Koa 是 Express 团队设计的一个新的 web 框架,旨在为 web 应用程序和 api 打造一个更小、更具表现力和更健壮的基础。通过异步函数,Koa 允许您丢弃回调并大大增加错误处理。Koa 没有在其核心中捆绑任何中间件,它提供了一套优雅的方法,使编写服务器变得快速和愉快。 Koa 应用包含一组中间件,这些中间件以类似于堆栈的方式组合和执行的。 尽管提供了大量有用的方法,Koa 仍然保持了较小的内存占用,因为没有捆绑中间件。

关键词:异步函数、中间件。这是 Koa “优雅”“愉快”“轻量”的核心

小广告:长期内推滴滴:why318why@gmail.com

1 基本流程

我们先准备了一个最简单的 koa2 搭建的app,包括基本的:

  • 创建 Koa 实例
  • 监听接口
// app.js
const Koa = require('koa')
const app = new Koa()
const port = 3000
app.listen(port)

1.1 app 实例创建

const app = new Koa()

通过 new Koa,我们先获得了一个 koa 实例 —— app。那么这个 app 是如何拼装的呢?

// application.js
constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    ...
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
}

这里主要把三种东西挂到 app 实例上:

  1. 配置
  2. 中间件池
  3. 伴随请求的上下文、请求对象、响应对象

1.2 app.listen

在获得 app 后,我们可以立即 listen,就启动了个什么也不做的 Koa 了。

app.listen(port, () => console.log('listen...'))
// listen 参数和 node 自带 http.createServer 返回实例的 listen 参数完全一致

现在我们看源码

// application.js
listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
}

参数是透传下去的,只是多了 http.createServer 调 this.callback。

注意这个this.callback 是执行的,也就是只在listen时执行一次,返回的函数才是每次请求都执行的。

this.callback

// application.js
callback() {
    const fn = compose(this.middleware);
    const handleRequest = (req, res) => {
      ...
    };
    return handleRequest;
}

this.callback 只做了两件事情:

  1. 用 compose 合并中间件,返回单一的中间件调用函数 fn,每次调用这个函数,就会把中间件池走一遍。(具体实现后面看)
  2. 返回一个函数 handleRequest,用来处理请求

1.3 请求处理

现在我们拿到了中间件调用函数 fn,并在每次请求调用 handleRequest

// application.js
const handleRequest = (req, res) => {
  const ctx = this.createContext(req, res);
  return this.handleRequest(ctx, fn);
};

handleRequest 做了几件事

  • 调用 this.createContext,把原生 req, res 构造成 koa 的 ctx(请求上下文)
  • 调用 this.handleRequest,把 ctx、fn 提供过去,处理请求并返回。

this.createContext

代码就不贴了,最终构造出一个这样的结构:

node学习.png

this.handleRequest

收到构造好的「上下文」和「中间件调用函数」,这里终于开始干活了。

代码核心在:

return fn(ctx).then(handleResponse).catch(onerror);
  • 先过中间件。给 fn 传入 ctx,通过中间件对 ctx 进行各种加工,返回了一个 Promise
  • 再处理返回。handleResponse 解构 ctx,处理status、body、header 等 HTTP 特性,最终调用原生res.end()返回

2 中间件机制

我们在 listen 前加两个中间件

const app = new Koa()
app.use(async (ctx, next) => {
  console.log('1-pre')
  await next()
  ctx.response.body += ' hello middleware1'
  console.log('1-next')
})
app.use(async (ctx, next) => {
  console.log('2-pre')
  await next()
  ctx.response.body += ' hello middleware2'
  console.log('2-next')
})
app.listen(port)

结果很明显

  • console 打出:1-pre 2-pre 2-next 1-next
  • body 返回:undefined hello middleware2 hello middleware1

前面我们了解到:

  • app 构造了 this.middleware 中间件池,并可以通过 app.use 注册中间件
  • compose 函数把所有中间件组合成一个异步函数,并在请求到来时调用

下面我们分别来看看这两部分做了什么。

2.1 中间件注册 —— app.use

use(fn) {
    ...
    this.middleware.push(fn);
}

很简单的 push

2.2 中间件执行 —— compose

compose 是 koa 中间件管理的精髓,最有特点的是支持异步,进而实现了洋葱圈。

bVLSos.png

compose 返回的函数被这样调用,进去以后再逐个取中间件执行,加工 ctx

fn(ctx)

所谓支持异步,就是:

  • 一个中间件 A 执行的时候,可以调一个异步的 next 函数,next 返回 Promise,并在 then 后继续执行 A 的逻辑
  • next 函数可以嵌套调另外一个中间件 B

这里的灵魂是 next 函数的实现,它作为一个指针递归取中间件,返回一个「取下个中间件并执行」的 Promise。

next 函数就是中间件执行的触发器

尝试简单实现下上面思路:

// compose 返回的函数,传入 ctx 过中间件
function whatComposeReturn (ctx) {
	let index = -1;
	function next() {
		index++;
		const fn = middleware[i];
		if (!fn) return Promise.resolve();
		return Promise.resolve(fn(ctx, next));
	}
	next();
}

但这里还有些边边角角的问题:

  1. 如果一个中间件调用了两次 next 怎么办?——指定index,阻止重复调用
  2. 如果 next 抛错了怎么办?——try catch

所以compose的实现:

function (context, next) {
    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 (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
}

3 流程图

最后,我们用一张图回顾下 Koa 从初始化到响应请求的整体流程

237565AB-35DB-4A6C-8249-65D4844D308E.png

小结

我们了解到:

  • 初始化和app 实例创建过程
  • 请求到来的处理流程:构造ctx —> 过中间件 --> 处理返回
  • 初始化阶段,app.use把中间件函数推入中间件池
  • compose 封装了一个函数,通过递归调用 next 实现中间件的逐个异步调用