跳至主要內容
Skip to content

Express 核心:中介軟體機制深度解析

Express 之所以強大且靈活,核心就在於**中介軟體(Middleware)**機制。理解它,你就理解了 Express 的一切。


一、 什麼是中介軟體?

1.1 概念

中介軟體是一個可以存取 reqresnext 的函式:

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, Second

3.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 - 結束,耗時 XXms

NOTE

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

特性ExpressKoa
中介軟體基於回調基於 async/await
洋蔥模型部分支援完整支援
內建功能較多極簡
社群龐大成熟較小但現代
錯誤處理需要 try-catch原生 Promise 支援

總結

類型說明
應用級app.use()
路由級router.use()
錯誤處理4 個參數 (err, req, res, next)
內建express.json(), express.static()
第三方morgan, cors, helmet

> **記住**:

  • 中介軟體順序決定執行順序
  • 一定要呼叫 next() 或發送回應
  • 錯誤處理中介軟體放最後

進階挑戰

  1. 實作一個中介軟體,追蹤每個請求的唯一 ID(Request ID)。
  2. 實作一個中介軟體,自動處理 async 錯誤(不用每個路由都 try-catch)。
  3. 比較 Express 和 Koa 在處理相同邏輯時的程式碼差異。

延伸閱讀與資源