这次轻松搞懂 Koa2 中间件原理

107 阅读2分钟

async/await

Koa2 中间件使用了 async/await 语法糖,所以搞懂以下这个例子,就能轻松搞懂了。

以这个例子为例,模仿中间件的写法,写一个 test 函数。里面的 fn 是异步函数,里面再次写上 await next1,next1 里再写 next2。

function test() {
  try {
    const fn = async() => {
      console.log('use1');
  
      await next1();
  
      console.log('use1-end');
  
    }
    return Promise.resolve(fn());
  } catch (error) {
    return Promise.reject(error)
  }
}

test()

async function next1() {
  try {
    const fn = async() => {
      console.log('use2');
  
      await next2();
  
      console.log('use2-end');
    }
  
    return Promise.resolve(fn());
  } catch (error) {
    return Promise.reject(error)
  }
}

async function next2() {
  try {
    const fn = async() => {
      console.log('use3');
  
      console.log('use3-end');
    }
  
    return Promise.resolve(fn1());
  } catch (error) {
    return Promise.reject(error)
  }
}

输出:

image.png

Promise 改写 async/await

假如,把 await 换成 Promise 的写法:

function test() {
  const fn = async() => {
    console.log('use1');

    // await next1();
    new Promise((resolve, reject) => {
      const fn = async() => {
        console.log('use2');
    
        // await next2();
        new Promise((resolve) => {
          try {
            const fn = async() => {
              console.log('use3');
              console.log('use3-end');
            }      
            return resolve(Promise.resolve(fn()));
          } catch (error) {
            return reject(Promise.reject(error))
          }

        }).then((res) => {
          console.log('use2-end');
        })
      }
      return resolve(Promise.resolve(fn()));

    }).then(() => {
      console.log('use1-end');
    })

  }
  return Promise.resolve(fn())
}

test()

async function next1() {
  const fn = async() => {
    console.log('use2');

    await next2();

    console.log('use2-end');
  }

  return Promise.resolve(fn());
}

async function next2() {
  const fn = async() => {
    console.log('use3');

    console.log('use3-end');
  }

  return Promise.resolve(fn());
}

输出也是:

image.png

可以发现,async/await 改成 Promise 后,Promise 里面嵌套了 Promise,同时可以看出如果很多中间件的话,书写会复杂化。

简化上面 Promise 写法:

new Promise((resolve, reject) => {
    console.log('use1');
    
    new Promise((resolve) => {
        const fn = () => console.log('use2');
        
        return resolve(Promise.resolve(fn())); // Promise 需等 resolve执行后,才执行 then
    }).then(() => {
        console.log('use2-end');
    })
    
    return resolve(Promise.resolve(fn()));
}).then(() => {
    console.log('use1-end');
})

理解这一点很重要,这是理解 Koa2 中间件原理的关键:Promise 需等 resolve 执行后,才执行 then,也就是说,只要 resolve 了,就可以执行 then。而 resolve 之前,都要等待。

所以就能理解先是里层的 Promise resolve 之后,再到外层的 Promise resolve。

Koa2 原理

理解了上面的那个例子,那么理解 Koa2 原理就简单了。核心是在 compose 函数里,这里通过递增 i 的方式,不断地往下执行 dispatch(i)

const http = require('http')

class Application {
  constructor() {
    this.middlewares = [];
  }
  
  use(fn) {
    this.middlewares.push(fn); // 把中间件添加到栈中
  }

  createContext(req, res) {
    const ctx = {}

    return ctx;
  }

  compose(middlewares) {
    return function(ctx) {
      const dispatch = (i) => {
        try {
          const fn = middlewares[i];
          //  const next = () => dispatch(i + 1);
          return Promise.resolve(fn(ctx, () => dispatch(i + 1))); // 递增 i,这是外部调用的 next 
        } catch (error) {
          return Promise.reject(error);
        }
      }

      dispatch(0);
    }
  }

  handleRequest(req, res) {
    const ctx = this.createContext(req, res); // 处理上下文
    const fn = this.compose(this.middlewares); // 处理中间件

    return fn(ctx); // 开始执行
  }

  listen() {
    const server = http.createServer(this.handleRequest.bind(this));// 防止在 handleRequest 执行时拿到的 this 指向 server
    server.listen(...arguments);
  }
}

module.exports = Application

所谓洋葱圈模型,即:

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

把上面的 aysnc/await 执行先后顺序搞懂了,也就搞懂了 Koa2 洋葱圈模型。

iShot_2024-02-25_17.52.46.png

参考

  1. 【Node】深入浅出 Koa 的洋葱模型