• Koa 中间件的执行


    Node.js 中请求的处理

    讨论 Koa 中间件前,先看原生 Node.js 中是如何创建 server 和处理请求的。

    node_server.js

    const http = require("http");
    const PORT = 3000;
    

    const server = http.createServer((req, res) => {
    res.end("hello world!");
    });

    server.listen(PORT);
    console.log(</span>server started at http://localhost:<span class="pl-s1"><span class="pl-pse">${</span><span class="pl-c1">PORT</span><span class="pl-pse">}</span></span><span class="pl-pds">);

    Koa 中请求的处理

    Koa 也是通过上面的 http.createServer 创建服务器处理请求的返回 res。 但在 Koa 的封装体系下,其提供了十分好用的中间件系统,可对请求 req 及返回 res 进行便捷地处理。

    koa/lib/application.js#L64

      listen(...args) {
        debug('listen');
    +    const server = http.createServer(this.callback());
        return server.listen(...args);
      }

    Koa 中的 hello world:

    server.js

    const Koa = require("koa");
    const app = new Koa();
    

    app.use(async ctx => {
    ctx.body = "Hello World";
    });

    app.listen(3000);

    Koa 中,涉及到对请求返回处理都是通过中间件完成的,像上面为样,返回页面一个 Hello World 文本,也是调用 app.useApplication 对象注册了个中间件来完成。

    Koa 中间件编写及使用

    Koa 中中间件即一个处理请求的方法,通过调用 app.use(fn) 后,中间件 fn 被保存到了内部一个中间件数组中。

    koa/lib/application.js#L105

    use(fn) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
        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;
      }

    通过上面的代码可看到,注册的中间件被压入 Application 对象的 this.middleware 数组。这里有对传入的方法进行判断,区分是否为生成器([generator])方法,因为较早版本的 Koa 其中间件是通过生成器来实现的,后面有 async/await 语法后转向了后者,所以更推荐使用后者,因此这里有废弃生成器方式的提示。

    因为中间件中需要进行的操作是不可控的,完全有可能涉及异步操作,比如从远端获取数据或从数据库查询数据后返回到 ctx.body,所以理论上中间件必需是异步函数。

    比如实现计算一个请求耗时的中间件,以下分别是通过普通函数配合 Promise 以及使用 async/await 方式实现的版本:

    来自官方 README 中使用 Promise 实现中间件的示例代码

    // Middleware normally takes two parameters (ctx, next), ctx is the context for one request,
    // next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion.
    

    app.use((ctx, next) => {
    const start = Date.now();
    return next().then(() => {
    const ms = Date.now() - start;
    console.log(</span><span class="pl-s1"><span class="pl-pse">${</span><span class="pl-smi">ctx</span>.<span class="pl-c1">method</span><span class="pl-pse">}</span></span> <span class="pl-s1"><span class="pl-pse">${</span><span class="pl-smi">ctx</span>.<span class="pl-smi">url</span><span class="pl-pse">}</span></span> - <span class="pl-s1"><span class="pl-pse">${</span>ms<span class="pl-pse">}</span></span>ms<span class="pl-pds">);
    });
    });

    来自官方 README 中使用 async/await 实现中间件的示例代码

    app.use(async (ctx, next) => {
      const start = Date.now();
      await next();
      const ms = Date.now() - start;
      console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
    });

    可以看到,一个中间件其签名是 (ctx,next)=>Promise,其中 ctx 为请求上下文对象,而 next 是这样一个函数,调用后将执行流程转入下一个中间件,如果当前中间件中没有调用 next,整个中间件的执行流程则会在这里终止,后续中间件不会得到执行。以下是一个测试。

    server.js

    app.use(async (ctx, next) => {
      console.log(1);
      next();
    });
    app.use(async (ctx, next) => {
      console.log(2);
    });
    app.use(async (ctx, next) => {
      console.log(3);
      ctx.body = "Hello, world!";
    });

    执行后控制台输出:

    $ node server.js
    1
    2

    访问页面也不会看到 Hello, world! 因为设置响应的代码 ctx.body = "Hello, world!"; 所在的中间件没有被执行。

    compose

    下面来看当多次调用 app.use 注册中间件后,这些中间件是如何被顺次执行的。

    中间件的执行是跟随一次请求的。当一个请求来到后台,中间件被顺次执行,在各中间件中对请求 requestresposne 进行各种处理。

    所以从 Koa 中处理请求的地方出发,找到中间件执行的源头。

    通过查看 lib/application.js 中相关代码:

    lib/application.js#L127

      callback() {
    +    const fn = compose(this.middleware);
    
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    
    const handleRequest = (req, res) =&gt; {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    
    return handleRequest;
    

    }

    可定位到存储在 this.middleware 中的中间件数组会传递给 compose 方法来处理,处理后得到一个函数 fn,即这个 compose 方法处理后,将一组中间件函数处理成了一个函数,最终在 handleRequest 处被调用,开启了中间件的执行流程。

    lib/application.js#L151

      handleRequest(ctx, fnMiddleware) {
        const res = ctx.res;
        res.statusCode = 404;
        const onerror = err => ctx.onerror(err);
        const handleResponse = () => respond(ctx);
        onFinished(res, onerror);
    +    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
      }

    compose 的签名长这样:compose([a, b, c, ...]),它来自另一个单独的仓库 koajs/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!");
      }
    

    /**
    * @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];
    if (i === middleware.length) fn = next;
    if (!fn) return Promise.resolve();
    try {
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
    } catch (err) {
    return Promise.reject(err);
    }
    }
    };
    }

    这个方法只做了两件事,

    • 定义了一个 dispatch 方法,
    • 然后调用它 dispatch(0)

    这里中间件从数组中取出并顺次执行的逻辑便在 dispatch 函数中。

    整体方法体中维护了一个索引 index 其初始值为 -1,后面每调用一次 dispatch 会加 1。当执行 dispatch(0) 时,从中间件数组 middleware 中取出第 0 个中间件并执行,同时将 dispatch(i+1) 作为 next 传递到下一次执行。

    let fn = middleware[i];
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

    所以这里就能理解,为什么中间件中必需调用 next,否则后续中间件不会执行。

    这样一直进行下去直到所有中间件执行完毕,此时 i === middleware.length,最后一个中间件已经执行完毕,next 是没有值的,所以直接 resolve 掉结束中间件执行流程。

    if (i === middleware.length) fn = next;
    if (!fn) return Promise.resolve();

    回到中间件被唤起的地方:

    lib/application.js

    fnMiddleware(ctx)
      .then(handleResponse)
      .catch(onerror);

    中间件完成后,流程到了 handleResponse

    总结

    从中间件执行流程可知道:

    • 中间件之间存在顺序的问题,先注册的先执行。
    • 中间件中需要调用 next 以保证后续中间件的执行。当然,如果你的中间件会根据一些情况阻止掉后续中间件的执行,那可以不调用 next,比如一个对请求进行权限校验的中间件可以这么写:
    app.use(async (ctx, next) => {
      // 获取权限数据相关的操作...
      if (valid) {
        await next();
      } else {
        ctx.throw(403, "没有权限!");
      }
    });

    相关资源

  • 相关阅读:
    人事面试13
    人事面试测试篇1
    人事面试16
    人事面试15
    人事面试测试篇3
    人事面试测试篇2
    人事面试14
    Oracle Compile 编译 无效对象
    Oracle 移动数据文件的操作方法
    Oracle 9i 从9.2.0.1升级到 9.2.0.6 步骤
  • 原文地址:https://www.cnblogs.com/Wayou/p/koa_middleware.html
Copyright © 2020-2023  润新知