zoukankan      html  css  js  c++  java
  • koa2 compose理解及模拟实现

    介绍

    Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

    学习koa-compose之前,先看一下这两张图

    基本使用

    const Koa = require('../../lib/application');
    
    // const Koa = require('koa');
    const app = new Koa();
    
    // x-response-time
    
    app.use(async (ctx, next) => {
      const start = Date.now();
      await next();
      const ms = Date.now() - start;
      ctx.set('X-Response-Time', `${ms}ms`);
    });
    
    // logger
    
    app.use(async (ctx, next) => {
      const start = Date.now();
      await next();
      const ms = Date.now() - start;
      console.log(`${ctx.method} ${ctx.url} - ${ms}`);
    });
    
    // response
    
    app.use(async ctx => {
      ctx.body = 'Hello World';
    });
    
    app.listen(3000);
    

    • 创建一个跟踪响应时间的日期
    • 等待下一个中间件的控制
    • 创建另一个日期跟踪持续时间
    • 等待下一个中间件的控制
    • 将响应主体设置为“Hello World”
    • 计算持续时间
    • 输出日志行
    • 计算响应时间
    • 设置 X-Response-Time 头字段
    • 交给 Koa 处理响应

    看完这个gif图,也可以思考下如何实现的。根据表现,可以猜测是next是一个函数,而且返回的可能是一个promise,被await调用。

    阅读koa-compose源码

    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!');
      }
    
      /**
       * @param {Object} context
       * @return {Promise}
       * @api public
       */
    
      return function(context, next) {
        // last called middleware #
        let index = -1;
        // 取出第一个中间件函数执行
        return dispatch(0);
    
        // 递归函数
        function dispatch(i) {
          if (i <= index) return Promise.reject(new Error('next() called multiple times'));
          index = i;
          let fn = middleware[i];
    
          // next的值为undefined,当没有中间件的时候直接结束
          // 其实这里可以去掉next参数,直接在下面fn = void 0,和之前的代码效果一样
          // if (i === middleware.length) fn = void 0;
          if (i === middleware.length) fn = next;
    
          if (!fn) return Promise.resolve();
    
          try {
            // fn就是中间件函数, dipatch(i)调用的就是第i个中间件函数
            // eg :                app.use((ctx,next) => { next()})
    
            // 第 1 次 reduce 的返回值,下一次将作为 a
            // arg => fn1(() => fn2(arg));
    
            // 第 2 次 reduce 的返回值,下一次将作为 a
            //         arg => (arg => fn1(() => fn2(arg)))(() => fn3(arg));
            // 等价于...
            //         arg => fn1(() => fn2(() => fn3(arg)));
    
            // 执行最后返回的函数连接中间件,返回值等价于...
            // fn1(() => fn2(() => fn3(() => {})));
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err);
          }
        }
      };
    }
    

    上面的代码等价于

    // 这样就可能更好理解了。
    // simpleKoaCompose
    const [fn1, fn2, fn3] = this.middleware;
    const fnMiddleware = function(context){
        return Promise.resolve(
          fn1(context, function next(){
            return Promise.resolve(
              fn2(context, function next(){
                  return Promise.resolve(
                      fn3(context, function next(){
                        return Promise.resolve();
                      })
                  )
              })
            )
        })
      );
    };
    
    
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
    

    也就是说koa-compose返回的是一个PromisePromise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。

    第一个next函数里也是返回的是一个PromisePromise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。

    第二个next函数里也是返回的是一个PromisePromise中取出第三个函数(app.use添加的中间件),传入context和第三个next函数来执行。

    第三个...

    以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。
    这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。

    模拟实现

    同步实现

    文件app.js

    // 模拟 Koa 创建的实例
    class app {
        constructor(){
            this.middlewares = []
        }
    
        use(fn){
            this.middlewares.push(fn)
        }
    
        compose() {
            // 递归函数
            let self = this;
            function dispatch(index) {
                 // 如果所有中间件都执行完跳出
                 if (index === self.middlewares.length) return;
        
                 // 取出第 index 个中间件并执行
                 const midFn = self.middlewares[index];
                 return midFn(() => dispatch(index + 1));
             }
    
             取出第一个中间件函数执行
             dispatch(0);
        }
    };
    
    module.exports = new app();
    

    上面是同步的实现,通过递归函数 dispatch 的执行取出了数组中的第一个中间件函数并执行,在执行时传入了一个函数,并递归执行了 dispatch,传入的参数 +1,这样就执行了下一个中间件函数,依次类推,直到所有中间件都执行完毕,不满足中间件执行条件时,会跳出,这样就按照上面案例中 1 3 5 6 4 2 的情况执行,测试例子如下(同步上、异步下)。

    文件sync-test.js

    const app = require("./app");
    
    app.use(next => {
        console.log(1);
        next();
        console.log(2);
    });
    
    app.use(next => {
        console.log(3);
        next();
        console.log(4);
    });
    
    app.use(next => {
        console.log(5);
        next();
        console.log(6);
    });
    
    app.compose();
    // 1
    // 3
    // 5
    // 6
    // 4
    // 2
    

    文件async-test.js

    const app = require("./app");
    
    // 异步函数
    function fn() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve();
                console.log("hello");
            }, 3000);
        });
    }
    
    app.use(async next => {
        console.log(1);
        await next();
        console.log(2);
    });
    
    app.use(async next => {
        console.log(3);
        await fn(); // 调用异步函数
        await next();
        console.log(4);
    });
    
    app.use(async next => {
        console.log(5);
        await next();
        console.log(6);
    });
    
    app.compose();
    // 1
    // 3
    // hello
    // 5
    // 6
    // 4
    // 2
    

    我们发现如果案例中按照 Koa 的推荐写法,即使用 async 函数,都会通过,但是在给 use 传参时可能会传入普通函数或 async 函数,我们要将所有中间件的返回值都包装成 Promise 来兼容两种情况,其实在 Koacompose 最后返回的也是 Promise,是为了后续的逻辑的编写,但是现在并不支持,下面来解决这两个问题。

    注意:后面 compose 的其他实现方式中,都是使用 sync-test.js 和 async-test.js 验证,所以后面就不再重复了。

    升级为异步,其实就是koa-compose的实现(简化版)
    compose() {
            // 递归函数
            let self = this;
            function dispatch(index) {
                // 异步实现
                // 如果所有中间件都执行完跳出,并返回一个 Promise
                if (index === self.middlewares.length) return Promise.resolve();
    
                // 取出第 index 个中间件并执行
                const route = self.middlewares[index];
    
                // 执行后返回成功态的 Promise
                return Promise.resolve(route(() => dispatch(index + 1)));
            }
    
            // 取出第一个中间件函数执行
            dispatch(0);
    }
    

    我们知道 async 函数中 await 后面执行的异步代码要实现等待,待异步执行后继续向下执行,需要等待 Promise,所以我们将每一个中间件函数在调用时最后都返回了一个成功态的 Promise,使用 async-test.js进行测试,发现结果为 1 3 hello(3s后) 5 6 4 2

    reduceRight实现(Redux旧版使用逆序归并)
    • 同步实现
    compose () {
        return self.middlewares.reduceRight((a, b) => () => b(a), () => {})();
    };
    

    上面的代码看起来不太好理解,我们不妨根据案例把这段代码拆解开,假设 middlewares 中存储的三个中间件函数分别为 fn1fn2fn3
    由于使用的是 reduceRight 方法,所以是逆序归并,第一次 a 代表初始值(空函数),b代表fn3,而执行 fn3 返回了一个函数,这个函数再作为下一次归并的 a,而 fn2作为b`,依次类推,过程如下:

    // 第 1 次 reduceRight 的返回值,下一次将作为 a
    () => fn3(() => {});
    
    // 第 2 次 reduceRight 的返回值,下一次将作为 a
    () => fn2(() => fn3(() => {}));
    
    // 第 3 次 reduceRight 的返回值,下一次将作为 a
    () => fn1(() => fn2(() => fn3(() => {})));
    

    由上面的拆解过程可以看出,如果我们调用了这个函数会先执行 fn1,如果调用 next 则会执行 fn2,如果同样调用 next 则会执行 fn3fn3 已经是最后一个中间件函数了,再次调 next 会执行我们最初传入的空函数,这也是为什么要将 reduceRight初始值设置成一个空函数,就是防止最后一个中间件调用 next 而报错。经过测试上面的代码不会出现顺序错乱的情况,但是在 compose 执行后,我们希望进行一些后续的操作,所以希望返回的是 Promise,而我们又希望传入给 use 的中间件函数既可以是普通函数,又可以是 async 函数,这就要我们的 compose 完全支持异步

    • 异步实现
    compose() {
        // reduceRight, 逆序归并
        return Promise.resolve(
            self.middlewares.reduceRight(
                (a, b) => () => Promise.resolve(b(a)),
                () => Promise.resolve()
            )()
        )
    }
    

    参考同步的分析过程,由于最后一个中间件执行后执行的空函数内一定没有任何逻辑,但为遇到异步代码可以继续执行(比如执行 next 后又调用了 then),都处理成了 Promise,保证了 reduceRight 每一次归并的时候返回的函数内都返回了一个 Promise,这样就完全兼容了 async普通函数,当所有中间件执行完毕,也返回了一个 Promise,这样 compose 就可以调用 then 方法执行后续逻辑。

    reduce(Redux新版使用正序归并)
    • 同步实现
    compose () {
        return self.middlewares.reduce((a, b) => arg => a(() => b(arg)))(() => {});
    };
    

    Redux 新版本中将 compose 的逻辑做了些改动,将原本的 reduceRight 换成 reduce,也就是说将逆序归并改为了正序,我们不一定和 Redux 源码完全相同,
    是根据相同的思路来实现串行中间件的需求。个人觉得改成正序归并后更难理解,所以还是将上面代码结合案例进行拆分,中间件依然是 fn1fn2fn3,由于reduce并没有传入初始值,所以此时 afn1,b 为 fn2

    // 第 1 次 reduce 的返回值,下一次将作为 a
    arg => fn1(() => fn2(arg));
    
    // 第 2 次 reduce 的返回值,下一次将作为 a
    arg => (arg => fn1(() => fn2(arg)))(() => fn3(arg));
    
    // 等价于...
    arg => fn1(() => fn2(() => fn3(arg)));
    
    // 执行最后返回的函数连接中间件,返回值等价于...
    fn1(() => fn2(() => fn3(() => {})));
    

    所以在调用 reduce 最后返回的函数时,传入了一个空函数作为参数,其实这个参数最后传递给了 fn3,也就是第三个中间件,这样保证了在最后一个中间件调用 next 时不会报错。

    • 异步实现
    compose() {
          // reduce版本
          return Promise.resolve(
              self.middlewares.reduce((a, b) => arg =>
                  Promise.resolve(a(() => b(arg)))
              )(() => Promise.resolve())
          );
    }
    
    使用async函数实现(仅记录)
    compose() {
         return (async function () {
        // 定义默认的 next,最后一个中间件内执行的 next
        let next = async () => Promise.resolve();
    
        // middleware 为每一个中间件函数,oldNext 为每个中间件函数中的 next
        // 函数返回一个 async 作为新的 next,async 执行返回 Promise,解决异步问题
        function createNext(middleware, oldNext) {
            return async () => {
                await middleware(oldNext);
            }
        }
    
        // 反向遍历中间件数组,先把 next 传给最后一个中间件函数
        // 将新的中间件函数存入 next 变量
        // 调用下一个中间件函数,将新生成的 next 传入
        for (let i = self.middlewares.length - 1; i >= 0; i--) {
            next = createNext(self.middlewares[i], next);
        }
    
        await next();
    })();
    }
    

    参考文章

    KOA2 compose 串联中间件实现(洋葱模型)
    学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

  • 相关阅读:
    IIS配置
    sql表值函数中实现类似split功能
    毕至居预约系统设计图第十二组
    毕至居预约系统研发需求分析——第十二组
    MASA Blazor多页签组件
    关于Linux Oracle下的表空间文件删除不干净的处理方法
    命令行下对apk签名
    android自定义绘制TableLayout,类似.net中DataGrid控件
    使用Arcgis tools fix后Android不能生成Apk包异常解决方法
    SyncML一种平台无关的信息同步标准协议
  • 原文地址:https://www.cnblogs.com/qiqiloved/p/13501199.html
Copyright © 2011-2022 走看看