Node.js 路由
手写路由表、路径参数、静态文件服务、404 与 405。
Key Points路由 = 把 `method + path` 映射到处理函数;用 `URL` 解析路径,用正则提取参数。
路由表
JavaScript
const http = require('node:http');
const routes = [
{ method: 'GET', pattern: /^\/$/, handler: (req, res) => res.end('首页') },
{ method: 'GET', pattern: /^\/users\/(\d+)$/, handler: (req, res, id) => res.end('用户 ' + id) },
{ method: 'GET', pattern: /^\/posts\/([\w-]+)$/, handler: (req, res, slug) => res.end('文章 ' + slug) },
];
const server = http.createServer((req, res) => {
const { pathname } = new URL(req.url, `http://${req.headers.host}`);
for (const r of routes) {
const m = pathname.match(r.pattern);
if (!m) continue;
if (r.method !== req.method) {
res.writeHead(405, { Allow: r.method });
return res.end('405 Method Not Allowed');
}
return r.handler(req, res, ...m.slice(1));
}
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('404 Not Found');
});
server.listen(3000);Output
$ curl http://127.0.0.1:3000/users/42
用户 42
$ curl -X POST http://127.0.0.1:3000/users/42
405 Method Not Allowed
$ curl http://127.0.0.1:3000/nope
404 Not Found静态文件
JavaScript
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(process.cwd(), 'public');
const TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.svg': 'image/svg+xml',
};
function serveStatic(pathname, res) {
// 关键:防止 ../ 穿越到项目外
const file = path.join(ROOT, path.normalize(pathname));
if (!file.startsWith(ROOT)) {
res.writeHead(403);
return res.end('403');
}
fs.stat(file, (err, st) => {
if (err || !st.isFile()) {
res.writeHead(404);
return res.end('404');
}
res.writeHead(200, {
'Content-Type': TYPES[path.extname(file)] || 'application/octet-stream',
'Content-Length': st.size,
});
fs.createReadStream(file).pipe(res);
});
}path.join(ROOT, pathname) does not block ../../etc/passwd — you must verify the result still starts with ROOT. This is the classic static-file vulnerability.用 Express 的样子
JavaScript
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('首页'));
app.get('/users/:id', (req, res) => res.send('用户 ' + req.params.id));
app.use(express.static('public'));
app.use((req, res) => res.status(404).send('404'));
app.listen(3000);Express packages all of the above — pattern matching, param extraction, static dirs, 404 fallback. Writing it by hand once matters because then you know what it does when things break.
路由表
JavaScript
const http = require('node:http');
const routes = [
{ method: 'GET', pattern: /^\/$/, handler: (req, res) => res.end('首页') },
{ method: 'GET', pattern: /^\/users\/(\d+)$/, handler: (req, res, id) => res.end('用户 ' + id) },
{ method: 'GET', pattern: /^\/posts\/([\w-]+)$/, handler: (req, res, slug) => res.end('文章 ' + slug) },
];
const server = http.createServer((req, res) => {
const { pathname } = new URL(req.url, `http://${req.headers.host}`);
for (const r of routes) {
const m = pathname.match(r.pattern);
if (!m) continue;
if (r.method !== req.method) {
res.writeHead(405, { Allow: r.method });
return res.end('405 Method Not Allowed');
}
return r.handler(req, res, ...m.slice(1));
}
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('404 Not Found');
});
server.listen(3000);Output
$ curl http://127.0.0.1:3000/users/42
用户 42
$ curl -X POST http://127.0.0.1:3000/users/42
405 Method Not Allowed
$ curl http://127.0.0.1:3000/nope
404 Not Found静态文件
JavaScript
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(process.cwd(), 'public');
const TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.svg': 'image/svg+xml',
};
function serveStatic(pathname, res) {
// 关键:防止 ../ 穿越到项目外
const file = path.join(ROOT, path.normalize(pathname));
if (!file.startsWith(ROOT)) {
res.writeHead(403);
return res.end('403');
}
fs.stat(file, (err, st) => {
if (err || !st.isFile()) {
res.writeHead(404);
return res.end('404');
}
res.writeHead(200, {
'Content-Type': TYPES[path.extname(file)] || 'application/octet-stream',
'Content-Length': st.size,
});
fs.createReadStream(file).pipe(res);
});
}path.join(ROOT, pathname) 不会自动挡住 ../../etc/passwd,必须显式检查结果是否仍在 ROOT 内。这是静态文件服务最常见的漏洞。用 Express 的样子
JavaScript
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('首页'));
app.get('/users/:id', (req, res) => res.send('用户 ' + req.params.id));
app.use(express.static('public'));
app.use((req, res) => res.status(404).send('404'));
app.listen(3000);Express 把上面的模式、参数提取、静态目录、404 兜底都封装好了。手写一遍的意义在于:出问题时你知道它在做什么。