ExpressJS (TypeScript) – Static Files 與 Template Engines
主題:TypeScript 中的模板引擎型別支援
簡介
在使用 Express 建立 Web 應用時,模板引擎 (Template Engine) 是將資料渲染成 HTML 的關鍵工具。
當我們以 TypeScript 撰寫 Express 專案時,除了要正確設定模板引擎本身,還必須讓 編譯器知道模板檔案的型別,才能享受到自動完成、型別檢查與錯誤提示等好處。
如果缺少型別支援,開發者往往會在執行階段才發現變數名稱寫錯、傳遞的參數不符合模板需求,造成除錯成本大幅提升。本文將說明在 TypeScript 專案中,如何為常見的 Express 模板引擎(如 Pug、EJS、Handlebars)提供完整的型別支援,並示範實作步驟與最佳實踐,讓你在開發過程中 寫得更安全、寫得更快。
核心概念
1. 為什麼需要「模板檔案的型別」
- 型別安全:在
res.render('view', data)時,編譯器可以檢查data是否符合模板所期待的屬性。 - IDE 補完:VSCode、WebStorm 等編輯器會根據型別資訊提供變數與屬性的自動完成。
- 維護友好:大型專案常有多個開發者共同維護,明確的型別宣告能減少溝通成本。
2. 基本的 TypeScript 設定
在 tsconfig.json 中,確保以下兩項設定已啟用:
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true, // 防止第三方型別套件衝突
"strict": true,
"baseUrl": "./src",
"paths": {
"*": ["node_modules/*", "src/types/*"]
}
}
}
skipLibCheck可以避免因第三方套件的型別檔案過舊而產生編譯錯誤。paths讓我們可以把自訂型別檔放在src/types目錄下,讓 TypeScript 自動找尋。
3. 為不同模板引擎建立型別宣告
3.1 Pug
Pug(原 Jade)在 TypeScript 社群中已有官方型別套件 @types/pug,但仍需要告訴 Express 渲染時的 locals 型別。
// src/types/pug.d.ts
import 'express';
declare global {
// 這裡定義所有 Pug 模板共用的 locals
interface PugLocals {
title: string;
user?: {
name: string;
email: string;
};
// 依需求自行擴充
}
// 為 Express 的 render 方法加上泛型
namespace Express {
interface Response {
render(view: string, options?: PugLocals): void;
}
}
}
重點:使用
declare global把型別掛到全域,讓所有res.render都能自動取得PugLocals。
3.2 EJS
EJS 的型別支援較少,我們可以自行建立簡易的型別檔:
// src/types/ejs.d.ts
import 'express';
declare global {
interface EJSTemplateData {
pageTitle: string;
items: string[];
// 其他變數自行加入
}
namespace Express {
interface Response {
render(view: string, options?: EJSTemplateData): void;
}
}
}
技巧:若專案有多個視圖使用不同的 locals,建議以 泛型 包裝
render,如下所示:
declare global {
namespace Express {
interface Response {
render<T = any>(view: string, options?: T): void;
}
}
}
這樣在每次呼叫 res.render 時,可以直接指定型別:
res.render<EJSTemplateData>('list.ejs', { pageTitle: '清單', items: ['A', 'B'] });
3.3 Handlebars (hbs)
Handlebars 官方有 @types/express-handlebars,但仍需要為 locals 提供型別:
// src/types/hbs.d.ts
import 'express';
declare global {
interface HbsLocals {
layout?: string;
title: string;
// 任意自訂屬性
[key: string]: any;
}
namespace Express {
interface Response {
render(view: string, options?: HbsLocals): void;
}
}
}
4. 讓 TypeScript 能夠讀取 .pug、.ejs、*.hbs 檔案
若你在程式碼中直接 import 模板(例如使用 require),需要告訴編譯器這些檔案的模組型別:
// src/types/templates.d.ts
declare module '*.pug' {
const compiled: (locals?: any) => string;
export default compiled;
}
declare module '*.ejs' {
const compiled: (data?: any) => string;
export default compiled;
}
declare module '*.hbs' {
const compiled: (context?: any) => string;
export default compiled;
}
程式碼範例
以下示範一個 完整的 Express + TypeScript + Pug 專案,涵蓋型別宣告、路由、錯誤處理與靜態檔案服務。
範例 1:專案結構
my-express-ts/
│
├─ src/
│ ├─ routes/
│ │ └─ index.ts
│ ├─ views/
│ │ ├─ layout.pug
│ │ └─ index.pug
│ ├─ public/
│ │ └─ css/
│ │ └─ style.css
│ ├─ types/
│ │ ├─ pug.d.ts
│ │ └─ templates.d.ts
│ └─ app.ts
│
├─ tsconfig.json
└─ package.json
範例 2:app.ts – 基本設定與型別匯入
// src/app.ts
import express, { Request, Response, NextFunction } from 'express';
import path from 'path';
// 先把自訂型別檔載入(只要檔名以 .d.ts 結尾,TS 會自動讀取)
import './types/pug';
import './types/templates';
const app = express();
const PORT = process.env.PORT || 3000;
// 1️⃣ 設定 view engine
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
// 2️⃣ 靜態檔案服務
app.use(express.static(path.join(__dirname, 'public')));
// 3️⃣ 內建路由
import indexRouter from './routes/index';
app.use('/', indexRouter);
// 4️⃣ 基本錯誤處理
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err);
res.status(500).send('Server Error');
});
app.listen(PORT, () => console.log(`Server listening on http://localhost:${PORT}`));
範例 3:路由檔 index.ts – 使用型別安全的 res.render
// src/routes/index.ts
import { Router, Request, Response, NextFunction } from 'express';
const router = Router();
router.get('/', (req: Request, res: Response, next: NextFunction) => {
// 直接使用已在 pug.d.ts 中定義的 PugLocals
const locals = {
title: '歡迎使用 Express + TypeScript',
user: {
name: 'Alice',
email: 'alice@example.com',
},
};
// ✅ TypeScript 會檢查 locals 是否符合 PugLocals
res.render('index', locals);
});
export default router;
範例 4:index.pug – 參考型別的模板
//- src/views/index.pug
extends layout
block content
h1= title
if user
p 您好,#{user.name}(#{user.email})
else
p 您尚未登入
範例 5:切換到 EJS 時的簡易改寫
// src/app.ts (改為 EJS)
app.set('view engine', 'ejs');
// src/routes/index.ts (使用 EJS 型別)
import { Router, Request, Response, NextFunction } from 'express';
import { EJSTemplateData } from '../types/ejs';
const router = Router();
router.get('/', (req: Request, res: Response<EJSTemplateData>, next: NextFunction) => {
const data = {
pageTitle: 'EJS 範例',
items: ['蘋果', '香蕉', '櫻桃'],
};
// 這裡會自動檢查 data 是否符合 EJSTemplateData
res.render('list.ejs', data);
});
範例 6:自訂 Handlebars Layout
// src/app.ts
import exphbs from 'express-handlebars';
app.engine('hbs', exphbs({
extname: '.hbs',
defaultLayout: 'main',
layoutsDir: path.join(__dirname, 'views/layouts')
}));
app.set('view engine', 'hbs');
{{!-- src/views/layouts/main.hbs --}}
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
{{{body}}}
</body>
</html>
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 忘記載入自訂型別檔 | TypeScript 仍會把 res.render 視為 any,失去型別檢查。 |
確保 import './types/*.d.ts' 或把檔案放在 tsconfig.json 的 typeRoots 中。 |
| 模板檔案路徑錯誤 | res.render('foo') 找不到 foo.pug,會拋出 runtime error。 |
使用 app.set('views', ...) 絕對路徑,並在 tsconfig 中設定 baseUrl。 |
| 模板 locals 與型別不一致 | 多傳或少傳屬性,編譯器不會警告(若未使用泛型)。 | 為每個視圖使用 泛型 res.render<YourLocals>('view', data),或在 pug.d.ts 中列舉所有可能的屬性。 |
| 第三方型別套件版本不匹配 | @types/pug 與 pug 主版本差距過大,會出現型別衝突。 |
使用 npm i -D @types/pug@latest,或在 tsconfig.json 設定 skipLibCheck: true。 |
| 靜態檔案路徑被 Express 解析成視圖路徑 | express.static 放在 app.use 的順序不當,會導致 /css/style.css 被當成模板渲染。 |
先 設定 express.static,再 設定路由與 view engine。 |
最佳實踐
- 分層管理型別:把所有模板相關的型別放在
src/types,並以*.d.ts命名,保持專案結構清晰。 - 使用泛型:在大型專案中,單一全局
PugLocals可能過於寬鬆,使用res.render<SpecificLocals>()能更精確。 - 保持型別同步:每次新增或變更模板變數時,同步更新對應的介面(Interface),避免「跑掉」的 UI。
- 結合 ESLint + @typescript-eslint:設定
@typescript-eslint/explicit-module-boundary-types,強迫每個路由檔明確標註返回型別。 - 測試渲染結果:使用
supertest搭配cheerio撰寫端到端測試,確保模板渲染的 HTML 結構符合預期。
實際應用場景
1. 企業內部儀表板
一個需要根據使用者權限顯示不同資料的儀表板,往往會在模板中使用大量條件判斷。透過 型別化的 locals,可以在開發階段就捕捉到遺漏的屬性或錯誤的資料型別,減少因 UI 錯誤導致的業務問題。
2. 多語系網站
多語系網站會把文字字典傳入模板,例如 { t: (key: string) => string }。若字典介面缺少某些關鍵字,編譯器會直接提醒開發者,避免在瀏覽器看到 undefined 的文字。
3. 電子商務平台的商品頁
商品頁面需要渲染商品名稱、價格、庫存、促銷資訊等多個欄位。使用 EJS + 泛型,可以在服務端保證每筆商品資料都有完整的欄位,前端模板不會因缺少欄位而發生渲染錯誤。
4. 靜態文件與模板混用
在同一個 Express 專案裡,同時提供 SPA 前端的靜態資源(如 React build)與 伺服器端渲染的 Handlebars 頁面。透過正確設定 express.static 的優先順序與型別化的 res.render,即可確保兩者互不干擾,且開發者在寫路由時能清楚分辨哪個回傳 HTML、哪個回傳 JSON。
總結
- 在 TypeScript 中使用 Express 模板引擎時,型別支援是提升開發效率與程式品質的關鍵。
- 透過 自訂全域型別宣告(
declare global)以及 泛型res.render<T>,可以讓編譯器檢查傳入模板的 locals,避免 runtime 錯誤。 - 為每種常見的模板引擎(Pug、EJS、Handlebars)建立對應的
.d.ts檔案,並在tsconfig.json中正確設定typeRoots或paths,即可讓 TypeScript 正確辨識.pug/.ejs/.hbs檔案。 - 實務上,在企業儀表板、多語系站點或電商平台等需要高度可靠 UI 的情境下,型別化的模板渲染能顯著降低除錯成本。
- 最後,別忘了配合 ESLint、單元測試與 CI,將型別檢查納入開發流程,才能真正做到「寫得安全、寫得快」的開發體驗。
祝你在 Express + TypeScript 的旅程中,寫出 型別安全、可維護 的 Web 應用! 🚀