第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('服务器感冒了,稍后再试~');

配图1 - 配图1

发起 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 请求是「流式」的,数据像水一样一点一点来,不是瞬间全到。

配图2 - 配图2


🔥 实战 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 相关的坑吗? 评论区聊聊,你是先踩坑还是先看教程的?老粉优先回复!👇

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。