仓库:mini-express
大家好,我是山月。
express
是 Node 中下载量最多的服务端框架,虽然大多归源于 webpack
的依赖。今天手写一个迷你版的 express
,对其内部实现一探究竟。
山月的代码实现
代码置于 shfshanyue/mini-code:code/express
可直接读源码,基本每一行都有注释。
使用 npm run example
或者 node example
可运行示例代码
关于 express 的个人看法
- 重路由的中间件设计。在
express
中所有中间件都会通过 path-to-regexp
去匹配路由正则,造成一定的性能下降 (较为有限)。
querystring
默认中间件。在 express 中,每次请求都内置中间件解析 qs,造成一定的性能下降 (在 koa 中为按需解析)。
- 无 Context 的设计。express 把数据存储在
req
中,当然也可自定义 req.context
用以存储数据。
res.send
直接扔回数据,无 ctx.body
灵活。
- 源码较难理解,且语法过旧,无 koa 代码清晰。
express
默认集成了许多中间件,如 static。
express 的中间件设计
在 express
中可把中间件分为应用级中间件与路由级中间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| app.use('/api', (req, res, next) => { console.log('Application Level Middleware: A') }, (req, res, next) => { console.log('Application Level Middleware: B') } )
app.get('/api', (req, res, next) => { console.log('Route Level Middleware: C') }, (req, res, next) => { console.log('Route Level Middleware: D') } )
|
在 express
中,使用数据结构 Layer
维护中间件,而使用 stack
维护中间件列表。
所有的中间件都挂载在 Router.prototype.stack
或者 Route.prototype.stack
下,数据结构如下。
- app.router.stack: 所有的应用级中间件(即
app.use
注册的中间件)。
- app.router.stack[0].route.stack: 某一应用级中间件的所有路由级中间件 (即
app.get
所注册的中间件)。
以下是上述代码关于 express
中间件的伪代码数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| const app = { stack: [ Layer({ path: '/api', handleRequest: 'A 的中间件处理函数' }), Layer({ path: '/api', handleRequest: 'B 的中间件处理函数' }), Layer({ path: '/api', handleRequest: 'dispatch: 用以执行该中间件下的所有路由级中间件', route: Route({ path: '/api', stack: [ Layer({ path: '/', handleRequest: 'C 的中间件处理函数' }), Layer({ path: '/', handleRequest: 'D 的中间件处理函数' }) ] }) }) ] }
|
根据以上伪代码,梳理一下在 express
中匹配中间件的流程:
- 注册应用级中间件,配置 handleRquest 与 path,并根据
path
生成 regexp
,如 /api/users/:id
生成 /^\/api\/users(?:\/([^\/#\?]+?))[\/#\?]?$/i
- 请求来临时,遍历中间件数组,根据中间件的
regexp
匹配请求路径,得到第一个中间件
- 第一个中间件中,若有 next 则回到第二步,找到下一个中间件
- 遍历结束
Application 的实现
在 Application
层只需要实现一个功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Application { constructor () { this._router = new Router() }
listen (...args) { const server = http.createServer(this.handle.bind(this)) server.listen(...args) }
handle (req, res) { const router = this._router router.handle(req, res) }
use (path, ...fns) { }
get (path, ...fns) { } }
|
中间件的抽象: Layer
- 中间件的抽象
- 中间件的匹配
中间件要完成几个功能:
- 如何确定匹配
- 如何处理请求
基于此设计以下数据结构
1 2 3 4 5 6
| Layer({ path, re, handle, options })
|
其中,正则用以匹配请求路径,根据 path
生成。那如何获取到路径中定义的参数呢?用捕获组。
此时祭出神器 path-to-regexp
,路径转化为正则。无论 Express
、Koa
等服务端框架,还是 React
、Vue
等客户端框架的路由部分,它对备受青睐。
1 2 3 4 5 6 7 8 9 10 11 12
| const { pathToRegexp } = require('path-to-regexp')
pathToRegexp('/')
p.pathToRegexp('/', [], { end: false })
pathToRegexp('/api/users/:id')
|
有了正则,关于匹配中间件的逻辑水到渠成,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
class Layer { constructor (path, handle, options) { this.path = path this.handle = handle this.options = options this.keys = [] this.re = pathToRegexp(path, this.keys, options) }
match (url) { const matchRoute = regexpToFunction(this.re, this.keys, { decode: decodeURIComponent }) return matchRoute(url) } }
|
中间件的收集
app.use
及 app.get
用以收集中间件,较为简单,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| class Application { use (path, ...fns) { this._router.use(path, ...fns) }
get (path, ...fns) { const route = this._router.route(path) route.get(...fns) } }
class Router { constructor () { this.stack = [] }
handle (req, res) { }
use (path, ...fns) { for (const fn of fns) { const layer = new Layer(path, fn) this.stack.push(layer) } }
route (path) { const route = new Route(path) const layer = new Layer(path, route.dispatch.bind(route), { end: true }) layer.route = route this.stack.push(layer) return route } }
|
其中,关于路由级中间件则由 Route.prototype.stack
专门负责收集,多个路由级中间件由 dispatch
函数组成一个应用中间件,这中间是一个洋葱模型,接下来讲到。
中间件与洋葱模型
洋葱模型实现起来也较为简单,使用 next
连接起所有匹配的中间件,按需执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| function handle (req, res) { const stack = this.stack let index = 0
const next = () => { let layer let match
while (!match && index < this.stack.length) { layer = stack[index++] match = layer.match(req.url) } if (!match) { res.status = 404 res.end('NOT FOUND SHANYUE') return } req.params = match.params layer.handle(req, res, next) } next() }
|
相较而言,路由级中间件洋葱模型的实现简单很多
1 2 3 4 5 6 7 8 9 10 11 12
| function dispatch (req, res, done) { let index = 0 const stack = this.stack const next = () => { const layer = stack[index++]
if (!layer) { done() } layer.handle(req, res, next) } next() }
|
结语
完。
等一下,记得吃早饭。