多图理解 Express

1,717 阅读4分钟

前置内容

Node 可以简单快速地开启 Http 服务:

const http = require("http");

// 创建http服务
const server = http.createServer((req, res) => {
  res.end("hello Node");
});
// 监听端口
server.listen(3000, () => {
  console.log("服务已启动");
});

这样一个 Node 服务就启动了,此时浏览器访问http://127.0.0.1:3000会返回hello Node,但是目前还没有路由,所以你会发现无论输入http://127.0.0.1:3000/hi或是其他路径都会返回相同的结果。因此 Node 服务端框架 express 提供了路由中间件请求处理等功能帮助我们高效的提供服务。

启动 express 服务

我们借助一个例子,来理解 express 如何启动服务

const express = require("express");
const app = express();

// 开启express服务
app.listen("3000", () => {
  console.log("服务已运行在3000端口上");
});

  • express 是一个函数,执行 express 会返回一个 app 函数,app 就被作为http.createServer的处理函数

  • 调用 app.listen 可以开启服务,本质还是通过http.createServer(app)开启 Node 服务

  • 观察 app 这个处理函数,它在原来参数基础上扩展了next函数,next可以控制下一个handle或者中间件的调用,并且将具体逻辑处理交给app.handle,实际上这是 express 中非常常见的代理模式,app 提供了 express 服务中所有请求唯一的入口

注册路由

const express = require("express");
const app = express();

app.get("/", (req, res, next) => {
  res.end("hello");
});

// 开启express服务
app.listen("3000", () => {
  console.log("服务已运行在3000端口上");
});

express 只需要调用app.get就可以完成 get 请求方法的路由注册,express 中路由注册的内部执行流程涉及到最重要的四个模块:app、router、route、layer,其层次为:

  • app层是express中的顶层设计,持有一个router路由对象
  • router层存放所有的路由信息,内部会持有layer存放在自己的栈中
  • layer在express中主要有三种(express中被统一抽象成了layer类):
    • route layer:持有route,保存在router中,
    • middleware layer:不会持有route,只会持有相应的handle函数,保存在router中,
    • handle layer:持有路由最终的handle,保存在route

以上我们对express中各模块的职责了解一番后,app.get路由注册的流程会是这样:

  1. app.get会调用router的route方法,这个方法会生成一个route和一个route layer,让route layer持有routeroute提供route layer一个dispatch方法作为handle,最后压入router的栈中
  2. 将所有的handle函数传递给新生成的route,route.get会生成handle layer保存相应的handle,并存放在自己的栈中

再通过源码来看看express具体实现:

// application.js

// methods是所有http方法集合,这里定义了app.get、app.post等方法
methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    // 懒初始化router对象
    this.lazyrouter();

    // 执行第一步,执行router的route方法
    var route = this._router.route(path);
    // 执行第二步,调用新创建的route.get方法,将handle传递出去
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});


// router.js
proto.route = function route(path) {
  // 生成一个新route
  var route = new Route(path);

  // 生成一个route layer
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  // 让layer持有route
  layer.route = route;

  // 将layer推入到栈中
  this.stack.push(layer);
  
  // 返回route对象
  return route;
};

// route.js
methods.forEach(function(method){
  Route.prototype[method] = function(){
    // 打平数组数据结构
    var handles = flatten(slice.call(arguments));

    // 遍历所有处理函数
    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      if (typeof handle !== 'function') {
        var type = toString.call(handle);
        var msg = 'Route.' + method + '() requires a callback function but got a ' + type
        throw new Error(msg);
      }

      // 创建一个handle layer, 每个handle layer都会保存一个handle函数
      var layer = Layer('/', {}, handle);
      layer.method = method;

      this.methods[method] = true;
      // handle layer被推入到route的栈中
      this.stack.push(layer);
    }

    return this;
  };
});

命中路由

由于我们通过app.get注册了一个路由,此时浏览器访问http://127.0.0.1:3000/会有相应的响应,但访问其他地址不会有响应,这表明路由已经在工作了。

现在命中路由的流程是这样:

  1. 根据前文我们知道express开启的服务所有请求都会走到app的逻辑中,app则执行app.handle方法,app.handle又把处理具体逻辑交给router.handle来实现,相当于把请求转给router层来处理
  2. 请求到了router层,router.handle会遍历自己stack中所有的layer(route layer和middleware layer, 例子中为route layer),并根据当前请求的url地址依次匹配注册时保存的路由path,当条件命中时会执行该layerhandle_request方法,这个方法就是注册时绑定在route layer上对应的route的dispatch方法
  3. route.dispatch的逻辑和router.handle相似,遍历执行自己stack中的handle layer,执行其handle_request方法,即执行注册路由时绑定的handle函数。这里express将调用下一个layer.handle_request交给了开发者,如果有多个handle layer,开发者可以显示使用express提供的函数(一般是next)来实现各个handle调用的逻辑

中间件

中间件注册

中间件是Web服务端框架的重要模块,服务端常常把系统中相同的逻辑交给中间件完成,从而实现认证鉴权日志错误处理数据转换等功能,express中中间件就是一个函数,我们可以使用app.use来添加一个中间件。

const express = require("express");
const app = express();

app.use(function(req, res, next) {
  console.log('当前请求地址:', req.url);
  // 注意,要显式调用next,否则请求/不会被匹配
  next()
})

app.get("/", (req, res, next) => {
  res.end("hello");
});

// 开启express服务
app.listen("3000", () => {
  console.log("服务已运行在3000端口上");
});

我们创建了一个打印请求路径的中间件,express注册中间件的内部流程是:

  1. app.use将所有handles处理函数传递给router.use方法
  2. router.use会遍历所有handles,每个handle都会创建一个middleware layer,将其保存在middleware layer上,与route layer不同的是,该layer不会持有route,最后将其压入router的栈中

中间件命中

实际上中间件还支持添加路径即app.use(path, ...fns),不加路径则是一个全局的中间件,能响应所有的请求,因为中间件的注册和路由注册相似,其命中过程也和路由相同,都是根据path寻找对应的layer,执行其绑定的handle函数。

  1. app.handle将处理逻辑交给router.handle
  2. router.handle通过path找到相应的middleware layer,由于例子中我们注册的是全局中间件,则会命中该middleware layer,将其取出执行绑定在该layer上的handle_request方法

路径匹配

express支持正则作为路径注册路由,可以使用以下几种方式:

  • app.get('some?path', handle) // 字符串正则
  • app.get('somepath/:id', handle) // 路径带参数
  • app.get(/somepath/, handle) // 支持路径为正则表达式

这些特性主要得益于path-to-regexp的支持,express中路由的命中是根据注册时保存的正则进行的。

总结

express把app设计为顶层结构,将app作为接口层暴露出去供开发者使用,内部将app的usegethandle方法具体处理逻辑交给router,相当于app是一层向外暴露的代理。同时express使用while可中断的路由匹配方式让开发者可以使用next控制handle函数的调用,开发者在定义路由或中间件时需注意彼此的顺序。