Express 与 Koa 框架
原生 http 模块手写路由,业务一多就难以维护。Express 是 Node 最流行的 Web 框架,用中间件与路由把 HTTP 服务组织得井井有条;Koa 是 Express 原班人马打造的下一代框架,用 async/await 实现更优雅的洋葱模型。两者都是学习后端开发的必经之路。
一、Express 简介与安装
Express 是轻量、灵活的 Node Web 框架,核心思想:路由 + 中间件。
bash
npm init -y
npm install expressjavascript
const express = require("express");
const app = express();
// 路由:方法 + 路径 + 处理函数
app.get("/", (req, res) => {
res.send("Hello Express");
});
app.listen(3000, () => console.log("服务已启动:http://localhost:3000"));二、基础路由
2.1 常用方法
javascript
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}`));
// 匹配所有方法
app.all("/health", (req, res) => res.send("ok"));
// 链式写法
app.route("/articles")
.get((req, res) => res.send("文章列表"))
.post((req, res) => res.send("新建文章"));2.2 路由参数与通配符
javascript
// 路由参数 :id —— 通过 req.params 获取
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id });
});
// 访问 /users/42 → { id: "42" }
// 可选参数(用 ? 标记)
app.get("/users/:id/:field?", (req, res) => {
console.log(req.params.field); // 可选的第二段
});
// 通配符:以 /users 开头的任意路径
app.get("/users/*", (req, res) => res.send("通配路径"));| 写法 | 匹配示例 | 说明 |
|---|---|---|
/users/:id | /users/42 | 参数段,:id 从 req.params 取 |
/users/:id? | /users、/users/42 | ? 表示可选 |
/users/* | /users/a/b | * 匹配任意多段 |
app.all | 任意方法 | 匹配所有 HTTP 方法 |
三、中间件概念与分类
中间件是 Express 的流水线:每个请求按注册顺序依次穿过一系列函数,每个函数要么结束响应,要么调用 next() 交给下一个。
3.1 中间件分类
| 分类 | 说明 | 示例 |
|---|---|---|
| 应用级 | 挂在整个 app 上,所有请求都经过 | 日志、鉴权 |
| 路由级 | 只对指定路径生效 | 特定接口的校验 |
| 内置 | Express 自带 | express.json、express.static |
| 错误处理 | 4 个参数,专门接住错误 | 统一错误响应 |
3.2 应用级与路由级
javascript
// 应用级中间件:所有请求先打印日志
app.use((req, res, next) => {
console.log(`${req.method} ${req.url} ${Date.now()}`);
next(); // 不调用 next 会卡住请求
});
// 路由级中间件:只对 /admin 开头的路径生效
const auth = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: "未登录" });
}
next();
};
app.use("/admin", auth);四、next() 与中间件执行流程
javascript
// 顺序执行:日志 → 计时 → 路由
app.use((req, res, next) => {
console.log("① 日志中间件");
next();
});
app.use((req, res, next) => {
console.log("② 计时中间件");
req.startTime = Date.now(); // 挂载数据给后面的中间件
next();
});
app.get("/hello", (req, res) => {
console.log("③ 路由处理");
res.send(`耗时 ${Date.now() - req.startTime}ms`);
});
// 不调用 next() 且不 res.end 的中间件,会让请求一直挂着| 行为 | 结果 |
|---|---|
调用 next() | 进入下一个中间件 / 路由 |
调用 res.send/json/end | 结束请求,不再向下 |
调用 next(err) | 跳过普通中间件,直接交给错误处理 |
| 什么都不做 | 请求卡死,直到超时 |
五、body 解析
请求体需要中间件解析后才能通过 req.body 访问:
javascript
const express = require("express");
const app = express();
// 解析 JSON 请求体(Content-Type: application/json)
app.use(express.json());
// 解析表单请求体(Content-Type: application/x-www-form-urlencoded)
app.use(express.urlencoded({ extended: true }));
app.post("/api/login", (req, res) => {
console.log(req.body); // { username: '...', password: '...' }
res.json({ ok: true });
});bash
curl -X POST -H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}' \
http://localhost:3000/api/login| 中间件 | 解析格式 | 结果挂载 |
|---|---|---|
express.json() | JSON | req.body |
express.urlencoded() | 表单编码 | req.body |
express.raw() | Buffer | req.body |
express.text() | 纯文本 | req.body |
六、静态资源
express.static 一行代码托管静态目录:
javascript
const path = require("path");
// 把 public 目录映射到根路径
app.use(express.static(path.join(__dirname, "public")));
// 也可以指定前缀:访问 /assets/xxx 时去 public 里找
app.use("/assets", express.static(path.join(__dirname, "public")));text
public/
index.html → http://localhost:3000/index.html
logo.png → http://localhost:3000/logo.png静态资源通常放在 API 路由之前挂载,避免命中路由逻辑。
七、模板渲染简介
Express 支持各种模板引擎(EJS、Pug 等),渲染出 HTML 页面:
bash
npm install ejsjavascript
// 设置模板引擎与目录
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.get("/users/:id", (req, res) => {
// 渲染 views/user.ejs,并传入数据
res.render("user", { id: req.params.id, name: "小明" });
});ejs
<!-- views/user.ejs -->
<h1>用户:<%= name %></h1>
<p>ID:<%= id %></p>| 模板引擎 | 特点 |
|---|---|
| EJS | 语法接近 HTML,易上手 |
| Pug(原 Jade) | 缩进式语法,简洁 |
| 服务端渲染 | 适合 SEO 与首屏需求 |
八、错误处理中间件
错误处理中间件有4 个参数,Express 靠参数个数识别它:
javascript
// 同步错误:路由中 throw 会自动传给错误中间件
app.get("/boom", (req, res) => {
throw new Error("出错了");
});
// 异步错误:Express 4 需手动 next(err);Express 5 会自动捕获 async 异常
app.get("/async-error", (req, res, next) => {
fs.readFile("missing.txt", (err, data) => {
if (err) return next(err); // 关键:把错误交给错误中间件
res.send(data);
});
});
// 统一错误处理中间件(必须放在所有路由之后)
app.use((err, req, res, next) => {
console.error("服务器错误:", err.message);
res.status(err.status || 500).json({
error: err.message || "服务器内部错误",
});
});| 要点 | 说明 |
|---|---|
| 位置 | 必须注册在所有路由之后 |
| 签名 | (err, req, res, next) 四个参数缺一不可 |
| 触发 | 任意中间件调用 next(err) 或抛出异常 |
| 兜底 | 没有错误中间件时,错误会返回默认的 HTML 错误页 |
九、Koa 简介
Koa 由 Express 原班团队开发,核心变化:中间件变成洋葱模型,全部用 async/await 编写。
9.1 洋葱模型
javascript
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
console.log("① 进入外层中间件");
await next(); // 等内层执行完
console.log("⑤ 回到外层中间件");
});
app.use(async (ctx, next) => {
console.log("② 进入中层中间件");
await next();
console.log("④ 回到中层中间件");
});
app.use(async (ctx) => {
console.log("③ 核心业务");
ctx.body = "Hello Koa"; // 设置响应体
});
// 输出顺序:① ② ③ ④ ⑤ —— 先进后出,像剥洋葱
app.listen(3000);| 对比 | Express 中间件 | Koa 中间件 |
|---|---|---|
| 模型 | 线性流水线(请求处理完不回头) | 洋葱模型(响应时还能再处理) |
| 异步 | 回调 + 手动 next(err) | async/await + await next() |
| 上下文 | req / res 分离 | 统一的 ctx 对象 |
9.2 ctx 对象
javascript
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx) => {
// 请求信息
console.log(ctx.method); // GET
console.log(ctx.url); // /api/hello
console.log(ctx.query); // { a: '1' }
// 响应:直接给 ctx.body 赋值
ctx.status = 200;
ctx.body = { message: "你好,Koa" }; // 对象自动 JSON 序列化
// 设置响应头
ctx.set("X-Server", "koa");
});
app.listen(3000);| ctx 属性 | 对应 | 说明 |
|---|---|---|
ctx.request / ctx.response | req / res | 原生请求与响应对象 |
ctx.method / ctx.url | 请求方法与地址 | |
ctx.query / ctx.params | 查询参数(路由参数需 @koa/router) | |
ctx.body | 响应体,对象自动转 JSON | |
ctx.status | 响应状态码 | |
ctx.set(name, value) | 设置响应头 |
十、Express vs Koa 对比
| 对比项 | Express | Koa |
|---|---|---|
| 发布者 | TJ 团队 | Express 原班人马 |
| 中间件模型 | 线性流水线 | 洋葱模型 |
| 异步语法 | 回调为主 | async/await 原生支持 |
| 内置功能 | 路由、静态、模板、body 解析 | 极简核心,路由等需装包(@koa/router) |
| 社区生态 | 非常庞大 | 较小 |
| 学习曲线 | 平缓 | 需要理解洋葱模型 |
| 适用项目 | 企业级全栈、快速原型 | 对中间件流程有精细控制需求 |
bash
# Koa 常用配套
npm install koa @koa/router koa-bodyparser koa-static十一、综合示例:REST API
用 Express 实现一个带中间件、鉴权、错误处理的完整示例:
javascript
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json());
// 简易内存数据库
const todos = [
{ id: "a1", title: "学习 Express", done: false },
];
// ① 日志中间件
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// ② 鉴权中间件(模拟 Token 校验)
app.use("/api", (req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: "缺少 Token" });
next();
});
// ③ 路由
app.get("/api/todos", (req, res) => {
res.json(todos);
});
app.post("/api/todos", (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: "title 必填" });
}
const todo = { id: crypto.randomUUID(), title, done: false };
todos.push(todo);
res.status(201).json(todo);
});
app.put("/api/todos/:id", (req, res) => {
const todo = todos.find((t) => t.id === req.params.id);
if (!todo) return res.status(404).json({ error: "不存在" });
Object.assign(todo, req.body);
res.json(todo);
});
app.delete("/api/todos/:id", (req, res) => {
const index = todos.findIndex((t) => t.id === req.params.id);
if (index === -1) return res.status(404).json({ error: "不存在" });
todos.splice(index, 1);
res.status(204).end();
});
// ④ 404 兜底
app.use((req, res) => {
res.status(404).json({ error: "接口不存在" });
});
// ⑤ 错误处理中间件
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: "服务器内部错误" });
});
app.listen(3000, () => console.log("API 服务:http://localhost:3000"));bash
# 测试:带 Token 增删改查
curl -H "Authorization: Bearer demo" http://localhost:3000/api/todos
curl -X POST -H "Authorization: Bearer demo" \
-H "Content-Type: application/json" \
-d '{"title":"学习 Koa"}' http://localhost:3000/api/todosExpress 与 Koa 一脉相承:Express 生态成熟、开箱即用,Koa 内核精简、流程可控。从 Express 入手建立路由与中间件的心智模型,再对比 Koa 的洋葱模型理解其差异,Node 后端开发的地基就算打牢了。