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

1. 引言

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

2. 实现

2.1 项目组织

在这里笔者实现的例子是一个博客网站上的分类专栏控件。分类专栏是一般通过后端获取的,但是这里笔者就将其模拟成直接域内获取一个数据categories.json,里面的内容如下:

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页面,代码如下所示:

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文件:

js 复制代码
import './components/category.js'

里面其实啥都没干,只是引入了一个category模块。那么就看一下这个category.js文件:

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中的内容很简单:

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函数,如下所示:

cpp 复制代码
// 使用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元素的子节点下。代码如下所示:

复制代码
js 复制代码
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文件如下所示:

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中引入,一个动态页面就组合起来了。

实现代码

相关推荐
WeiXiao_Hyy9 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡26 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone32 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js