Skip to content

浅析koa2中间件

Posted on:2020年8月10日 at 18:20

koa2 是一个轻量的Node.js http框架。

koa2 采用asyncawait来处理异步,koa2 实例的 use 函数的参数都是中间件。

先来看一个 koa2 的核心小 demo

// 中间件的仓库
const arr = [
  async next => {
    console.log(1);
    await next();
    console.log(2);
  },
  async next => {
    console.log(3);
    await new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(console.log(4));
      }, 1000);
    }); // 异步操作 await 会等待后面的promise resolve 后再向下执行
    await next();
    console.log(5);
  },
  async next => {
    console.log(6);
  },
  async next => {
    // 不会执行 因为上一个函数中没有执行next
    console.log(7);
    await next();
    console.log(8);
  },
  async next => {
    // 不会执行 因为前面的函数中没有执行next
    console.log(9);
  },
];

function fun(arr) {
  function dispose(index) {
    const currentFun = arr[index];
    const next = dispose.bind(null, index + 1);
    return currentFun(next); // 尾递归
  }

  dispose(0);
}

fun(arr); // 先打印 1 3 一秒后打印4 6 5 2

code 开始 (新建一个 ware.js 文件)

const http = require("http");
const urlParser = require("url"); // 解析url字符串和url对象

class Middleware {
  constructor() {
    this.wares = []; // 存储中间件
  }

  use(fun) {
    this.wares.push(fun); // 收集中间件
    return this;
  }

  /* 中间件处理的核心 */
  handleMiddleware(wareList) {
    return ctx => {
      // 中间件调用
      const dispose = index => {
        const currentFun = wareList[index];
        return new Promise((resolve, reject) => {
          try {
            // 使用Promise.resolve 包装 currentFun 防止外部传入的currentFun为一个普通函数
            /* dispose.bind(null, index + 1)就是next 让dispose继续执行下一个中间件
                            如果没有在中间件中调用dispose.bind(null, index + 1) 则不会再去获取下一个中间件
                        */
            return resolve(currentFun(ctx, dispose.bind(null, index + 1)));
          } catch (e) {
            return Promise.reject(e);
          }
        });
      };

      // 立即执行一下仓库的第一个中间件
      dispose(0);
    };
  }

  createContext(req, res) {
    const { method, url } = req;
    const { query } = urlParser.parse(url, true);

    // ... 这里远比这个复杂, 我们只做一个简单的包装
    return {
      method,
      url,
      query,
      res,
    };
  }

  serverHandle() {
    return (req, res) => {
      // 当请求来的时候我们去触发中间件
      const fn = this.handleMiddleware(this.wares);
      // 得到当前请求的上下文对象
      const ctx = this.createContext(req, res);
      fn(ctx);
    };
  }

  listen(...args) {
    const app = http.createServer(this.serverHandle()); // 这里只是为了模拟得到一个http服务
    app.listen(...args); // 直接交给node原生的http模块处理
  }
}

module.exports = Middleware;

测试 (同一目录下新建一个 demo.js 文件)

const Demo = require("./ware");

const app = new Demo();

app.use(async (ctx, next) => {
  await next();
  console.log(`${ctx.method} ${ctx.url}`);
});

app.use(async ctx => {
  ctx.res.end("hello world");
});

app.listen(5000, () => {
  console.log("http://localhost:5000");
});

Usage

node demo.js

浏览器访问 http://localhost:5000 => hello world