
為什麼需要模組化與模板化?
在開始實作之前,先讓我們談談概念。所謂模組化設計,簡單來說就是將網頁劃分為可重複利用的元件或區塊,每個元件各司其職。想像一下樂高積木——我們可以將網站切割成「標頭(header)」、「導覽菜單(nav)」、「頁尾(footer)」等模組,然後如同組裝積木一般把它們拼湊成完整頁面。這樣做有幾個明顯的好處:
- 重複利用:相同的內容只需撰寫一次,例如整個網站共用同一份導航欄程式碼,不必在每個 HTML 檔案中重寫。
- 一致性:由於共用同一模組,各頁面的風格和行為保持一致,使用者體驗更佳。
- 易於維護:未來若需調整某個區塊(例如增加導覽選單項目),只要修改一處,所有引用該模組的頁面都會同步更新,減少遺漏風險。
除了模組化,模板化開發也是提升效率的關鍵之一。模板化指的是將頁面中的動態內容與呈現結構分離。我們可以建立一份含有特殊占位符的 HTML 模板,並在需要時使用 JavaScript 將資料替換進這些占位符,產生實際顯示的內容。例如,您有一組產品清單資料,要產生多個格式相同的商品卡片版面,模板引擎可以自動迭代資料產生重複的 HTML 結構,而不需要手動拼接字串或重複撰寫標籤。這種做法能讓程式碼更乾淨 (DRY, Don't Repeat Yourself),也更容易維護。
接下來,我們將深入探討如何在不使用任何前端框架的情況下,利用原生的 HTML、CSS、JavaScript 來實現上述模組化與模板化的技巧。從概念到實作,一步步帶您建立清晰的理解與操作能力。
模組化設計的理念與實踐
在模組化設計中,我們著重於將網頁切割為獨立的部分,每一部分負責一種明確的功能或內容。例如,一個常見的網站可能由以下幾個部分組成:
- 頁首:包含網站的 Logo、標題、導覽列等。
- 主內容區:隨頁面而異的內容,本身可能又拆分成更小的元件。
- 側邊欄(如有):導航連結、最新文章列表等。
- 頁尾:版權宣告、聯絡資訊、社群媒體連結等。
透過模組化,我們可以為上述每個部分建立獨立的 HTML 檔案或模板片段。例如,header.html 負責頁首內容,footer.html 負責頁尾內容,等等。主頁面則透過某種方式將這些部分「嵌入」進來。這種拆分方式帶來了關注點分離(Separation of Concerns)的好處:每個檔案只專注於自身的結構,讓我們可以更輕鬆地定位和修改特定區塊的程式碼。
使用 HTML partials(區塊文件)重用頁面片段
HTML 本身並沒有提供類似「include」的語法來直接在一個檔案中包含另一個 HTML。不過,我們可以借助 JavaScript 來實現相似的效果。基本思路是:在主頁面的適當位置放一個佔位容器,然後用 JavaScript 動態載入外部 HTML 檔的內容,插入到容器中。
以下是一個簡單的模組化範例。我們將建立三個文件:header.html(頁首)、footer.html(頁尾)以及主要的頁面 index.html。我們也會撰寫一小段純 JavaScript 程式碼來載入這些部分。
1. 建立可重用的 HTML 區塊檔案
先準備頁首和頁尾的 HTML 檔案。
<!-- header.html -->
<header>
<h1>我的網站</h1>
<nav>
<a href="index.html">首頁</a> |
<a href="about.html">關於我們</a> |
<a href="contact.html">聯絡我們</a>
</nav>
<hr>
</header>
<!-- footer.html --> <footer> <hr> <p>版權所有 © 2025 我的網站。</p> </footer>
以上兩個檔案各自定義了頁首和頁尾的結構。我們特意將它們獨立成檔案,以便在多個頁面中包含使用。
2. 主頁面中放置佔位容器
在 index.html(主頁面)中,使用特殊的屬性來標記出要插入外部內容的位置。我們可以自定義一個屬性,例如 data-include:
<!-- index.html (局部片段) -->
<body>
<!-- 頁首區塊的佔位容器 -->
<div data-include="header.html"></div>
<!-- 主內容區 -->
<main>
<h2>歡迎光臨!</h2>
<p>這是我的網站首頁。</p>
</main>
<!-- 頁尾區塊的佔位容器 -->
<div data-include="footer.html"></div>
<!-- 引入包含功能的腳本 -->
<script src="include.js"></script>
</body>
在上述程式碼中,我們使用 <div data-include="header.html"></div> 作為佔位符,告訴程式碼「在這裡插入 header.html 的內容」。同樣地,頁尾部分放了一個 data-include="footer.html" 的容器。這些 data-include 屬性是我們自行定義的標記,瀏覽器不會直接做任何處理——需要靠我們下一步的 JavaScript 來識別它們並執行包含操作。
3. 實現純 JavaScript 的 include 功能
接下來,撰寫 include.js 腳本,負責尋找所有具備 data-include 屬性的元素,載入對應的檔案內容並插入頁面中。以下是一個簡易的實作:
// include.js
function includeHTML() {
// 取得頁面中所有含 data-include 特性的元素
const includeElems = document.querySelectorAll('[data-include]');
includeElems.forEach(elem => {
const url = elem.getAttribute('data-include');
if (!url) return;
// 使用 Fetch API 讀取外部 HTML 檔內容
fetch(url).then(response => {
if (!response.ok) throw new Error(`載入 ${url} 失敗: ${response.status}`);
return response.text();
}).then(html => {
elem.innerHTML = html; // 將取得的 HTML 字串插入容器
elem.removeAttribute('data-include'); // 移除屬性避免重複載入
includeHTML(); // 遞迴調用,以處理可能的巢狀包含
}).catch(err => {
console.error(err);
elem.innerHTML = `<p style="color:red;">載入失敗: ${url}</p>`;
});
});
}
// 等待 DOM 完全載入後執行包含
window.addEventListener('DOMContentLoaded', includeHTML);
這段程式碼逐一處理頁面中的每個包含容器:
- 利用 fetch 發送 HTTP 請求取得對應檔案的內容(例如讀取 header.html 的文字)。
- 讀取完成後,將返回的 HTML 字串插入到對應的 <div> 容器內 (elem.innerHTML = html)。
- 插入後,移除該容器的 data-include 屬性,然後再次呼叫 includeHTML()。這樣做的用意是支援巢狀的包含──如果載入進來的片段本身還有要包含的部分,遞迴呼叫可確保將它們也處理掉。
- 若載入過程發生錯誤,例如檔案不存在或網路失敗,就在容器中顯示錯誤訊息,並在主控台印出錯誤資訊(方便除錯)。
使用上述機制,我們的 index.html 在載入時將自動把 header.html 和 footer.html 內容抓取並嵌入。從使用者的角度看,整個網頁仍然是完整的一體的,但對開發者而言,我們已經成功地把頁首和頁尾模組化了。在後續其他頁面(例如 about.html, contact.html)中,也可以使用相同的方法 <div data-include="header.html"></div> 來重用頁首內容,確保全站一致。
提示:上述方法需要透過 HTTP(S) 協定存取檔案,因此若直接在本機以檔案路徑開啟 index.html,fetch 可能因瀏覽器的安全限制而失敗。建議您在本機架設一個簡易伺服器(例如使用 VS Code 的 Live Server 外掛、Python SimpleHTTPServer、Node.js 等)來進行測試。這樣瀏覽器會以 http://localhost 的方式載入頁面,允許 fetch 正常抓取部分檔案。
模組化的 CSS 與 JavaScript 考量
模組化開發不僅涉及 HTML 結構,還包括相關的 CSS 和 JavaScript。當我們把 HTML 拆分成多個部分時,也需要考慮樣式和行為的組織:
CSS 部分:可以將不同元件的樣式寫在同一個 CSS 檔中,透過範圍限定(例如以元件最外層容器的 ID 或類別當前綴)避免衝突。例如為頁首樣式添加 .site-header nav { ... } 限定作用範圍。如果希望各模組的 CSS 分開管理,也可以建立對應的 CSS 檔案(如 header.css、footer.css),在主頁面中以 <link> 引用。為了減少 HTTP 請求數量,開發完成後可考慮將 CSS 合併或透過構建工具壓縮打包。
JavaScript 部分:類似地,不同模組若有專屬的互動行為,可以寫在各自的 JS 檔案中(如 header.js 處理導航欄的互動),在需要的頁面中以 <script> 引入。但要注意避免重複執行:例如,如果每個頁面都載入同一份 header.js,那包含的函式或事件綁定會在每頁面刷新時重設。這通常沒問題,但請避免在同一頁面多次載入同一支腳本,否則可能出現衝突或效能問題。
透過以上策略,我們將網站的結構、樣式、行為都進行了一定程度的模組化。接下來,我們把重點轉向模板設計,看看如何利用純 JavaScript 來實現前端模板機制,動態產生頁面內容。
模板引擎概念與純 JavaScript 模板實作
當我們談到「模板引擎」,指的是一種使用預先定義的模板來產生動態內容的技術。在模板中,我們會放入特殊的佔位符(placeholder)或標記,等到真正渲染時再以實際資料取代那些佔位符。這種方式可以讓資料(內容)與表現(結構)分離,使代碼更整潔且易於維護。
舉個例子,假設有一份模板如下:
<h1>{{title}}</h1>
<ul>
{{#names}}
<li>{{name}}</li>
{{/names}}
</ul>
如果您看過類似 Handlebars 或 Mustache 的模板語法,應該對這種 {{...}} 語法不陌生。上面這段模板描述了一個清單結構:有一個標題和一個名稱列表。{{title}} 和 {{name}} 是佔位符,表示未來將插入實際的標題文字和姓名資料;{{#names}} ... {{/names}} 則是一種區段語法,表示區塊內的內容會對資料陣列反覆迭代。假設我們有以下的資料物件:
const data = {
title: "好友名單",
names: [
{ name: "小明" },
{ name: "小華" }
]
};
將資料套入模板後,最終輸出的 HTML 應該是:
<h1>好友名單</h1> <ul> <li>小明</li> <li>小華</li> </ul>
可以看到,模板的好處在於一次定義,重複使用。我們不需要硬碼兩個 <li>,而是由模板引擎根據資料自動迭代產生。當需要修改清單項目的結構時(例如每個名字旁加一個圖示),只需改動模板部分即可,資料不必更改,大幅降低出錯和遺漏的機率。
輕量級的純 JS 模板範例
市面上有許多成熟的模板引擎(例如 Mustache、Handlebars、Nunjucks 等),但在這裡我們不使用它們,而是展示利用純 JavaScript 實現簡單模板替換的思路,幫助理解模板引擎的原理。
我們來實作一個最簡單的情境:假設要動態產生一組產品列表,每個產品包含名稱和價格兩個屬性。我們可以先在 HTML 中定義一個模板片段,使用自定義的佔位符語法(例如 {{name}}、{{price}}),然後用 JS 將資料帶入。
<!-- 定義模板(使用 <script> tag 作為容器,不會被渲染) -->
<script type="text/template" id="product-item-tpl">
<li class="product-item">
<strong>{{name}}</strong> - <span class="price">{{price}}</span>
</li>
</script>
<ul id="productList"></ul>
<script>
// 取得模板字串
const template = document.getElementById('product-item-tpl').innerHTML.trim();
// 模擬產品資料陣列
const products = [
{ name: "咖啡豆", price: "$300" },
{ name: "紅茶葉", price: "$250" },
{ name: "綠茶葉", price: "$180" }
];
let resultHTML = "";
for (const item of products) {
// 將每筆資料代入模板佔位符
let entry = template;
entry = entry.replace("{{name}}", item.name);
entry = entry.replace("{{price}}", item.price);
resultHTML += entry;
}
// 將產生的所有 <li> 插入至清單 <ul>
document.getElementById('productList').innerHTML = resultHTML;
</script>
在上述程式碼中,我們做了以下幾件事:
- 使用一個隱藏的 <script type="text/template"> 區塊來存放 HTML 模板結構(注意:type="text/template" 讓瀏覽器忽略這段 script 的內容,不會執行或顯示它)。這裡定義了一個清單項目的 HTML 模樣,帶有 {{name}} 和 {{price}} 兩個佔位符。
- 利用 JavaScript 抓取模板字串內容,然後準備好產品資料的陣列。
- 遍歷每筆資料,對於每個產品項目,複製一份模板字串,使用 string.replace() 將其中的佔位符替換為實際資料。例如第一項產品會將 {{name}} 替換成 "咖啡豆",{{price}} 替換成 "$300"。
- 將替換後得到的 <li> 字串累加到最終的結果字串中。迴圈結束後,resultHTML 就包含了多個 <li>...</li> 組成的清單。
- 最後,將 resultHTML 插入頁面中預留的 <ul id="productList"> 裡,瀏覽器即可渲染出我們想要的清單。
這樣,我們就完成了利用純 JS 替換模板的過程。雖然這個例子非常輕量且只處理了最基本的佔位符替換,但它已經體現了模板引擎的核心原理。在更進階的模板引擎中,您可以使用更豐富的語法,例如條件判斷(如 {{#if condition}}...{{/if}})、循環列表(類似我們上面示範的用法)、甚至可以定義輔助函數來格式化數字或日期等。
HTML <template> 元素與 DOM 模板
值得一提的是,HTML5 引入了一個原生支援模板的元素 <template>。它可以在 HTML 中先行定義一段片段,其內容在頁面載入時不會被瀏覽器渲染,直到我們用 JavaScript 明確地將其實例化。相比用 <script type="text/template"> 放字串,<template> 的優點是其中可以直接寫標籤且保持排版,編輯時有語法加亮提示。使用方式如下:
<template id="itemTemplate">
<div class="item">
<h3 class="name"></h3>
<p class="desc"></p>
</div>
</template>
<div id="itemContainer"></div>
<script>
const templateEl = document.getElementById('itemTemplate');
const container = document.getElementById('itemContainer');
const itemsData = [
{ name: "項目A", desc: "說明文字A" },
{ name: "項目B", desc: "說明文字B" }
];
for (const data of itemsData) {
// 複製 <template> 內的 DOM 範本
const content = templateEl.content.cloneNode(true);
// 填入資料
content.querySelector('.name').textContent = data.name;
content.querySelector('.desc').textContent = data.desc;
// 插入實際 DOM
container.appendChild(content);
}
</script>
上面的程式碼示範了 <template> 元素的用法:透過 templateEl.content.cloneNode(true) 複製出其中定義的 DOM 範本,然後再用 JS 操作找到裡面的元素節點填入資料,最後附加到網頁中。<template> 元素提供了更貼近 DOM 操作的模板方式,避免我們進行繁瑣又容易出錯的字串取代。在需要大量產生複雜節點時,這種方法尤其便於管理。
管理模板與部分的策略
當專案越做越大時,可能會出現非常多的模組與模板。這時我們需要講究一些管理策略,確保效能和平衡開發便利性:
檔案組織:將相關的模組檔案放入對應的資料夾,例如建立 partials/ 資料夾存放所有 HTML 區塊檔(header, footer 等),templates/ 資料夾存放純前端用途的模板檔。良好的結構可以讓團隊開發時更容易找到所需的檔案。
避免過多請求:如果一個頁面需要包含許多 HTML 區塊(partials),每個都透過 fetch 獲取,將產生多次 HTTP 請求,對效能有負面影響。解決方式之一是在開發階段使用打包或併檔工具,將多個片段合併為一個檔案,或在部署階段開啟伺服器端的模板引擎預先渲染,減少前端請求數量。另外,也可以考慮使用前述的 <script type="text/template"> 或 <template>,一次載入所有模板內容在頁面中,雖然初始下載體積變大,但避免了後續多次的請求開銷。
快取機制:善用瀏覽器快取來提高包含片段的載入速度。靜態的部分檔案(如 header.html)通常可以被瀏覽器快取,第一次抓取後,後續頁面讀取相同 URL 時會更快。如果您採用 SPA(單頁應用)的方式動態載入多個部分,可以實作一個客戶端快取:將已載入過的片段內容暫存於變數中,下次需要相同片段時直接重用,無需再發送請求。
維護與同步:定期檢視各個模板與模組,確保命名一致且用途明確。為模板中的佔位符採用具有辨識度的名稱(例如 {{product_price}} 比單純 {{price}} 更清晰)以避免混淆。如果項目有國際化需求,模板中可能還需要支持多語系文字的替換,這又是另一層面的管理,可以透過鍵值對映的方式來替換佔位符為對應語系內容。
透過以上策略,我們可以在保持程式碼乾淨的同時,兼顧性能與維護便利性。模組化讓我們有效地管理網頁的結構塊,模板化使我們能優雅地處理重複的模式化內容。兩者結合,對於中型以上的前端專案將大有助益。
在進入最後的常見問答集錦之前,我們來回顧一下重點:模組化著重於把頁面的結構拆解成可重用的部分,而模板化著重於把資料和版型解耦,以程式自動生成重複的結構。理解這兩者並靈活運用,您就能在不用框架的情況下,大幅提升開發效率和程式碼的可維護性。
常見問題解答 Q&A
最後整理幾個在實務操作中常見的問題,提供實用的解答與建議:
Q: 模組化 HTML 如何避免重複載入相同內容?
A: 如果在同一頁面中多次包含相同的區塊(例如頁面上有兩處都需要載入 sidebar.html),可以採用快取的方式避免重複請求。具體做法是在 JavaScript 中維護一個物件,例如 let cache = {}, 每次 fetch 後將結果存入快取,如 cache[url] = responseText。下次再遇到相同的 url 欲載入時,先檢查 cache,有的話直接使用快取的內容,而不再發送新的請求。另外,伺服器端也可以設定適當的快取 header,讓瀏覽器自動快取這些靜態片段檔案,減少重複下載的開銷。
Q: 純 JavaScript 要如何實現類似伺服器端的 include 功能?
A: 正如本文章示範的,我們可以使用 JavaScript 的 Fetch API 或 XMLHttpRequest 來讀取外部 HTML 檔,再將其插入主頁面 DOM 中。步驟包括:在 HTML 放置佔位元素、撰寫 JS 函式抓取檔案並插入內容、於頁面載入時呼叫該函式。需要注意的是,這種方法屬於客戶端包含,必須在瀏覽器端執行,因此依賴使用者瀏覽器允許執行 JS 且網路連線正常。相比之下,伺服器端包含(如 PHP 的 include 或模板引擎語法)是在發送給用戶之前就處理好的。兩種方式各有利弊:純 JS 包含無需後端支持但對 SEO 和初始載入速度可能有些影響;伺服器包含則需要後端環境但對使用者更透明。
Q: 可不可以直接使用 <iframe> 等方式來重用 HTML 區塊?
A: 理論上可以用 <iframe src="header.html"></iframe> 或 <object data="header.html"></object> 來嵌入一個外部 HTML,但並不建議這樣做。原因是 iframe 會將內容放在獨立的子文件中,與主頁面隔離:這意味著主頁面的樣式可能無法直接作用在 iframe 內的內容,兩者的 JavaScript 也在不同作用域,溝通不便。另外,iframe 本身有一些先天缺點,例如增加頁面複雜度、影響 SEO(搜尋引擎可能難以索引iframe裡的內容)、以及同步滾動/高度自適應等問題需要額外處理。因此,除非有需要嵌入外部第三方內容的特殊需求,一般不會用 iframe 來實現頁面內部的模組重用。
Q: 使用 JavaScript 動態載入的內容會不會有 SEO 上的疑慮?
A: 這確實是值得注意的問題。傳統上,搜尋引擎抓取網頁主要看初始載入的 HTML。如果主要內容是透過 JS 載入的,早期的搜尋引擎可能忽略這些後載內容。不過現在主流搜尋引擎(如 Google)已經能執行部分 JavaScript,因此能索引透過 JS 動態產生的內容。但不同搜尋引擎支援程度不同,而且如果您的網站高度仰賴 JS 來呈現內容,SEO 成效可能仍不及傳統多頁式網站。解決方法包括:對於關鍵內容使用預渲染或後端渲染,或在頁面中提供基本的內容結構作為 fallback。總之,如果SEO是重中之重,建議評估使用伺服器端模板來產生完整HTML,再透過模組化的概念去維護那些模板。
Q: 前端模板與後端模板有何差異?我應該選擇哪一種?
A: 前端模板是在使用者的瀏覽器中,由 JavaScript 根據資料生成內容;後端模板則是在伺服器生成好 HTML 再發送給用戶。前端模板的優點是互動性強,適合單頁應用(SPA)或需要即時更新介面的場景;缺點是首屏渲染可能較慢且依賴瀏覽器執行。後端模板優點是首次載入快速且 SEO 友好,因為使用者拿到的就是完整的 HTML;缺點是每次更新內容都需要重新請求或刷新頁面。很多現代網站會採用前後端結合的方式:初始頁面透過後端渲染送出,之後的交互再由前端模板動態處理,以取得兩者優勢的平衡。就純 HTML/CSS/JS 而言,如果沒有後端支援環境,那前端模板是你的主要選擇;反之,若已經有後端架構,也可利用伺服器端模板引擎進行模組化,再在前端做最小程度的動態更新。
透過以上的介紹與範例,您應該對於如何在無框架的情況下進行 HTML 模組化和模板化開發有了更深入的理解。掌握這些技巧,將能讓您在日後面對大型網站或重複性高的介面時如魚得水。不論是共用的頁面元件,還是大量資料驅動的內容產生,都可以運用本文提到的方法來簡化工作、提升維護效率。希望您在實作的過程中持續探索、靈活運用,打造出結構清晰且易於維護的網站!