通过JS模板引擎实现动态模块组件(Vite+JS+Handlebars)
笔记哥 /
05-27 /
47点赞 /
0评论 /
961阅读
# 1. 引言
在上一篇文章[《实现一个前端动态模块组件(Vite+原生JS)》](https://charlee44.com/post.html?id=9c60f118a4f847138fd5b6a665fd3aca)中,笔者通过原生的JavaScript实现了一个动态的模块组件。但是这个实现并不完善,最大的问题就是功能逻辑并没有完全分开。比如模块的HTML:
```HTML
```
其实只是静态内容,动态的内容其实在JavaScript中实现:
```js
const categoryList = document.querySelector(".category-list");
categories.forEach((category) => {
const categoryItem = document.createElement("li");
categoryItem.innerHTML = `
${category.firstCategory.name} ${category.firstCategory.articleCount}篇`;
if (category.secondCategories.length != 0) {
categoryItem.innerHTML += `
```
不过有时会遇到与项目的依赖包环境不匹配问题,或者网络漏洞问题,这个时候就需要升级或者降级一些依赖包。
另外,对于开发环境仅需的依赖(package.json中的devDependencies节点),可以使用 --save-dev 或 -D 标志来安装:
```shell
npm install --save-dev
```
## 2.2 优化代码
既然使用Handlebars模板引擎了,那么表达网页结构和内容的部分就不再是HTML元素而是Handlebars模板了,因此将category.html修改成category.handlebars,其内容如下:
```HTML
```
HTML元素部分我们已经很熟悉了,关键在于Handlebars模板引擎部分。`{{#each}}`和`{{/each}}`是Handlebars的一个块表达式,可以将其理解成`foreach`语句,用于遍历数组。这里我们分别遍历了一级分类专栏(`{{#each categories}}`)和二级分类专栏(`{{#each secondCategories}}`)。
另一个值得说明的是`{{name}}`、`{{iconAddress}}`、`{{articleCount}}`这些都是用来展示具体数据的占位符,Handlebars会在渲染时用实际的数据替换这些占位符。不过相信读者也发现了,一级分类的占位符(`{{firstCategory.name}}`)和二级分类的占位符(`{{name}}`)并不一致。其实这与传入到Handlebars模板进行展开时的数据参数有关,再次看一下数据:
```json
[
{
"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,具体代码如下:
```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模板的实现真的是简洁多了,这就是使用轮子的好处吧。首先可以先看一下模块导入:
```js
import Handlebars from "handlebars";
import templateSource from "./category.handlebars?raw";
```
第一句表示导入Handlebars依赖包,第二局则是导入category模板。注意这里的`?raw`是不能省略的,这里意思是将category.handlebars按照裸数据导入,其实也就是文本字符串。这其实Vite项目中才提供的能力,也可以使用`fetch`语句来获取。
然后从远端获取数据,与之前的案例实现一样:
```js
const response = await fetch("/categories.json");
if (!response.ok) {
throw new Error("网络无响应");
}
const categories = await response.json();
```
最后是将Handlebars模板展开成具体的HTML元素,加载到页面中:
```js
// 编译模板
const template = Handlebars.compile(templateSource);
// 渲染模板
const renderedHtml = template({
categories,
});
// 将渲染好的HTML插入到页面中
document.getElementById("category-section-placeholder").innerHTML =
renderedHtml;
```
如上所述,在真正模板展开的时候,要传递数据进行模板函数结构,比如这里的从远端获取的分类专栏数据`categories`。当然,如果想传其他的数据也行,将其组合成Object对象进入到`template`接口中即可。
## 2.3 运行结果
category.css基本没有变化,如下所示:
```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)》](https://charlee44.com/post.html?id=9c60f118a4f847138fd5b6a665fd3aca) 的对比可以体会到,模板引擎确实是一项顺理成章的技术,在实现了动态网页特性的同时,又兼顾了程序模块化的思维,值得进行学习和使用。
[代码实现](https://download.csdn.net/download/charlee44/90919773)
分类专栏
-
${category.secondCategories
.map(
(subcategory) => `
-
${subcategory.name} ${subcategory.articleCount}篇
`
)
.join("")}
本文来自投稿,不代表本站立场,如若转载,请注明出处:http//www.knowhub.vip/share/2/3742
- 热门的技术博文分享
- 1 . ESP实现Web服务器
- 2 . 从零到一:打造高效的金仓社区 API 集成到 MCP 服务方案
- 3 . 使用C#构建一个同时问多个LLM并总结的小工具
- 4 . .NET 原生驾驭 AI 新基建实战系列Milvus ── 大规模 AI 应用的向量数据库首选
- 5 . 在Avalonia/C#中使用依赖注入过程记录
- 6 . [设计模式/Java] 设计模式之工厂方法模式
- 7 . 5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
- 8 . SQL 中的各种连接 JOIN 的区别总结!
- 9 . JavaScript 中防抖和节流的多种实现方式及应用场景
- 10 . SaltStack 远程命令执行中文乱码问题
- 11 . 推荐10个 DeepSeek 神级提示词,建议搜藏起来使用
- 12 . C#基础:枚举、数组、类型、函数等解析
- 13 . VMware平台的Ubuntu部署完全分布式Hadoop环境
- 14 . C# 多项目打包时如何将项目引用转为包依赖
- 15 . Chrome 135 版本开发者工具(DevTools)更新内容
- 16 . 从零创建npm依赖,只需执行一条命令
- 17 . 关于 Newtonsoft.Json 和 System.Text.Json 混用导致的的序列化不识别的问题
- 18 . 大模型微调实战之训练数据集准备的艺术与科学
- 19 . Windows快速安装MongoDB之Mongo实战
- 20 . 探索 C# 14 新功能:实用特性为编程带来便利