• koa中间执行机制


    start


    基于 koa 2.11 按以下流程分析:

    const Koa = require('koa');
    const app = new Koa();
    
    const one = (ctx, next) => {
      console.log('1-Start');
      next();
      ctx.body = { text: 'one' };
      console.log('1-End');
    }
    const two = (ctx, next) => {
      console.log('2-Start');
      next();
      ctx.body = { text: 'two' };
      console.log('2-End');
    }
    
    const three = (ctx, next) => {
      console.log('3-Start');
      ctx.body = { text: 'three' };
      next();
      console.log('3-End');
    }
    
    app.use(one);
    app.use(two);
    app.use(three);
    
    app.listen(3000);
    

    app.use()


    use 方法定义在 koa/lib/application.js 中:

    use(fn) {
      // check middleware type, must be a function
      if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
      // 兼容 generator
      if (isGeneratorFunction(fn)) {
        deprecate('Support for generators will be removed in v3. ' +
                  'See the documentation for examples of how to convert old middleware ' +
                  'https://github.com/koajs/koa/blob/master/docs/migration.md');
        fn = convert(fn);
      }
      debug('use %s', fn._name || fn.name || '-');
      
      // 存储中间
      this.middleware.push(fn);
      return this;
    }
    

    this.middleware

    这就是一个数组,用来存放所有中间件,然后按顺序执行。

    this.middleware = [];
    

    app.listen()


    这个方法定义在 koa/lib/application.js 中:

    listen(...args) {
      debug('listen');
      
      // 创建 http 服务并监听
      const server = http.createServer(this.callback());
      return server.listen(...args);
    }
    

    this.callback()

    callback() {
      // 处理中间件
      const fn = compose(this.middleware);
    
      if (!this.listenerCount('error')) this.on('error', this.onerror);
    
      const handleRequest = (req, res) => {
        // 创建 Context
        const ctx = this.createContext(req, res);
        // 执行中间件处理请求和响应
        return this.handleRequest(ctx, fn);
      };
    
      return handleRequest;
    }
    

    this.handleRequest

    handleRequest(ctx, fnMiddleware) {
      const res = ctx.res;
      res.statusCode = 404;
      const onerror = err => ctx.onerror(err);
      // 将响应发出的函数
      const handleResponse = () => respond(ctx);
      onFinished(res, onerror);
      // 这里会将 ctx 传给中间件进行处理,
      // 当中间件流程走完后,
      // 会执行 then 函数将响应发出
      return fnMiddleware(ctx).then(handleResponse).catch(onerror);
    }
    

    respond(ctx)

    function respond(ctx) {
      // 省略其他代码
      // ...
      // 发出响应
      res.end(body);
    }
    
    

    捋一捋流程,由上面的代码可以知道,存放中间的数组是通过 compose 方法进行处理,然后返回一个fnMiddleware函数,接着将 Context 传递给这个函数来进行处理,当fnMiddleware执行完毕后就用respond方法将响应发出。

    compose(this.middleware)


    compose 函数通过koa-compose引入:

    const compose = require('koa-compose');
    

    compose 定义在koajs/compose/index.js

    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) {
        // 这个 index 是标识上一次执行的中间件是第几个
        let index = -1
        
        // 执行第一个中间件
        return dispatch(0)
        function dispatch (i) {
          // 检查中间件是否已经执行过,
          // 举个例子,当执行第一个中间件时 dispatch(0),
          // i = 0, index = -1, 说明没有执行过,
          // 然后 index = i, 而 index 通过闭包保存,
          // 如果执行了多次,就会报错
          if (i <= index) return Promise.reject(new Error('next() called multiple times'))
          index = i
          
          // 通过传入的索引从数组中获取中间件
          let fn = middleware[i]
          
          // 如果当前索引等于中间件数组的长度,
          // 说明已经中间件执行完毕,
          // fn 为 fnMiddleware(ctx) 时没有传入的第二个参数,
          // 即 fn = undefined
          if (i === middleware.length) fn = next
          // fn 为 undefined, 返回一个已经 reolved 的 promise
          if (!fn) return Promise.resolve()
          
          try {
            // 执行中间件函数并将 dispatch 作为 next 函数传入
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err)
          }
        }
      }
    }
    

    结束执行流程

    现在来捋一下 fnMiddleware的执行流程:

    // fnMiddleware 接收两个参数
    function (context, next) {
      // ....
    }
    
    // 将 context 传入,并没有传入 next,
    // 所以第一次执行时是没有传入 next 的
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
    

    next == undefined 时会结束中间件执行,流程如下:

    function dispatch (i) {
      //...
      
      // 通过传入的索引从数组中获取中间件,
      // 但是因为已经执行完了所有中间件,
      // 所以当前 i 已经等于数组长度,
      // 即 fn = undefined
      let fn = middleware[i]
    
      // 如果当前索引等于中间件数组的长度,
      // 说明已经中间件执行完毕,
      // 又因为 fnMiddleware(ctx) 时没有传入的第二个参数 next,
      // 所以 fn = undefined
      if (i === middleware.length) fn = next
      
      // fn 为 undefined, 返回一个已经 reolved 的 promise
      // 中间件执行流程结束
      if (!fn) return Promise.resolve()
      
      // ...
    }
    

    中间件执行流程

    上面先说了结束流程,现在说一下如何顺序执行,形成洋葱模型:

    function dispatch (i) {
      // ...省略其他代码
      
    	try {
        // 分步骤说明
        // 首先通过 bind 将 dispatch 构建为 next 函数
        const next = dispatch.bind(null, i + 1);
        // 将 ctx, next 传入执行当前中间件,
        // 当在中间件中调用 next() 时,
        // 本质上是调用 diapatch(i + 1),
        // 也就是从数组中获取下一个中间件进行执行,
        // 在这时,会中断当前中间件的执行流程转去执行下一个中间件,
        // 只有当下一个中间件执行完毕,才会恢复当前中间件的执行
        const result = fn(context, next);
        // 中间件执行完毕,返回已经 resolve 的 promise,
        // 那么上一个中间件接着执行剩下的流程,
        // 这样就形成了洋葱模型
        return Promise.resolve(result);
      } catch (err) {
        return Promise.reject(err)
      }
    }
    

    开头的例子执行结果如下:

    const one = (ctx, next) => {
      console.log('1-Start');
      next();
      ctx.body = { text: 'one' };
      console.log('1-End');
    }
    const two = (ctx, next) => {
      console.log('2-Start');
      next();
      ctx.body = { text: 'two' };
      console.log('2-End');
    }
    
    const three = (ctx, next) => {
      console.log('3-Start');
      ctx.body = { text: 'three' };
      next();
      console.log('3-End');
    }
    
    // 1-Start
    // 2-Start
    // 3-Start
    // 3-End
    // 2-End
    // 1-End
    // 而 ctx.body 最终为 { text: 'one' }
    

    next()


    没有调用 next()

    // 没有调用 next() 函数
    app.use((ctx, next) => {
      console.log('Start');
      ctx.body = { text: 'test' };
      console.log('End');
    });
    

    因为 next 函数本质上就是通过dispatch(i + 1)来调用下一个中间件,如果没有调用 next 函数,就无法执行下一个中间件,那么就代表当前中间件流程执行结束。

    多次调用 next()

    app.use((ctx, next) => {
      console.log('Start');
      ctx.body = { text: 'test' };
      // 多次调用 next 函数
      next(); // 本质上是 dispatch(i + 1)
      next(); // 本质上是 dispatch(i + 1)
      console.log('End');
    });
    

    这里假设 nextdispatch(3),那么 index 就为 2,第一次执行 next 函数时,会发生如下逻辑:

    // index == 2
    // i == 3
    // 不会报错
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    // 赋值后 index 为 3 了 
    index = i
    

    假设第三个中间件是最后一个中间件,那么执行完第一次 next 函数会立即执行第二个 next 函数,依然执行这个逻辑,但是 index 已经为 3 了,所以会导致报错:

    // index == 3
    // i == 3
    // 报错
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    index = i
    
  • 相关阅读:
    Drupal Coder 模块远程命令执行分析(SA-CONTRIB-2016-039)
    Python 实现 ZoomEye API SDK
    程序员互动联盟第一届编码大赛第二题解题分享
    python中各进制之间的转换
    记一次ctf比赛解密题的解决(可逆加密基本破解之暴力破解)
    使用JsonConfig控制JSON lib序列化
    openMRS项目
    Harmonic Number(调和级数+欧拉常数)
    Pairs Forming LCM(素因子分解)
    Uva 11395 Sigma Function (因子和)
  • 原文地址:https://www.cnblogs.com/guolao/p/12290246.html
Copyright © 2020-2023  润新知