第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 的中间件是一个函数,它有机会拦截和处理请求,然后决定是「放行」还是「拦截」。

配图1 - 配图1

一个最简单的中间件示例

// 中间件练习: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.getres.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');
});

预期输出:每次请求,终端都会打印请求计数和时间戳。

💡 一句话解释:两个中间件各司其职——一个计数,一个加时间戳,路由完全不用改。

配图2 - 配图2


💪 进阶 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.bodyreq.paramsreq.query 是获取请求数据的 3 种方式

延伸学习资源
- Express 官方文档(英文,但例子清晰)
-《深入浅出 Node.js》- 第 4 章(朴灵著,中文经典)
- 视频:B 站「Express.js 入门到实战」系列

互动钩子

🤔 你在写 Node.js 服务器时,遇到过最奇葩的 bug 是什么?是忘写 next() 卡死,还是 req.body 拿不到数据?评论区聊聊,老粉优先回复!


下章预告:学会了 Express 基础,下一章我们用它来做一个「个人博客 API」——带文章列表、评论、标签功能的完整后端服务。敬请期待!

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