由浅入深理解express源码(三)
回顾
上一次的迭代中,主要是实现了简化版的router,并对/get/:id 式的路由进行解析。同时实现app.Methods相应的接口
实现目标
gitee: gitee.com/kaisela/mye…
本次迭代主要是实现了app.param,app.use,以及req.query中参数的提取工作。其实在本次迭代中app.param和query足以形成一个迭代,再加上app.use内容就比较多,不过我还是将它们放在一个迭代中,还请读者多费些时间去理解。因为理解到这一层了,express的真面目已经揭开一大半了
项目结构
express3
|
|-- lib
| |-- middleware // 新增 中间件文件夹
| | |-- query.js // 新增 实现req.query提取的中间件
|
| |-- router // 实现简化板的router
| | |-- index.js // 实现路由的遍历等功能
| | |-- layer.js // 装置path,method cb对应的关系
| | |-- route.js // 将path和fn的关系实现一对多
| |-- express.js //负责实例化application对象
| |-- application.js //包裹app层
| |-- utils.js // 新增,目前只是用于query中间件的实现的所需的工具函数
|
|-- examples
| |-- index.js // express 实现的使用例子
|
|-- test
| |
| |-- index.js // 自动测试examples的正确性
|
|-- index.js //框架入口
|-- package.json // node配置文件
复制代码
重要概念引入
中间件
在express中,中间件其实是一个介于web请求来临后到调用处理函数前整个流程体系中间调用的组件。其本质是一个函数,内部可以访问修改请求和响应对象,并调整接下来的处理流程。
express官方给出的解释如下:
Express 是一个自身功能极简,完全是由路由和中间件构成一个的 web 开发框架:从本质上来说,一个 Express 应用就是在调用各种中间件。
中间件(Middleware) 是一个函数,它可以访问请求对象(request object (req)), 响应对象(response object (res)), 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为 next 的变量。
中间件的功能包括:
- 执行任何代码。
- 修改请求和响应对象。
- 终结请求-响应循环。
- 调用堆栈中的下一个中间件。
如果当前中间件没有终结请求-响应循环,则必须调用 next() 方法将控制权交给下一个中间件,否则请求就会挂起。
Express 应用可使用如下几种中间件:
使用可选则挂载路径,可在应用级别或路由级别装载中间件。另外,你还可以同时装在一系列中间件函数,从而在一个挂载点上创建一个子中间件栈。
所以对于迭代二来说Router和Route类中的this.stack属性内部的每个handle都是一个中间件,根据使用接口不同区别了应用级中间件和路由级中间件,而四个参数的处理函数就是错误处理中间件,对于内置中间件我们暂时还未涉及,而app.use接口将要实现的就是嵌入第三方中间件
在express中的中间件其实和java中面相切面编程中的拦截器的作用基本一致。可以在某一类接口调用之前,使用中间件做统一处理。比如:app.param 也是一种中间件,只是它针对的只是对参数处理。而use和router都是针对请求路径来处理。
问题分析
本次迭代主要是实现了app.param,app.use,以及req.query中参数的提取工作
app.use的官方api
app.use(function(req, res, next) { // 将会拦截所有请求
res.send('Hello World');
});
app.use('/abcd', function (req, res, next) { // 将会拦截路径为 /abcd 的请求
next();
});
app.use('/abc?d', function (req, res, next) { // 将会拦截路径为 /abcd 和 /abd 的请求
next();
});
app.use(/\/abc|\/xyz/, function (req, res, next) { // 将会拦截路径为 /abc 和 /xyz 的请求
next();
});
复制代码
以上为app.use的一些用法示例,由于use方法和router的参数很相似,只是少了method这个变量。所以在express的源码中,use方法注册的中间件的数据结构将使用router的第一层(Router)中的stack存储,只是use注册的Layer中少了route对象
app.param的官方api
app.param('id', function (req, res, next, id) { // 当注册路由为 .../:id/...形式时会被此中间件拦截
console.log('CALLED ONLY ONCE');
next();
});
app.param(['id', 'page'], function (req, res, next, value) { // 拦截含有id 或者 page参数的路由请求
console.log('CALLED ONLY ONCE with', value);
next();
});
// 以上两种方式的另一种写法,二者选其一 ,文中和测试用例中我们以上一种为例
app.param(function(param, option) {
return function (req, res, next, val) {
if (val == option) {
next();
}
else {
next('route');
}
}
});
// using the customized app.param()
app.param('id', 1337);
复制代码
param的方法的结构就较为简单,分为参数param和callback两种,其二者的关系为一对多的关系,在express的源码中实现是放在Router类中,数据结构由params对象和_params数组两种方式存储,第一种书写方式只需要用到params对象,第二种书写方式则是后面所有的param注册,都是使用前面return的中间件函数。此文中对第二种书写方式不做详解,请自行看源码理解
Router中params的结构为{param:[fn,fn...]}
req.query
主要是对请求路径中的query部分进行解析,主要使用的方法为parseurl,querystring.parse。url转换后的结构示例如下
url.parse (user:pass@host.com:8080/users/user.…);
属性名 | 值 |
---|---|
href | user:pass@host.com:8080/users/user.… |
protocol | http |
slashes | true |
host | host.com:8080 |
auth | user:pass |
hostname | host.com |
port | 8080 |
pathname | /users/user.php |
search | ?userName=Lulingniu&age=40&sex=male |
path | /users/user.php?userName=Lulingniu&age=40&sex=male |
query | userName=Lulingniu&age=40&sex=male |
hash | #namel |
数据结构
-------------- ----- ----------
| Application | -------------------------------> | params |
| | | ----- ------------- | |-param |
| |-router | ----> | | Layer | | |-callbacks|
-------------- | 0 | |-path | ----- ----------
application | | |-callbacks| router
|-----|--------------|
| | Layer |
| 1 | |-path |
| | |-callbacks|
|-----|--------------|
| | Layer |
| 2 | |-path |
| | |-callbacks|
|-----|--------------|
| ... | ... |
----- --------------
router
复制代码
对于query的实现,其实就是在所有路由注册前面加上了一个处理query的中间件,和中间件的结构图一样,只是这里的中间件是一个特定的函数
代码解析
此次迭代中新增的代码比较多,也比较零碎,因此我在文件的注释中前面加了一个“迭代编号:新增”的字样,来表示此段代码是在此迭代中新增的。
app.use
application.js中新增use接口,主要是调用router中的use方法
/**
* 3:新增 暴露给用户注册中间件的结构,主要调用router的use方法
* @param {*} fn
*/
app.use = function use(fn) {
let offset = 0
let path = '/'
if (typeof fn !== 'function') {
let arg = fn
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0]
}
if (typeof arg !== 'function') {
offset = 1
path = fn
}
}
let fns = slice.call(arguments, offset)
if (fns.length === 0) {
throw new TypeError('app.use() require a middlewaare function')
}
this.lazyrouter()
let router = this._router
fns.forEach(function (fn) {
router.use(path, fn)
})
}
复制代码
router中新增use方法,主要是完成对中间件的注册,在handle中遍历
/**
* 3:新增 主要用于注册路由相关的中间件,此迭代中,在注册query中间件中使用到
* @param {*} fn
*/
proto.use = function use(fn) {
let path = '/'
let offset = 0
// 为app.use 接口准备,第一个参数可能时路径的正则表达式
if (typeof fn !== 'function') {
let arg = fn
while (Array.isArray(arg) && arg.length != 0) {
arg = arg[0]
}
if (typeof arg !== 'function') {
offset = 1
path = arg
}
}
let callbacks = slice.call(arguments, offset)
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}
// 将中间件加入到stack栈中,方便handle函数遍历中执行
for (let i = 0; i < callbacks.length; i++) {
let fn = callbacks[i]
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires a middleware function but not a ' + gettype(fn))
}
let layer = new Layer(path, {
strict: false,
end: false
}, fn)
layer.route = undefined
this
.stack
.push(layer)
}
}
复制代码
application.js中新增param接口,主要是调用router中的param方法
/**
* 3:新增 实现app的param接口
* @param {*} name 参数名称 可以是数组 或者 字符串
* @param {*} fn 需要处理的中间件
*/
app.param = function param(name, fn) {
this.lazyrouter()
// 如果name是数组时,分割调用自身
if (Array.isArray(name)) {
for (let i = 0; i < name.length; i++) {
this.param(name[i], fn)
}
return this
}
this
._router
.param(name, fn)
return this
}
复制代码
router中新增param方法,主要是完成对param中间件的注册,在handle中处理
/**
* 3:新增 对传过来的参数进行拦截,将参数拦截相关存入到params中,在handle中进行分解执行
*/
proto.param = function param(name, fn) {
if (typeof name === 'function') {
this
._params
.push(name)
return
}
if (name[0] === ':') {
name = name.substr(1)
}
let params = this._params
let len = this._params.length
let ret
for (let i = 0; i < len; i++) {
if (ret = params[i](name, fn)) {
fn = ret
}
}
(this.params[name] = this.params[name] || []).push(fn)
}
复制代码
router中的handle方法中新增对use中间件的遍历逻辑,主要是通过是否有route来判断。新增process_params方法对params对象的处理,主要是和layer.keys进行比较,匹配到的时候逐个执行param所对应的callbacks。在process_params中使用param递归遍历keys,使用paramCallback的递归对param对应的callbacks进行遍历。这里就不具体贴代码了,大家自行移步git看代码
/**
* 遍历stack数组,并处理函数, 将res req 传给route
*/
proto.handle = function handle(req, res, out) {
...
next() //第一次调用next
function next(err) {
...
// 3:修改 对req调用handle时的初始值进行保存,返回处理函数,以便随时恢复初始值
let done = restore(out, req, 'baseUrl', 'next', 'params')
...
// 3: 新增path ,用于获取除query之外的path
let path = getPathname(req)
if (!path) {
return done(layerError)
}
let layer
let match
let route
while (match !== true && idx < stack.length) { //从数组中找到匹配的路由
layer = stack[idx++]
match = matchLayer(layer, path)
route = layer.route
if (typeof match !== 'boolean') {
layerError = layerError || match
}
if (match !== true) {
continue
}
// 3:新增,原逻辑中不可能存在route没有的情况,在3中加入中间件,其route为undefined
if (!route) {
continue
}
...
}
if (match !== true) { // 循环完成没有匹配的路由,调用最终处理函数
return done(layerError)
}
req.params = Object.assign({}, layer.params) // 将解析的‘/get/:id’ 中的id剥离出来
// 3:新增,主要是处理params
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err)
}
if (route) {
//调用route的dispatch方法,dispatch完成之后在此调用next,进行下一次循环
return layer.handle_request(req, res, next)
}
// 3:新增,加入handle_error处理
trim_prefix(layer, layerError, '', path)
})
}
function trim_prefix(layer, layerError, layerPath, path) {
if (layerPath.length !== 0) {
let c = path[layerPath.length]
if (c && c !== '/' && c !== '.')
return next(layerError)
}
if (layerError) {
layer.handle_error(layerError, req, res, next)
} else {
layer.handle_request(req, res, next)
}
}
}
复制代码
restore方法为一个高阶函数,主要作用是对一个对象的初始值进行存储,在返回的函数中以便随时恢复
/**
* 3:新增 对obj对象的一些属性进行恢复出厂设置
* @param {*} fn 恢复值之后需要调用的函数
* @param {*} obj 需要恢复值的对象
* @param {*} augments[i+2] obj需要恢复的属性
*/
function restore(fn, obj) {
let props = new Array(arguments.length - 2)
let vals = new Array(arguments.length - 2)
// 保存函数调用时,obj对应属性的值
for (let i = 0; i < props.length; i++) {
props[i] = arguments[i + 2]
vals[i] = obj[props[i]]
}
return function () {
// 调用函数时,对obj属性值进行恢复
for (let i = 0; i < props.length; i++) {
obj[props[i]] = vals[i]
}
fn.apply(this, arguments);
}
}
复制代码
还有一个是query中间件介绍,在utils中通过compileQueryParser来确定querysting调用的是那个方法,默认值是在qs和querystring中做选择,当然你也可以自己写处理方法。在路由初始化的时候进行中间件的注册
/**
* 对路由实现装载,实例化
*/
app.lazyrouter = function () {
if (!this._router) {
this._router = new Router()
// 3:新增 注册处理query的中间件
this
._router
.use(query(this.get('query parser fn')))
}
}
/**
* 3:新增 处理req.url query部分的中间件
*/
let merge = require('utils-merge')
let parseUrl = require('parseurl')
let qs = require('qs')
module.exports = function query(options) {
let opts = merge({}, options)
let queryparse = qs.parse
if (typeof options === 'function') {
queryparse = options
opts = undefined
}
if (opts !== undefined && opts.allowPrototypes === undefined) {
opts.allowPrototypes = true
}
return function query(req, res, next) {
if (!req.query) {
let val = parseUrl(req).query
req.query = queryparse(val, opts)
}
next()
}
}
复制代码
exammple/index.js 在入口文件中加入了一些新的测试用例
// 3:新增 输出传入的id,和name时拦截处理参数
app.param([
'id', 'name'
], function (req, res, next, val, name) {
if (name == 'id') {
req.params.id = ((val - 0) + 3) + ''
}
if (name == 'name') {
req.params[name] = req.params[name] + ' param'
}
next()
})
// 3:新增 当路径为/get 时拦截处理query
app.use('/get', function (req, res, next) {
for (key in req.query) {
req.query[key] = req.query[key] + ' use'
}
next()
})
// 测试param处理id ,name
app.post('/user/:id/:name', function (req, res) {
res.end(JSON.stringify(req.params))
})
// 测试param处理id
app.post('/user/:id', function (req, res) {
res.end(JSON.stringify(req.params))
})
// 测试param处理name
app.post('/name/:name', function (req, res) {
res.end(JSON.stringify(req.params))
})
app.get('/get', function (req, res) {
res.end(JSON.stringify(req.query))
})
// 输出传入的id
app.get('/get/:id', function (req, res) {
res.end(`{"id":${res.params.id}}`)
})
复制代码
test/index.js 测试exapmles中的代码,验证是否按照地址的不同,进了不同的回调函数
// 测试get: /get 带query
it('GET /get', (done) => {
request
.get('/get?test=once')
.expect(200)
.end((err, res) => {
if (err)
return done(err)
let params = JSON.parse(res.text)
assert.equal(params.test, 'once use', 'res.text must has prototype test and the value must be once use') // 经过use方法处理后的test为once+ use = once use
done()
})
})
// 如果走的不是examples中的post:/user/:id/:name 测试不通过
it('POST /user/12/kaisela', (done) => {
request
.post('/user/12/kaisela')
.expect(200)
.end((err, res) => {
if (err)
return done(err)
let params = JSON.parse(res.text)
assert.equal(params.id, '15', 'id must be 15') // 经过param方法处理后的id为12+3 = 15
assert.equal(params.name, 'kaisela param', 'name must be kaisela param')
// 经过param方法处理后的id为kaisela+ param = kaisela param
done()
})
})
// 如果走的不是examples中的post:/user/:id测试不通过
it('POST /user/17', (done) => {
request
.post('/user/17')
.expect(200)
.end((err, res) => {
if (err)
return done(err)
let params = JSON.parse(res.text)
// 经过param方法处理后的id为17+3 = 20
assert.equal(params.id, '20', 'id must be 20')
done()
})
})
// 如果走的不是examples中的post:/name/:name测试不通过
it('POST /name/ke', (done) => {
request
.post('/name/ke')
.expect(200)
.end((err, res) => {
if (err)
return done(err)
let params = JSON.parse(res.text)
// 经过param方法处理后的id为ke+ param = ke param
assert.equal(params.name, 'ke param', 'name must be ke param')
done()
})
})
复制代码
test测试结果如下:
写在最后
到此为止,express的两个比较重要的功能算是基本完成,虽然还有很多细节要完善。对于use方法可以路由嵌套的功能也许还要花一个篇幅讲解,看后面的时间吧。还有request,response的封装,模版引擎以及错误处理中间件。尤其是模版引擎,目前算是一点未引入
下期预告
完善router,实现错误处理中间件 和use更多用法实现