第4章 4.1 http 模块基础
⚠️ 课前必读:本系列是 Node.js 从入门到精通,不是 Python 系列哦!上一章「3.6 综合实战:并发爬虫」我们用 http 模块搭了爬虫骨架,这一章我们来彻底搞懂这个骨架的根——http 模块本身。学完你会发现,原来「上网」这件事在 Node.js 里就是几行代码的事。
🎯 为什么要学这个?
场景
周一早上,你打开浏览器输入 www.baidu.com,瞬间看到了百度首页。你有没有想过:这个页面是怎么「飞」到你电脑上的?
类比一下:这就像你去餐厅点餐。你喊了一声「服务员,来份宫保鸡丁」(发请求),服务员记下你的需求告诉厨房(路由处理),厨房做完菜服务员端给你(发响应)。HTTP 就是这套「点餐-做菜-上菜」的规则。
痛点
- 想做个「天气查询小工具」?得先能发起 HTTP 请求
- 想搭个「个人博客网站」?得先能接收用户请求
- 想做「后端 API」?HTTP 是地基
学完本文,你就能:用 Node.js 搭一个自己的 HTTP 服务器,像点外卖一样发请求、收响应。
🧱 基础 25 分钟:核心概念
什么是 HTTP?
HTTP(HyperText Transfer Protocol) = 超文本传输协议。说人话:一套规定「你怎么跟网站说话」的规则。
类比:就像打电话必须用普通话一样,你跟网站「说话」也必须用 HTTP 这套「普通话」。
http 模块是什么?
Node.js 自带的一个核心模块,让你能用代码当「浏览器」或当「网站服务器」。
两种模式:
- 客户端模式:发请求,像浏览器一样「点餐」
- 服务端模式:收请求,像餐厅一样「做菜」
第一个 HTTP 服务器(5 分钟)
const http = require('http');
// 创建一个「餐厅」—— 能接收顾客的请求
const server = http.createServer((req, res) => {
// req = 顾客点的菜(请求信息)
// res = 厨房返回的菜(响应)
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello, World!');
});
// 让餐厅开门营业,监听 3000 号门牌
server.listen(3000, () => {
console.log('服务器已启动,访问 http://localhost:3000');
});
保存为 server.js,运行:
node server.js
打开浏览器访问 http://localhost:3000,看到 Hello, World! 就成功了!
理解 req 和 res
req(请求对象) = 客人的「点菜单」
const server = http.createServer((req, res) => {
console.log('请求方法:', req.method); // GET / POST / PUT / DELETE
console.log('请求地址:', req.url); // /index.html /api/user
console.log('请求头:', req.headers); // 浏览器的个人信息
res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
res.end('收到请求了!');
});
res(响应对象) = 厨房的「出餐口」
const server = http.createServer((req, res) => {
// 1. 先告诉浏览器「我要上什么菜」(状态码 + 内容类型)
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8' // 告诉浏览器这是 JSON
});
// 2. 再把「菜」端出来(响应内容)
res.end(JSON.stringify({ message: '这是 JSON 格式的数据' }));
});
HTTP 状态码(必须知道 3 个)
| 状态码 | 含义 | 场景 |
|---|---|---|
| 200 | 成功 | 正常返回数据 |
| 404 | 没找到 | 访问的页面不存在 |
| 500 | 服务器错误 | 代码出 bug 了 |
// 模拟 404
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('页面走丢了~');
// 模拟 500
res.writeHead(500, {'Content-Type': 'text/plain'});
res.end('服务器感冒了,稍后再试~');

发起 HTTP 请求(客户端模式)
想「点外卖」?用 http.get():
const http = require('http');
// 请求百度首页
http.get('http://www.baidu.com', (res) => {
console.log('状态码:', res.statusCode);
console.log('响应头:', res.headers);
let data = '';
res.on('data', (chunk) => {
data += chunk; // 一块一块收集数据
});
res.on('end', () => {
console.log('收到数据长度:', data.length, '字节');
});
}).on('error', (err) => {
console.log('请求失败:', err.message);
});
敲黑板:HTTP 请求是「流式」的,数据像水一样一点一点来,不是瞬间全到。

🔥 实战 35 分钟:3 个递进小项目
项目 1(5 分钟):你的第一个网页服务器
需求:做一个显示「当前时间」的网页服务器
const http = require('http');
const server = http.createServer((req, res) => {
const now = new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai'
});
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
res.end(`
<!DOCTYPE html>
<html>
<head><title>当前时间</title></head>
<body>
<h1>现在时间是:${now}</h1>
<p>这是 Node.js 给你造的网页!</p>
</body>
</html>
`);
});
server.listen(3000, () => {
console.log('打开浏览器访问 http://localhost:3000');
});
预期输出:浏览器显示一个带时间的 HTML 页面
一句话解释:res.end() 里放 HTML 字符串,浏览器就能渲染成网页了。
项目 2(15 分钟):天气查询小工具
需求:输入城市名,查询天气
思路:用 http.get() 请求天气 API,获取 JSON 数据,解析后展示
const http = require('http');
function queryWeather(city) {
// 免费的天气 API,不需要 key
const url = `http://wttr.in/${encodeURIComponent(city)}?format=j1`;
return new Promise((resolve, reject) => {
http.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(data);
const current = json.current_condition[0];
console.log(`\n🌤️ ${city} 天气快报`);
console.log(`温度:${current.temp_C}°C`);
console.log(`天气:${current.weatherDesc[0].value}`);
console.log(`风速:${current.windspeedKmph} km/h`);
resolve();
} catch (e) {
reject(new Error('解析天气数据失败'));
}
});
}).on('error', reject);
});
}
// 测试
queryWeather('北京').then(() => {
return queryWeather('上海');
}).then(() => {
return queryWeather('东京');
});
运行结果:
🌤️ 北京 天气快报
温度:22°C
天气:晴
风速:12 km/h
🌤️ 上海 天气快报
温度:25°C
天气:多云
风速:8 km/h
🌤️ 东京 天气快报
温度:28°C
天气:晴
风速:15 km/h
一句话解释:把 URL 里的城市名换成你的城市,就能查任意地方的天气。
项目 3(15 分钟):待办事项 API 服务器
需求:搭一个 RESTful 风格的 API,能增删查待办事项
什么是 RESTful?就是一种「约定俗成的 URL 命名方式」:
- GET /todos = 查所有
- POST /todos = 添加
- DELETE /todos/:id = 删除某个
const http = require('http');
const fs = require('fs');
const PORT = 3000;
const DB_FILE = './todos.json';
// 初始化空文件
if (!fs.existsSync(DB_FILE)) {
fs.writeFileSync(DB_FILE, '[]');
}
function sendJSON(res, statusCode, data) {
res.writeHead(statusCode, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
res.end(JSON.stringify(data));
}
const server = http.createServer((req, res) => {
// CORS 预检
if (req.method === 'OPTIONS') {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
});
return res.end();
}
const [path, query] = req.url.split('?');
const pathParts = path.split('/').filter(Boolean); // ['todos'] 或 ['todos', '3']
// GET /todos 或 GET /todos/3
if (req.method === 'GET' && pathParts[0] === 'todos') {
const todos = JSON.parse(fs.readFileSync(DB_FILE));
if (pathParts[1]) {
const id = parseInt(pathParts[1]);
const todo = todos.find(t => t.id === id);
if (todo) {
sendJSON(res, 200, todo);
} else {
sendJSON(res, 404, { error: '没找到这个待办' });
}
} else {
sendJSON(res, 200, todos);
}
return;
}
// POST /todos - 添加新待办
if (req.method === 'POST' && pathParts[0] === 'todos') {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
const { text } = JSON.parse(body);
const todos = JSON.parse(fs.readFileSync(DB_FILE));
const newTodo = {
id: Date.now(),
text: text,
done: false,
createdAt: new Date().toISOString()
};
todos.push(newTodo);
fs.writeFileSync(DB_FILE, JSON.stringify(todos, null, 2));
sendJSON(res, 201, newTodo);
});
return;
}
// DELETE /todos/:id
if (req.method === 'DELETE' && pathParts[0] === 'todos' && pathParts[1]) {
const id = parseInt(pathParts[1]);
let todos = JSON.parse(fs.readFileSync(DB_FILE));
const index = todos.findIndex(t => t.id === id);
if (index === -1) {
return sendJSON(res, 404, { error: '没找到' });
}
const deleted = todos.splice(index, 1)[0];
fs.writeFileSync(DB_FILE, JSON.stringify(todos, null, 2));
sendJSON(res, 200, { message: '删除了', deleted });
return;
}
sendJSON(res, 404, { error: '不存在的接口' });
});
server.listen(PORT, () => {
console.log(`待办 API 已启动: http://localhost:${PORT}`);
console.log('接口列表:');
console.log(' GET /todos - 查看所有');
console.log(' POST /todos - 添加 {text: "xxx"}');
console.log(' DELETE /todos/:id - 删除某个');
});
测试方法(另开终端,用 curl 命令):
# 查看所有
curl http://localhost:3000/todos
# 添加
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"text":"学完这一章"}'
# 删除
curl -X DELETE http://localhost:3000/todos/1234567890
预期输出:curl 命令返回 JSON 格式的响应数据
一句话解释:这个「小 API」其实就是数据库的简化版,所有数据存在 todos.json 文件里。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:JSON 数据累加,文件变成乱码
// ❌ 错误:每次追加变成无效 JSON
fs.appendFile(DB_FILE, JSON.stringify(newTodo));
// ✅ 正确:先读、增、写
let todos = JSON.parse(fs.readFileSync(DB_FILE));
todos.push(newTodo);
fs.writeFileSync(DB_FILE, JSON.stringify(todos, null, 2));
坑 2:异步 data 事件的 await 用错位置
// ❌ 错误:await 在 data 回调里,不影响外层
let data = '';
req.on('data', async (chunk) => {
await processChunk(chunk); // 这 await 没用!
data += chunk;
});
// ✅ 正确:用 Promise 包装整个请求
function getRequestBody(req) {
return new Promise((resolve) => {
let data = '';
req.on('data', chunk => { data += chunk; });
req.on('end', () => resolve(data));
});
}
const body = await getRequestBody(req);
坑 3:没设置 Content-Type,浏览器看不懂
// ❌ 错误:浏览器当纯文本处理
res.end('{ "ok": true }');
// ✅ 正确:明确告诉浏览器这是 JSON
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end('{ "ok": true }');
坑 4:端口被占用不知道
// ❌ 错误:直接崩掉
server.listen(3000);
// ✅ 正确:捕获错误,友好提示
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log('端口被占用了!试试 3001');
}
});
server.listen(3000);
坑 5:响应后继续写数据
// ❌ 错误:res.end() 后又 res.write()
res.end('结束');
res.write('还能写吗?'); // 报错!
// ✅ 正确:用标志位确保只结束一次
let ended = false;
if (!ended) {
ended = true;
res.end('OK');
}
性能小技巧:善用缓存
查天气每次都请求外部 API,慢!加个缓存:
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
function getCachedWeather(city) {
const key = city;
const cached = cache.get(key);
if (cached && Date.now() - cached.time < CACHE_TTL) {
console.log('命中缓存:', city);
return Promise.resolve(cached.data);
}
return queryWeather(city).then(data => {
cache.set(key, { time: Date.now(), data });
return data;
});
}
调试技巧:console.log 大法
const server = http.createServer((req, res) => {
console.log('\n收到请求!');
console.log('方法:', req.method);
console.log('路径:', req.url);
console.log('头:', JSON.stringify(req.headers));
res.on('finish', () => {
console.log('响应完成!\n');
});
// ... 业务逻辑
});
✏️ 练习题
练习 1(1 分钟):改端口
- 输入:把天气查询工具的端口改成
8080 - 预期输出:curl 命令访问
http://localhost:8080/weather?city=北京返回天气 - 提示:找
listen(3000, ...)改成listen(8080, ...)
练习 2(2 分钟):加个判断
- 输入:在项目 1 的时间服务器里,加一个判断:如果 URL 是
/morning才显示「早上好」,否则显示「当前时间」 - 预期输出:
/返回时间,/morning返回「早上好」 - 提示:
req.url === '/morning'
练习 3(2 分钟):换个数据源
- 输入:用
http://wttr.in/Shanghai?format=j1查上海天气 - 预期输出:显示上海的温度和天气
- 提示:把
queryWeather('北京')里的城市名改成Shanghai
练习 4(3 分钟):串起来
- 输入:在待办 API 的 POST 逻辑里,添加待办后调用天气查询,打印出「添加成功,当前天气:XX°C」
- 预期输出:curl 添加待办后,服务器控制台打印天气
- 提示:在
sendJSON前调用queryWeather('北京')
练习 5(5 分钟):debug 找茬
- 输入:以下代码运行时报错
Cannot read property 'temp_C' of undefined - 预期输出:说出原因并修复
- 提示:API 返回的数据结构是什么?检查
json.current_condition
const json = JSON.parse(data);
console.log(json.current_condition[0].temp_C); // 报错行
作业:个人书签管理 API
需求:用 http 模块做一个书签管理服务
功能点:
1. GET /bookmarks - 查看所有书签(返回 [{id, url, title}])
2. POST /bookmarks - 添加书签({url, title})
3. DELETE /bookmarks/:id - 删除书签
4. GET /bookmarks/:id - 查看单个书签
数据存储:存到 bookmarks.json 文件
加分项:
- 自动检测 URL 是否可访问(用 http.get 发请求)
- 给书签加上「分类标签」
验收标准:
- node server.js 能跑起来
- 4 个接口都能用 curl 测试通过
- 代码有注释
提交:评论区贴代码或 GitHub 链接
📚 总结
本文学了 3 件事:
1. HTTP 是「请求-响应」的规则,Node.js 用 req 收请求、res 发响应
2. http.createServer() 搭服务器,http.get() 发请求
3. 数据是「流式」来的,要用事件监听慢慢收
延伸资源:
- MDN HTTP 文档 - 最权威的 HTTP 教程
- Node.js http 模块官方文档 - 英文但最准确
- 《Node.js 实战:第 2 版》- 进阶好书
下章预告:
学会了搭服务器,但有个问题还没解决:一个服务器怎么区分不同页面?比如 /home、/about、/api/users 分别返回不同内容?下一章「4.2 路由与请求处理」我们来揭开这个谜底。
你在做项目时遇到过 HTTP 相关的坑吗? 评论区聊聊,你是先踩坑还是先看教程的?老粉优先回复!👇

评论(0)