五:处理不同的请求——路由入门

一、回顾与本篇目标

上一篇我们学会了用 request.urlrequest.method 来区分不同的请求,用 if...else 返回不同的内容。这已经是一个最原始的路由实现了。

但那个实现有两个明显的问题:

  1. 代码会越来越臃肿:如果网站有几十个页面,就要写几十个 if...else 分支,代码会变得又长又难维护。
  2. 查询参数要手动解析request.url 返回的是原始字符串(如 /user?name=张三&age=28),要拿到 name 的值,你得自己写代码去截取和拆分。

本篇的目标就是解决这两个问题:

  1. 学会用更清晰的方式组织路由逻辑。
  2. 学会解析查询参数——URL 中 ? 后面的部分。
  3. 学会解析路径参数——URL 路径中动态变化的部分(如 /user/123 中的 123)。
  4. 认识 Postman——一个专门用来测试后端 API 的工具。

二、什么是路由

路由这个词听起来很高端,其实它的意思很简单:根据请求的方法和路径,决定由哪段代码来处理这个请求

你在生活中每天都在接触类似的概念:

  • 打客服电话,“查询话费请按 1,办理业务请按 2,人工服务请按 0”——这就是语音菜单的路由。不同的按键(路径)对应不同的处理流程。
  • 去政府办事大厅,取号之后显示屏告诉你“请到 3 号窗口”——这就是窗口路由。不同的业务类型(请求方法)对应不同的窗口(处理函数)。

在后端开发中,路由就是:

  • GET /users → 调用“获取用户列表”的函数
  • GET /users/123 → 调用“获取 ID 为 123 的用户”的函数
  • POST /users → 调用“创建新用户”的函数
  • DELETE /users/123 → 调用“删除 ID 为 123 的用户”的函数

三、查询参数:URL 中问号后面的部分

打开任何一个网站,你经常会在地址栏看到这样的 URL:

https://www.example.com/search?keyword=手机&page=2&sort=price

这个 URL 可以分为两部分:

  • /search:路径,告诉服务器“我要用搜索功能”。
  • ?keyword=手机&page=2&sort=price:查询参数,告诉服务器“搜索的关键词是手机,显示第 2 页,按价格排序”。

查询参数的格式是:?键1=值1&键2=值2&键3=值3

  • ? 表示查询参数的开始。
  • & 用来分隔多个键值对。
  • = 左边是参数名,右边是参数值。

在 Node.js 中解析查询参数

Node.js 内置的 url 模块可以帮你自动解析查询参数,不需要手动截取字符串。

// query-demo.js —— 解析查询参数

const http = require('http');
const url = require('url');  // 内置模块,专门处理 URL

const server = http.createServer(function (request, response) {
  // 用 url.parse 解析 request.url
  // 第二个参数 true 表示把查询参数自动转成对象
  let parsedUrl = url.parse(request.url, true);

  let pathname = parsedUrl.pathname;    // 路径部分(如 /search)
  let query = parsedUrl.query;          // 查询参数对象

  console.log('路径:' + pathname);
  console.log('查询参数:', query);

  if (pathname === '/search') {
    let keyword = query.keyword || '未提供关键词';
    let page = query.page || 1;
    response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('你搜索的关键词是:' + keyword + ',页码:' + page);
  } else {
    response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('404 - 页面未找到');
  }
});

server.listen(3000, function () {
  console.log('服务已启动,试试访问:');
  console.log('  http://localhost:3000/search?keyword=手机&page=2');
  console.log('  http://localhost:3000/search?keyword=电脑');
  console.log('  http://localhost:3000/search');
});

逐行解释:

  • url.parse(request.url, true):把 request.url(原始字符串)解析成一个对象。第二个参数 true 表示把查询参数也解析成对象。
  • parsedUrl.pathname:路径部分。例如 URL 是 /search?keyword=手机,pathname 就是 '/search'
  • parsedUrl.query:查询参数对象。例如 URL 是 /search?keyword=手机&page=2,query 就是 { keyword: '手机', page: '2' }
  • 注意:查询参数的值永远是字符串query.page'2'(字符串),不是 2(数字)。如果需要做数学运算,要用 Number()parseInt() 转换。

分别访问上面终端提示的三个地址,观察返回结果。第三个地址没有提供查询参数,query.keywordundefined,所以代码里用了 || 来设置默认值。

四、路径参数:URL 中动态变化的部分

查询参数适合传递可选的、辅助性的信息(关键词、页码、排序方式)。但有一种场景查询参数就不太合适了——表示资源标识的时候。

比如你要获取 ID 为 123 的用户信息。用查询参数写是:

/users?id=123

这种写法虽然能工作,但不够直观。RESTful API 的惯例是用路径参数

/users/123

123 直接嵌在路径里,成为路径的一部分。这个 123 就是一个路径参数,它的值会根据请求而变化——/users/123 获取用户 123,/users/456 获取用户 456。

路径参数的解析稍微复杂一点,因为 Node.js 内置的 url 模块不会自动识别它。我们需要自己写代码来匹配和提取。

手动解析路径参数

// path-param-demo.js —— 手动解析路径参数

const http = require('http');
const url = require('url');

const server = http.createServer(function (request, response) {
  let parsedUrl = url.parse(request.url, true);
  let pathname = parsedUrl.pathname;  // 如 /user/123

  // 检查路径是否匹配 /user/数字 的模式
  // 用 startsWith 判断是否以 /user/ 开头
  if (pathname.startsWith('/user/')) {
    // 截取 /user/ 后面的部分,就是用户 ID
    let userId = pathname.replace('/user/', '');

    response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('你请求的是用户 ID:' + userId);
  } else if (pathname === '/users') {
    response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('用户列表');
  } else {
    response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('404 - 页面未找到');
  }
});

server.listen(3000, function () {
  console.log('服务已启动,试试访问:');
  console.log('  http://localhost:3000/user/123');
  console.log('  http://localhost:3000/user/456');
  console.log('  http://localhost:3000/users');
});

解析逻辑

  • pathname.startsWith('/user/'):判断路径是否以 /user/ 开头。
  • pathname.replace('/user/', ''):把 /user/ 替换成空字符串,剩下的就是用户 ID。

这种方法能用,但在复杂的场景下(比如多个路径参数、可选参数)手动解析会变得非常繁琐。后面我们学 Express 框架的时候,它会用更优雅的方式自动处理路径参数。现在先用这个简单的方法理解原理。

五、把路由逻辑拆成一个独立模块

在上一篇里,我们把“处理请求的函数”拆成了 handler.js。但 handler.js 里如果写几十个 if...else,仍然很难维护。

更好的做法是:把路由的定义集中管理,每个路由对应一个处理函数。这是一种简单但实用的“路由表”模式。

// router.js —— 路由模块

const url = require('url');

// 路由表:路径 → 处理函数
const routes = {};

// 注册路由的函数
function addRoute(method, path, handler) {
  let key = method + ' ' + path;
  routes[key] = handler;
}

// 处理请求的函数(交给 createServer 的回调使用)
function handleRequest(request, response) {
  let parsedUrl = url.parse(request.url, true);
  let pathname = parsedUrl.pathname;
  let method = request.method;
  let query = parsedUrl.query;

  // 查找路由表
  let key = method + ' ' + pathname;
  let handler = routes[key];

  if (handler) {
    // 找到了对应的处理函数,调用它
    handler(request, response, query, pathname);
  } else {
    // 没找到,返回 404
    response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('404 - 页面未找到');
  }
}

module.exports = {
  addRoute: addRoute,
  handleRequest: handleRequest
};

这个模块做了三件事:

  1. routes 对象存储路由表。键是 "方法 路径"(如 "GET /"),值是对应的处理函数。
  2. addRoute 用来注册路由。
  3. handleRequest 是给 createServer 用的回调函数。它在路由表中查找匹配的处理函数,找到就调用,找不到就返回 404。

使用这个路由模块的主文件:

// app.js —— 主入口

const http = require('http');
const router = require('./router.js');

// 注册路由:首页
router.addRoute('GET', '/', function (request, response) {
  response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  response.end('欢迎来到首页!');
});

// 注册路由:关于页面
router.addRoute('GET', '/about', function (request, response) {
  response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  response.end('这是关于页面。');
});

// 注册路由:搜索(使用查询参数)
router.addRoute('GET', '/search', function (request, response, query) {
  let keyword = query.keyword || '未提供关键词';
  response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  response.end('搜索关键词:' + keyword);
});

// 注册路由:用户详情(路径参数)
// 注意:这里 /user/* 的匹配逻辑需要处理函数内部自己解析
router.addRoute('GET', '/user-detail', function (request, response, query, pathname) {
  // 这个路由通过查询参数传递用户 ID:/user-detail?id=123
  let userId = query.id || '未知';
  response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  response.end('用户 ID:' + userId);
});

// 创建服务,使用路由模块的处理函数
const server = http.createServer(router.handleRequest);

server.listen(3000, function () {
  console.log('服务已启动,试试访问:');
  console.log('  http://localhost:3000/');
  console.log('  http://localhost:3000/about');
  console.log('  http://localhost:3000/search?keyword=JavaScript');
  console.log('  http://localhost:3000/user-detail?id=123');
});

现在路由的注册非常清晰:一行 addRoute,对应一个路径的处理逻辑。增加新路由只需要加一行注册代码,不需要去翻 if...else 的大长链。

六、认识 Postman:不用浏览器也能测试 API

到目前为止,我们测试后端接口的方式都是在浏览器地址栏输入 URL,然后看页面返回了什么。但这种方式有两个局限:

  • 只能发 GET 请求:浏览器的地址栏只能触发 GET。要测试 POST 请求,你得自己写一个 HTML 表单。
  • 查看响应信息不方便:想看状态码、响应头,得打开 F12 开发者工具。想格式化 JSON 响应,还得装插件。

Postman 就是专门解决这些问题的工具。它是一个独立的应用程序,可以模拟浏览器发送任何类型的 HTTP 请求,并把请求和响应的信息展示得清清楚楚。

安装 Postman

访问 https://www.postman.com/downloads/,下载对应你操作系统的版本,安装。免费版完全够用,不需要付费。

发送你的第一个 GET 请求

  1. 打开 Postman。
  2. 确保左上角的请求方法选的是 GET
  3. 在旁边的地址栏输入 http://localhost:3000/(确保你的服务在运行)。
  4. 点击蓝色的 Send 按钮。
  5. 下方的 Response 区域会显示服务器返回的内容。你还能看到状态码(200 OK)、响应时间、响应体的大小等信息。

发送带查询参数的 GET 请求

在 Postman 地址栏旁边有一个 Params 标签。点击它,你会看到一个表格。在表格里填写键值对:

  • KEY:keyword,VALUE:JavaScript
  • KEY:page,VALUE:1

Postman 会自动把这些参数拼到 URL 后面。点击 Send,效果和你在浏览器访问 /search?keyword=JavaScript&page=1 完全一样。

发送 POST 请求

  1. 把请求方法从 GET 改为 POST
  2. 地址栏输入 http://localhost:3000/register(使用上一篇的 method-demo.js)。
  3. 在下方选择 Body 标签。
  4. 选择 x-www-form-urlencoded(这是表单提交的格式)。
  5. 在表格里填写键值对:username: 张三password: 123456
  6. 点击 Send,查看服务器返回的响应。

Postman 会是你后端开发中使用频率最高的工具之一。以后每写一个新的接口,就先用 Postman 测试通过,再让前端调用。

七、本篇综合演示:一个带有路由的完整服务

下面这个完整示例整合了本篇学到的所有内容——路由模块、查询参数、路径参数、GET 和 POST 的处理。

项目结构:

项目文件夹/
├── app.js          ← 主入口,注册路由并启动服务
├── router.js       ← 路由模块

router.js(和上面第三节的一样,不再重复贴)

app.js(完整版):

// app.js —— 完整的路由示例

const http = require('http');
const router = require('./router.js');

// ========== 注册路由 ==========

// 首页
router.addRoute('GET', '/', function (request, response) {
  response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  response.end(`
    <h1>欢迎来到我的后端服务</h1>
    <ul>
      <li><a href="/about">关于页面</a></li>
      <li><a href="/search?keyword=Node.js">搜索示例</a></li>
      <li><a href="/user?id=123">用户详情(查询参数)</a></li>
    </ul>
  `);
});

// 关于页面
router.addRoute('GET', '/about', function (request, response) {
  response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  response.end('<h2>关于本站</h2><p>这是一个学习后端开发的示例项目。</p>');
});

// 搜索(使用查询参数)
router.addRoute('GET', '/search', function (request, response, query) {
  let keyword = query.keyword || '未提供关键词';
  let page = query.page || '1';
  response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  response.end('搜索关键词:' + keyword + '\n页码:' + page);
});

// 用户详情(使用查询参数传递 ID)
router.addRoute('GET', '/user', function (request, response, query) {
  let userId = query.id;
  if (!userId) {
    response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('错误:缺少用户 ID 参数。用法:/user?id=123');
    return;
  }
  // 模拟从数据库查找用户
  response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
  response.end(JSON.stringify({
    id: userId,
    name: '用户' + userId,
    email: 'user' + userId + '@example.com'
  }));
});

// 注册(GET 返回表单,POST 处理提交)
router.addRoute('GET', '/register', function (request, response) {
  response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  response.end(`
    <h2>用户注册</h2>
    <form method="POST" action="/register">
      <input name="username" placeholder="用户名" /><br/><br/>
      <input name="email" placeholder="邮箱" /><br/><br/>
      <button type="submit">注册</button>
    </form>
  `);
});

router.addRoute('POST', '/register', function (request, response) {
  // POST 请求的数据在请求体中,需要手动读取
  let body = '';
  request.on('data', function (chunk) {
    body += chunk;  // 拼接数据块
  });
  request.on('end', function () {
    // 数据读取完毕,body 是原始字符串如 "username=张三&email=test@example.com"
    response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('注册成功!收到的数据:\n' + body);
  });
});

// ========== 启动服务 ==========
const server = http.createServer(router.handleRequest);

server.listen(3000, function () {
  console.log('服务已启动!');
  console.log('  首页:      http://localhost:3000/');
  console.log('  搜索:      http://localhost:3000/search?keyword=test');
  console.log('  用户详情:  http://localhost:3000/user?id=123');
  console.log('  注册页面:  http://localhost:3000/register');
});

代码中值得注意的新知识:

  • 返回 JSON/user 路由返回了 application/json 类型的数据。用 JSON.stringify() 把 JavaScript 对象转成 JSON 字符串。
  • 读取 POST 请求体:POST 的数据在请求体中,不是一次性到达的,而是分成数据块(chunk)陆续到达。需要监听 requestdata 事件来收集数据,监听 end 事件来处理完整数据。这是 Node.js 原生处理 POST 请求体的标准方式。
  • 状态码 400:当请求缺少必要参数时,返回 400 Bad Request,告诉客户端“你发的请求有问题”。

八、本篇动手练习

练习 1:增加新路由

app.js 里增加两个新路由:

  • GET /contact → 返回一段联系方式的 HTML。
  • GET /api/time → 返回当前服务器时间的 JSON({"time":"2025-03-15T10:30:00.000Z"})。

练习 2:处理更多查询参数

修改 /search 路由,让它支持三个查询参数:keyword(关键词)、page(页码)、limit(每页条数)。全部有默认值,如果没传就用默认值。用 Postman 测试不同参数组合。

练习 3:用 Postman 测试 POST 请求

确保第五节的完整示例正在运行。打开 Postman,发一个 POST 请求到 /register,Body 选 x-www-form-urlencoded,填上 usernameemail。观察服务器返回的内容。

九、本篇小结

这一篇你学会了后端开发中最重要的概念之一——路由:

  • 路由的本质:根据请求的方法和路径,决定由哪段代码来处理。
  • 查询参数:URL 中 ? 后面的键值对(?keyword=手机&page=2),用 url.parse() 解析,适合传递可选的辅助信息。
  • 路径参数:嵌在 URL 路径中的动态值(/user/123 中的 123),需要手动用 startsWith 或正则匹配提取。适合标识资源。
  • 路由表模式:把路径和对应的处理函数集中管理,避免 if...else 长链。
  • Postman:测试 API 的专业工具,可以发送 GET、POST 等各种请求,清晰展示请求和响应的全部信息。
  • 读取 POST 请求体:通过监听 dataend 事件来接收客户端提交的数据。

到本篇为止,你已经能用 Node.js 原生代码写出一个支持多种路由、能处理查询参数和 POST 数据的 HTTP 服务了。虽然还有一些繁琐的地方(手动解析路径参数、手动读取 POST 请求体),但这些痛点正是下一篇要学 Express 框架时要解决的。

下一篇预告

下一篇——《Express 框架——让后端开发更简单》:Express 是目前 Node.js 生态中最流行的后端框架。它会帮你自动处理路由、自动解析查询参数和路径参数、自动读取请求体。原来需要几十行代码写的东西,用 Express 几行就能搞定。我们会用 Express 把前面写的所有功能全部重写一遍,让你直观感受“框架带来的效率提升”。

后端零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容