处理 GET 与 POST
本教程共 76 篇 · 第 27 篇 · 更新于 2026-07-25 · 约 6 分钟阅读
27. 处理 GET 与 POST
本节目标:解析查询字符串、读取 POST 请求体、处理表单和 JSON 数据。
上一章搭了一个能回显请求的服务器,但离「可用」还差一点:GET 请求的查询参数怎么拆?POST 过来的表单、JSON 又怎么读?这一章就把这两件事彻底搞懂。
解析 GET 查询参数
浏览器地址栏里那种 ?name=Tom&age=20 的字符串,在 Node.js 里最好用 WHATWG 标准的 URL 对象来解析。url.parse 是旧时代的产物,v24 里虽然还能跑,但官方已经不推荐,新项目别碰。
用 URL 构造函数解析
import http from 'node:http';
const server = http.createServer((req, res) => {
// req.url 只有路径和查询字符串,没有协议和主机
// 所以需要提供一个 base URL 来补全
const baseUrl = `http://${req.headers.host}`;
const parsed = new URL(req.url, baseUrl);
console.log('Pathname:', parsed.pathname);
console.log('Search:', parsed.search);
console.log('Name:', parsed.searchParams.get('name'));
res.end('Check console');
});
server.listen(3000);
URLSearchParams 提供了不少实用方法:
parsed.searchParams.get('name'); // 取单个值
parsed.searchParams.getAll('tag'); // 同名参数取全部
parsed.searchParams.has('age'); // 判断是否存在
parsed.searchParams.append('new', '1'); // 追加
parsed.searchParams.toString(); // 转回查询字符串
Tip如果查询参数里带中文或特殊符号,浏览器会自动 encode。
URLSearchParams在get()时会自动 decode,不用你手动decodeURIComponent。
一个实用的查询参数示例
下面这个服务器根据 ?type=json|html 返回不同格式的数据:
import http from 'node:http';
const server = http.createServer((req, res) => {
const parsed = new URL(req.url, `http://${req.headers.host}`);
if (parsed.pathname !== '/user') {
res.statusCode = 404;
res.end('Not Found');
return;
}
const type = parsed.searchParams.get('type') || 'json';
const name = parsed.searchParams.get('name') || 'Guest';
if (type === 'html') {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`<h1>Hello, ${name}</h1>`);
} else {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ name }));
}
});
server.listen(3000, () => {
console.log('Try: http://localhost:3000/user?name=Tom&type=html');
});
读取 POST 请求体
POST 的数据不在 URL 里,而是在请求体中。客户端通常会通过 Content-Type 头部告诉服务端 body 的格式,常见的有三种:
application/jsonapplication/x-www-form-urlencodedtext/plain
服务端的第一步,是把流里的二进制片段拼成完整的字符串。
通用 body 收集函数
我写了一个小技巧,几乎每个原生 HTTP 项目都会用到:
function collectBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
req.on('error', reject);
});
}
用 Buffer.concat 而不是字符串拼接,是为了避免多字节字符被截断导致的乱码。这个坑我踩过不止一次。
处理 JSON 数据
import http from 'node:http';
const server = http.createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/api/data') {
res.statusCode = 404;
res.end('Not Found');
return;
}
try {
const body = await collectBody(req);
const data = JSON.parse(body);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ received: data }));
} catch (err) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
server.listen(3000);
测试:
curl -X POST http://localhost:3000/api/data \
-H "Content-Type: application/json" \
-d '{"name":"Tom","age":20}'
处理表单数据(URL 编码)
HTML 表单默认以 application/x-www-form-urlencoded 提交,数据格式跟查询字符串一样:name=Tom&age=20。Node.js 内置了 querystring 模块来解析它。
import http from 'node:http';
import querystring from 'node:querystring';
const server = http.createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/submit') {
res.statusCode = 404;
res.end('Not Found');
return;
}
const body = await collectBody(req);
const data = querystring.parse(body);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`<p>收到:姓名 ${data.name},年龄 ${data.age}</p>`);
});
server.listen(3000);
querystring.parse 会尽量智能转换,但遇到复杂结构时它会把同名键变成数组。如果你需要更强大的表单解析(比如文件上传),后面章节会讲 multer,这里先掌握原生方案。
一个完整的表单示例
光讲 API 有点干,我们来做一个「能跑」的完整例子。服务端提供一个表单页面,用户填写后 POST 回来,服务端再展示结果。
import http from 'node:http';
import querystring from 'node:querystring';
function collectBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
req.on('error', reject);
});
}
const formHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>注册</title>
</head>
<body>
<h1>用户注册</h1>
<form method="POST" action="/register">
<p>用户名:<input name="username" required></p>
<p>邮箱:<input type="email" name="email" required></p>
<p><button type="submit">提交</button></p>
</form>
</body>
</html>
`;
const server = http.createServer(async (req, res) => {
if (req.method === 'GET' && req.url === '/') {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(formHtml);
return;
}
if (req.method === 'POST' && req.url === '/register') {
const body = await collectBody(req);
const data = querystring.parse(body);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`
<h1>注册成功</h1>
<p>用户名:${data.username}</p>
<p>邮箱:${data.email}</p>
<a href="/">返回</a>
`);
return;
}
res.statusCode = 404;
res.end('Not Found');
});
server.listen(3000, () => {
console.log('Open http://localhost:3000');
});
这个例子虽然简单,但已经包含了 GET 页面渲染、POST 表单接收、数据解析和 HTML 响应的完整链路。你完全可以在这个骨架上继续扩展。
关于 Content-Type 的注意事项
客户端发来的 Content-Type 可能带有 charset 后缀,比如 application/json; charset=utf-8。如果你需要判断类型,别用严格相等:
const contentType = req.headers['content-type'] || '';
if (contentType.startsWith('application/json')) {
// 解析 JSON
}
if (contentType.startsWith('application/x-www-form-urlencoded')) {
// 解析表单
}