Express 核心:中介軟體機制深度解析
Express 之所以強大且靈活,核心就在於**中介軟體(Middleware)**機制。理解它,你就理解了 Express 的一切。
一、 什麼是中介軟體?
1.1 概念
中介軟體是一個可以存取 req、res 和 next 的函式:
javascript
function middleware(req, res, next) {
// 做一些事情...
next(); // 繼續下一個中介軟體
}1.2 執行流程
1.3 基本使用
javascript
const express = require("express");
const app = express();
// 應用級中介軟體
app.use((req, res, next) => {
console.log("Time:", Date.now());
next();
});
// 路由
app.get("/", (req, res) => {
res.send("Hello World");
});
app.listen(3000);二、 中介軟體類型
2.1 應用級中介軟體
javascript
// 所有請求都會經過
app.use((req, res, next) => {
console.log("All requests");
next();
});
// 特定路徑
app.use("/api", (req, res, next) => {
console.log("API requests only");
next();
});
// 特定方法 + 路徑
app.get("/users", (req, res, next) => {
// ...
});2.2 路由級中介軟體
javascript
const router = express.Router();
router.use((req, res, next) => {
console.log("Router middleware");
next();
});
router.get("/users", (req, res) => {
res.json([]);
});
app.use("/api", router);2.3 錯誤處理中介軟體
必須有 4 個參數:
javascript
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: "Something went wrong!" });
});2.4 內建中介軟體
javascript
// JSON 解析
app.use(express.json());
// URL 編碼解析
app.use(express.urlencoded({ extended: true }));
// 靜態檔案
app.use(express.static("public"));2.5 第三方中介軟體
javascript
const morgan = require("morgan");
const cors = require("cors");
const helmet = require("helmet");
app.use(morgan("combined")); // 日誌
app.use(cors()); // 跨域
app.use(helmet()); // 安全標頭三、 next() 機制
3.1 繼續執行
javascript
app.use((req, res, next) => {
console.log("First");
next(); // 必須呼叫,否則請求會卡住
});
app.use((req, res, next) => {
console.log("Second");
next();
});
app.get("/", (req, res) => {
res.send("Done");
});
// 輸出: First, Second3.2 跳過剩餘中介軟體
使用 next('route') 跳到下一個路由:
javascript
app.get(
"/user/:id",
(req, res, next) => {
if (req.params.id === "0") {
next("route"); // 跳過此路由的其他中介軟體
} else {
next();
}
},
(req, res) => {
res.send("Regular user");
}
);
app.get("/user/:id", (req, res) => {
res.send("Special user 0");
});3.3 錯誤處理
傳遞任何參數給 next() 會跳到錯誤處理:
javascript
app.use((req, res, next) => {
try {
throw new Error("Something broke!");
} catch (err) {
next(err); // 跳到錯誤處理中介軟體
}
});
// 錯誤處理中介軟體(必須放最後)
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});四、 洋蔥模型
4.1 概念
請求像剝洋蔥一樣,進入時由外向內,回應時由內向外:
4.2 實現方式
javascript
app.use(async (req, res, next) => {
console.log("A - 開始");
const start = Date.now();
await next(); // 等待後續中介軟體完成
const duration = Date.now() - start;
console.log(`A - 結束,耗時 ${duration}ms`);
});
app.use(async (req, res, next) => {
console.log("B - 開始");
await next();
console.log("B - 結束");
});
app.get("/", (req, res) => {
console.log("處理請求");
res.send("Hello");
});
// 輸出順序:
// A - 開始
// B - 開始
// 處理請求
// B - 結束
// A - 結束,耗時 XXmsNOTE
Express 原生是基於回調的,不是真正的洋蔥模型。Koa 完整支援 async/await 洋蔥模型。
五、 實戰:常用中介軟體
5.1 請求日誌
javascript
function requestLogger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
});
next();
}
app.use(requestLogger);5.2 認證中介軟體
javascript
function authenticate(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
return res.status(401).json({ error: "No token provided" });
}
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: "Invalid token" });
}
}
// 使用
app.get("/api/profile", authenticate, (req, res) => {
res.json(req.user);
});5.3 權限檢查
javascript
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
// 使用
app.delete("/api/users/:id", authenticate, authorize("admin"), (req, res) => {
res.json({ message: "User deleted" });
});5.4 請求驗證
javascript
function validateBody(schema) {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: "Validation error",
details: error.details.map((d) => d.message),
});
}
next();
};
}
// 使用 Joi
const createUserSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
});
app.post("/api/users", validateBody(createUserSchema), (req, res) => {
res.json({ created: req.body });
});5.5 請求限流
javascript
const rateLimit = new Map();
function rateLimiter(windowMs, maxRequests) {
return (req, res, next) => {
const ip = req.ip;
const now = Date.now();
if (!rateLimit.has(ip)) {
rateLimit.set(ip, { count: 1, resetTime: now + windowMs });
return next();
}
const limit = rateLimit.get(ip);
if (now > limit.resetTime) {
limit.count = 1;
limit.resetTime = now + windowMs;
return next();
}
if (limit.count >= maxRequests) {
return res.status(429).json({ error: "Too many requests" });
}
limit.count++;
next();
};
}
app.use(rateLimiter(60000, 100)); // 每分鐘 100 次六、 中介軟體順序
6.1 順序很重要
javascript
// ❌ 錯誤:解析 body 之前就讀取
app.post("/api", (req, res) => {
console.log(req.body); // undefined!
});
app.use(express.json());
// ✅ 正確:先解析再使用
app.use(express.json());
app.post("/api", (req, res) => {
console.log(req.body); // { ... }
});6.2 推薦順序
javascript
const app = express();
// 1. 安全標頭
app.use(helmet());
// 2. 日誌
app.use(morgan("combined"));
// 3. CORS
app.use(cors());
// 4. Body 解析
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 5. Cookie 解析
app.use(cookieParser());
// 6. 靜態檔案
app.use(express.static("public"));
// 7. 路由
app.use("/api", apiRouter);
// 8. 404 處理
app.use((req, res) => {
res.status(404).json({ error: "Not Found" });
});
// 9. 錯誤處理(必須最後)
app.use((err, req, res, next) => {
res.status(500).json({ error: "Internal Server Error" });
});七、 Express vs Koa
| 特性 | Express | Koa |
|---|---|---|
| 中介軟體 | 基於回調 | 基於 async/await |
| 洋蔥模型 | 部分支援 | 完整支援 |
| 內建功能 | 較多 | 極簡 |
| 社群 | 龐大成熟 | 較小但現代 |
| 錯誤處理 | 需要 try-catch | 原生 Promise 支援 |
總結
| 類型 | 說明 |
|---|---|
| 應用級 | app.use() |
| 路由級 | router.use() |
| 錯誤處理 | 4 個參數 (err, req, res, next) |
| 內建 | express.json(), express.static() |
| 第三方 | morgan, cors, helmet |
> **記住**:
- 中介軟體順序決定執行順序
- 一定要呼叫
next()或發送回應 - 錯誤處理中介軟體放最後
進階挑戰
- 實作一個中介軟體,追蹤每個請求的唯一 ID(Request ID)。
- 實作一個中介軟體,自動處理 async 錯誤(不用每個路由都 try-catch)。
- 比較 Express 和 Koa 在處理相同邏輯時的程式碼差異。