KoaJs框架--源码解析

6/13/2021 NodeJs

# Koa与Express区别

参考 (opens new window)

两个框架都对http进行了封装。相关的api都差不多

Koa 洋葱模型,中间件执行顺序是 U 形的,在每一次 next() 后,都会进入下一个中间件,等最后一个中间件执行完以后,再从最后一个中间件的 next() 之后的代码往回执行

Express 直线型,按照顺序一个一个 next 下去

express

  • express内置了许多中间件可供使用,而koa没有。
  • express包含路由,视图渲染等特性,而koa只有http模块。
  • express的中间件模型为线型,而koa的中间件模型为U型,也可称为洋葱模型构造中间件。
  • express通过回调实现异步函数,在多个回调、多个中间件中写起来容易逻辑混乱。
// express写法
app.get('/test', function (req, res) {
    fs.readFile('/file1', function (err, data) {
        if (err) { res.status(500).send('read file1 error'); }
        fs.readFile('/file2', function (err, data) {
            if (err) { res.status(500).send('read file2 error'); }
            res.type('text/plain');
            res.send(data);
        });
    });
});
// koa通过generator和async/await使用同步来处理异步,好于callback和promise
app.use(async (ctx, next) => {
    await next();
    var data = await doReadFile();
    ctx.response.type = 'text/plain';
    ctx.response.body = data;
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Koa.js

参考 (opens new window)

  • koa 源码的 lib 目录
lib
  |- application.js   ---  入口文件:Application 类初始化
  |- context.js      ---   上下文:所有的req及res的方法集,控制哪些方法可读或者可写
  |- request.js
  |- response.js
1
2
3
4
5

# application.js

Application: 基本服务器框架 Context: 服务器框架基本数据结构的封装,用以 http 请求解析及响应 *Middleware: 中间件,也是洋葱模型的核心机制

application.js里面封装了很多方法:

  • use 方法: 初始化完 koa 实例后(const app = new Koa()),调用 app.use() 去挂载中间件,判断中间件为 function还是为 generator function 类型,将中间件函数 push 到 middleware数组中,循环调用 middleware 中的方法去执行,

  • listen 方法:在 use 完中间件之后app.listen(3000) ,node 原生启动 http 服务的方法--http.createServer

  • compose 方法:生成洋葱模型,通过 koa-compose 包实现。 fn(context, dispatch.bind(null, i + 1)),中间件的第二个参数 next就是dispatch.bind(null, i + 1) ,执行 next() 实际上是下一个中间件的执行,这样await next() --后面所有中间件串联执行,即下上文中间件部分的执行顺序。

  • createContext 方法const ctx = this.createContext(req, res) 将 req, res 及 this.request, this.response 都挂载到了 context 上,并通过赋值理清了循环引用层级关系。

  • handleRequest 方法this.handleRequest(ctx, fn)拿到 ctx 和 compose 生成的洋葱模型,开始逐一消费中间件。

# 手写koa框架源码

  • req 对象代表了一个HTTP请求,其具有一些属性来保存请求中的一些数据,比如query string,parameters,body,HTTP headers等等;

  • res 对象代表了当一个HTTP请求到来时,Express 程序返回的HTTP响应。res.send([body])发送HTTP响应

const http = require('http')

function compose (middlewares) {
  return ctx => {
    const dispatch = (i) => {
      const middleware = middlewares[i]
      if (i === middlewares.length) {  return }
      // app.use((ctx, next) => {}), 取出当前中间件,并执行
      // 在中间件中调用 next() 时,控制权交给下一个中间件;如果未调用,则接下来的中间件不会执行
      return middleware(ctx, () => dispatch(i+1))
    }
    return dispatch(0)
  }
}

// 使用 Context 对 req/res 进行了封装,并把 req/res 中多个属性代理到 Context 中,方便访问
class Context {
  constructor(req, res){ this.req = req; this.res = res }
}

class Application {
  constructor () { this.middlewares = [] }
  listen (...args) {
    const server = http.createServer(this.callback())  // 在listen 中处理请求并监听端口号
    server.listen(...args)
  }
  // app.callback() 将返回handleRequest 函数,方便测试
  callback () {
    return async (req, res) => {
      const ctx = new Context(req, res)
      // 使用 compose 合成所有中间件,在中间件中会做一些:路由解析、Body解析、 异常处理、统一认证等
      const fn = compose(this.middlewares)
      try { await fn(ctx) } catch (e) {
        ctx.res.statusCode = 500   // 异常处理函数---状态码
        ctx.res.end('Internel Server Error')
      }
      ctx.res.end(ctx.body)
    }
  }

  // 注册中间件,并收集在中间件数组中
  use (middleware) { this.middlewares.push(middleware) }
}

module.exports = Application
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  • app.use的回调函数依然是原生的http.crateServer回调函数,而在koa中回调参数是一个Context对象。

  • 构建 Context 在 koa 中,app.use 的回调参数为一个 ctx 对象,而非原生的 req/res。因此要构建一个 Context 对象,并使用 ctx.body 构建响应:

app.use(ctx => ctx.body = 'hello, world'): 通过在 http.createServer 回调函数中进一步封装 Context 实现 Context(req, res): 以 request/response 数据结构为主体构造 Context 对象