一、回顾与本篇目标
上一篇我们学会了用 request.url 和 request.method 来区分不同的请求,用 if...else 返回不同的内容。这已经是一个最原始的路由实现了。
但那个实现有两个明显的问题:
- 代码会越来越臃肿:如果网站有几十个页面,就要写几十个
if...else分支,代码会变得又长又难维护。 - 查询参数要手动解析:
request.url返回的是原始字符串(如/user?name=张三&age=28),要拿到name的值,你得自己写代码去截取和拆分。
本篇的目标就是解决这两个问题:
- 学会用更清晰的方式组织路由逻辑。
- 学会解析查询参数——URL 中
?后面的部分。 - 学会解析路径参数——URL 路径中动态变化的部分(如
/user/123中的123)。 - 认识 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.keyword 是 undefined,所以代码里用了 || 来设置默认值。
四、路径参数: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
};
这个模块做了三件事:
- 用
routes对象存储路由表。键是"方法 路径"(如"GET /"),值是对应的处理函数。 addRoute用来注册路由。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 请求
- 打开 Postman。
- 确保左上角的请求方法选的是 GET。
- 在旁边的地址栏输入
http://localhost:3000/(确保你的服务在运行)。 - 点击蓝色的 Send 按钮。
- 下方的 Response 区域会显示服务器返回的内容。你还能看到状态码(200 OK)、响应时间、响应体的大小等信息。
发送带查询参数的 GET 请求
在 Postman 地址栏旁边有一个 Params 标签。点击它,你会看到一个表格。在表格里填写键值对:
- KEY:
keyword,VALUE:JavaScript - KEY:
page,VALUE:1
Postman 会自动把这些参数拼到 URL 后面。点击 Send,效果和你在浏览器访问 /search?keyword=JavaScript&page=1 完全一样。
发送 POST 请求
- 把请求方法从 GET 改为 POST。
- 地址栏输入
http://localhost:3000/register(使用上一篇的method-demo.js)。 - 在下方选择 Body 标签。
- 选择 x-www-form-urlencoded(这是表单提交的格式)。
- 在表格里填写键值对:
username: 张三、password: 123456。 - 点击 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)陆续到达。需要监听
request的data事件来收集数据,监听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,填上 username 和 email。观察服务器返回的内容。
九、本篇小结
这一篇你学会了后端开发中最重要的概念之一——路由:
- 路由的本质:根据请求的方法和路径,决定由哪段代码来处理。
- 查询参数:URL 中
?后面的键值对(?keyword=手机&page=2),用url.parse()解析,适合传递可选的辅助信息。 - 路径参数:嵌在 URL 路径中的动态值(
/user/123中的123),需要手动用startsWith或正则匹配提取。适合标识资源。 - 路由表模式:把路径和对应的处理函数集中管理,避免
if...else长链。 - Postman:测试 API 的专业工具,可以发送 GET、POST 等各种请求,清晰展示请求和响应的全部信息。
- 读取 POST 请求体:通过监听
data和end事件来接收客户端提交的数据。
到本篇为止,你已经能用 Node.js 原生代码写出一个支持多种路由、能处理查询参数和 POST 数据的 HTTP 服务了。虽然还有一些繁琐的地方(手动解析路径参数、手动读取 POST 请求体),但这些痛点正是下一篇要学 Express 框架时要解决的。
下一篇预告
下一篇——《Express 框架——让后端开发更简单》:Express 是目前 Node.js 生态中最流行的后端框架。它会帮你自动处理路由、自动解析查询参数和路径参数、自动读取请求体。原来需要几十行代码写的东西,用 Express 几行就能搞定。我们会用 Express 把前面写的所有功能全部重写一遍,让你直观感受“框架带来的效率提升”。
后端零基础入门,每周更新。













暂无评论内容