通过JS模板引擎实现动态模块组件(Vite+JS+Handlebars)

news2025/7/19 7:00:07

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;
}

运行结果与之前的实现一致,如下所示:

图1 分类专栏组件的显示结果

3. 结语

通过本例和上一篇文章《实现一个前端动态模块组件(Vite+原生JS)》 的对比可以体会到,模板引擎确实是一项顺理成章的技术,在实现了动态网页特性的同时,又兼顾了程序模块化的思维,值得进行学习和使用。

代码实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2387503.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

fvm install 下载超时 过慢 fvm常用命令、flutter常用命令

Git 配置问题 确保 Git 使用的是 HTTPS&#xff0c;而不是 SSH。如果你有 .gitconfig&#xff0c;确保没有配置奇怪的代理&#xff1a; git config --global --get http.proxy git config --global --get https.proxy如果有代理设置且不需要&#xff0c;取消代理&#xff1a;…

Python正则表达式:30秒精通文本处理

一、概述 1. 含义 正则表达式是一种记录文本规则的代码工具&#xff0c;用于描述字符串的结构和模式。它广泛应用于字符串的匹配、查找、替换、提取等操作。 2. 特点 语法复杂&#xff1a;符号多、规则灵活&#xff0c;可读性较差。功能强大&#xff1a;可以精确控制字符串…

Introduction to SQL

目录 SQL特点 ​编辑 Select-From-Where Statements Meaning of Single-Relation Query Operational Semantics * In SELECT clauses Complex Conditions in WHERE Clause PATTERNS NULL Values Three-Valued Logic Multirelation Queries Aggregations NULL’s Ig…

计算机视觉---YOLOv3

YOLOv3讲解 一、YOLOv3 核心架构与创新 YOLOv3&#xff08;2018年发布&#xff09;在YOLOv2基础上进行了全面升级&#xff0c;通过多尺度预测、更强大的骨干网络和优化的分类损失函数&#xff0c;显著提升了检测精度&#xff0c;尤其是小目标检测能力&#xff0c;同时保持了实…

#RabbitMQ# 消息队列进阶

目录 消息可靠性 一 生产者的可靠性 1 生产者的重连 2 生产者的确认 (1 Confirm* (2 Return 二 MQ的可靠性 1 数据持久化 2 Lazy Queue* 三 消费者的可靠性 1 消费者确认机制 2 消费失败处理 3 业务幂等性 四 延迟消息 消息可靠性 在消息队列中&#xff0c;可靠性…

【深度学习】损失“三位一体”——从 Fisher 的最大似然到 Shannon 的交叉熵再到 KL 散度,并走进 PET·P-Tuning微调·知识蒸馏的实战

一页速览&#xff1a; 1912 Fisher 用最大似然把「让数据出现概率最高」变成参数学习&#xff1b; 1948 Shannon 把交叉熵解释成「最短平均编码长度」&#xff1b; 1951 Kullback-Leibler 用相对熵量化「多余信息」。 三条历史线落到今天深度学习同一个损失——交叉熵。 也…

5 分钟速通密码学!

让我们开始第一部分&#xff1a;密码学基础 (Cryptography Basics)。 第一部分&#xff1a;密码学基础 (Cryptography Basics) 1. 什么是密码学&#xff1f; 想象一下&#xff0c;在古代战争中&#xff0c;将军需要向远方的部队传递作战指令。如果直接派人送信&#xff0c;信…

Linux——IP协议

1. 现实意义 • IP协议&#xff1a;提供一种能力&#xff0c;把数据报从主机A跨网络送到主机B • TCP/IP协议&#xff1a;核心功能&#xff0c;把数据100%可靠的从主机A跨网络送到主机B 注&#xff1a;TCP协议负责百分百可靠&#xff0c;通过三次握手、滑动窗口、拥塞控制、延…

Linux信号量(32)

文章目录 前言一、POSIX 信号量信号量的基础知识信号量的基本操作 二、基于环形队列实现生产者消费者模型环形队列单生产单消费模型多生产多消费模型 总结 前言 加油&#xff0c;加油&#xff01;&#xff01;&#xff01; 一、POSIX 信号量 信号量的基础知识 互斥、同步 不只…

技术视界 | 打造“有脑有身”的机器人:ABC大脑架构深度解析(上)

ABC大脑架构&#xff1a;连接大模型与物理世界的具身智能新范式 在具身智能和类人机器人技术快速发展的背景下&#xff0c;如何高效整合“大模型的认知理解能力”与“对真实物理世界的精准控制”&#xff0c;成为当前智能体系统设计中最具挑战性也是最关键的问题之一。尽管大语…

使用堡塔和XShell

使用堡塔和XShell 一、SSH协议介绍 SSH为SecureShell的缩写&#xff0c;由IETF的网络小组(NetworkWorkingGroup)所制定;SSH为建立在应用层基础上的安全协议。SSH是较可靠&#xff0c;专为远程登录会话和其他网络服务提供安全性的协议。利用SSH协议可以有效防止远程管理过程中…

软件项目交付阶段,验收报告记录了什么?有哪些标准要求?

软件项目交付阶段&#xff0c;验收报告扮演着至关重要的角色&#xff0c;它相当于一份详尽的“成绩单”&#xff0c;具体记录了项目完成的具体情况以及是否达到了既定的标准。 项目基本信息 该环节将展示软件项目的核心信息&#xff0c;包括项目名称、开发团队构成、项目实施…

LightGBM的python实现及参数优化

文章目录 1. LightGBM模型参数介绍2. 核心优势3. python实现LightGBM3.1 基础实现3.1.1 Scikit-learn接口示例3.1.2 Python API示例 3.2 模型调优3.2.1 GridSearchCV简介3.2.2 LightGBM超参调优3.2.3 GridSearchCV寻优结果解读 在之前的文章 Boosting算法【AdaBoost、GBDT 、X…

封装渐变堆叠柱状图组件附完整代码

组件功能 这是一个渐变堆叠柱状图组件&#xff0c;主要功能包括&#xff1a; 在一根柱子上同时显示高、中、低三种危险级别数据使用渐变色区分不同危险级别&#xff08;高危红色、中危橙色、低危蓝色&#xff09;悬停显示详细数据信息&#xff08;包括总量和各级别数据&#…

山东大学软件学院创新项目实训开发日志——第十三周

目录 1.开展prompt工程&#xff0c;创建个性化AI助理&#xff0c;能够基于身份实现不同角度和语言风格的回答。 2.对输出进行格式化&#xff0c;生成特定格式的会议计划文档。 3.学习到的新知识 本阶段我所做的工作 1.开展prompt工程&#xff0c;创建个性化AI助理&#xff…

Cesium 透明渐变墙 解决方案

闭合路径修复 通过增加额外点确保路径首尾相接 透明渐变效果 使用RGBA颜色模式实现从完全不透明到完全透明的平滑渐变 参数可调性 提供多个可调参数&#xff0c;轻松自定义颜色、高度和圆环尺寸 完整代码实现 <!DOCTYPE html> <html> <head><meta …

day022-定时任务-故障案例与发送邮件

文章目录 1. cron定时任务无法识别命令1.1 故障原因1.2 解决方法1.2.1 对命令使用绝对路径1.2.2 在脚本开头定义PATH 2. 发送邮件2.1 安装软件2.2 配置邮件信息2.3 巡检脚本与邮件发送2.3.1 巡检脚本内容2.3.2 制作时任务发送邮件 3. 调取API发送邮件3.1 编写文案脚本3.2 制作定…

新增 git submodule 子模块

文章目录 1、基本语法2、添加子模块后的操作3、拉取带有submodule的仓库 git submodule add 是 Git 中用于将另一个 Git 仓库作为子模块添加到当前项目中的命令。 子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录&#xff0c;同时保持它们各自的提交历史独立。 1、基…

List优雅分组

一、前言 最近小永哥发现&#xff0c;在开发过程中&#xff0c;经常会遇到需要对list进行分组&#xff0c;就是假如有一个RecordTest对象集合&#xff0c;RecordTest对象都有一个type的属性&#xff0c;需要将这个集合按type属性进行分组&#xff0c;转换为一个以type为key&…

Linux 使用 Docker 安装 Milvus的两种方式

一、使用 Docker Compose 运行 Milvus (Linux) 安装并启动 Milvus Milvus 在 Milvus 资源库中提供了 Docker Compose 配置文件。要使用 Docker Compose 安装 Milvus&#xff0c;只需运行 wget https://github.com/milvus-io/milvus/releases/download/v2.5.10/milvus-standa…