中间件

1. 前言

这节我们一起来学习 Koa 的中间件使用方式,实现原理,中间件使用是 Koa 在工作中最常使用的场景,能把我们很多重复的工作抽象出来,减少工作量,同时让整体代码健壮性大大提高,在工作中我们经常会遇到对所有的请求 Request,Response 进行拦截处理,比如统计接口耗时,每个接口调用次数,Resonse Gzip 压缩等,这类全局的拦截处理,都能很好的用中间件解决,本章需要掌握的重点有:

  • 理解洋葱模型
  • 掌握中间件的基本使用
  • 了解中间件实现原理

平时使用中间件最容易出错的地方是异步中间件使用是 next 方法 忘记 await 了,这个点需要注意。
本章中难点是中间件的原理理解,理解起来有困难的话,大家可以本地跑下代码,Debug 下代码运行逻辑,如果刚开始学习。 Koa,也可以不用深究这块知识,先在脑子有个印象,日后再学习。

2. 洋葱模型

讲中间件前必须得讲「洋葱模型」,Koa 的中间件的设计是基于「洋葱模型」。
2.中间件 - 图1

2.中间件 - 图2

他和 Express 和其他常用的框架相比,最大的区别是一个中间件可以执行两次,不再是常规的自上而下去执行,我们看段Demo。

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. const PORT = 3000;
  4. app.use(async (ctx, next)=>{
  5. console.log(1)
  6. await next();
  7. console.log(1)
  8. });
  9. app.use(async (ctx, next) => {
  10. console.log(2)
  11. await next();
  12. console.log(2)
  13. })
  14. app.use(async (ctx, next) => {
  15. console.log(3)
  16. })
  17. app.listen(PORT);
  18. console.log(`http://localhost:3000`);

打印结果如下:

  1. 1
  2. 2
  3. 3
  4. 2
  5. 1

从打印就过来看,每个函数先执行 next 前面的代码,再执行其他函数代码,其他执行完成后,再回来执行 next 后面的代码,这就是我们讲的洋葱模型。

3. 中间件概念

理解了中间件的概念后,我们来理解下中间件,中间件这个词,听起来很高大上,是一个通用的概念,并不是 Koa 独有,网上经常听见 中间件团队,感性的理解就是链接上游下游,经常我们会把很多通用的逻辑用中间件来封装,的举几个例子,大家来理解下(其实大家用 Redux这类库也是有中间件的概念,思路是一样的)。

比如我们有很多接口用用户权限校验,写些伪代码如下:

  1. // 用户权限校验,假设返回 false 表示没有权限,true 为有权限,都是些伪代码
  2. function auth() {
  3. }
  4. app.get("/api/getOrderList",(request,res) => {
  5. const result = auth()
  6. if(!result){
  7. throw new Error("用户没有权限")
  8. }
  9. })
  10. app.get("/api/orderDetail/:id",(request,res) => {
  11. const result = auth()
  12. if(!result){
  13. throw new Error("用户没有权限")
  14. }
  15. })

大家可以看到,这样写起来,每个 api 对应的接口都要来写一遍,会疯掉,而且代码也不好看,那我们希望,进入接口前,先去执行认证这个模块。

2.中间件 - 图3

这样我们在 auth 这个方法里把认证相关逻辑处理下,是不是整个代码健壮行就提升不少,耦合性也大大降低,以后要改认证的逻辑就只需要在 auth 方法里处理就好。

  1. const auth = (ctx, next) => {
  2. const user = ctx.getCurrentUser() // 假设有这么个方法
  3. if(!user.login){
  4. throw new Error(" need login")
  5. }
  6. next();
  7. }

4. 中间件使用

4.1 基本语法

先来个最简单的 demo。

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. app.use( (ctx, next) => {
  4. console.log('the first middleware')
  5. next()
  6. });
  7. app.listen(3000);

可以看到中间件创建起来还是非常简洁的,首先 通过 app.use(ctx, next) 方法创建,有 2 个参数,一个是上下文 ctx,一个是 next 函数,next 函数的作用是串联起所有的中间件,初学的时候可以不用深究,知道这样用就行,随着对 Koa 越来越熟悉,可以再翻回来看,那还有异步的中间件,异步中间件声明如下:

  1. app.use( async (ctx, next) => {
  2. console.log('the first middleware')
  3. await next()
  4. });

通过 asyncawait 就组成了异步中间件,这样在中间件里就可以做异步处理了, asyncawait 用起来非常方便,Node.js 也支持的非常好,同步功能也能实现,所以我们工作中更多的使用异步中间件。

异步中间件大家需要注意的是,next 返回的是个 Promsie 对象,所以一定要在 用 await next() 才行,不然 不执行

那我们前面介绍了 Koa 的中间件是基于「洋葱模型」,所以在使用的时候,一般有下面 4 种方式。

4.2 next 前面执行逻辑

比如我们需要个中间件是打印每个请求进来的 Url,Http Method,来分析日志。

  1. const logger = (ctx, next) => {
  2. console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`)
  3. next()
  4. }
  5. app.use(logger)

对于类似这种我们只需要在请求进来的时候做处理,就把逻辑写到 next 的前面就好。

4.3 next 后面执行逻辑

比如我们在开发中经常会遇到一个问题是,对 Http 请求做 Gzip 压缩,这样浏览器请求很更快,我们想象下,这个中间件,只需要在 Response 这个层面做处理就好,代码如下:

  1. const gzip = async (ctx, next) => {
  2. await next() // 在 next 后面执行
  3. // 后续中间件执行完成后将响应体转换成 gzip
  4. let body = ctx.body
  5. if (!body) return
  6. // 支持 options.threshold
  7. if (options.threshold && ctx.length < options.threshold) return
  8. if (isJSON(body)) body = JSON.stringify(body)
  9. // 设置 gzip body,修正响应头
  10. const stream = zlib.createGzip() // zlib 第三方压缩库,这里我就不引用了
  11. stream.end(body)
  12. ctx.body = stream
  13. ctx.set('Content-Encoding', 'gzip')
  14. }
  15. app.use(gzip)

类似上面这样的只需要处理 Response 的中间件,就写在 next 后面。

4.4 next 前后都用执行逻辑

假设我们有这样的需求,需要统计每个请求消耗的时间(对比在页面里,我们看某个方法执行来多长时间),那我们需要在 请求进来打印一个时间,请求出去打印一个时间,2个之差就是我们需要的了,代码如下:

  1. const logger = async (ctx, next) => {
  2. const startTime = Date.now();
  3. console.log(`${startTime} ${ctx.request.method} ${ctx.request.url}`)
  4. await next()
  5. console.log(`use time: ${Date.now() - startTime} `)
  6. }
  7. app.use(logger)

4.5 不执行后面逻辑

大家想一下,我最开始举的权限的例子,假设某个请求进来,在权限这个中间件里判断当前这个用户就是没有权限,那我们应该结束这个请求,直接返回错误信息给用户。

  1. app.use(async (ctx, next) => {
  2. if (await checkUser(ctx)) {
  3. await next();
  4. } else {
  5. ctx.response.body = 'no permission';
  6. ctx.response.status = 403;
  7. }
  8. });

在 else 这个地方,我们没有调用 await next() 这个时候后续的都不会执行,直接返回,这种场景也是挺场景的。

上面我们总结了中间件 4 种常用的方法,基本覆盖了平常使用的场景,大家可以好好看看。

5. 多个中间件(洋葱模型)

很多时候,我们的的项目会有很多个中间件,这里就会出现执行顺序的问题,那中间件的设计原则是,谁在前面先执行谁,举个例子(记得翻看之前的洋葱模型)。

  1. const one = (ctx, next) => {
  2. console.log('>> one')
  3. next()
  4. console.log('<< one')
  5. }
  6. const two = (ctx, next) => {
  7. console.log('>> two')
  8. next()
  9. console.log('<< two')
  10. }
  11. const three = (ctx, next) => {
  12. console.log('>> three')
  13. next();
  14. console.log('<< three')
  15. }
  16. app.use(one)
  17. app.use(two)
  18. app.use(three)
  19. 结果如下
  20. >> one
  21. >> two
  22. >> three
  23. << three
  24. << two
  25. << one

上面的代码的执行步骤是 :

  1. 最外层的中间件首先执行。
  2. 调用next函数,把执行权交给下一个中间件。
  3. 最内层的中间件最后执行。
  4. 执行结束后,把执行权交回上一层的中间件。
  5. 最外层的中间件收回执行权之后,执行 next 函数后面的代码。

注释中的执行过程移出来了,这样更加直观。

6. 实现原理

在讲实现原理之前,我们可以思考下,假设是你,你会怎么去设计一个中间件系统,最简单的就是每个请求过来之前先执行中间件代码,类似下面这样:

  1. function compose(middlewareArray){
  2. return function (context, next){
  3. middlewareArray.map(fn => fn(context))
  4. next()
  5. }
  6. }
  7. const middleware1 = () => {console.log(1)}
  8. const middleware2 = () => {console.log(2)}
  9. compose([middleware1, middleware2],function(context){
  10. console.log('next')
  11. })

这是个最简单的 Request 中间件,还不支持异步,本质就是 callback 回调。
那我们看看Koa是怎么实现的,学习下它的思想。

  1. // middleware 是一个函数的集合,函数也就是我们的各种中间件.
  2. function compose (middleware) {
  3. // 判断中间件数组是否符合要求
  4. if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  5. for (const fn of middleware) {
  6. if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  7. }
  8. /**
  9. * @param {Object} context
  10. * @return {Promise}
  11. * @api public
  12. */
  13. return function (context, next) {
  14. // last called middleware #
  15. let index = -1
  16. return dispatch(0)
  17. function dispatch (i) {
  18. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  19. index = i
  20. let fn = middleware[i] // 查找当前的中间件
  21. // 如果当前中间件是最后一个 那么设定next为当前中间件
  22. if (i === middleware.length) fn = next
  23. if (!fn) return Promise.resolve()
  24. try {
  25. // 执行当前中间件,并且设定next为下一个中间件.
  26. return Promise.resolve(fn(context, function next () {
  27. return dispatch(i + 1)
  28. }))
  29. } catch (err) {
  30. return Promise.reject(err)
  31. }
  32. }
  33. }
  34. }
  35. const one = (ctx, next) => {
  36. console.log('>> one')
  37. next()
  38. console.log('<< one')
  39. }
  40. const two = (ctx, next) => {
  41. console.log('>> two')
  42. next()
  43. console.log('<< two')
  44. }
  45. const three = (ctx, next) => {
  46. console.log('>> three')
  47. next()
  48. console.log('<< three')
  49. }
  50. const middleware = [one, two, three]
  51. compose(middleware)().then()

代码执行结果要不要贴一下,有助于学生更容易理解。

上面这段代码,就是 Koa 中间件的核心逻辑了,第一次进入的时候,执行第一个中间件,然后设定第二个中间件为,传入第一个中间件的 next 函数,然后递归下去,直到最后一个中间件执行结束后,在逐个释放中间件函数,直到最后一个中间件的时候,next 为空,下一轮会直接返回 Promise.resolve,可能初学者第一次看有些懵,再拆分下:

  1. function midware() {
  2. const three = (ctx, next) => {
  3. console.log('>> three')
  4. next()
  5. console.log('<< three')
  6. }
  7. const two = (ctx, three) => {
  8. console.log('>> two')
  9. three()
  10. console.log('<< two')
  11. }
  12. return Promise.resolve((ctx,two ) => {
  13. console.log('>> one')
  14. two()
  15. console.log('<< one')
  16. })
  17. }

这样看起来就好理解了,建议初学者把这 2 段代码 copy 到控制台执行一遍,理解下逻辑,理解这段代码就理解 类似这种中间件的设计技巧,假设你一次看不太明白,不用死磕,不影响你是用 Koa ,先头脑里有个概念,先看其他,后面再看。

再抛个问题出来,上面的举例的中间件都是作用在所有请求上面,那要是我只想在某一个请求上用某个中间件应该怎么处理。

比如有个请求是上传文件的,需要一个中间件处理这个一个请求,应该怎么办,这里卖个关子,大家可以去看 Koa 的路由章节。

7. 小结

这节把 「洋葱模型」,中间件使用,设计做了梳理。