找回密码
 立即注册
首页 业界区 业界 实现一个前端动态模块组件(Vite+原生JS)

实现一个前端动态模块组件(Vite+原生JS)

暴灵珊 4 天前
1. 引言

在前面的文章《使用Vite创建一个动态网页的前端项目》中我们实现了一个动态网页。不过这个动态网页的实用价值并不高,在真正实际的项目中我们希望的是能实现一个动态的模块组件。具体来说,就是有一个页面控件同时在多个页面中使用,那么我们肯定想将这个页面控件封装起来,以便每个页面需要的时候调用一下就可以生成。注意,这个封装起来模块组件应该要包含完整的HTML+JavaScript+CSS,并且要根据从后端访问的数据来动态填充页面内容。其实像VUE这样的前端框架就是这种设计思路,同时这也是GUI程序开发的常见思维模式。
2. 实现

2.1 项目组织

在这里笔者实现的例子是一个博客网站上的分类专栏控件。分类专栏是一般通过后端获取的,但是这里笔者就将其模拟成直接域内获取一个数据categories.json,里面的内容如下:
  1. [
  2.   {
  3.     "firstCategory": {
  4.       "articleCount": 4,
  5.       "iconAddress": "三维渲染.svg",
  6.       "name": "计算机图形学"
  7.     },
  8.     "secondCategories": [
  9.       {
  10.         "articleCount": 2,
  11.         "iconAddress": "opengl.svg",
  12.         "name": "OpenGL/WebGL"
  13.       },
  14.       {
  15.         "articleCount": 2,
  16.         "iconAddress": "专栏分类.svg",
  17.         "name": "OpenSceneGraph"
  18.       },
  19.       { "articleCount": 0, "iconAddress": "threejs.svg", "name": "three.js" },
  20.       { "articleCount": 0, "iconAddress": "cesium.svg", "name": "Cesium" },
  21.       { "articleCount": 0, "iconAddress": "unity.svg", "name": "Unity3D" },
  22.       {
  23.         "articleCount": 0,
  24.         "iconAddress": "unrealengine.svg",
  25.         "name": "Unreal Engine"
  26.       }
  27.     ]
  28.   },
  29.   {
  30.     "firstCategory": {
  31.       "articleCount": 4,
  32.       "iconAddress": "计算机视觉.svg",
  33.       "name": "计算机视觉"
  34.     },
  35.     "secondCategories": [
  36.       {
  37.         "articleCount": 0,
  38.         "iconAddress": "图像处理.svg",
  39.         "name": "数字图像处理"
  40.       },
  41.       {
  42.         "articleCount": 0,
  43.         "iconAddress": "特征提取.svg",
  44.         "name": "特征提取与匹配"
  45.       },
  46.       {
  47.         "articleCount": 0,
  48.         "iconAddress": "目标检测.svg",
  49.         "name": "目标检测与分割"
  50.       },
  51.       { "articleCount": 4, "iconAddress": "SLAM.svg", "name": "三维重建与SLAM" }
  52.     ]
  53.   },
  54.   {
  55.     "firstCategory": {
  56.       "articleCount": 11,
  57.       "iconAddress": "地理信息系统.svg",
  58.       "name": "地理信息科学"
  59.     },
  60.     "secondCategories": []
  61.   },
  62.   {
  63.     "firstCategory": {
  64.       "articleCount": 31,
  65.       "iconAddress": "代码.svg",
  66.       "name": "软件开发技术与工具"
  67.     },
  68.     "secondCategories": [
  69.       { "articleCount": 2, "iconAddress": "cplusplus.svg", "name": "C/C++" },
  70.       { "articleCount": 19, "iconAddress": "cmake.svg", "name": "CMake构建" },
  71.       { "articleCount": 2, "iconAddress": "Web开发.svg", "name": "Web开发" },
  72.       { "articleCount": 7, "iconAddress": "git.svg", "name": "Git" },
  73.       { "articleCount": 1, "iconAddress": "linux.svg", "name": "Linux开发" }
  74.     ]
  75.   }
  76. ]
复制代码
这个数据的意思是将分类专类分成一级分类专栏和二级分类专栏,每个专栏都有名称、文章数、图标地址属性,这样便于我们填充到页面中。
新建一个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页面,代码如下所示:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3.   <head>
  4.     <meta charset="UTF-8" />
  5.     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7.     <title>Vite App</title>
  8.   </head>
  9.   <body>
  10.    
  11.       
  12.    
  13.    
  14.   </body>
  15. </html>
复制代码
基本都没有什么变化,只是增加了一个名为category-section-placeholder的元素,这个元素会用来挂接在js中动态创建的分类专栏目录元素。
接下来看main.js文件:
  1. import './components/category.js'
复制代码
里面其实啥都没干,只是引入了一个category模块。那么就看一下这个category.js文件:
  1. import "./category.css";
  2. // 定义一个变量来存储获取到的分类数据
  3. let categoriesJson = null;
  4. // 使用MutationObserver监听DOM变化
  5. const observer = new MutationObserver((mutations) => {
  6.   mutations.forEach((mutation) => {
  7.     if (
  8.       mutation.type === "childList" &&
  9.       mutation.target.id === "category-section-placeholder"
  10.     ) {
  11.       // 在这里调用函数来填充数据
  12.       populateCategories(categoriesJson);
  13.     }
  14.   });
  15. });
  16. // 配置观察选项
  17. const config = { childList: true, subtree: true };
  18. // 开始观察目标节点
  19. const targetNode = document.getElementById("category-section-placeholder");
  20. observer.observe(targetNode, config);
  21. // 获取分类数据
  22. async function fetchCategories() {
  23.   try {
  24.     const backendUrl = import.meta.env.VITE_BACKEND_URL;
  25.     const response = await fetch("/categories.json");
  26.     if (!response.ok) {
  27.       throw new Error("网络无响应");
  28.     }
  29.     categoriesJson = await response.json();
  30.     // 加载Category.html内容
  31.     fetch("/src/components/category.html")
  32.       .then((response) => response.text())
  33.       .then((data) => {
  34.         document.getElementById("category-section-placeholder").innerHTML =
  35.           data;
  36.       })
  37.       .catch((error) => {
  38.         console.error("Failed to load Category.html:", error);
  39.       });
  40.   } catch (error) {
  41.     console.error("获取分类专栏失败:", error);
  42.   }
  43. }
  44. // 填充分类数据
  45. function populateCategories(categories) {
  46.   if (!categories || !Array.isArray(categories)) {
  47.     console.error("Invalid categories data:", categories);
  48.     return;
  49.   }
  50.   const categoryList = document.querySelector(".category-list");
  51.   categories.forEach((category) => {
  52.     const categoryItem = document.createElement("li");
  53.     categoryItem.innerHTML = `
  54.         
  55.           <img src="https://www.cnblogs.com/category/${category.firstCategory.iconAddress}" alt="${category.firstCategory.name}" >
  56.           ${category.firstCategory.name} ${category.firstCategory.articleCount}篇`;
  57.     if (category.secondCategories.length != 0) {
  58.       categoryItem.innerHTML += `        
  59.           <ul >
  60.             ${category.secondCategories
  61.               .map(
  62.                 (subcategory) => `
  63.               <li>
  64.                 <img src="https://www.cnblogs.com/category/${subcategory.iconAddress}" alt="${subcategory.name}" >
  65.                 ${subcategory.name} ${subcategory.articleCount}篇
  66.               </li>
  67.             `
  68.               )
  69.               .join("")}
  70.           </ul>
  71.         
  72.         `;
  73.     }
  74.     categoryList.appendChild(categoryItem);
  75.   });
  76. }
  77. // 确保DOM完全加载后再执行
  78. document.addEventListener("DOMContentLoaded", fetchCategories);
复制代码
这个文件里面的内容比较多,那么我们就按照代码的执行顺序进行讲解。
document.addEventListener("DOMContentLoaded", fetchCategories);表示当index.html这个页面加载成功后,就执行fetchCategories这个函数。在这个函数通过fetch接口获取目录数据,通过也通过fetch接口获取category.html。category.html中的内容很简单:
  1.     <h3>分类专栏</h3>
  2.     <ul >
  3.     </ul>
复制代码
fetch接口是按照文本的方式来获取category.html的,在这里的document.getElementById("category-section-placeholder").innerHTML = data;表示将这段文本序列化到category-section-placeholder元素的子节点中。程序执行到这里并没有结束,通过对DOM的变化监听,继续执行populateCategories函数,如下所示:
  1. // 使用MutationObserver监听DOM变化
  2. const observer = new MutationObserver((mutations) => {
  3.   mutations.forEach((mutation) => {
  4.     if (
  5.       mutation.type === "childList" &&
  6.       mutation.target.id === "category-section-placeholder"
  7.     ) {
  8.       // 在这里调用函数来填充数据
  9.       populateCategories(categoriesJson);
  10.     }
  11.   });
  12. });
  13. // 配置观察选项
  14. const config = { childList: true, subtree: true };
  15. // 开始观察目标节点
  16. const targetNode = document.getElementById("category-section-placeholder");
  17. observer.observe(targetNode, config);
复制代码
populateCategories的具体实现思路是:现在分类专栏的数据已经有了,根节点元素category-list也已经知道,剩下的就是通过数据来拼接HTML字符串,然后序列化到category-list元素的子节点下。代码如下所示:
  1. [/code][code]const categoryList = document.querySelector(".category-list");
  2. categories.forEach((category) => {
  3. const categoryItem = document.createElement("li");
  4. categoryItem.innerHTML = `
  5.    
  6.         <img src="https://www.cnblogs.com/category/${category.firstCategory.iconAddress}" alt="${category.firstCategory.name}" >
  7.         ${category.firstCategory.name} ${category.firstCategory.articleCount}篇`;
  8. if (category.secondCategories.length != 0) {
  9.     categoryItem.innerHTML += `        
  10.         <ul >
  11.         ${category.secondCategories
  12.             .map(
  13.             (subcategory) => `
  14.             <li>
  15.             <img src="https://www.cnblogs.com/category/${subcategory.iconAddress}" alt="${subcategory.name}" >
  16.             ${subcategory.name} ${subcategory.articleCount}篇
  17.             </li>
  18.         `
  19.             )
  20.             .join("")}
  21.         </ul>
  22.    
  23.     `;
  24. }
  25. categoryList.appendChild(categoryItem);
复制代码
其实思路很简单对吧?最后根据需要实现组件的样式,category.css文件如下所示:
  1. /* Category.css */
  2. .category-section {
  3.     background-color: #fff;
  4.     border: 1px solid #e0e0e0;
  5.     border-radius: 8px;
  6.     padding: 1rem;
  7.     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  8.     font-family: Arial, sans-serif;
  9.     max-width: 260px;
  10.     /* 确保不会超出父容器 */
  11.     overflow: hidden;
  12.     /* 处理溢出内容 */
  13. }
  14. .category-section h3 {
  15.     font-size: 1.2rem;
  16.     color: #333;
  17.     border-bottom: 1px solid #e0e0e0;
  18.     padding-bottom: 0.5rem;
  19.     margin: 0 0 1rem;
  20.     text-align: left;
  21.     /* 向左对齐 */
  22. }
  23. .category-list {
  24.     list-style: none;
  25.     padding: 0;
  26.     margin: 0;
  27. }
  28. .category-list li {
  29.     margin: 0.5rem 0;
  30. }
  31. .category-item,
  32. .subcategory-item {
  33.     display: flex;
  34.     align-items: center;
  35.     text-decoration: none;
  36.     color: #333;
  37.     transition: color 0.3s ease;
  38. }
  39. .category-item:hover,
  40. .subcategory-item:hover {
  41.     color: #007BFF;
  42. }
  43. .category-icon,
  44. .subcategory-icon {
  45.     width: 24px;
  46.     height: 24px;
  47.     margin-right: 0.5rem;
  48. }
  49. .category-name,
  50. .subcategory-name {
  51.     /* font-weight: bold; */
  52.     display: flex;
  53.     justify-content: space-between;
  54.     width: 100%;
  55.     color:#000
  56. }
  57. .article-count {
  58.     color: #000;
  59.     font-weight: normal;   
  60. }
  61. .subcategory-list {
  62.     list-style: none;
  63.     padding: 0;
  64.     margin: 0.5rem 0 0 1.5rem;
  65. }
  66. .subcategory-list li {
  67.     margin: 0.25rem 0;
  68. }
  69. .subcategory-list a {
  70.     text-decoration: none;
  71.     color: #555;
  72.     transition: color 0.3s ease;
  73. }
  74. .subcategory-list a:hover {
  75.     color: #007BFF;
  76. }
复制代码
最后显示的结果如下图所示:
5.png

3. 结语

总结一下前端动态模块组件的实现思路:JavaScript代码永远是主要的,HTML页面就好比是JavaScript的处理对象,过程就跟你用C++/Java/C#/Python读写文本文件一样,其实没什么不同。DOM是浏览器解析处理HTML文档的对象模型,但是本质上HTML是个文本文件(XML文件),需要做的其实就是将HTML元素、CSS元素以及动态数据组合起来,一个动态模块组件就实现了。最后照葫芦画瓢,依次实现其他的组件模块在index.html中引入,一个动态页面就组合起来了。
实现代码

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
1.jpg
2.jpg
3.jpg
4.jpg
您需要登录后才可以回帖 登录 | 立即注册