我们本章来探讨一下 Koa.js
当中的中间件,在使用 Koa.js
的过程当中,经常会接触到中间件这个概念,之前在学习 Redux
的过程当中,里面也有一个中间件(middleware
)的概念,所以就打算抽点时间,整理一下中间件的相关概念,关于 Redux
当中的 middleware
会另起篇幅来进行介绍
什么是中间件
中间件是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的
上面是 维基百科 当中对于中间件的解释,看上去可能比较抽象,其实简单的总结来说,所谓中间件,就是处在服务业务与用户应用中间的软件(架构),主要用来将具体业务和底层逻辑解耦的组件
在深入展开之前,我们先来看看中间件的洋葱模型
中间件的洋葱模型
关于洋葱模型,也不说复杂了,直接通过几个例子来了解一下到底什么是中间件的洋葱模型,先以 express
为例
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
| var express = require('express') var app = express()
app.use(function middleware1(req, res, next) { console.log('A middleware1 开始') next() console.log('B middleware1 结束') })
app.use(function middleware2(req, res, next) { console.log('C middleware2 开始') next() console.log('D middleware2 结束') })
app.use(function middleware3(req, res, next) { console.log('E middleware3 开始') next() console.log('F middleware3 结束') })
app.get('/', function handler(req, res) { res.send('ok') console.log('======= G =======') })
if (module.parent) { module.exports = app } else { app.listen(8080) }
A middleware1 开始 C middleware2 开始 E middleware3 开始 ======= G ======= F middleware3 结束 D middleware2 结束 B middleware1 结束
|
运行结果的示意图如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| -------------------------------------- | middleware1 | | ---------------------------- | | | middleware2 | | | | ------------------- | | | | | middleware3 | | | | | | | | | next next next ——————————— | | | 请求 ——————————————————> | handler | — 收尾工作-> | 响应 <————————————————— | G | | | | | A | C | E ——————————— F | D | B | | | | | | | | | ------------------- | | | ---------------------------- | --------------------------------------
顺序 A -> C -> E -> G -> F -> D -> B \---------------/ \----------/ ↓ ↓ 请求响应完毕 收尾工作
|
在 Redux
的里面也有一个中间件(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 36 37 38 39 40 41 42 43 44 45 46 47
| function middleware1(store) { return function (next) { return function (action) { console.log('A middleware1 开始') next(action) console.log('B middleware1 结束') }; }; }
function middleware2(store) { return function (next) { return function (action) { console.log('C middleware2 开始') next(action) console.log('D middleware2 结束') }; }; }
function middleware3(store) { return function (next) { return function (action) { console.log('E middleware3 开始') next(action) console.log('F middleware3 结束') } } }
function reducer(state, action) { if (action.type === 'MIDDLEWARE_TEST') { console.log('======= G =======') } return {} }
var store = Redux.createStore( reducer, Redux.applyMiddleware( middleware1, middleware2, middleware3 ) )
store.dispatch({ type: 'MIDDLEWARE_TEST' })
|
运行结果的示意图如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| -------------------------------------- | middleware1 | | ---------------------------- | | | middleware2 | | | | ------------------- | | | | | middleware3 | | | | | | | | | next next next ——————————— | | | dispatch —————————————> | reducer | — 收尾工作->| nextState <————————————— | G | | | | | A | C | E ——————————— F | D | B | | | | | | | | | ------------------- | | | ---------------------------- | --------------------------------------
顺序 A -> C -> E -> G -> F -> D -> B \---------------/ \----------/ ↓ ↓ 更新 state 完毕 收尾工作
|
当然,在 Koa.js
当中也是一样的
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
| const Koa = require('koa') let app = new Koa()
const middleware1 = async (ctx, next) => { console.log('A middleware1 开始') await next() console.log('B middleware1 结束') }
const middleware2 = async (ctx, next) => { console.log('C middleware2 开始') await next() console.log('D middleware2 结束') }
const middleware3 = async (ctx, next) => { console.log('E middleware3 开始') await next() console.log('F middleware3 结束') }
app.use(middleware1) app.use(middleware2) app.use(middleware3)
app.use(async(ctx, next) => { ctx.body = 'hello world' console.log('======= G =======') })
app.listen(3000)
|
可以发现,结果是一致的,至于为什么会出现以上的结果,这也就是本篇文章需要说到的地方,在 Koa.js
当中,是通过一个中间件引擎 koa-compose
模块来实现的,也就是 Koa.js
实现洋葱模型的核心引擎
Koa.js 的切面
Koa.js
的切面是由中间件机制实现的,一个中间件一般有两个切面,遵循先进后出的切面执行顺序,类似入栈出栈的顺序,可以参考下图
中间件原理
通过之前的洋葱模型可以看出,中间件的在 await next()
前后的操作,很像数据结构的一种场景,就是先进后出的『栈』,同时又有统一上下文管理操作数据,在 Koa.js
当中最为人所知的便是基于『洋葱模型』的 HTTP
中间件处理流程
Koa.js
当中的洋葱模式可以拆解成以下几个元素
所以综上所述,我们也可以总结出一下中间件的相关特性
- 有统一的
Context
- 操作先进后出(栈数据结构)
- 有控制先进后出的机制的
next()
方法
- 有提前结束机制
这样子我们可以单纯用 Promise
做个简单的实现如下
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 46 47 48 49 50
| let context = { data: [] }
async function middleware1(ctx, next) { console.log('A middleware1 开始') ctx.data.push(1) await next() console.log('B middleware1 结束') ctx.data.push(6) }
async function middleware2(ctx, next) { console.log('C middleware2 开始') ctx.data.push(2) await next() console.log('D middleware2 结束') ctx.data.push(5) }
async function middleware3(ctx, next) { console.log('E middleware3 开始') ctx.data.push(3) await next() console.log('F middleware3 结束') ctx.data.push(4) }
Promise.resolve(middleware1(context, async () => { return Promise.resolve(middleware2(context, async () => { return Promise.resolve(middleware3(context, async () => { console.log('======= G =======') return Promise.resolve() })); })); })) .then(() => { console.log('end') console.log('context = ', context) })
|
我们可以简单的梳理一下上述示例的执行流程
- 生命周期就是
Promise.resolve
的嵌套
- 中间件就是
middleware1
、middleware2
和 middleware3
- 中间件在生命周期中,就是
Promise.resolve(middleware)
嵌套中执行中间件
middleware1
前置操作是 A
- 等待嵌套的
middleware2
middleware2
前置操作是 C
- 等待嵌套的
middleware3
middleware3
前置操作是 E
middleware3
前置操作是 F
middleware2
后置操作是 D
middleware1
后置操作是 B
实现
通过上一节中的中间件原理,我们可以看出,单纯的使用 Promise
嵌套是可以直接实现中间件流程的,虽然可以实现,但是 Promise
嵌套会产生代码的可读性和可维护性的问题,也带来了中间件扩展问题,所以需要把 Promise
嵌套实现的中间件方式进行高度抽象,达到可以自定义中间件的层数,在这种情况下,我们就可以采用 async/await
来进行实现,我们先来理清实现过程当中需要的步骤
- 中间件队列
- 处理中间件队列,并将上下文
Context
传进去
- 中间件的流程控制器
next()
- 异常处理
根据上一节的中间的原理,我们可以抽象出
- 每一个中间件需要封装一个
Promise
- 利用洋葱模型的先进后出操作,对应
promise.resolve()
的前后操作
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
| function compose(middleware) {
if (!Array.isArray(middleware)) { throw new TypeError(`Middleware stack must be an array!`) }
return function (ctx, 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 (i === middleware.length) { fn = next }
if (!fn) { return Promise.resolve() }
try { return Promise.resolve(fn(ctx, _ => { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } }
|
下面就让我们来简单是试用一下
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
| let middleware = [] let context = { data: [] }
middleware.push(async (ctx, next) => { console.log('A middleware1 开始') ctx.data.push(1) await next() console.log('B middleware1 结束') ctx.data.push(6) })
middleware.push(async (ctx, next) => { console.log('C middleware2 开始') ctx.data.push(2) await next() console.log('D middleware2 结束') ctx.data.push(5) })
middleware.push(async (ctx, next) => { console.log('E middleware3 开始') ctx.data.push(3) await next() console.log('F middleware3 结束') ctx.data.push(4) })
const fn = compose(middleware)
fn(context) .then(_ => { console.log('end') console.log('context = ', context) })
|