Node.js Express 入门
用 Node 自带的 http 模块写接口,每个路由都要手动解析 URL 再 if 判断,接口一多代码就乱成一团。这篇介绍 Express,npm 生态里最流行的 Node Web 框架,它把路由、参数解析、响应发送这些重复劳动封装好,我们只写业务代码。Node.js 事件循环 里已经见识过 http 模块的写法,现在看 Express 在它之上做了什么。
为什么用 Express
Express 是构建在 http 模块之上的轻量级 Web 框架,不改变 Node 的运行方式,只是把常用操作封装成更顺手的 API。
| 场景 | http 模块 | Express |
|---|---|---|
| 定义路由 | 手动解析 req.url 再 if 判断 | app.get(“/users”, handler) 一行搞定 |
| 取查询参数 | 手写 URL 解析 | req.query.page 直接读 |
| 返回 JSON | JSON.stringify 再拼响应头 | res.json(data) |
框架本身只做路由和中间件两件事,数据库、模板、验证等能力全部靠第三方插件扩展,用多少装多少,所以它够轻量,npm 下载量常年第一。
安装 Express
先初始化项目再安装依赖,npm 的完整流程在 Node.js npm 入门 里讲过,这里直接操作。
# 在项目目录初始化 package.json
npm init -y
# 安装 express 并写入 dependencies
npm install express
安装完成后打开 package.json,dependencies 里能看到 express 的版本号。这一系列文章用的都是 Node 20 LTS,包管理器 npm 已经内置,不需要单独安装。
最简服务器
// server.js
const express = require("express");
const app = express();
// 定义根路径路由,浏览器访问 http://localhost:3000 会看到返回的文本
app.get("/", (req, res) => {
res.send("Hello Express");
});
// 启动服务器并监听 3000 端口
app.listen(3000, () => {
console.log("服务器已启动,访问 http://localhost:3000");
});
node server.js
app.get 的第一个参数是路径,第二个参数是处理函数。处理函数接收 req 和 res 两个对象,req 携带请求信息,res 负责发送响应。app.listen(3000) 启动服务器,第二个回调在启动成功时执行。
路由参数
路径参数 req.params
/users/123 这类地址里的 123 是路径参数,路由里用冒号声明变量名。
app.get("/users/:id", (req, res) => {
res.send(`用户 ID 是 ${req.params.id}`);
});
访问 /users/7 时 req.params.id 的值是 7。冒号后的名字可以任意起,取值时用同名属性即可。
查询参数 req.query
/users?age=18 问号后面的部分是查询参数,Express 自动解析成对象挂在 req.query 上。
app.get("/users", (req, res) => {
res.send(`筛选年龄 ${req.query.age}`);
});
查询参数适合表达筛选和分页,比如 ?age=18 和 ?page=2&size=10,多个参数用 & 连接。
请求体 req.body
POST 请求的数据放在请求体里,Express 默认不解析,需要注册 express.json() 中间件。先记住这个固定写法,中间件的原理在下一篇 Node.js Express 中间件 详细讲。
app.use(express.json());
app.post("/users", (req, res) => {
res.send(`收到 ${req.body.name}`);
});
路由方法
Express 为每种 HTTP 方法提供了对应的声明函数。
| 方法 | 声明函数 | 典型用途 |
|---|---|---|
| GET | app.get | 查询数据 |
| POST | app.post | 创建数据 |
| PUT | app.put | 整体更新数据 |
| DELETE | app.delete | 删除数据 |
app.get("/users", (req, res) => res.send("查询用户列表"));
app.post("/users", (req, res) => res.send("创建用户"));
app.put("/users/:id", (req, res) => res.send(`更新用户 ${req.params.id}`));
app.delete("/users/:id", (req, res) => res.send(`删除用户 ${req.params.id}`));
同一个路径搭配不同方法,就是四个相互独立的路由。浏览器地址栏只能发 GET 请求,测试其他方法用 curl 或者 Postman。
# 用 curl 发送 POST 请求
curl -X POST http://localhost:3000/users
响应 API
res 对象提供几个常用的发送响应方法。
app.get("/text", (req, res) => {
res.send("纯文本"); // 发送文本,自动设置 Content-Type
});
app.get("/json", (req, res) => {
res.json({ name: "张三", age: 18 }); // 发送 JSON
});
app.get("/status", (req, res) => {
res.status(201).json({ ok: true }); // 先设状态码再发 JSON,链式调用
});
res.send 会按内容自动识别类型,传字符串返回文本,传对象也会转成 JSON。res.json 专门发送 JSON,接口场景优先用它。res.status 设置状态码,可以与其他方法链式调用。
常见坑
路由顺序影响匹配结果
Express 按注册顺序匹配路由,先注册的先命中。如果 /users/:id 写在 /users/new 前面,访问 /users/new 时 new 会被当成 id 参数。
// 错误写法,/users/new 永远匹配不到
app.get("/users/:id", (req, res) => res.send("详情"));
app.get("/users/new", (req, res) => res.send("新建页"));
// 正确写法,固定路径放在动态路径之前
app.get("/users/new", (req, res) => res.send("新建页"));
app.get("/users/:id", (req, res) => res.send("详情"));
res.json 和 res.send 不要混用
一个请求只能发送一次响应,同一个处理函数里重复调用会抛 ERR_HTTP_HEADERS_SENT 错误。接口统一用 res.json,简单文本用 res.send,别在同一个处理函数里两个都用。
实践
用 Express 重写 http 模块版的用户服务器,支持路径参数和查询参数,顺便把四种方法全部用上。
// server.js
const express = require("express");
const app = express();
app.use(express.json());
// 模拟用户数据,先用内存数组,后面的数据库篇会替换成真数据库
const users = [
{ id: 1, name: "张三", age: 20 },
{ id: 2, name: "李四", age: 18 },
];
// 用户列表,支持 ?age=18 按年龄筛选
app.get("/users", (req, res) => {
const { age } = req.query;
if (age) {
return res.json(users.filter((u) => u.age === Number(age)));
}
res.json(users);
});
// 单个用户详情,id 来自路径参数
app.get("/users/:id", (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) {
return res.status(404).json({ message: "用户不存在" });
}
res.json(user);
});
// 创建用户,数据来自请求体
app.post("/users", (req, res) => {
const user = { id: users.length + 1, ...req.body };
users.push(user);
res.status(201).json(user);
});
// 更新用户
app.put("/users/:id", (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex((u) => u.id === id);
if (index === -1) {
return res.status(404).json({ message: "用户不存在" });
}
users[index] = { id, ...req.body };
res.json(users[index]);
});
// 删除用户
app.delete("/users/:id", (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex((u) => u.id === id);
if (index === -1) {
return res.status(404).json({ message: "用户不存在" });
}
users.splice(index, 1);
res.json({ message: "删除成功" });
});
app.listen(3000, () => {
console.log("服务器已启动,访问 http://localhost:3000");
});
node server.js
# 另开一个终端依次测试
curl http://localhost:3000/users
curl "http://localhost:3000/users?age=18"
curl http://localhost:3000/users/1
curl -X POST -H "Content-Type: application/json" -d "{\"name\":\"王五\",\"age\":25}" http://localhost:3000/users
curl -X PUT -H "Content-Type: application/json" -d "{\"name\":\"王五\",\"age\":26}" http://localhost:3000/users/3
curl -X DELETE http://localhost:3000/users/2
路由和参数的写法到这里就齐了,下一篇 Node.js Express 中间件 讲 express.json() 这类中间件到底做了什么,以及如何做全局错误处理。