zoukankan      html  css  js  c++  java
  • 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
    
  • 相关阅读:
    ACM ICPC 2008–2009 NEERC MSC A, B, C, G, L
    POJ 1088 滑雪 DP
    UVA 11584 最短回文串划分 DP
    POJ 2531 Network Saboteur DFS+剪枝
    UVa 10739 String to Palindrome 字符串dp
    UVa 11151 Longest Palindrome 字符串dp
    UVa 10154 Weights and Measures dp 降维
    UVa 10271 Chopsticks dp
    UVa 10617 Again Palindrome 字符串dp
    UVa 10651 Pebble Solitaire 状态压缩 dp
  • 原文地址:https://www.cnblogs.com/guolao/p/12290246.html
Copyright © 2011-2022 走看看