结构一览
最近学习了一下 express 的源码,梳理了一下它内部几个要素之间的关系
代码
我用以下事例表示 express 涉及的几个要素:trigger, middleware, route 和 Error-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
要素解析
结构简图
为了方便剖析这个应用的结构,我画了下面这个示意图:
application 和 router
可以看出,application 这个对象下有一个 router,它是 application 在上面几种方法调用是懒创建并挂载到它的 _router 属性上。application 的很多操作其实是调用 router 上的对应的操作。
router 有几个重要的方法:
use创建中间件METHOD包括all和get等,创建param创建一个参数触发器handle请求到来时触发,实现layer的next串行执行process_params在中间件和路由的handle调用之前的处理
layer
实际上 layer 有两个层级:
- 第一层是存放在
router的stack属性上 - 第二层是
route专有的,存放在route的stack属性上
它主要的属性有:
handle:上面get和use参数中的函数params:比如/user/:id上面的/:id提取出来的内容path:比如上面get,use的第一个字符串参数/userregexp:比如把/user/:id转化为正则,中间件和路由转换出来的是不一样的
route
路由,上图中 2-, 3-, 4-* 对应的内容。可以理解为一次 get 方法的调用,就会生成一个 layer,并为它挂上一个 route。
它有两个重要的方法:
METHOD,包括all和get等,他们的作用是创建layer并存储在自己的stack上dispatch,实现layer的next串行触发,和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。
一级的 layer 的 handle 方法是框架提供的,是 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库生成正则时,传递的参数不同