express 结构剖析

251 阅读5分钟

结构一览

最近学习了一下 express 的源码,梳理了一下它内部几个要素之间的关系

代码

我用以下事例表示 express 涉及的几个要素:trigger, middleware, routeError-handling middleware

创建 app 应用

首先是创建一个 app 应用:

const express = require('express')
const app = express()

中间件

然后使用 app.use 方法,创建一个中间件,它第一个参数 path 可以省略,表示所有路径都生效:

// middleware
app.use(
   '/user',
   (req, res, next) => {
      console.log('中间件 /user: 1 - 1')
      next()
   },
   (req, res, next) => {
      console.log('中间件 /user: 1 - 2')
      next()
   }
)

路由

接下来,我们使用 app.METHOD 中的 get 方法,一口气创建几个路由:注意第一个和后面的 path 的不同:

// route
app.get(
   '/user',
   (req, res, next) => {
      console.log('get /user: 2 - 1')
   }
)

// route
app.get(
   '/user/:id',
   (req, res, next) => {
      console.log('get /user/:id: 3 - 1')
      if (req.params.id === '0') {
          next('id 不能为 0')
      }
      next()
      console.log('')
   },
   (req, res, next) => {
      console.log('get /user/:id: 3 - 2')
      next()
   },
   (req, res, next) => {
      console.log('get /user/:id: 3 - 3')
      next()
   },
)

// route
app.get(
   '/user/:id',
   (req, res, next) => {
      console.log('get /user/:id: 4 - 1')
      next()
   },
   (req, res, next) => {
      console.log('get /user/:id: 4 - 2')
      next()
   },
)

参数触发

然后使用 app.param 是一个 trigger to route parameters,上面中间件或路由路径中如果有一次或多次 :id 的匹配,则会先触发一次调用,再调用对应的回调。

const { queryUserInfoById } = require('./service')
app.param('id', (req, res, next, sourceValue, key) => {
   req.userInfo = queryUserInfoById()
   console.log('id, params: ')
   next()
})

错误处理中间件

我们在最后放一个错误拦截的中间件。

// Error-handling middleware
app.use((err, req, res, next) => {
   console.log('error handler:', err)
})

监听端口

最后开始监听 10000 端口,整个服务就设置起来了

app.listen(10000, () => {
   console.log('listen....')
})

调用一下

正常流程

我们访问 http://localhost:10000/user/1 ,可以看到控制台如下输出:

中间件 /user: 1 - 1
中间件 /user: 1 - 2
id, params: 
get /user/:id: 3 - 1
get /user/:id: 3 - 2
get /user/:id: 3 - 3
get /user/:id: 4 - 1
get /user/:id: 4 - 2

异常流程

而如果访问 http://localhost:10000/user/0 ,则会从 3-1 处的 next 处将错误信息直接传递到错误处理中间件处:

中间件 /user: 1 - 1
中间件 /user: 1 - 2
id, params: 
get /user/:id: 3 - 1
error handler: id 不能为 0

要素解析

结构简图

为了方便剖析这个应用的结构,我画了下面这个示意图:

image.png

application 和 router

可以看出,application 这个对象下有一个 router,它是 application 在上面几种方法调用是懒创建并挂载到它的 _router 属性上。application 的很多操作其实是调用 router 上的对应的操作。

router 有几个重要的方法:

  • use 创建中间件
  • METHOD 包括 allget 等,创建
  • param 创建一个参数触发器
  • handle 请求到来时触发,实现 layernext 串行执行
  • process_params 在中间件和路由的 handle 调用之前的处理

layer

实际上 layer 有两个层级:

  • 第一层是存放在 routerstack 属性上
  • 第二层是 route 专有的,存放在 routestack 属性上

它主要的属性有:

  • handle:上面 getuse 参数中的函数
  • params:比如/user/:id 上面的 /:id 提取出来的内容
  • path:比如上面 getuse 的第一个字符串参数 /user
  • regexp:比如把 /user/:id 转化为正则,中间件和路由转换出来的是不一样的

route

路由,上图中 2-, 3-, 4-* 对应的内容。可以理解为一次 get 方法的调用,就会生成一个 layer,并为它挂上一个 route

它有两个重要的方法:

  • METHOD,包括 allget 等,他们的作用是创建 layer 并存储在自己的 stack
  • dispatch,实现 layernext 串行触发,和 router 的逻辑大体一致

调用顺序

  • 首先对 router.stack 进行遍历,匹配 layer。如果匹配上了就调用它的 handle 执行
  • 如果某个 param 被触发,则将 router._params 这个 map 中对应的 callback 进行执行一次,执行完了再执行触发它的中间件或路由的 handle,后序这个请求仍然触发了这个参数,则不额外执行 callback
  • 如果这是一个路由,即 layer.route 存在,则把 route.stack 下的 layer 依次执行,执行完后再回到 router 上下一个一级 layer执行
  • 如果 next 方法传递了参数,则会跳到最近的错误处理中间件去执行

要素说明

中间件

上面 app.use 方法,通过传入一个 path 加两个 handle,生成了两个 layer

在执行的过程中,主要是通过 !layer.route 来判断。

如果浏览器发出的是 /user/1 的请求,app.use('/user', fn) 是可以匹配上的。

路由

上面 app.get 方法,虽然有多个 handle,但是只是生成一个一级的 layer,在这个 layer 上挂载了一个 route 对象。这个 route.stack 上储存了多个二级的 layer

一级的 layerhandle 方法是框架提供的,是 route.dispatch.bind(route)。这个方法会去让 route.stack 上的二级 layer 串行调用。

另外,浏览器发出 /user/1 的请求,可以看到 app.get('/user', fn),也就是上面的2-* 并没有匹配到。只有 app.get('/user/:id', fn) 才能匹配到。

参数触发

app.param 并不会创建一个新的 layer

在请求到来时,如果某个中间件或路由,匹配到了请求的路径,而且又带有 :param 这样的形式,就会触发对应的参数回调。

如果这个请求中,这个参数被触发过了,则不会再次触发了。

比如示例代码中,服务器已经根据 :id 来对 userInfo 进行了查询,并将其储存在 req 上。后面甬道的代码就可以直接调用 req.userInfo 来获取具体的用户信息。

错误处理中间件

这是中间件的一种,同样是使用 app.use 来创建。但是和普通中间件不同的是,它的 handle 参数是 4 个。源码中也是通过 handle.length === 4 来判定这是一个错误处理中间件。

一般我们把它放在最后,用来接收前面的中间件和路由中的错误,而抛出错误的方式是带参数调用 next 方法。

总结

几种要素的特点
名称判定方式特点
中间件!layer.route前缀匹配 path|
路由layer.route整体匹配 path |
参数触发器\不生成一个 layer
错误处理中间件layer.handle.length === 4接收带参数调用的 next 方法的参数
中间件和路由的几点不同
  • 中间件是 use 调用,路由是 METHOD 调用
  • 中间件的 handle 里面,req.url 会删掉 path,并放在 req.baseUrl 里买呢。路由的不会有变化
  • 中间件是前缀匹配,路由是整体匹配。他们调用 path-to-regexp 库生成正则时,传递的参数不同