express的实现|3.完善路由

791 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 5 天,点击查看活动详情

王志远,微医前端技术部

先看应用

关于路由,上文只是简单的实现了单一请求匹配单一应答,对于express的路由系统而言,其实还有更重要的两个特性没有体现

  • 多匹配机制
  • 多回调机制
多匹配机制

即同一个方法下的同一个路径被监听了多次,此时如果上层函数不执行next方法,下层监听函数是不会被执行的;如下

app.flage = false;
app.get("/hello", function (req, res, next) {
  app.flage = !app.flage;
  function isOk() {
    // 判断是否向下执行的逻辑
    return app.flage;
  }
  // res.end('/hello')
  console.log("我是第一个监听函数", app.flage);
  if (isOk()) {
    next();
  } else {
    res.end("not continue");
  }
});

app.get("/hello", function (req, res) {
  console.log("我是第二个监听函数");
  res.end("/hello");
});

上面代码很简单,就是设定了一个开关,为true时则执行next函数继续向下执行,为false则直接返回,启动服务请求接口,会发现如下现象

控制台显示如下

多回调机制

即回调函数传递多个,同样采用next控制是否执行栈中的下一个

app.get(
  "/",
  function (req, res, next) {
    console.log(1);
    next();
  },
  function (req, res, next) {
    console.log(2);
    next();
  },
  function (req, res, next) {
    console.log(3);
    next();
  },
  function (req, res, next) {
    console.log(4);
    next();
    res.end("/ matched");
  }
);

效果和上文相似,调动next则会继续向下执行

再看实现

理解 next 原理

要实现这些效果,我们先想想这样一个需求

有一个数组,组成数据结构如下,{match(){},handler(next){}} , 我希望当上一个 handler 调用 next 了之后,才执行数组的下一项

如何实现?

了解过CO(就是大神 TJ 实现的【对generator迭代器实现处理】的库,个人认为async+await是这个库的一个官方版)就会发现,逻辑完全一致,都是【将是否继续循环的控制权反转给被循环者本身】

既然要控制,我们自然不能简单的循环了,而是得递归。举例论证:

let stack = [
  {
    handle(next) {
      console.log("1, begin");
      setTimeout(() => {
        next();
        console.log("1, done");
      }, 5000);
    },
  },
  {
    handle(next) {
      console.log("2,done");
    },
  },
];

那我们需要实现的就是

  • 打印'1, begin'
  • 过五秒打印1, done2, done

首先,我们很自然的能想到,如果next就是下一个函数呢?这样不就满足【执行 next 时执行下一个函数】的需求了吗?于是我们写出如下代码

let index = 0; // 当前正在执行的数组项索引
function dispatch(stack) {
  let item = stack[index];
  item.handle(stack[++index]); // 先自增 再取值
}

这样,我们就执行了第一项和第二项;接着,我们要递归所有

let index = 0; // 当前正在执行的数组项索引
function dispatch(stack, out) {
  if (stack.length === index) return typeof out == "Function" && out(); // 递归结束 ,如果存在结束钩子 则执行
  let item = stack[index++]; // 取值完后自增
  item.handle(dispatch.bind(null, stack));
}

有了这个基础,我们再去看express的路由next就能更轻松一点了。

实现 express-router

先思考实现思路

  • 每个【路径+方法】对应一个路由表(数组),路由表由处理函数组成,形成一个(Layer)
  • Router持有路由表(数组),路由表由上面的Layer组成

我们先来看张图

角色梳理

这里面有三个角色

  1. 路由表:由层组成,是个数组,其层的组成是指【处理函数】
  2. 路由对象:持有路由表,其路由表的层的组成是指【路径+方法 对应着 路由表】
  3. 层:为了统一数组组成而抽象出来的数据结构

Route.js

const Layer = require("./layer");
const methods = require("methods");

// 每个层都有一个 route 属性
function Route() {
  this.stack = [];
}
// 处理函数
Route.prototype.dispatch = function (req, res, out) {
  let idx = 0;
  let method = req.method.toLowerCase(); // 获取请求的方法
  let dispatch = () => {
    if (idx === this.stack.length) {
      return out();
    }
    let layer = this.stack[idx++];
    if (layer.method === method) {
      // 获取内部的第一层判断方法是否匹配
      layer.handle_request(req, res, dispatch);
    } else {
      dispatch();
    }
  };
  dispatch();
};
methods.forEach((method) => {
  // 被调用时 将对应的处理函数数组映射成多个层,并存入路由表对象持有的队列中
  Route.prototype[method] = function (handlers) {
    handlers.forEach((handler) => {
      let layer = new Layer("", handler);
      layer.method = method;
      this.stack.push(layer);
    });
  };
});

module.exports = Route;

Layer.js

function Layer(path, handler) {
  this.path = path;
  this.handler = handler;
  this.method = "";
}

Layer.prototype.match = function (pathname) {
  return this.path === pathname;
};

Layer.prototype.handler_request = function (req, res, next) {
  this.handler((req, res, next));
};

module.exports = Layer;
逻辑梳理

首先,一个应用得有一个对应的路由

function Application() {
  this._router = new Router();
}

其次,就是请求的订阅发布了。

订阅:得在应用初始化时进行方法的监听,即app上的get、post等路由方法交由路由处理

methods.forEach((method) => {
  Application.prototype[method] = function (path, ...handlers) {
    this._router[method](path, handlers);
  };
});

进入router层,在方法被触发时做三件事

  1. 创建对应的层,存入自己的队列中
  2. 对层进行初始化,赋值上对应的【path+method】和处理函数
  3. 将处理逻辑交由层里面的路由表(route)进行处理
function Router() {
  this.stack = [];
}
methods.forEach((method) => {
  Router.prototype[method] = function (path, handlers) {
    // 用户调[method]方法时,传递了多个处理函数
    let route = this.route(path); // 构建一个 route
    // 将处理逻辑交由层里面的路由表进行处理
    route[method](handlers);
  };
});
Router.prototype.route = function (path) {
  let route = new Route();
  let layer = new Layer(path, route.dispatch.bind(route)); // 给当前调用 get 方法 放入一层
  layer.route = route; // 每个层都有一个 route 属性
  this.stack.push(layer);
  return route;
};

发布:得改写应用对象的监听逻辑,将逻辑处理交由路由

Application.prototype.listen = function (port, cb) {
  let server = http.createServer((req, res) => {
    // 应用提供一个对均无法匹配的兼容处理函数
    function done() {
      res.end(`Cannot ${req.method} ${req.url}`);
    }
    this._router.handle(req, res, done);
  });
  server.listen(...arguments);
};

路由的handle方法只做一件事,递归存储的Layer,所有匹配到的Layer依次执行

重点【依次】,其实就是在说【多匹配机制】,这个时候我们就要采用next的思路了。

Router.prototype.handle = function (req, res, out) {
  let { pathname } = url.parse(req.url); // 获取请求的路径
  let idx = 0;
  let dispatch = () => {
    // exress 需要通过 next 函数来迭代
    if (idx === this.stack.length) {
      return out();
    }
    let layer = this.stack[idx++];
    if (layer.match(pathname)) {
      layer.handler_request(req, res, dispatch);
    } else {
      dispatch();
    }
  };
  dispatch();
};

Layerhandler_request其实就是调用了一下初始化Layer时存入的handle函数,回想一下上面,那不就是路由表的next的吗?这就通了,相当了是:Application -> Router -> Layer -> Route -> Layer 的处理逻辑

Layer 间会需要一次next逻辑,Route 间也会需要一次,此时需要注意的是,Layer 的out函数是 express 在listen中提供的done方法,而 Route 的out函数则是 Layer 的 next 方法

至此,我们就基本理解了epxress的运作流程啦

代码仓库