Skip to content

手写 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.jsApplication 类:use / get / post / listen
lib/router/index.jsRouter:路由注册 + 请求分发 + 中间件链
lib/router/layer.jsLayer:路径匹配(支持 :param
lib/router/route.jsRoute:按 HTTP 方法分发到具体 handler

交互演示

服务运行在 https://api.jaogo.info,通过 pnpm devpnpm 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.routeundefined有 Route 实例
匹配方式前缀匹配精确匹配
方法检查不检查检查 HTTP method
典型用途日志、鉴权、CORS业务处理