实现一个前端动态模块组件(Vite+原生JS)
1. 引言
在前面的文章《使用Vite创建一个动态网页的前端项目》中我们实现了一个动态网页。不过这个动态网页的实用价值并不高,在真正实际的项目中我们希望的是能实现一个动态的模块组件。具体来说,就是有一个页面控件同时在多个页面中使用,那么我们肯定想将这个页面控件封装起来,以便每个页面需要的时候调用一下就可以生成。注意,这个封装起来模块组件应该要包含完整的HTML+JavaScript+CSS,并且要根据从后端访问的数据来动态填充页面内容。其实像VUE这样的前端框架就是这种设计思路,同时这也是GUI程序开发的常见思维模式。
2. 实现
2.1 项目组织
在这里笔者实现的例子是一个博客网站上的分类专栏控件。分类专栏是一般通过后端获取的,但是这里笔者就将其模拟成直接域内获取一个数据categories.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开发" }
]
}
]
这个数据的意思是将分类专类分成一级分类专栏和二级分类专栏,每个专栏都有名称、文章数、图标地址属性,这样便于我们填充到页面中。
新建一个components目录,在这个目录中新建category.html、category.js、category.css这三个文件,正如前文所说的,我们希望这个模块组件能同时具有结构、行为和样式的能力。这样,这个项目的文件组织结构如下所示:
my-native-js-app
├── public
│ └── categories.json
├── src
│ ├── components
│ │ ├── category.css
│ │ ├── category.html
│ │ └── category.js
│ └── main.js
├── index.html
└── package.json
2.2 具体解析
先看index.html页面,代码如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app">
<div id="category-section-placeholder"></div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
基本都没有什么变化,只是增加了一个名为category-section-placeholder
的元素,这个元素会用来挂接在js中动态创建的分类专栏目录元素。
接下来看main.js文件:
import './components/category.js'
里面其实啥都没干,只是引入了一个category模块。那么就看一下这个category.js文件:
import "./category.css";
// 定义一个变量来存储获取到的分类数据
let categoriesJson = null;
// 使用MutationObserver监听DOM变化
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "childList" &&
mutation.target.id === "category-section-placeholder"
) {
// 在这里调用函数来填充数据
populateCategories(categoriesJson);
}
});
});
// 配置观察选项
const config = { childList: true, subtree: true };
// 开始观察目标节点
const targetNode = document.getElementById("category-section-placeholder");
observer.observe(targetNode, config);
// 获取分类数据
async function fetchCategories() {
try {
const backendUrl = import.meta.env.VITE_BACKEND_URL;
const response = await fetch("/categories.json");
if (!response.ok) {
throw new Error("网络无响应");
}
categoriesJson = await response.json();
// 加载Category.html内容
fetch("/src/components/category.html")
.then((response) => response.text())
.then((data) => {
document.getElementById("category-section-placeholder").innerHTML =
data;
})
.catch((error) => {
console.error("Failed to load Category.html:", error);
});
} catch (error) {
console.error("获取分类专栏失败:", error);
}
}
// 填充分类数据
function populateCategories(categories) {
if (!categories || !Array.isArray(categories)) {
console.error("Invalid categories data:", categories);
return;
}
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);
});
}
// 确保DOM完全加载后再执行
document.addEventListener("DOMContentLoaded", fetchCategories);
这个文件里面的内容比较多,那么我们就按照代码的执行顺序进行讲解。
document.addEventListener("DOMContentLoaded", fetchCategories);
表示当index.html这个页面加载成功后,就执行fetchCategories
这个函数。在这个函数通过fetch
接口获取目录数据,通过也通过fetch接口获取category.html。category.html中的内容很简单:
<div class="category-section">
<h3>分类专栏</h3>
<ul class="category-list">
</ul>
</div>
fetch接口是按照文本的方式来获取category.html的,在这里的document.getElementById("category-section-placeholder").innerHTML = data;
表示将这段文本序列化到category-section-placeholder
元素的子节点中。程序执行到这里并没有结束,通过对DOM的变化监听,继续执行populateCategories
函数,如下所示:
// 使用MutationObserver监听DOM变化
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "childList" &&
mutation.target.id === "category-section-placeholder"
) {
// 在这里调用函数来填充数据
populateCategories(categoriesJson);
}
});
});
// 配置观察选项
const config = { childList: true, subtree: true };
// 开始观察目标节点
const targetNode = document.getElementById("category-section-placeholder");
observer.observe(targetNode, config);
populateCategories
的具体实现思路是:现在分类专栏的数据已经有了,根节点元素category-list
也已经知道,剩下的就是通过数据来拼接HTML字符串,然后序列化到category-list
元素的子节点下。代码如下所示:
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);
其实思路很简单对吧?最后根据需要实现组件的样式,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. 结语
总结一下前端动态模块组件的实现思路:JavaScript代码永远是主要的,HTML页面就好比是JavaScript的处理对象,过程就跟你用C++/Java/C#/Python读写文本文件一样,其实没什么不同。DOM是浏览器解析处理HTML文档的对象模型,但是本质上HTML是个文本文件(XML文件),需要做的其实就是将HTML元素、CSS元素以及动态数据组合起来,一个动态模块组件就实现了。最后照葫芦画瓢,依次实现其他的组件模块在index.html中引入,一个动态页面就组合起来了。
实现一个前端动态模块组件(Vite+原生JS)的更多相关文章
- ASP.NET给前端动态添加修改 CSS样式JS 标题 关键字
有很多网站读者能换自己喜欢的样式,还有一些网站想多站点共享后端代码而只动前段样式,可以采用动态替换CSS样式和JS. 如果是webform 开发,可以用下列方法: 流程是首先从数据中或者xml读取数据 ...
- ASP.NET给前端动态添加修改 CSS样式JS 标题 关键字(转载)
原文地址:http://www.cnblogs.com/xbhp/p/6392225.html 有很多网站读者能换自己喜欢的样式,还有一些网站想多站点共享后端代码而只动前段样式,可以采用动态替换CSS ...
- 前端必备基础知识之--------原生JS发送Ajax请求
原生JS发送Ajax请求 ajax({ type: 'POST', url: 'http://10.110.120.123:2222', // data: param, contentType: 'a ...
- 常用原生JS方法总结(兼容性写法)
经常会用到原生JS来写前端...但是原生JS的一些方法在适应各个浏览器的时候写法有的也不怎么一样的... 今天下班有点累... 就来总结一下简单的东西吧…… 备注:一下的方法都是包裹在一个EventU ...
- 原生JS—实现图片循环切换的两种方法
今天我们主要讲讲如何使用原生JS实现图片的循环切换的方法.多余的话我们就不多说了,我们一个一个开始讲吧. 1 原生JS实现图片循环切换 -- 方法一 在上栗子之前我们先简单介绍一下所用的一些知识点. ...
- 原生JS—实现图片循环切换及监测鼠标滚动切换图片
今天我们主要讲讲如何使用原生JS实现图片的循环切换的方法以及如何检测鼠标滚动循环切换图片.多余的话我们就不多说了,我们一个一个开始讲吧. 1 原生JS实现图片循环切换 -- 方法一 在上栗子之前我们 ...
- 用原生JS从零到一实现Redux架构
前言 最近利用业余时间阅读了胡子大哈写的<React小书>,从基本的原理讲解了React,Redux等等受益颇丰.眼过千遍不如手写一遍,跟着作者的思路以及参考代码可以实现基本的Demo,下 ...
- 原生js打地鼠
我们要做的是一个打地鼠的游戏,只用原生js 1.导入需要的图片 2.编写页面css样式demo.css *{ margin:0; padding:0; } .game{ position: relat ...
- 原生js实现随着滚动条滚动,导航会自动切换的效果
最近学习前端,把前面用原生js写的一段有关tab切换效果的代码贴上,实现的效果比较简陋,请大家见谅 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1 ...
- 原生js实现一个侧滑删除取消组件(item slide)
组件,本质上是解决某个问题封装的类,在此记录原生js实现侧滑删除 先上效果图 实现思路 1. 确定渲染的数据结构 2. 思考划分布局,总的有两个主要的模块:内容区域和按钮区域 2.1 内容区域保持宽度 ...
随机推荐
- 宝塔导入mysql数据库后,phpmyadmin可以登录,本地Navicat无法登录
问题描述:宝塔导入mysql数据库后,phpmyadmin可以登录,本地Navicat无法登录 问题排查:1.检查服务器3306端口是否开启,如果为云服务器,需要登录云服务器后台安全组设置开启: 2. ...
- go module基本使用
前提 go版本为1.13及以上 官方文档 如果你想更深层次的了解GO MODULE的意义及开发者们的顾虑,可以直接访问官方文档(EN) https://github.com/golang/go/wik ...
- NumPy学习7
今天学习了: 13, NumPy字符串处理函数 14, NumPy数学函数 15, NumPy算术运算 numpy_test7.py : import numpy as np ''' 13, NumP ...
- Linux环境 Oracle 监听和服务 日常操作
文章目录 一.Oracle监听 1.1. 查看Oracle监听运行状态 1.2. 启动 ...
- 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
引言 最近遇到了一个 ActiveMQ 消费端的问题:在没有消息时,日志频繁打印,每秒打印2000多条空消息,导致日志文件迅速膨胀,甚至影响系统性能.经过一番排查,最终定位到问题根源并成功解决.本文将 ...
- swich语句
1.switch语句格式 括号内的是待匹配内容,然后case后的是被匹配内容,如果括号内的内容与case后的内容一致,则会打印语句体 . 2.实操(后面的省略了) 3.注意事项 1.case后面的值不 ...
- 【HTML】步骤进度条组件
HTML步骤进度条 效果图 思路 分份: 有多少个步骤就可以分成多少分,每份宽度应该为100%除以步骤数,故以上效果图中的每份宽度应该为25%,每份用一个div. 每份: 每份中可以看成是三个元素,一 ...
- unigui的ServerModule的重要属性【8】
ServerModule是unigui的重要模块. uniGUI 服务器的内部结构. 每个 uniGUI 服务器都有一个ServerModule的副本, 每台服务器创建一次, 同时根据用户活动动态创建 ...
- FireDAC开发DataSnap应用系统【3】-使用TFDJSONDatasets的CRUD功能
类别 说明 TFDJSONDeltas 包含异动的delta的类别.客户端存放deltade对象 TFDJSONDeltasWriter 把deltas写入TFDJSONDeltas TFDJSOND ...
- Mybatis-Plus中的@TableId
简介 在 MyBatis Plus 中,@TableId 注解是用于标记实体类中的主键字段.它可以更方便地处理主键相关的操作,如自动填充主键值或识别主键字段. 用法 public class User ...