前言

在使用Koa.js过程中,会发现中间件的使用都是这样子的,如以下代码所示。

  1. const Koa = require('koa');
  2. let app = new Koa();
  3. const middleware1 = async (ctx, next) => {
  4. console.log(1);
  5. await next();
  6. console.log(6);
  7. }
  8. const middleware2 = async (ctx, next) => {
  9. console.log(2);
  10. await next();
  11. console.log(5);
  12. }
  13. const middleware3 = async (ctx, next) => {
  14. console.log(3);
  15. await next();
  16. console.log(4);
  17. }
  18. app.use(middleware1);
  19. app.use(middleware2);
  20. app.use(middleware3);
  21. app.use(async(ctx, next) => {
  22. ctx.body = 'hello world'
  23. })
  24. app.listen(3001)
  25. // 启动访问浏览器
  26. // 控制台会出现以下结果
  27. // 1
  28. // 2
  29. // 3
  30. // 4
  31. // 5
  32. // 6

为什么会出现以上的结果, 这个主要是Koa.js的一个中间件引擎 koa-compose模块来实现的,也就是Koa.js实现洋葱模型的核心引擎。

中间件原理

洋葱模型可以看出,中间件的在 await next() 前后的操作,很像数据结构的一种场景——“栈”,先进后出。同时,又有统一上下文管理操作数据。综上所述,可以总结出一下特性。

  • 有统一 context
  • 操作先进后出
  • 有控制先进后出的机制 next
  • 有提前结束机制

这样子我们可以单纯用 Promise 做个简单的实现如下

  1. let context = {
  2. data: []
  3. };
  4. async function middleware1(ctx, next) {
  5. console.log('action 001');
  6. ctx.data.push(1);
  7. await next();
  8. console.log('action 006');
  9. ctx.data.push(6);
  10. }
  11. async function middleware2(ctx, next) {
  12. console.log('action 002');
  13. ctx.data.push(2);
  14. await next();
  15. console.log('action 005');
  16. ctx.data.push(5);
  17. }
  18. async function middleware3(ctx, next) {
  19. console.log('action 003');
  20. ctx.data.push(3);
  21. await next();
  22. console.log('action 004');
  23. ctx.data.push(4);
  24. }
  25. Promise.resolve(middleware1(context, async() => {
  26. return Promise.resolve(middleware2(context, async() => {
  27. return Promise.resolve(middleware3(context, async() => {
  28. return Promise.resolve();
  29. }));
  30. }));
  31. }))
  32. .then(() => {
  33. console.log('end');
  34. console.log('context = ', context);
  35. });
  36. // 结果显示
  37. // "action 001"
  38. // "action 002"
  39. // "action 003"
  40. // "action 004"
  41. // "action 005"
  42. // "action 006"
  43. // "end"
  44. // "context = { data: [1, 2, 3, 4, 5, 6]}"

引擎实现

通过上一节中的中间件原理,可以看出,单纯用Promise 嵌套可以直接实现中间件流程。虽然可以实现,但是Promise嵌套会产生代码的可读性和可维护性的问题,也带来了中间件扩展问题。
所以需要把Promise 嵌套实现的中间件方式进行高度抽象,达到可以自定义中间件的层数。这时候需要借助前面几章提到的处理 Promise嵌套的神器async/await
我们先理清楚需要的步骤

  • 中间件队列
  • 处理中间件队列,并将上下文context传进去
  • 中间件的流程控制器next
  • 异常处理

根据上一节分析中间的原理,我们可以抽象出

  1. function compose(middleware) {
  2. if (!Array.isArray(middleware)) {
  3. throw new TypeError('Middleware stack must be an array!');
  4. }
  5. return function(ctx, next) {
  6. let index = -1;
  7. return dispatch(0);
  8. function dispatch(i) {
  9. if (i < index) {
  10. return Promise.reject(new Error('next() called multiple times'));
  11. }
  12. index = i;
  13. let fn = middleware[i];
  14. if (i === middleware.length) {
  15. fn = next;
  16. }
  17. if (!fn) {
  18. return Promise.resolve();
  19. }
  20. try {
  21. return Promise.resolve(fn(ctx, () => {
  22. return dispatch(i + 1);
  23. }));
  24. } catch (err) {
  25. return Promise.reject(err);
  26. }
  27. }
  28. };
  29. }

试用中间件引擎

  1. let middleware = [];
  2. let context = {
  3. data: []
  4. };
  5. middleware.push(async(ctx, next) => {
  6. console.log('action 001');
  7. ctx.data.push(2);
  8. await next();
  9. console.log('action 006');
  10. ctx.data.push(5);
  11. });
  12. middleware.push(async(ctx, next) => {
  13. console.log('action 002');
  14. ctx.data.push(2);
  15. await next();
  16. console.log('action 005');
  17. ctx.data.push(5);
  18. });
  19. middleware.push(async(ctx, next) => {
  20. console.log('action 003');
  21. ctx.data.push(2);
  22. await next();
  23. console.log('action 004');
  24. ctx.data.push(5);
  25. });
  26. const fn = compose(middleware);
  27. fn(context)
  28. .then(() => {
  29. console.log('end');
  30. console.log('context = ', context);
  31. });
  32. // 结果显示
  33. // "action 001"
  34. // "action 002"
  35. // "action 003"
  36. // "action 004"
  37. // "action 005"
  38. // "action 006"
  39. // "end"
  40. // "context = { data: [1, 2, 3, 4, 5, 6]}"