Node.js 路由

手写路由表、路径参数、静态文件服务、404 与 405。

Intermediate
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.

Friends & Partners