第4章 4.4 Express 框架入门
⚠️ 说明:本章是「Node.js 从入门到精通」系列,但博主会用 Python 教学的风格来讲——先类比再实战,保证你看完就能动手。
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了用 Node.js 原生的 http 模块搭服务器,发现一个问题:代码太啰嗦了。光是一个「根据不同 URL 返回不同页面」的功能,就要写一堆 if...else 判断,头疼得很。
痛点来了:你是不是也遇到过——
- 想快速搭一个 Web API,但不想折腾配置?
- 想加一个「请求日志」功能,发现要改好多地方?
- 想读取 JSON 数据,手动
JSON.parse了一整页?
这一章我们学 Express,它就是来解决这些痛苦的。简单说:Express = Node.js 原生 http 模块的「智能遥控器」,原来要写 50 行的事,它 5 行搞定。
学完本文,你能独立搭一个带路由、有中间件、能处理 JSON 的小 API。
🧱 基础 25 分钟:核心概念
4.4.1 Express 是什么?
生活类比:想象你去餐厅点菜。
- 原生 http = 你要自己走进厨房,跟厨师说「我要一份番茄炒蛋,用哪个锅,放多少油,火候多大」——太累了。
- Express = 你只需要跟服务员说「来一份番茄炒蛋」,服务员帮你处理后面的事。
Express 就是那个「服务员」,它封装了 Node.js 原生的麻烦操作,让你只关注「我要什么」,不用管「怎么做的」。
4.4.2 安装 Express
先创个文件夹,练手用:
mkdir express-start && cd express-start
npm init -y
npm install express
💡 一句话解释:
npm init -y初始化项目,npm install express装框架依赖。
4.4.3 第一个 Express 服务
// app.js
const express = require('express'); // ① 引入 Express
const app = express(); // ② 创建 app 实例
app.get('/hello', (req, res) => { // ③ 定义路由:GET /hello
res.send('你好,世界!');
});
app.listen(3000, () => { // ④ 启动服务器,监听 3000 端口
console.log('服务器跑起来了,访问 http://localhost:3000/hello');
});
运行:
node app.js
浏览器访问 http://localhost:3000/hello,看到「你好,世界!」就成功了。
💡 一句话解释:第①行「请 Express 出山」,第②行「初始化它的能力」,第③行「告诉它见到
/hello请求就回复」,第④行「开大门迎客」。
4.4.4 路由基础:GET 和 POST
类比:路由就像餐厅的菜单,不同的菜品(URL)触发不同的厨师处理。
// 路由练习:app.js
const express = require('express');
const app = express();
// GET 请求 - 查
app.get('/menu', (req, res) => {
res.send('这里是菜单:番茄炒蛋、土豆丝、红烧肉');
});
// GET 请求 - 带参数
app.get('/menu/:id', (req, res) => {
const id = req.params.id; // 获取 URL 参数
res.send(`你查看的是第 ${id} 道菜`);
});
// POST 请求 - 增
app.post('/order', (req, res) => {
res.send('订单已收到,正在准备!');
});
app.listen(3000, () => {
console.log('路由练习服务器启动');
});
运行后测试:
# 测试 GET /menu
curl http://localhost:3000/menu
# 输出:这里是菜单:番茄炒蛋、土豆丝、红烧肉
# 测试 GET /menu/1
curl http://localhost:3000/menu/1
# 输出:你查看的是第 1 道菜
# 测试 POST /order(-X POST 表示 POST 方法)
curl -X POST http://localhost:3000/order
# 输出:订单已收到,正在准备!
💡 一句话解释:
req.params是 Express 帮你拆出来的 URL 参数,POST请求用curl -X POST模拟。
4.4.5 中间件:请求的「安检通道」
类比:中间件就像机场的安检流程——每个旅客(请求)都要过一遍,才能登机(到达终点)。
Express 的中间件是一个函数,它有机会拦截和处理请求,然后决定是「放行」还是「拦截」。

一个最简单的中间件示例:
// 中间件练习:app.js
const express = require('express');
const app = express();
// ① 自定义中间件:记录每次请求
const logger = (req, res, next) => {
console.log(`[${new Date().toLocaleString()}] ${req.method} ${req.url}`);
next(); // 关键!必须调用 next() 放行
};
// ② 注册中间件(所有路由都会先经过它)
app.use(logger);
// ③ 路由
app.get('/hello', (req, res) => {
res.send('Hello!');
});
app.listen(3000, () => {
console.log('中间件练习服务器启动');
});
运行后访问 http://localhost:3000/hello,终端会打印:
[2024/1/15 14:30:00] GET /hello
💡 一句话解释:
app.use()注册中间件,next()是「放行证」,不调用它请求就卡住了。
4.4.6 处理 JSON 数据
上一章我们手动 JSON.parse,现在 Express 有内置中间件一键搞定:
// JSON 解析:app.js
const express = require('express');
const app = express();
// 启用 JSON 请求体解析
app.use(express.json());
// POST 接收 JSON
app.post('/api/user', (req, res) => {
const { name, age } = req.body; // 直接拿到解析后的对象
res.json({
success: true,
message: `收到用户 ${name},年龄 ${age}`
});
});
app.listen(3000, () => {
console.log('JSON 处理服务器启动');
});
测试:
curl -X POST http://localhost:3000/api/user \
-H "Content-Type: application/json" \
-d '{"name":"小明","age":18}'
输出:
{"success":true,"message":"收到用户 小明,年龄 18"}
💡 一句话解释:
express.json()中间件自动把 JSON 字符串变成对象,req.body直接用。
🔥 实战 35 分钟:3 个递进小项目
项目 1(5 分钟):Hello Express 服务器
目标:搭一个能访问的服务器,理解核心 API。
// project1.js
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('<h1>欢迎来到 Express 小站</h1>');
});
app.get('/time', (req, res) => {
res.json({
now: new Date().toLocaleString(),
timestamp: Date.now()
});
});
app.listen(PORT, () => {
console.log(`✨ 服务器已启动 http://localhost:${PORT}`);
});
预期输出:浏览器访问 http://localhost:3000/time 看到当前时间。
💡 一句话解释:这是 Express 的「Hello World」,搞懂
app.get和res.json就够了。
项目 2(15 分钟):菜单 API
目标:从 JSON 文件读取菜单数据,实现「查菜单」「加菜」「删菜」三个功能。
第一步:创建 menu.json 数据文件:
{
"menu": [
{"id": 1, "name": "番茄炒蛋", "price": 15},
{"id": 2, "name": "土豆丝", "price": 12},
{"id": 3, "name": "红烧肉", "price": 35}
]
}
第二步:写 API 服务器:
// project2.js
const express = require('express');
const fs = require('fs');
const app = express();
const PORT = 3000;
app.use(express.json());
// 读取菜单(从 JSON 文件)
const loadMenu = () => {
const data = fs.readFileSync('menu.json', 'utf-8');
return JSON.parse(data);
};
// 保存菜单(写入 JSON 文件)
const saveMenu = (data) => {
fs.writeFileSync('menu.json', JSON.stringify(data, null, 2));
};
// GET /menu - 查看全部菜单
app.get('/menu', (req, res) => {
const data = loadMenu();
res.json(data);
});
// GET /menu/:id - 查看单道菜
app.get('/menu/:id', (req, res) => {
const data = loadMenu();
const dish = data.menu.find(d => d.id === parseInt(req.params.id));
if (dish) {
res.json(dish);
} else {
res.status(404).json({ error: '这道菜不存在' });
}
});
// POST /menu - 添加新菜
app.post('/menu', (req, res) => {
const data = loadMenu();
const newDish = {
id: data.menu.length + 1,
name: req.body.name,
price: req.body.price
};
data.menu.push(newDish);
saveMenu(data);
res.json({ success: true, dish: newDish });
});
// DELETE /menu/:id - 删除菜
app.delete('/menu/:id', (req, res) => {
const data = loadMenu();
const id = parseInt(req.params.id);
const index = data.menu.findIndex(d => d.id === id);
if (index !== -1) {
const deleted = data.menu.splice(index, 1)[0];
saveMenu(data);
res.json({ success: true, deleted });
} else {
res.status(404).json({ error: '这道菜不存在' });
}
});
app.listen(PORT, () => {
console.log(`🍜 菜单 API 服务器已启动 http://localhost:${PORT}`);
});
测试:
# 查看全部菜单
curl http://localhost:3000/menu
# 查看第2道菜
curl http://localhost:3000/menu/2
# 添加新菜
curl -X POST http://localhost:3000/menu \
-H "Content-Type: application/json" \
-d '{"name":"宫保鸡丁","price":28}'
# 删除第1道菜
curl -X DELETE http://localhost:3000/menu/1
💡 一句话解释:这是完整的「增删改查」API,用到了路由参数、JSON 解析、文件读写。
项目 3(15 分钟):带请求日志的待办清单 API
目标:组合中间件 + CRUD API,加一个「请求计数器」功能。
// project3.js
const express = require('express');
const app = express();
const PORT = 3000;
app.use(express.json());
// 中间件1:请求计数器
let requestCount = 0;
const counter = (req, res, next) => {
requestCount++;
console.log(`📊 第 ${requestCount} 次请求: ${req.method} ${req.url}`);
next();
};
app.use(counter);
// 中间件2:时间戳
const timestamp = (req, res, next) => {
req.requestTime = new Date().toISOString();
next();
};
app.use(timestamp);
// 模拟数据库
let todos = [
{ id: 1, text: '学 Express', done: false },
{ id: 2, text: '写项目', done: false }
];
// GET /todos - 查看全部
app.get('/todos', (req, res) => {
res.json({
count: todos.length,
requestTime: req.requestTime,
todos
});
});
// POST /todos - 添加
app.post('/todos', (req, res) => {
const newTodo = {
id: todos.length + 1,
text: req.body.text,
done: false
};
todos.push(newTodo);
res.json({ success: true, todo: newTodo });
});
// PUT /todos/:id - 标记完成
app.put('/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find(t => t.id === id);
if (todo) {
todo.done = !todo.done;
res.json({ success: true, todo });
} else {
res.status(404).json({ error: '待办不存在' });
}
});
// DELETE /todos/:id - 删除
app.delete('/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = todos.findIndex(t => t.id === id);
if (index !== -1) {
todos.splice(index, 1);
res.json({ success: true });
} else {
res.status(404).json({ error: '待办不存在' });
}
});
app.listen(PORT, () => {
console.log(`✅ 待办清单 API 已启动 http://localhost:${PORT}`);
console.log('💡 试试访问 http://localhost:3000/todos');
});
预期输出:每次请求,终端都会打印请求计数和时间戳。
💡 一句话解释:两个中间件各司其职——一个计数,一个加时间戳,路由完全不用改。

💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:忘记调用 next()
// ❌ 错误:请求会一直转圈
app.use((req, res, next) => {
console.log('我执行了');
// 忘了 next(),请求卡住了
});
// ✅ 正确
app.use((req, res, next) => {
console.log('我执行了');
next(); // 放行
});
坑 2:req.body 是 undefined
// ❌ 错误:没有启用 JSON 解析中间件
app.post('/api/data', (req, res) => {
console.log(req.body); // undefined
});
// ✅ 正确:先启用中间件
app.use(express.json());
app.post('/api/data', (req, res) => {
console.log(req.body); // 正常拿到数据
});
坑 3:路由顺序写反
// ❌ 错误:通配路由写前面,具体路由永远匹配不到
app.get('*', (req, res) => res.send('404'));
app.get('/hello', (req, res) => res.send('Hello')); // 这行永远不到
// ✅ 正确:具体路由写前面,通配路由放最后
app.get('/hello', (req, res) => res.send('Hello'));
app.get('*', (req, res) => res.send('404'));
坑 4:异步操作没等完成就响应
// ❌ 错误:fs.readFile 是异步的,还没读完就响应了
app.get('/data', (req, res) => {
fs.readFile('data.json', 'utf-8', (err, data) => {
// 这里还没执行完
});
res.send('读取中...'); // 先返回了
});
// ✅ 正确:用 async/await 或回调里 res.send
app.get('/data', (req, res) => {
fs.readFile('data.json', 'utf-8', (err, data) => {
if (err) return res.status(500).send('读取失败');
res.json(JSON.parse(data));
});
});
坑 5:端口被占用不知道
// ✅ 加个错误处理,端口占用时报错明确
app.listen(PORT, () => {
console.log(`服务器启动成功`);
}).on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ 端口 ${PORT} 已被占用,请换端口或停掉其他服务`);
} else {
console.error('❌ 服务器启动失败:', err);
}
});
性能小贴士:静态文件用 express.static
// ❌ 手动写路由返回静态文件,慢
app.get('/static/*', (req, res) => {
const file = req.params[0];
res.sendFile(__dirname + '/public/' + file);
});
// ✅ 用内置中间件,自动处理缓存和 MIME 类型
app.use(express.static('public'));
调试技巧:打印请求详情
// 加一个「能看到所有请求细节」的中间件
app.use((req, res, next) => {
console.log('--- 请求详情 ---');
console.log('方法:', req.method);
console.log('URL:', req.url);
console.log('参数:', req.params);
console.log('查询:', req.query);
console.log('请求体:', req.body);
console.log('----------------');
next();
});
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):改端口
- 输入:把项目 1 的端口从 3000 改成 8080
- 预期输出:服务器启动时显示 http://localhost:8080
- 提示:改 PORT 变量的值即可
练习 2(2 分钟):加个判断
- 输入:在项目 1 的 /time 路由里,加一个判断,如果是晚上 6 点后访问,返回「晚安」
- 预期输出:18:00 后访问 /time 显示晚安,其他时间显示时间
- 提示:用 new Date().getHours() 获取当前小时
练习 3(2 分钟):处理新数据
- 输入:给项目 2 的菜单 JSON 加一条「糖醋排骨 {id: 4, name: "糖醋排骨", price: 38}」,然后用 curl 测试
- 预期输出:GET /menu 返回 4 条数据
- 提示:手动改 JSON 文件,或用 POST 添加
练习 4(2 分钟):串联项目
- 输入:把项目 2 的菜单 API 改成用项目 3 的「请求计数器」中间件
- 预期输出:每次访问菜单 API,终端打印请求计数
- 提示:把项目 3 的 counter 中间件复制过去,用 app.use(counter) 注册
练习 5(2 分钟):找错
- 输入:以下代码为什么会卡住?如何修复?
const express = require('express');
const app = express();
app.use(express.json());
app.post('/test', (req, res) => {
const { name } = req.body;
console.log('收到:', name);
// 这里忘了写 res.send 或 res.json
});
app.listen(3000);
- 预期输出:修复后返回
{"received": true} - 提示:请求没收到响应,Postman/curl 会一直转圈
作业题(30 分钟 - 2 小时)
作业:做一个「图书管理 API」
- 需求描述:管理图书馆的图书,支持添加图书、查询图书、删除图书、修改借阅状态
- 功能点:
1.GET /books- 查看全部图书
2.GET /books/:id- 按 ID 查看单本书
3.POST /books- 添加新书(书名、作者、是否借出)
4.PUT /books/:id- 修改借阅状态
5.DELETE /books/:id- 删除图书 - 加分项:
1. 加一个「请求日志中间件」,记录每次操作
2. 数据持久化到books.json文件 - 验收标准:
- 能跑起来不报错
- curl 测试每个接口都有正确响应
- 代码有适当注释
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
一句话总结本文学到的 3 个核心点:
1. Express 是 Node.js 的「智能服务员」,app.get/post/use 是它的核心 API
2. 中间件是「请求安检通道」,next() 是放行证
3. req.body、req.params、req.query 是获取请求数据的 3 种方式
延伸学习资源:
- Express 官方文档(英文,但例子清晰)
-《深入浅出 Node.js》- 第 4 章(朴灵著,中文经典)
- 视频:B 站「Express.js 入门到实战」系列
互动钩子:
🤔 你在写 Node.js 服务器时,遇到过最奇葩的 bug 是什么?是忘写
next()卡死,还是req.body拿不到数据?评论区聊聊,老粉优先回复!
下章预告:学会了 Express 基础,下一章我们用它来做一个「个人博客 API」——带文章列表、评论、标签功能的完整后端服务。敬请期待!

评论(0)