手写路由
本教程共 76 篇 · 第 28 篇 · 更新于 2026-07-25 · 约 8 分钟阅读
28. 手写路由
本节目标:不用框架手写一个路由系统,理解 Web 框架背后的原理。
Express 这样的框架帮我们省了很多事,但路由的本质并不神秘——它就是一个「根据方法和路径,找到对应处理函数」的映射表。这一章我们不用任何框架,从零写一个能跑的路由系统,顺带理解框架底层是怎么干活的。
最朴素的路由:if/else
如果你只有两三个接口,if/else 完全够用:
import http from 'node:http';
const server = http.createServer((req, res) => {
const parsed = new URL(req.url, `http://${req.headers.host}`);
if (req.method === 'GET' && parsed.pathname === '/') {
res.end('Home');
} else if (req.method === 'GET' && parsed.pathname === '/about') {
res.end('About');
} else if (req.method === 'GET' && parsed.pathname === '/users') {
res.end('User list');
} else {
res.statusCode = 404;
res.end('Not Found');
}
});
server.listen(3000);
接口一多,这套写法就崩了。代码变成「箭头形」缩进,改一个路由要在一堆条件里翻来找去。我们需要把「路由定义」和「处理逻辑」解耦。
抽一个 Router 类
路由系统的核心结构是一张表,每条记录存着方法、路径模式和处理函数。请求进来时,按顺序匹配,命中就执行。
import http from 'node:http';
class Router {
constructor() {
this.routes = [];
}
get(path, handler) {
this.routes.push({ method: 'GET', path, handler });
}
post(path, handler) {
this.routes.push({ method: 'POST', path, handler });
}
put(path, handler) {
this.routes.push({ method: 'PUT', path, handler });
}
del(path, handler) {
this.routes.push({ method: 'DELETE', path, handler });
}
// 把路由表转成请求处理函数
handler() {
return (req, res) => {
const parsed = new URL(req.url, `http://${req.headers.host}`);
const route = this.routes.find(r => {
return r.method === req.method && r.path === parsed.pathname;
});
if (route) {
route.handler(req, res, parsed);
} else {
res.statusCode = 404;
res.end('Not Found');
}
};
}
}
用起来已经很接近 Express 了:
const router = new Router();
router.get('/', (req, res) => {
res.end('Home');
});
router.get('/about', (req, res) => {
res.end('About');
});
router.post('/users', (req, res) => {
res.statusCode = 201;
res.end('User created');
});
const server = http.createServer(router.handler());
server.listen(3000);
支持动态路由参数
/users/123 和 /users/456 应该匹配同一条路由,123 和 456 作为参数提取出来。我们需要把路径字符串转成能匹配模式的东西。
最简单的方案是用正则:把 :id 这种占位符替换成 ([^/]+),然后生成 RegExp。
function pathToRegex(path) {
const pattern = path.replace(/:([^/]+)/g, '([^/]+)');
const keys = [];
path.replace(/:([^/]+)/g, (_, key) => keys.push(key));
return {
regex: new RegExp(`^${pattern}$`),
keys,
};
}
改造后的 Router:
class Router {
constructor() {
this.routes = [];
}
add(method, path, handler) {
const { regex, keys } = pathToRegex(path);
this.routes.push({ method, path, regex, keys, handler });
}
get(path, handler) { this.add('GET', path, handler); }
post(path, handler) { this.add('POST', path, handler); }
put(path, handler) { this.add('PUT', path, handler); }
del(path, handler) { this.add('DELETE', path, handler); }
handler() {
return (req, res) => {
const parsed = new URL(req.url, `http://${req.headers.host}`);
for (const route of this.routes) {
if (route.method !== req.method) continue;
const match = parsed.pathname.match(route.regex);
if (match) {
// 提取参数
const params = {};
route.keys.forEach((key, i) => {
params[key] = match[i + 1];
});
// 把 params 挂到 req 上,方便 handler 使用
req.params = params;
route.handler(req, res, parsed);
return;
}
}
res.statusCode = 404;
res.end('Not Found');
};
}
}
现在可以写带参数的路由了:
router.get('/users/:id', (req, res) => {
res.end(`User ID: ${req.params.id}`);
});
router.get('/posts/:postId/comments/:commentId', (req, res) => {
res.end(`Post ${req.params.postId}, Comment ${req.params.commentId}`);
});
加上查询参数和 body 支持
路由系统再完善一点,把 URLSearchParams 挂到 req.query,再提供一个收集 body 的辅助方法:
class Router {
// ...前面的代码不变
handler() {
return async (req, res) => {
const parsed = new URL(req.url, `http://${req.headers.host}`);
req.query = parsed.searchParams;
for (const route of this.routes) {
if (route.method !== req.method) continue;
const match = parsed.pathname.match(route.regex);
if (match) {
req.params = {};
route.keys.forEach((key, i) => {
req.params[key] = match[i + 1];
});
// 如果 handler 返回 Promise,自动 await
await Promise.resolve(route.handler(req, res, parsed));
return;
}
}
res.statusCode = 404;
res.end('Not Found');
};
}
}
Note这里我把
handler()的返回函数改成了async,因为后面处理 POST 请求时 handler 里可能要awaitbody。Node.js 的http.createServer支持异步回调, Promise 里的异常不会逃逸,但如果你的 handler 可能抛错,最好还是包一层try/catch。
完整可运行的示例
下面是一个带 CRUD 的「用户管理」服务器,全部基于上面写的 Router,零依赖:
import http from 'node:http';
// ---------- 路由工具 ----------
function pathToRegex(path) {
const keys = [];
const pattern = path.replace(/:([^/]+)/g, (_, key) => {
keys.push(key);
return '([^/]+)';
});
return { regex: new RegExp(`^${pattern}$`), keys };
}
class Router {
constructor() {
this.routes = [];
}
add(method, path, handler) {
const { regex, keys } = pathToRegex(path);
this.routes.push({ method, regex, keys, handler });
}
get(path, handler) { this.add('GET', path, handler); }
post(path, handler) { this.add('POST', path, handler); }
put(path, handler) { this.add('PUT', path, handler); }
del(path, handler) { this.add('DELETE', path, handler); }
handler() {
return async (req, res) => {
const parsed = new URL(req.url, `http://${req.headers.host}`);
req.query = parsed.searchParams;
for (const route of this.routes) {
if (route.method !== req.method) continue;
const match = parsed.pathname.match(route.regex);
if (!match) continue;
req.params = {};
route.keys.forEach((k, i) => (req.params[k] = match[i + 1]));
try {
await Promise.resolve(route.handler(req, res, parsed));
} catch (err) {
console.error(err);
if (!res.headersSent) {
res.statusCode = 500;
res.end('Internal Server Error');
}
}
return;
}
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Not Found' }));
};
}
}
// ---------- body 收集工具 ----------
function collectBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
req.on('error', reject);
});
}
// ---------- 内存数据层 ----------
let users = [
{ id: '1', name: 'Tom', email: 'tom@example.com' },
{ id: '2', name: 'Jerry', email: 'jerry@example.com' },
];
function nextId() {
return String(Math.max(...users.map(u => Number(u.id)), 0) + 1);
}
// ---------- 定义路由 ----------
const router = new Router();
// 列出用户(支持 ?name=xxx 过滤)
router.get('/users', (req, res) => {
let result = users;
const nameFilter = req.query.get('name');
if (nameFilter) {
result = users.filter(u => u.name.includes(nameFilter));
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(result));
});
// 获取单个用户
router.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(user));
});
// 创建用户
router.post('/users', async (req, res) => {
const body = await collectBody(req);
let data;
try {
data = JSON.parse(body);
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
return;
}
const user = { id: nextId(), name: data.name, email: data.email };
users.push(user);
res.statusCode = 201;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(user));
});
// 更新用户
router.put('/users/:id', async (req, res) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
const body = await collectBody(req);
let data;
try {
data = JSON.parse(body);
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
return;
}
Object.assign(user, data);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(user));
});
// 删除用户
router.del('/users/:id', (req, res) => {
const idx = users.findIndex(u => u.id === req.params.id);
if (idx === -1) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
users.splice(idx, 1);
res.statusCode = 204;
res.end();
});
// ---------- 启动服务器 ----------
const server = http.createServer(router.handler());
server.listen(3000, () => {
console.log('API server at http://localhost:3000');
console.log('Routes:');
console.log(' GET /users');
console.log(' GET /users/:id');
console.log(' POST /users');
console.log(' PUT /users/:id');
console.log(' DELETE /users/:id');
});
测试命令:
# 列出所有用户
curl http://localhost:3000/users
# 按名字过滤
curl "http://localhost:3000/users?name=Tom"
# 创建用户
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# 更新用户
curl -X PUT http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Tommy"}'
# 删除用户
curl -X DELETE http://localhost:3000/users/1
这个路由系统还能往哪扩
现在你已经有了一个 80 行的核心路由。如果想继续完善,可以考虑这几个方向:
- 中间件:在
handler()里加一组beforeHandlers,匹配路由前先依次执行,用来做日志、CORS、鉴权。 - 子路由:支持
router.use('/api', subRouter),把路由按模块拆分。 - 通配符:把
*匹配加进去,做 404 兜底或全局拦截。
不过真到了那一步,也许就是该引入 Express 或 Fastify 的时候了。自己写路由最大的价值不是取代框架,而是理解框架背后的设计思路。