文章

关于中间件

使用 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 进行授权