夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node基础

  • 《MySQL》学习笔记

  • Midway

    • Midway基础
    • 洋葱模型
      • 什么是 AOP
      • 洋葱模型
      • Why 使用洋葱模型
      • 深入洋葱模型
        • app.use 方法
        • listen 方法和 callback 方法
        • compose 函数
      • 总结
  • Nest

  • 其他

  • 服务端
  • Midway
夜猫子
2023-03-10
目录

洋葱模型

# 中间件与洋葱模型

洋葱模型在像 Koa, egg, midway等 node 框架中被广泛运用。如下图就是洋葱模型:

洋葱模型

# 什么是 AOP

AOP 为 Aspect Oriented Programming 的缩写,中文意思为:面向切面编程,它是函数式编程 (opens new window)的一种衍生范式。

假如我想把一个苹果(源数据)处理成果盘(最终数据)我该怎么做?

1.苹果(源数据) ----> 2. 洗苹果 ----> 3.切苹果 ---->4.放入盘子 ----> 5.果盘(最终数据)

共有 5 个步骤,如果我想升级一下果盘,打算在切苹果之前先削皮,放入盘子后摆成五角星形状那么我的步骤应该如下:

1.苹果(源数据) ----> 2.洗苹果 ----> 3.削皮 ----> 4.切苹果 ----> 5.放入盘子 ----> 6.摆成五角星形状 ----> 7.果盘(最终数据)

上面每个步骤都可以看成相应的方法,步骤 3 和 6 加入与否都不影响我制作出果盘这个结果,可以看出这样是非常灵活的

其实这就是生活中面向切面编程的例子, 换句话说,就是在现有程序中,加入或减去一些功能不影响原有的代码功能。

什么是

# 洋葱模型

洋葱模型其实就是中间件处理流程,中间件的生命周期大致有:

  • 前期处理
  • 交给并等待其他中间件处理
  • 后期处理

多个中间件处理,就形成了所谓的洋葱模型,是 AOP 面向切面编程的一种应用。

以 Koa.js 为例:

const Koa = require('koa');
const app = new Koa();

// 中间件1
app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(2);
});

// 中间件 2 
app.use(async (ctx, next) => {
    console.log(3);
    await next();
    console.log(4);
});

app.listen(8000, '0.0.0.0', () => {
    console.log(`Server is starting`);
});

// 输出结果为:
// 1
// 3
// 4
// 2
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

在 koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。

在洋葱模型中,每一层相当于一个中间件,用来处理特定的功能,比如错误处理、Session 处理等等。其处理顺序先是 next() 前请求(Request,从外层到内层)然后执行 next() 函数,最后是 next() 后响应(Response,从内层到外层),也就是说每一个中间件都有两次处理时机。

# Why 使用洋葱模型

假如没有洋葱模型,当我们的中间件依赖于其他中间件的逻辑时,该如何处理呢?

比如我们需要知道一个请求或者操作 db 的耗时是多少,而且想要获取其他中间件的信息。在 koa 中,我们可以使用 async await 的方式结合洋葱模型做到。

app.use(async(ctx, next) => {
  const start = new Date();
  await next();
  const delta = new Date() - start;
  console.log (`请求耗时: ${delta} MS`);
  console.log('拿到上一次请求的结果:', ctx.state.baiduHTML);
})

app.use(async(ctx, next) => {
  // 处理 db 或者进行 HTTP 请求
  ctx.state.baiduHTML = await axios.get('http://baidu.com');
})

app.listen(9000, '0.0.0.0', () => {
    console.log(`Server is starting`);
});
// 输出结果为可能为:
// 请求耗时:758 MS
// 拿到上一次请求的结果:{
// 	status:200
//	...
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

如果没有洋葱模型,这就很难实现了。

# 深入洋葱模型

还是以 koa 为例,分析一下内部的实现。

# app.use 方法

use 方法做了一件事,维护得到的 middleware 中间件数组。

  use(fn) {
    // ...
    // 维护中间件数组——middleware
    this.middleware.push(fn);
    return this;
  }
1
2
3
4
5
6

# listen 方法和 callback 方法

执行 app.listen 方法的时候,其实是 Node.js 原生 http 模块 createServer 方法创建了一个服务,其回调为一个 callback 方法。callback 方法中有一个非常重要的函数:compose 函数,它的返回是一个 Promise 函数。

  listen(...args) {
    debug('listen');
    // node http 创建一个服务
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  callback(){
    const fn = compose(this.middleware); // 返回值是一个函数 
    const handleRequest = (req, res) => {
      // 创建 ctx 上下文环境
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn); // 将 ctx 放入 fn 中处理并返回处理结果
    };
    return handleRequest;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

handleRequest 中会执行 compose 函数中返回的 Promise 函数并返回结果。

  handleRequest(ctx, fn) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    // 执行 compose 中返回的函数,将结果返回
    return fn(ctx).then(handleResponse).catch(onerror);
  }
1
2
3
4
5
6
7
8
9

# compose 函数

基本逻辑:

// 函数处理的数据
let context = {}

function middleware_01 (cxt) {
  console.log('1')
  middleware_02(cxt)
  console.log('2')
}

function middleware_02 (cxt) {
  console.log('3')
  console.log('4')
}

// 调用中间件 compose 函数
function compose () {
  // 默认调用第一个中间件
  middleware_01(context)
}

compose()
// 1
// 3
// 4
// 2
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

基本逻辑是实现了,但每次都要指示调用的函数名字,不够灵活。

参考 koa-compose 这个库中的compose 函数引。其实现如下所示:

这里先不考虑一些特殊边界情况,将其简易化,完成基本的功能实现。

function compose (middleware){
    // ...
    return function (context,next){
    	// 一开始的时候传入为 0,后续会递增
        return dispatch(0)
        function dispatch (i){
            // 获取当前中间件
            let fn=middleware[i]
            // 当 fn 为空时表示已经执行到最后一个中间件
            // dispatch 递归回调结束,从当前中间件开始执行 next() 后面部分的代码
           	if (!fn) return Promise.resolve() 
            // 执行中间件,第一个是上下文,第二个是 next 函数。
            // 也就是说中间件执行 next 的时候也就是调用 dispatch 函数的时候
            return Promise.resolve(fn(conntext, function next () { dispatch(i + 1) })
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

具体流程:

第一次执行:fn 为第一个中间件函数。传入当前上下文,以及一个 next函数(执行下一个 dispatch)。

第二次执行:dispatch 传入参数为 2, fn 为空,这时候执行 if (!fn) return Promise.resolve(),然后执行当前中间件 next() 之后的代码,然后依次往上,从而形成了洋葱模型。

实现:

const middleware = []
let mw1 = async function (ctx, next) {
    console.log("next前,第一个中间件")
    await next()
    console.log("next后,第一个中间件")
}
let mw2 = async function (ctx, next) {
    console.log("next前,第二个中间件")
    await next()
    console.log("next后,第二个中间件")
}
let mw3 = async function (ctx, next) {
    console.log("第三个中间件,没有next了")
}

function use(mw,middleware) {
  middleware.push(mw);
}

function compose(middleware) {
  return (ctx, next) => {
    return dispatch(0);
    function dispatch(i) {
      const fn = middleware[i];
      if (!fn) return;
      return fn(ctx, dispatch.bind(null, i+1));
    }
  }
}

use(mw1,middleware); // 压入执行栈
use(mw2,middleware);
use(mw3,middleware);

const fn = compose(middleware)(); // 组合中间件
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

# 总结

Koa 的洋葱模型指的是以 next() 函数为分割点,先由外到内执行 Request 的逻辑,再由内到外执行 Response 的逻辑。通过洋葱模型,将多个中间件之间通信等变得更加可行和简单。其实现的原理并不是很复杂,主要是 compose 方法。

编辑 (opens new window)
上次更新: 2025/3/12 17:54:26
Midway基础
开篇词

← Midway基础 开篇词→

最近更新
01
IoC 解决了什么痛点问题?
03-10
02
如何调试 Nest 项目
03-10
03
Provider注入对象
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式