首页 / Node.js 教程 / 手写路由

Node.js 教程

手写路由

本教程共 76 篇 · 第 28 篇 · 更新于 2026-07-25 · 约 8 分钟阅读

Node.js路由HTTP框架原理手写

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 应该匹配同一条路由,123456 作为参数提取出来。我们需要把路径字符串转成能匹配模式的东西。

最简单的方案是用正则:把 :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 里可能要 await body。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 行的核心路由。如果想继续完善,可以考虑这几个方向:

  1. 中间件:在 handler() 里加一组 beforeHandlers,匹配路由前先依次执行,用来做日志、CORS、鉴权。
  2. 子路由:支持 router.use('/api', subRouter),把路由按模块拆分。
  3. 通配符:把 * 匹配加进去,做 404 兜底或全局拦截。

不过真到了那一步,也许就是该引入 Express 或 Fastify 的时候了。自己写路由最大的价值不是取代框架,而是理解框架背后的设计思路。