Appearance
手写 Express 路由系统
来源:
express/,从零实现 Express 核心路由机制(Application → Router → Layer/Route),理解中间件链、路径匹配、参数提取的底层原理。
速查导航
架构总览
express() → new Application()
├─ app.use() → Router.use() → Layer(path, handler)
├─ app.get() → Router.get() → Route → Layer(path, handler, method)
├─ app.post() → Router.post() → Route → Layer(path, handler, method)
└─ app.listen() → http.createServer → Router.handler(req, res, done)
└─ 遍历 stack → layer.match(pathname)
├─ 有 route → dispatch() → 匹配 method
└─ 无 route → 直接执行 handler(中间件)四个核心文件:
| 文件 | 职责 |
|---|---|
express.js | 工厂函数,返回 Application 实例 |
application.js | Application 类:use / get / post / listen |
lib/router/index.js | Router:路由注册 + 请求分发 + 中间件链 |
lib/router/layer.js | Layer:路径匹配(支持 :param) |
lib/router/route.js | Route:按 HTTP 方法分发到具体 handler |
交互演示
服务运行在
https://api.jaogo.info,通过pnpm dev或pnpm dev:express-core启动。
核心实现
Application
js
const Router = require('./lib/router/index')
const http = require('http')
const methods = require('methods')
function Application() {}
// 动态挂载 HTTP 方法到原型(get/post/put/delete...)
methods.forEach(method => {
Application.prototype[method] = function (path, ...args) {
this._lazyRouter()
this.router[method](path, args)
}
})
// 懒加载 Router(首次调用时才创建)
Application.prototype._lazyRouter = function () {
if (!this.router) {
this.router = new Router()
}
}
// 中间件注册
Application.prototype.use = function (...args) {
this._lazyRouter()
this.router.use(...args)
}
// 启动服务
Application.prototype.listen = function (...args) {
this._lazyRouter()
const server = http.createServer((req, res) => {
const done = () => {
res.statusCode = 404
res.end('Cannot ' + req.method + ' ' + req.url)
}
this.router.handler(req, res, done)
})
server.listen(...args)
}要点:
methods模块提供所有 HTTP 方法名,通过forEach批量挂载- Router 采用懒加载,避免未使用时浪费资源
http.createServer的回调直接委托给Router.handler
Router
js
const url = require('url')
const Route = require('./route')
const Layer = require('./layer')
function Router() {
this.stack = []
}
// 注册路由:创建 Route + 外层 Layer
Router.prototype.route = function (path) {
const route = new Route(path)
const layer = new Layer(path, route.dispatch.bind(route))
layer.route = route
this.stack.push(layer)
return route
}
// 中间件:创建无 route 的 Layer(前缀匹配)
Router.prototype.use = function (...args) {
let path = '/'
let handlers = args
if (typeof args[0] !== 'function') {
path = args[0]
handlers = args.slice(1)
}
handlers.forEach(handler => {
const layer = new Layer(path, handler)
layer.route = undefined // 标记为中间件
this.stack.push(layer)
})
}
// 请求分发:遍历 stack,逐个匹配
Router.prototype.handler = function (req, res, done) {
const { pathname } = url.parse(req.url, true)
const requestMethod = req.method.toLowerCase()
let i = 0
const next = () => {
if (i >= this.stack.length) return done()
const layer = this.stack[i++]
if (!layer.match(pathname)) return next()
// 提取路径参数
req.params = Object.assign(req.params || {}, layer.getParams())
if (!layer.route) {
// 中间件:直接执行,传入 next 继续链
layer.handler(req, res, next)
} else {
// 路由:检查 HTTP 方法是否匹配
if (layer.route.methods[requestMethod]) {
layer.handler(req, res, next)
} else {
next()
}
}
}
next()
}要点:
stack是核心数据结构,存储所有 Layer- 中间件和路由共用同一个 stack,按注册顺序执行
next()递归调用实现中间件链- 路径参数通过
layer.getParams()提取并挂到req.params
Layer
js
function Layer(path, handler) {
this.path = path
this.handler = handler
}
Layer.prototype.match = function (pathname) {
// 中间件:前缀匹配
if (!this.route) {
if (this.path === '/') return true
return pathname.startsWith(this.path + '/') || pathname === this.path
}
// 路由:精确匹配(支持 :param)
return this._matchPath(pathname)
}
// 路径匹配 + 参数提取
Layer.prototype._matchPath = function (pathname) {
const routePath = this.path
if (!routePath.includes(':')) {
return routePath === pathname
}
// 将 :param 转为正则捕获组
const keys = []
const pattern = routePath.replace(/:([^/]+)/g, (_, key) => {
keys.push(key)
return '([^/]+)'
})
const regex = new RegExp('^' + pattern + '$')
const match = pathname.match(regex)
if (!match) return false
this._params = {}
keys.forEach((key, i) => {
this._params[key] = match[i + 1]
})
return true
}要点:
- 中间件用前缀匹配(
/api匹配/api/users) - 路由用精确匹配(
/user/:id只匹配/user/42) :param通过正则捕获组提取参数值
Route
js
const Layer = require('./layer')
const methods = require('methods')
function Route(path) {
this.stack = []
this.path = path
this.methods = {}
}
// 动态挂载 HTTP 方法
methods.forEach(method => {
Route.prototype[method] = function (...handlers) {
handlers.flat().forEach(handler => {
const layer = new Layer(this.path, handler)
layer.method = method
this.methods[method] = true
this.stack.push(layer)
})
}
})
// 分发请求:遍历 route.stack 匹配 method
Route.prototype.dispatch = function (req, res, out) {
let i = 0
const next = () => {
if (i >= this.stack.length) return out()
const layer = this.stack[i++]
if (layer.method === req.method.toLowerCase()) {
layer.handler(req, res, next)
} else {
next()
}
}
next()
}要点:
- Route 内部也有
stack,存储同一路径下不同方法的 handler methods字段记录该路由支持哪些 HTTP 方法dispatch是外层 Router 调用的入口,负责方法级分发
请求处理流程
以 GET /user/42 为例:
1. http.createServer 收到请求
2. → Router.handler(req, res, done)
3. → 遍历 stack[0]: Layer('/', middleware)
4. → match('/') → true(中间件前缀匹配)
5. → 执行 middleware(req, res, next) → 打印日志 → next()
6. → 遍历 stack[1]: Layer('/', route.dispatch)
7. → match('/user/42') → true(路由精确匹配)
8. → 检查 route.methods['get'] → 存在
9. → 执行 route.dispatch(req, res, next)
10. → 遍历 route.stack,找到 method='get' 的 Layer
11. → layer.getParams() → { id: '42' }
12. → 执行 handler(req, res) → res.end('👤 User 页面')关键设计
为什么需要 Layer 和 Route 两层 stack?
Router.stack = [
Layer { path: '/', handler: middleware }, ← 中间件
Layer { path: '/', route: Route { ← 路由
stack: [
Layer { method: 'get', handler: fn1 },
Layer { method: 'get', handler: fn2 }
]
}}
]- 外层 Router.stack:按注册顺序混合存储中间件和路由
- 内层 Route.stack:存储同一路径下不同方法的 handler
- 这种设计让
app.get('/', fn1, fn2)的多个 handler 能按顺序执行
中间件 vs 路由的区别
| 特征 | 中间件(use) | 路由(get/post) |
|---|---|---|
| Layer.route | undefined | 有 Route 实例 |
| 匹配方式 | 前缀匹配 | 精确匹配 |
| 方法检查 | 不检查 | 检查 HTTP method |
| 典型用途 | 日志、鉴权、CORS | 业务处理 |