ExpressJS (TypeScript) – Static Files 與 Template Engines
EJS / Handlebars / Pug 介紹(如需模板)
簡介
在 Node.js 生態系中,Express 是最常被使用的 Web 框架,而 TypeScript 則為 JavaScript 帶來靜態型別,讓程式碼更安全、更易於維護。
開發一個完整的網站時,除了 API 邏輯,靜態資源(CSS、圖片、前端套件)與 動態頁面模板 同樣不可或缺。透過 Express 內建的 static middleware,我們可以快速提供檔案服務;而使用模板引擎(如 EJS、Handlebars、Pug)則能在伺服器端產生 HTML,讓資料與版面緊密結合。
本篇文章將以 TypeScript 為基礎,說明如何在 Express 中設定靜態檔案與三大熱門模板引擎,並提供實作範例、常見陷阱與最佳實踐,幫助讀者快速上手、在實務專案中靈活選擇合適的渲染方式。
核心概念
1. 靜態檔案服務
Express 的 express.static 中介層可以把目錄映射成 URL 路徑,讓瀏覽器直接取得 CSS、JS、圖片等資源。
// src/app.ts
import express, { Request, Response, NextFunction } from 'express';
import path from 'path';
const app = express();
// 1️⃣ 設定靜態目錄
app.use('/public', express.static(path.resolve(__dirname, '..', 'public')));
// 測試路由
app.get('/', (req: Request, res: Response) => {
res.send('Hello Express + TypeScript');
});
export default app;
重點:
path.resolve可保證在不同作業系統上取得正確的絕對路徑。- 建議將靜態路徑掛載在子路徑(如
/public),避免與 API 路由衝突。
2. 為什麼需要模板引擎?
- 分離關注點:把 HTML 標記與程式邏輯分開,讓前端設計師與後端開發者可以平行作業。
- 資料驅動:在伺服器端直接把資料注入 HTML,減少前端額外的 AJAX 請求。
- 重用版型:透過 layout、partial(部件)機制,避免重複撰寫相同結構。
目前最常見的三種模板引擎:
| 引擎 | 語法風格 | 優點 | 常見使用情境 |
|---|---|---|---|
| EJS | 類似 HTML,使用 <% %> 標籤 |
上手快、與 HTML 幾乎無縫 | 小型專案、快速原型 |
| Handlebars | Mustache 風格,支援 block helpers | 強大的 helper 系統,易於擴充 | 中大型專案、需要自訂 helper |
| Pug | 縮排式 DSL(類似 Python) | 省去大量閉合標籤,寫起來更簡潔 | 喜歡「寫程式碼」感的開發者 |
以下分別示範在 TypeScript + Express 中的設定與基本使用。
3. EJS 設定與範例
3.1 安裝與設定
npm i ejs
npm i -D @types/ejs
// src/app.ts(續)
import ejs from 'ejs';
// 2️⃣ 設定 view engine 為 EJS
app.set('view engine', 'ejs');
// 設定 views 目錄(預設是 /views)
app.set('views', path.resolve(__dirname, '..', 'views'));
3.2 基本範例
檔案結構
/views
└─ index.ejs
/public
└─ css
└─ style.css
<!-- views/index.ejs -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title><%= title %></title>
<link rel="stylesheet" href="/public/css/style.css">
</head>
<body>
<h1>Hello <%= user.name %>!</h1>
<ul>
<% users.forEach(u => { %>
<li><%= u.name %> - <%= u.age %>歲</li>
<% }) %>
</ul>
</body>
</html>
// src/routes/home.ts
import { Router, Request, Response } from 'express';
const router = Router();
router.get('/', (req: Request, res: Response) => {
const data = {
title: 'EJS 範例',
user: { name: '小明' },
users: [
{ name: '阿華', age: 28 },
{ name: '小美', age: 22 },
],
};
// 3️⃣ 渲染模板
res.render('index', data);
});
export default router;
技巧:EJS 允許直接在模板中寫 JavaScript 表達式,若需要較複雜的邏輯,建議在路由層先處理好資料,保持模板「只負責呈現」的原則。
4. Handlebars 設定與範例
4.1 安裝與設定
npm i express-handlebars
npm i -D @types/express-handlebars
// src/app.ts(續)
import exphbs from 'express-handlebars';
// 2️⃣ 設定 Handlebars
app.engine('hbs', exphbs.engine({
extname: '.hbs', // 使用 .hbs 副檔名
defaultLayout: 'main', // 版型檔案 (views/layouts/main.hbs)
layoutsDir: path.resolve(__dirname, '..', 'views', 'layouts'),
partialsDir: path.resolve(__dirname, '..', 'views', 'partials'),
}));
app.set('view engine', 'hbs');
app.set('views', path.resolve(__dirname, '..', 'views'));
4.2 Layout 與 Partial
檔案結構
/views
├─ layouts
│ └─ main.hbs
├─ partials
│ └─ header.hbs
└─ home.hbs
{{!-- views/layouts/main.hbs --}}
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/public/css/style.css">
</head>
<body>
{{> header}} {{!-- 引入 partial --}}
{{{body}}} {{!-- 渲染子模板 --}}
</body>
</html>
{{!-- views/partials/header.hbs --}}
<header>
<h1>My Site</h1>
<nav>
<a href="/">Home</a> |
<a href="/about">About</a>
</nav>
</header>
{{!-- views/home.hbs --}}
{{!-- 只寫內容,layout 會自動套用 --}}
<h2>Welcome, {{user.name}}!</h2>
<p>你目前有 {{items.length}} 件商品在購物車。</p>
<ul>
{{#each items}}
<li>{{this.name}} - ${{this.price}}</li>
{{/each}}
</ul>
// src/routes/home.ts(續)
router.get('/', (req: Request, res: Response) => {
const ctx = {
title: 'Handlebars 範例',
user: { name: '阿珍' },
items: [
{ name: '筆記型電腦', price: 29900 },
{ name: '滑鼠', price: 850 },
],
};
res.render('home', ctx);
});
小技巧:Handlebars 的
{{> partialName}}讓部件化變得非常直觀;若需要自訂 helper,可在exphbs.engine({ helpers: {...} })中註冊。
5. Pug 設定與範例
5.1 安裝與設定
npm i pug
npm i -D @types/pug
// src/app.ts(續)
app.set('view engine', 'pug');
app.set('views', path.resolve(__dirname, '..', 'views'));
5.2 Pug 語法示例
檔案結構
/views
└─ dashboard.pug
//- views/dashboard.pug
doctype html
html(lang="zh-TW")
head
meta(charset="UTF-8")
title= title
link(rel="stylesheet", href="/public/css/style.css")
body
h1 Dashboard
p Hello #{user.name},今天是 #{date}
ul
each item in items
li #{item.name} - $#{item.price}
// src/routes/dashboard.ts
router.get('/dashboard', (req: Request, res: Response) => {
const data = {
title: 'Pug 範例',
user: { name: '小王' },
date: new Date().toLocaleDateString('zh-TW'),
items: [
{ name: '鍵盤', price: 1200 },
{ name: '螢幕', price: 8500 },
],
};
res.render('dashboard', data);
});
注意:Pug 以縮排代表階層,若縮排不正確會拋出
IndentationError,建議在 IDE 中開啟「顯示空白字元」功能,避免混用 Tab 與 Space。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
| 靜態檔案路徑錯誤 | express.static 的相對路徑寫錯會導致 404。 |
使用 path.resolve(__dirname, '..', 'public') 絕對化路徑。 |
| 模板檔案副檔名不一致 | Express 會根據 view engine 判斷副檔名,若檔名錯誤會無法渲染。 |
統一設定 extname(如 Handlebars 設 .hbs),檔案名稱須相符。 |
| 資料注入未經驗證 | 直接把使用者輸入渲染到模板,容易產生 XSS。 | 使用模板引擎的自動 escaping(EJS、Handlebars、Pug 預設皆有),若需原始 HTML,使用 {{{rawHtml}}}(Handlebars)或 !{rawHtml}(EJS)前務必做好 sanitize-html 處理。 |
| 過度在模板內寫商業邏輯 | 例如在 EJS 內大量迴圈或條件,難以維護。 | 把資料整理的邏輯放在路由或服務層,模板只負責顯示。 |
| Cache 設定不當 | 開發階段若開啟模板快取,修改模板不會即時生效。 | 在 app.set('env', 'development') 時關閉快取;正式環境可啟用 app.enable('view cache') 提升效能。 |
| 缺少 Layout/Partial | 每個頁面都重寫相同的 <head>、<header>,維護成本高。 |
使用 layout(如 Handlebars 的 defaultLayout)與 partial(EJS include、Pug include)做部件化。 |
最佳實踐總結:
- 統一路徑管理:所有檔案路徑皆使用
path.resolve,避免平台差異。 - 環境分離:開發環境關閉 view cache,正式環境開啟。
- 安全第一:依賴模板引擎自動 escaping,若需 raw HTML 必須先 sanitize。
- 部件化設計:盡可能將共用區塊抽成 partial / include,減少重複。
- 型別安全:在 TypeScript 中為
res.render的資料建立介面(interface),提升 IDE 自動完成與錯誤檢查。
實際應用場景
| 場景 | 推薦模板引擎 | 為什麼選它 |
|---|---|---|
| 企業內部管理系統(大量表單、共用版型) | Handlebars | 強大的 helper 與 layout 機制,易於維護大型版面。 |
| 快速原型或小型部落格 | EJS | 直接寫 HTML,學習曲線最低,適合快速上線。 |
| 技術文件、開發者工具 UI | Pug | 縮排語法讓程式碼更乾淨,特別適合大量動態產生的標籤(如表格、樹狀結構)。 |
| 多語系網站 | 任一(配合 i18n 中介層) | 只要在渲染前注入翻譯字典,三者皆可支援。 |
| SEO 友好的服務端渲染 | Handlebars / Pug | 兩者皆支援完整的 HTML 輸出,搜尋引擎可直接爬取。 |
範例:假設要建置一個「商品列表」頁面,若資料量不大且設計師熟悉 HTML,使用 EJS 能讓設計稿直接貼上;若未來需要加入「購物車」共用區塊,則可逐步遷移到 Handlebars,把購物車區塊抽成 partial,減少改動範圍。
總結
在 Express + TypeScript 的開發環境裡,靜態檔案服務 只需要一行 express.static 就能完成;而 模板引擎 則提供了從簡單到進階的多樣選擇:
- EJS:寫起來最像原生 HTML,適合快速開發與小型專案。
- Handlebars:支援 layout、partial 與自訂 helper,適合中大型應用。
- Pug:縮排式語法讓標記更精簡,適合喜歡「程式碼即文件」的開發者。
掌握上述設定與最佳實踐,配合 TypeScript 的型別保護,能大幅提升專案的可讀性、可維護性與安全性。未來在實際專案中,只要根據 功能需求、團隊熟悉度與維護成本 來挑選合適的模板引擎,即可在前後端協作上事半功倍。祝開發順利,玩得開心!