03-中间件实现原理

nobility 发布于 2020-07-08 1906 次阅读


中间件实现原理

Express

在Express对象中存储一系列请求方法注册的中间件队列,使用use()get()等方法向队列中添加中间件对象,中间件对象包括路径和方法两个属性,当请求匹配到中间件中的请求路径前缀时就会执行该中间件方法

next方法是一个有名的立即执行函数,当第一次匹配时会立即执行,并将该函数传递给用户中间件的方法为参数,若用户调用next方法时会调用我们定义好的next方法执行下一个中间件方法

const http = require("http");

/**
 * 校验参数,并返回一个中间件对象
 * @param  {...any} args 路径或中间件回调函数的可变参数
 */
function checkout(...args) {
  const middleware = {};
  if (typeof args[0] === "string") {
    middleware.path = args[0];
    middleware.queue = args.slice(1);
  } else {
    middleware.path = "/";
    middleware.queue = args.slice(0);
  }
  return middleware;
}
/**
 * 为http中的请求响应对象扩展方法
 * @param {Request} req 请求对象
 * @param {Response} res 响应对象
 */
function extendsMethod(req, res) {
  res.send = (data) => {
    res.setHeader("Content-type", "application/json");
    res.end(JSON.stringify(data))
  }
}
/**
 * 根据请求响应对象返回正真要触发的路由列表
 * @param {Request} req 请求对象
 * @param {Response} res 响应对象
 * @param {Routers} routers Express中的路由对象
 */
function getRealRouters(req, res, routers) {
  let curRouters = []; //存储可能要触发路由列表
  let realRouters = []; //要真实触发的路由列表
  curRouters.push(...routers.use);  //use中所有中间件都可能会被执行
  curRouters.push(...routers[req.method.toLowerCase()]);  //根据请求方法获取相应可能执行的中间件
  //由于中间件队列变量是小写的所以要要将请求方法转小写
  curRouters.forEach(item => {
    if (req.url.indexOf(item.path) === 0) { //请求路径与中间件路径前缀匹配时
      //前缀匹配是为了注册前缀中间件可被执行到
      realRouters.push(...item.queue); //会将中间件中的方法加入到要触发的中间件中
    }
  })
  return realRouters;
}
class Express {
  constructor() {
    this.routers = {
      use: [], //存放use方法注册的中间件
      get: [], //存放get方法注册的中间件
    }
  }
  use(...args) {
    const middleware = checkout(...args);
    this.routers.use.push(middleware); //添加到use队列
  }
  get(...args) {
    const middleware = checkout(...args);
    this.routers.get.push(middleware); //添加到get队列
  }
  listen(...args) {
    const server = http.createServer((req, res) => {
      extendsMethod(req, res); //扩展req和res对象方法
      const realRouters = getRealRouters(req, res, this.routers); //获取要执行的中间件队列
      (function next() {
        const middleware = realRouters.shift();
        if (middleware) { //取出一个中间件方法,若不为null
          middleware(req, res, next); //则执行该方法,并将req和res参数传递給该中间件
          //最重要的就是将next方法本身传递给该中间件,若中间件内部调用next方法就又会执行该立即执行函数
        }
      })();//立即执行
    });

    server.listen(...args);
  }
}
module.exports = () => {
  //工厂函数,外部调用该方法即可返回一个Express实例
  return new Express()
}

Koa2

在Koa对象中存储只存储中间对应的方法,使用use()方法向队列中添加中间件对象

next方法是一个有名的立即执行函数,当第一次匹配时会立即执行,使用函数对象的bind()方法进行参数绑定,并将包装后的ctx参数和该函数传递给用户中间件的方法为参数,若用户调用next方法时会调用我们定义好的next方法执行下一个中间件方法

将中间件函数的执行结果使用promise包装,兼容非async函数的中间件

const http = require("http");

/**
 * 将请求对象和响应对象包装成ctx对象
 * @param {Request} req 请求对象
 * @param {Response} res 响应对象
 */
function extendsMethod(req, res) {
  return {
    req,
    res
  };
}
class Koa {
  constructor() {
    this.middlewares = [];  //用于存放中间件方法
  }
  use(...args) {
    this.middlewares.push(...args); //添加到中间件队列
    return this; //链式调用
  }
  listen(...args) {
    const server = http.createServer((req, res) => {
      const ctx = extendsMethod(req, res);  //包装成ctx对象
      const middlewares = this.middlewares; //暂存中间件队列
      //由于中间件队列是地址引用,所以不能使用出队方式,否则下次访问时就没了
      (function next(i) {
        const middleware = middlewares[i];  //取出第i个中间件
        if (!middleware) return;  //若取出的是null,说明后面已经没有中间件了,所以什么也不用做
        try {
          return Promise.resolve(middleware(ctx, next.bind(null, i + 1)));
          //兼容非async函数,包装成promise
          //next.bind(null, i + 1)返回一个参数为i + 1的next函数,用于递归向下调用
        } catch (error) {
          return Promise.reject(error);
        }
      })(0); //立即执行
    });

    server.listen(...args);
  }
}
module.exports = Koa;
此作者没有提供个人介绍
最后更新于 2020-07-08