通過JS模板引擎實現動態模塊組件(Vite+JS+Handlebars)
1. 引言
在上一篇文章《實現一個前端動態模塊組件(Vite+原生JS)》中,筆者通過原生的JavaScript實現了一個動態的模塊組件。但是這個實現并不完善,最大的問題就是功能邏輯并沒有完全分開。比如模塊的HTML:
<div class="category-section">
<h3>分類專欄</h3>
<ul class="category-list">
</ul>
</div>
其實只是靜態內容,動態的內容其實在JavaScript中實現:
const categoryList = document.querySelector(".category-list");
categories.forEach((category) => {
const categoryItem = document.createElement("li");
categoryItem.innerHTML = `
<a href="#" class="category-item">
<img src="category/${category.firstCategory.iconAddress}" alt="${category.firstCategory.name}" class="category-icon">
<span class="category-name">${category.firstCategory.name} <span class="article-count">${category.firstCategory.articleCount}篇</span></span>`;
if (category.secondCategories.length != 0) {
categoryItem.innerHTML += `
<ul class="subcategory-list">
${category.secondCategories
.map(
(subcategory) => `
<li><a href="#" class="subcategory-item">
<img src="category/${subcategory.iconAddress}" alt="${subcategory.name}" class="subcategory-icon">
<span class="subcategory-name">${subcategory.name} <span class="article-count">${subcategory.articleCount}篇</span></span>
</a></li>
`
)
.join("")}
</ul>
</a>
`;
}
categoryList.appendChild(categoryItem);
一般來說,HTML負責網頁結構和內容,CSS控制樣式和布局,JavaScript實現交互和動態功能。因此,最好把動態的部分也加入到HTML中去,不僅邏輯上更加清晰,像一些調試樣式的操作也更加方便。不過這樣的話,HTML部分就不是一些單純的HTML元素了,而是一個生成HTML頁面的模板字符串。
考慮一下如何實現從模板字符串展開成HTML元素的操作。如果只是單獨的變量那好做,比如圖表控件統計的格式,我們可以在模板字符串中加上一些特殊的標識符,比如使用“{{}}”將其包裹起來,然后在其展開之前通過正則表達式查找替換出成后端獲取的變量即可。但是如果是數組變量怎么辦呢?在展開之前我們是不知道數組變量的個數的,比如案例中分類專欄的個數。那么我們就要寫類似于for循環的標識符,然后識別并展開成HTML元素。
這樣的實現思路感覺就略顯麻煩了,筆者反正是不愿意去碰很抽象的正則表達式的。好在其實這個問題早就有了解決方案,那就是模板引擎。前端的模板引擎有很多種,像Vue這樣的前端框架甚至自帶,筆者這里使用的是Handlebars。使用模板引擎不僅僅只有前面筆者論述的兩點,但是這里的案例沒有用到,筆者就不進行論述了。
2. 實現
2.1 安裝依賴包
那么我們就使用Handlebars來改造之前的案例。首先需要安裝Handlebars,通過VS Code打開的終端中輸入如下指令:
npm install handlebars --save
Handlebars依賴包就安裝到當前項目的環境中了,我們可以在package.json中看到:
{
"name": "my-native-js-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^6.3.5"
},
"dependencies": {
"handlebars": "^4.7.8"
}
}
另外,在這里筆者就簡單介紹一下依賴包的安裝。對于一個前端項目來說,依賴包的安裝是非常重要的,接手新項目的時候,往往是項目本地的代碼沒有問題,依賴庫的安裝反而很麻煩。一般來說,如果是初次接手項目,需要安裝所有的依賴包:
npm install
但是有時候會遇到網絡問題安裝不上,可以通過設置代理解決,或者更換依賴包源地址。如果需要安裝特定的包,那么指令就是:
npm install <package-name>
不過有時會遇到與項目的依賴包環境不匹配問題,或者網絡漏洞問題,這個時候就需要升級或者降級一些依賴包。
另外,對于開發環境僅需的依賴(package.json中的devDependencies節點),可以使用 --save-dev 或 -D 標志來安裝:
npm install <package-name> --save-dev
2.2 優化代碼
既然使用Handlebars模板引擎了,那么表達網頁結構和內容的部分就不再是HTML元素而是Handlebars模板了,因此將category.html修改成category.handlebars,其內容如下:
<div class="category-section">
<h3>分類專欄</h3>
<ul class="category-list">
{{#each categories}}
<li>
<a href="#" class="category-item">
<img src="category/{{firstCategory.iconAddress}}" alt="{{firstCategory.name}}"
class="category-icon" />
<span class="category-name">
{{firstCategory.name}}
<span class="article-count">
{{firstCategory.articleCount}}篇
</span>
</span>
<ul class="subcategory-list">
{{#each secondCategories}}
<li>
<a href="#" class="subcategory-item">
<img src="category/{{iconAddress}}" alt="{{name}}"
class="subcategory-icon">
<span class="subcategory-name">
{{name}}
<span class="article-count">{{articleCount}}篇</span>
</span>
</a>
</li>
{{/each}}
</ul>
</a>
</li>
{{/each}}
</ul>
</div>
HTML元素部分我們已經很熟悉了,關鍵在于Handlebars模板引擎部分。{{#each}}和{{/each}}是Handlebars的一個塊表達式,可以將其理解成foreach語句,用于遍歷數組。這里我們分別遍歷了一級分類專欄({{#each categories}})和二級分類專欄({{#each secondCategories}})。
另一個值得說明的是{{name}}、{{iconAddress}}、{{articleCount}}這些都是用來展示具體數據的占位符,Handlebars會在渲染時用實際的數據替換這些占位符。不過相信讀者也發現了,一級分類的占位符({{firstCategory.name}})和二級分類的占位符({{name}})并不一致。其實這與傳入到Handlebars模板進行展開時的數據參數有關,再次看一下數據:
[
{
"firstCategory": {
"articleCount": 4,
"iconAddress": "三維渲染.svg",
"name": "計算機圖形學"
},
"secondCategories": [
{
"articleCount": 2,
"iconAddress": "opengl.svg",
"name": "OpenGL/WebGL"
},
{
"articleCount": 2,
"iconAddress": "專欄分類.svg",
"name": "OpenSceneGraph"
},
{ "articleCount": 0, "iconAddress": "threejs.svg", "name": "three.js" },
{ "articleCount": 0, "iconAddress": "cesium.svg", "name": "Cesium" },
{ "articleCount": 0, "iconAddress": "unity.svg", "name": "Unity3D" },
{
"articleCount": 0,
"iconAddress": "unrealengine.svg",
"name": "Unreal Engine"
}
]
},
{
"firstCategory": {
"articleCount": 4,
"iconAddress": "計算機視覺.svg",
"name": "計算機視覺"
},
"secondCategories": [
{
"articleCount": 0,
"iconAddress": "圖像處理.svg",
"name": "數字圖像處理"
},
{
"articleCount": 0,
"iconAddress": "特征提取.svg",
"name": "特征提取與匹配"
},
{
"articleCount": 0,
"iconAddress": "目標檢測.svg",
"name": "目標檢測與分割"
},
{ "articleCount": 4, "iconAddress": "SLAM.svg", "name": "三維重建與SLAM" }
]
},
{
"firstCategory": {
"articleCount": 11,
"iconAddress": "地理信息系統.svg",
"name": "地理信息科學"
},
"secondCategories": []
},
{
"firstCategory": {
"articleCount": 31,
"iconAddress": "代碼.svg",
"name": "軟件開發技術與工具"
},
"secondCategories": [
{ "articleCount": 2, "iconAddress": "cplusplus.svg", "name": "C/C++" },
{ "articleCount": 19, "iconAddress": "cmake.svg", "name": "CMake構建" },
{ "articleCount": 2, "iconAddress": "Web開發.svg", "name": "Web開發" },
{ "articleCount": 7, "iconAddress": "git.svg", "name": "Git" },
{ "articleCount": 1, "iconAddress": "linux.svg", "name": "Linux開發" }
]
}
]
結合這個數據的結構來說,Handlebars使用了一種上下文或者作用域的概念:當進入一個{{#each}}循環時,當前上下文會變成數組中的當前元素。因此在第一層循環中獲取分類專欄的名稱是{{firstCategory.name}},而在第二層循環中分類專欄的名稱則可以省略成{{name}},其他變量也是同理。應該來說,Handlebars模板內容與HTML結構的文本非常接近了,保證了動態特性的同時還隔離了HTML頁面的結構組織和交互行為。最直觀的說法就是,調試樣式方便了,不用在HTML字符串中寫class、id了,而是可以像在寫在靜態頁面中一樣寫在模板中。
接下來看一下改進之后的category.js,具體代碼如下:
import "./category.css";
import Handlebars from "handlebars";
import templateSource from "./category.handlebars?raw";
async function loadCategory() {
try {
const response = await fetch("/categories.json");
if (!response.ok) {
throw new Error("網絡無響應");
}
const categories = await response.json();
// 編譯模板
const template = Handlebars.compile(templateSource);
// 渲染模板
const renderedHtml = template({
categories,
});
// 將渲染好的HTML插入到頁面中
document.getElementById("category-section-placeholder").innerHTML =
renderedHtml;
} catch (error) {
console.error("獲取分類專欄失敗:", error);
}
}
document.addEventListener("DOMContentLoaded", loadCategory);
相比之前的實現,使用Handlebars模板的實現真的是簡潔多了,這就是使用輪子的好處吧。首先可以先看一下模塊導入:
import Handlebars from "handlebars";
import templateSource from "./category.handlebars?raw";
第一句表示導入Handlebars依賴包,第二局則是導入category模板。注意這里的?raw是不能省略的,這里意思是將category.handlebars按照裸數據導入,其實也就是文本字符串。這其實Vite項目中才提供的能力,也可以使用fetch語句來獲取。
然后從遠端獲取數據,與之前的案例實現一樣:
const response = await fetch("/categories.json");
if (!response.ok) {
throw new Error("網絡無響應");
}
const categories = await response.json();
最后是將Handlebars模板展開成具體的HTML元素,加載到頁面中:
// 編譯模板
const template = Handlebars.compile(templateSource);
// 渲染模板
const renderedHtml = template({
categories,
});
// 將渲染好的HTML插入到頁面中
document.getElementById("category-section-placeholder").innerHTML =
renderedHtml;
如上所述,在真正模板展開的時候,要傳遞數據進行模板函數結構,比如這里的從遠端獲取的分類專欄數據categories。當然,如果想傳其他的數據也行,將其組合成Object對象進入到template接口中即可。
2.3 運行結果
category.css基本沒有變化,如下所示:
/* Category.css */
.category-section {
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
max-width: 260px;
/* 確保不會超出父容器 */
overflow: hidden;
/* 處理溢出內容 */
}
.category-section h3 {
font-size: 1.2rem;
color: #333;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 0.5rem;
margin: 0 0 1rem;
text-align: left;
/* 向左對齊 */
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
}
.category-list li {
margin: 0.5rem 0;
}
.category-item,
.subcategory-item {
display: flex;
align-items: center;
text-decoration: none;
color: #333;
transition: color 0.3s ease;
}
.category-item:hover,
.subcategory-item:hover {
color: #007BFF;
}
.category-icon,
.subcategory-icon {
width: 24px;
height: 24px;
margin-right: 0.5rem;
}
.category-name,
.subcategory-name {
/* font-weight: bold; */
display: flex;
justify-content: space-between;
width: 100%;
color:#000
}
.article-count {
color: #000;
font-weight: normal;
}
.subcategory-list {
list-style: none;
padding: 0;
margin: 0.5rem 0 0 1.5rem;
}
.subcategory-list li {
margin: 0.25rem 0;
}
.subcategory-list a {
text-decoration: none;
color: #555;
transition: color 0.3s ease;
}
.subcategory-list a:hover {
color: #007BFF;
}
運行結果與之前的實現一致,如下所示:

3. 結語
通過本例和上一篇文章《實現一個前端動態模塊組件(Vite+原生JS)》 的對比可以體會到,模板引擎確實是一項順理成章的技術,在實現了動態網頁特性的同時,又兼顧了程序模塊化的思維,值得進行學習和使用。

浙公網安備 33010602011771號