关于中间件
使用 node 构建 web 应用时,并不单单响应一个简单的 hello world,在一个实际的业务中,我们也许会做这些:
- 请求方法的判断。
- URL 的路径解析。
- URL 中查询字符串解析。
- Cookie 的解析。
- Basic 认证。
- 表单数据的解析。
- 任意格式文件的上传处理。
这样一个完整的项目中需要处理很多的细节,当然你也可以都写在一起,但这样代码的耦合程度太高了,而且以后维护起来也令人头大。
为此引入中间件(middleware)来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。
理解中间件的最简单的方式是实现一个基础的中间件模式,一个中间件其实就是一个函数。
一个简单的中间件模式需要一个 use 方法来进行中间件的注册,需要一个 run 来执行这些注册的中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const app = {
fns: [],
callback(ctx) {
console.log(ctx)
},
use(fn) {
this.fns.push(fn)
},
run(ctx) {
let index = 0
const next = () => {
index++
}
this.fns.forEach((fn, idx) => {
if (index === idx) fn(ctx, next)
})
index === this.fns.length && this.callback(ctx)
},
}
使用一下:
1
2
3
4
5
6
7
8
9
10
11
12
app.use((ctx, next) => {
ctx.name = "Blum"
next()
})
app.use((ctx, next) => {
ctx.gender = "girl"
next()
})
app.run({})
// 打印:{name:"Blum",gender:"girl"}
关于 run 函数还有更加优雅的写法:
1
2
3
4
5
6
7
8
9
function run(ctx, stack) {
const next = () => {
const middleware = stack.shift()
if (middleware) {
middleware(ctx, next) // 递归调用
}
}
next()
}
再来看看 koa-compose 的中间件:
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
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!")
}
return function (context, next) {
// 采用闭包将索引缓存,来实现调用计数
let index = -1
return dispatch(0)
function dispatch(i) {
// 防止next()方法重复调用
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 {
// 包装next()返回值为Promise对象
return Promise.resolve(fn(context, dispatch.bind(null, 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
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms || 1))
}
const arr = []
const stack = []
// type Middleware<T> = (context: T, next: Koa.Next) => any;
stack.push(async (context, next) => {
arr.push(1)
await wait(1)
await next()
await wait(1)
arr.push(6)
})
stack.push(async (context, next) => {
arr.push(2)
await wait(1)
await next()
await wait(1)
arr.push(5)
})
stack.push(async (context, next) => {
arr.push(3)
await wait(1)
await next()
await wait(1)
arr.push(4)
})
await compose(stack)({})
// arr = [1,2,3,4,5,6]
当 i 为 3 时,
1
2
3
let fn = middleware[i] //fn=undefined
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve() //!fn为true
直接返回 resolve,之后就执行 next()后面的函数
1
2
3
4
5
6
7
stack.push(async (context, next) => {
arr.push(3)
await wait(1)
await next()
await wait(1)
arr.push(4)
})
执行完后返回第二个 next() 后面继续往下执行,知道所有的中间件执行完毕。
这便是众人皆知的“洋葱模型”。你也可以选择只添加前置的处理,就是 await next()前面的操作
,或者后面的处理。
每个中间件足够的小而美,职责单一,同时多个中间件又具备良好的逻辑拓展性和可组合性,并且易于测试。这个设计模式真是太“漂亮”了。
本文由作者按照 CC BY 4.0 进行授权