当前位置: 首页 > news >正文

原生Web Components构建分层HTML组件系统

先问大家一个问题:当你在做多个页面,发现同一套“卡片 + 头像 + 按钮 + 弹窗”被复制了几十遍,改一处样式要全局搜索替换的时候,是不是开始怀疑人生?

我在早期的业务页面里,就经常遇到这种局面。HTML 结构复制粘贴、CSS 类名互相覆盖、JavaScript 逻辑散落在各个script标签里。后来接触并落地了一套“分层 HTML 组件系统”的思路,才慢慢把这类问题理顺。这篇文章就围绕“分层”和“组件化”两个关键词,从概念到实战,完整拆解一套基于原生 Web Components 的轻量级组件系统。无论你是刚接触前端组件化,还是想在项目中减少框架依赖,都有一定参考价值。

1. 什么是分层 HTML 组件系统

1.1 从“写网页”到“造组件”

传统写网页的方式,是把 HTML 结构、CSS 样式、JavaScript 逻辑全部堆在一个页面里。页面简单时还好,一旦业务复杂,就会出现几个典型问题:

  • 相同的页面片段(卡片、弹窗、表格)无法复用。
  • CSS 类名冲突难以排查。
  • JavaScript 逻辑与 DOM 结构强耦合,改一处牵一发动全身。
  • 新人接手代码时,很难快速判断某段逻辑属于哪个功能模块。

组件化的核心思路,就是把可复用的 UI 和逻辑封装成一个独立单元。这个单元有自己的结构、样式和行为,能被外部稳定调用。就像积木一样,单块积木内部可以很复杂,但对外只需要暴露一个“插拔接口”。

1.2 分层到底分的是什么

“分层 HTML 组件系统”中的 layered,指的是在组件内部继续做职责拆分。一个组件不再是一坨混在一起的代码,而是分成多个层面:

分层职责对应技术
模板层定义组件的 HTML 结构<template>、innerHTML
样式层定义组件的外观与布局<style>、Shadow DOM 样式隔离
逻辑层处理交互、事件、状态更新Custom Element 生命周期、JavaScript 方法
数据层约定外部传入的属性、组件内部状态observedAttributes、attributeChangedCallback

这样拆分之后,每个层面都有清晰的边界。模板层只负责“长什么样”,逻辑层只负责“怎么交互”,数据层只负责“怎么流动”。当需要修改一个按钮颜色时,不需要去翻逻辑代码;当需要修改交互行为时,也不需要从一堆结构字符串里找位置。

1.3 这套系统适合谁

  • 想脱离大型框架,用原生 HTML/CSS/JavaScript 实现组件化开发的前端初学者。
  • 需要在多页面项目中保持 UI 统一,又不想引入过重构建流程的团队。
  • 正在学习 Web Components、Custom Elements、Shadow DOM 的开发者。
  • 想理解组件内部“分层思想”,为后续学习 Vue/React 组件机制打基础的人。

读完本文,你可以独立实现一个带样式隔离、属性传参、事件通信的组件系统,并把它嵌入到普通 HTML 页面中使用。

2. 技术选型:为什么选原生 Web Components

2.1 原生方案的优势

实现 HTML 组件系统,通常有几条路线:

方案优点缺点
原生 Web Components浏览器原生支持、无需构建工具、可跨框架复用API 相对底层,需要自己处理一些细节
Vue/React 单文件组件生态完善、模板语法友好需要引入框架和构建流程,组件与框架绑定
jQuery 插件式组件上手简单、历史兼容性好全局命名空间污染、样式隔离弱
自行封装 JavaScript 类 + 渲染字符串灵活度高需要自己维护完整渲染和更新流程,工程量大

我不主张所有场景都抛弃框架,但如果你只是想在一批静态页面或多页面项目中统一 UI 组件,原生 Web Components 是很划算的选择。它不需要 Node 环境,不需要 Webpack,直接在浏览器里就能跑。

2.2 核心浏览器 API

这套方案依赖三个关键 API:

  • Custom Elements(自定义元素):允许你注册一个新的 HTML 标签,例如<user-card>,并把它绑定到一个类上。
  • Shadow DOM(影子 DOM):给组件创建一个独立的 DOM 子树,组件内部的样式不会泄漏到外部,外部样式也不会随意影响组件内部。
  • <template>标签:定义一个不会被浏览器渲染的 HTML 片段,JavaScript 可以在需要时克隆它。

这三个能力组合起来,就是“分层组件系统”的技术底座。

2.3 浏览器兼容性

现代浏览器(Chrome、Edge、Firefox、Safari)基本都支持 Custom Elements 和 Shadow DOM。如果只需要支持现代浏览器,可以放心使用。如果需要兼容很旧的浏览器,建议先做能力检测,再决定是否降级处理。

if ('customElements' in window) { // 支持 Custom Elements } else { // 降级方案或提示用户升级浏览器 }

这点在实际项目中很重要,可以在入口处加类似判断。

3. 环境准备与项目结构

3.1 运行环境

本文示例只需要:

  • 现代浏览器(Chrome/Edge 最新版即可)。
  • 一个文本编辑器或 IDE(VS Code、WebStorm 都可以)。
  • 不需要安装 Node.js、不需要打包工具。

版本需要根据你的项目实际情况调整,本文重点演示实现思路。

3.2 项目目录结构

建议按下面的结构组织代码。虽然这是一个纯前端示例,但合理的目录结构能帮助后续维护。

layered-component-system/ ├── index.html ├── css/ │ └── app.css └── js/ ├── components/ │ ├── user-card.js │ └── user-list.js └── app.js
  • js/components/放组件类,每个组件一个文件。
  • js/app.js放页面初始化逻辑。
  • index.html是组件的使用方,只负责引入脚本、组合组件。

3.3 一个最简组件的分层拆解

先看一个最基础的组件,它展示一段文字:

// 文件路径:js/components/hello-box.js class HelloBox extends HTMLElement { connectedCallback() { this.innerHTML = ` <style> .box { border: 1px solid #ddd; padding: 16px; border-radius: 8px; } </style> <div class="box">你好,分层组件!</div> `; } } customElements.define('hello-box', HelloBox);

在页面中使用:

<!-- index.html --> <hello-box></hello-box>

这个组件虽然简单,但已经包含三个层面的雏形:

  • 模板层:<div class="box">结构。
  • 样式层:<style>里的边框和间距。
  • 逻辑层:connectedCallback生命周期回调,负责在挂载时渲染内容。

不过这个写法还不算真正意义上的“分层”,因为它把模板和逻辑揉在一起了。接下来我们会逐步改造成更清晰的分层结构。

4. 核心实现:逐层搭建组件

4.1 模板层:使用<template>分离结构

把 HTML 结构放到<template>里,好处是结构代码不再混在 JavaScript 字符串中,便于阅读和修改。

先在 HTML 中定义一个模板:

<template id="user-card-template"> <div class="user-card"> <img class="avatar" alt="用户头像" /> <div class="info"> <p class="name"></p> <p class="role"></p> </div> <button class="follow-btn">关注</button> </div> </template>

然后在组件逻辑中克隆这个模板:

class UserCard extends HTMLElement { connectedCallback() { const template = document.getElementById('user-card-template'); const content = template.content.cloneNode(true); this.appendChild(content); } }

这样一来,模板层就从逻辑层中独立出来了。后续如果结构变化,直接改<template>即可,不需要动 JavaScript。

4.2 样式层:Shadow DOM 实现样式隔离

上面直接appendChild模板内容,样式是全局共享的。组件之间的类名一旦相同,就可能互相干扰。

更推荐使用 Shadow DOM。只要在构造器中调用attachShadow,组件内部就自动拥有隔离的样式作用域。

class UserCard extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); const template = document.getElementById('user-card-template'); const content = template.content.cloneNode(true); shadow.appendChild(content); } }

这时,模板里的样式只对组件内部生效。比如模板里有.user-card { ... },外部页面的.user-card样式不会影响它,组件内部样式也不会泄漏出去。

为了让组件完全自包含,通常会把<style>也放进<template>里:

<template id="user-card-template"> <style> .user-card { display: flex; align-items: center; gap: 12px; padding: 16px; border: 1px solid #e5e7eb; border-radius: 12px; background: #fff; } .avatar { width: 48px; height: 48px; border-radius: 50%; background: #e0e0e0; } .name { font-size: 16px; font-weight: 600; margin: 0; } .role { font-size: 13px; color: #888; margin: 4px 0 0; } .follow-btn { margin-left: auto; border: none; background: #1a73e8; color: #fff; padding: 8px 16px; border-radius: 8px; cursor: pointer; } </style> <div class="user-card"> <img class="avatar" alt="用户头像" /> <div class="info"> <p class="name"></p> <p class="role"></p> </div> <button class="follow-btn">关注</button> </div> </template>

这就是“样式层”落地的最好方式:隔离、清晰、不污染外部。

4.3 逻辑层:生命周期与交互

Custom Elements 提供了几个生命周期回调:

回调触发时机
constructor元素实例化时,适合初始化状态、创建 Shadow DOM
connectedCallback元素被插入文档时,适合绑定事件、执行首次渲染
disconnectedCallback元素从文档移除时,适合解绑事件、清理定时器
attributeChangedCallback监听属性变化时触发

下面是一个带交互的示例。按钮点击后切换“关注/已关注”状态:

class UserCard extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); const template = document.getElementById('user-card-template'); this.shadow = shadow; shadow.appendChild(template.content.cloneNode(true)); this.isFollowing = false; } connectedCallback() { const btn = this.shadow.querySelector('.follow-btn'); btn.addEventListener('click', this.toggleFollow.bind(this)); this.render(); } toggleFollow() { this.isFollowing = !this.isFollowing; this.render(); } render() { const btn = this.shadow.querySelector('.follow-btn'); if (this.isFollowing) { btn.textContent = '已关注'; btn.style.background = '#999'; } else { btn.textContent = '关注'; btn.style.background = '#1a73e8'; } } } customElements.define('user-card', UserCard);

在这个实现里,逻辑层通过toggleFollow方法控制状态,通过render方法同步视图。状态和渲染是分离的,这就是逻辑层的核心职责。

4.4 数据层:属性与响应式更新

组件往往需要从外部接收数据。Custom Elements 的 standard 做法是观察标签属性。

先声明要观察的属性:

static get observedAttributes() { return ['name', 'role', 'avatar']; }

然后在构造器中渲染初始值,在attributeChangedCallback中响应属性变化:

class UserCard extends HTMLElement { static get observedAttributes() { return ['name', 'role', 'avatar']; } constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); const template = document.getElementById('user-card-template'); shadow.appendChild(template.content.cloneNode(true)); this.shadow = shadow; } connectedCallback() { this.render(); } attributeChangedCallback() { this.render(); } render() { const name = this.getAttribute('name') || '未知用户'; const role = this.getAttribute('role') || '普通成员'; const avatar = this.getAttribute('avatar') || ''; const nameEl = this.shadow.querySelector('.name'); const roleEl = this.shadow.querySelector('.role'); const avatarEl = this.shadow.querySelector('.avatar'); nameEl.textContent = name; roleEl.textContent = role; if (avatar) { avatarEl.src = avatar; } } } customElements.define('user-card', UserCard);

使用方式:

<user-card name="张三" role="前端工程师" avatar="https://example.com/avatar.png"> </user-card>

这样,外部只需要通过属性传值,组件内部自动更新视图。数据层和模板层通过render()方法桥接起来。

4.5 组件间通信:自定义事件

组件之间不能互相直接操作 DOM,更合适的做法是通过自定义事件向上层广播。

例如,在用户列表组件中,当点击某个用户卡片的“关注”按钮时,把用户名传给外部页面:

class UserCard extends HTMLElement { connectedCallback() { const btn = this.shadow.querySelector('.follow-btn'); btn.addEventListener('click', () => { this.dispatchEvent(new CustomEvent('user-follow', { detail: { name: this.getAttribute('name') }, bubbles: true, composed: true })); }); } }

外部页面监听:

const card = document.querySelector('user-card'); card.addEventListener('user-follow', (event) => { console.log('关注了用户:', event.detail.name); });

这里的bubbles: truecomposed: true很关键。bubbles让事件能向父节点冒泡,composed让事件能穿透 Shadow DOM 边界传递到文档层。如果不设置这两个值,在 Shadow DOM 内部触发的事件外部很难收到。

5. 完整实战:实现一个分层可复用的用户卡片系统

5.1 需求分析

我们来实现一个用户卡片系统,包含两个组件:

  • user-card:单张用户卡片,展示姓名、角色、头像,支持关注/取消关注。
  • user-list:用户列表容器,接收一组用户数据,渲染多个user-card

页面整体结构:

页面 └── user-list ├── user-card ├── user-card └── user-card

5.2 创建项目目录

按第 3 节的目录结构创建文件:

layered-component-system/ ├── index.html ├── css/ │ └── app.css └── js/ ├── components/ │ ├── user-card.js │ └── user-list.js └── app.js

5.3 编写 index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>分层 HTML 组件系统示例</title> <link rel="stylesheet" href="css/app.css"> </head> <body> <main class="page"> <h1>团队成员列表</h1> <user-list id="team-list"></user-list> </main> <script src="js/components/user-card.js"></script> <script src="js/components/user-list.js"></script> <script src="js/app.js"></script> </body> </html>

5.4 编写全局样式

/* 文件路径:css/app.css */ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; margin: 0; padding: 40px 20px; } .page { max-width: 720px; margin: 0 auto; } .page h1 { font-size: 24px; color: #1a1a1a; margin-bottom: 24px; }

这些是页面级样式,只作用于页面本身,不会渗透到组件内部的 Shadow DOM 中,正好用来验证样式隔离效果。

5.5 实现 user-card 组件

// 文件路径:js/components/user-card.js class UserCard extends HTMLElement { static get observedAttributes() { return ['name', 'role', 'avatar']; } constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); this.shadow = shadow; this.isFollowing = false; const template = document.createElement('template'); template.innerHTML = ` <style> .user-card { display: flex; align-items: center; gap: 12px; padding: 16px; border: 1px solid #e5e7eb; border-radius: 12px; background: #fff; transition: box-shadow 0.2s ease; } .user-card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); } .avatar { width: 48px; height: 48px; border-radius: 50%; background: #e0e0e0; flex-shrink: 0; object-fit: cover; display: block; } .info { min-width: 0; } .name { font-size: 16px; font-weight: 600; margin: 0; color: #1a1a1a; } .role { font-size: 13px; color: #888; margin: 4px 0 0; } .follow-btn { margin-left: auto; border: none; background: #1a73e8; color: #fff; padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 14px; transition: background 0.2s ease; flex-shrink: 0; } .follow-btn.following { background: #9ca3af; } </style> <div class="user-card"> <img class="avatar" alt="用户头像" /> <div class="info"> <p class="name"></p> <p class="role"></p> </div> <button class="follow-btn">关注</button> </div> `; shadow.appendChild(template.content.cloneNode(true)); } connectedCallback() { const btn = this.shadow.querySelector('.follow-btn'); btn.addEventListener('click', this.handleFollowClick.bind(this)); this.render(); } handleFollowClick() { this.isFollowing = !this.isFollowing; this.render(); this.dispatchEvent(new CustomEvent('user-follow', { detail: { name: this.getAttribute('name'), following: this.isFollowing }, bubbles: true, composed: true })); } attributeChangedCallback() { this.render(); } render() { const name = this.getAttribute('name') || '未知用户'; const role = this.getAttribute('role') || '普通成员'; const avatar = this.getAttribute('avatar') || ''; this.shadow.querySelector('.name').textContent = name; this.shadow.querySelector('.role').textContent = role; const avatarEl = this.shadow.querySelector('.avatar'); if (avatar) { avatarEl.src = avatar; } const btn = this.shadow.querySelector('.follow-btn'); if (this.isFollowing) { btn.textContent = '已关注'; btn.classList.add('following'); } else { btn.textContent = '关注'; btn.classList.remove('following'); } } } customElements.define('user-card', UserCard);

组件分层说明:

  • 样式层:Shadow DOM 内的<style>保证卡片样式不影响外部页面,外部样式也进不来。
  • 模板层:template.innerHTML定义卡片结构。
  • 逻辑层:connectedCallback中绑定事件,handleFollowClick处理交互。
  • 数据层:observedAttributes+attributeChangedCallback完成外部属性到内部视图的同步。

5.6 实现 user-list 组件

列表容器组件负责接收数据,循环渲染user-card。因为要在 JavaScript 中动态创建组件,所以这里没有用模板字符串,而是直接创建 DOM 元素。

// 文件路径:js/components/user-list.js class UserList extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); this.shadow = shadow; const style = document.createElement('style'); style.textContent = ` .list-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 16px; } .list-container { display: flex; flex-direction: column; gap: 12px; } .empty-tip { color: #999; font-size: 14px; padding: 24px; text-align: center; border: 1px dashed #ddd; border-radius: 12px; } `; shadow.appendChild(style); const title = document.createElement('p'); title.className = 'list-title'; title.textContent = this.getAttribute('title') || '用户列表'; this.titleEl = title; shadow.appendChild(title); const container = document.createElement('div'); container.className = 'list-container'; this.containerEl = container; shadow.appendChild(container); } connectedCallback() { this.render(); } set users(value) { this._users = value; this.render(); } get users() { return this._users || []; } render() { // 清空容器 while (this.containerEl.firstChild) { this.containerEl.removeChild(this.containerEl.firstChild); } const list = this.users; if (list.length === 0) { const emptyTip = document.createElement('div'); emptyTip.className = 'empty-tip'; emptyTip.textContent = '暂无用户数据'; this.containerEl.appendChild(emptyTip); return; } list.forEach((user) => { const card = document.createElement('user-card'); card.setAttribute('name', user.name); card.setAttribute('role', user.role); if (user.avatar) { card.setAttribute('avatar', user.avatar); } this.containerEl.appendChild(card); }); } } customElements.define('user-list', UserList);

user-list组件内部有一个render()方法,外部可以通过users属性传入数据:

const list = document.querySelector('#team-list'); list.users = [ { name: '张三', role: '前端工程师' }, { name: '李四', role: '后端工程师' }, { name: '王五', role: '产品经理' } ];

5.7 编写入口脚本

// 文件路径:js/app.js const teamData = [ { name: '张三', role: '前端工程师', avatar: '' }, { name: '李四', role: '后端工程师', avatar: '' }, { name: '王五', role: '产品经理', avatar: '' } ]; const userList = document.querySelector('#team-list'); userList.users = teamData; // 监听关注事件 userList.addEventListener('user-follow', (event) => { console.log(`关注操作:${event.detail.name},状态:${event.detail.following ? '已关注' : '未关注'}`); });

5.8 运行与验证

直接用浏览器打开index.html

预期效果:

  • 页面展示一个列表容器组件。
  • 列表内动态渲染三张用户卡片。
  • 点击“关注”按钮,文字变成“已关注”,按钮颜色变灰。
  • 点击按钮后,浏览器控制台输出关注日志。

在浏览器中右键检查,能看到user-card下有一个#shadow-root,组件的内部结构都在这个 shadow root 中。这也是判断组件是否生效的最直观方式。

5.9 结果说明

到这里,一个完整的“分层 HTML 组件系统”就已经可以运行了。

  • user-card是基础单元,负责单卡片的展示与交互。
  • user-list是容器组件,负责数据的循环渲染与布局。
  • 组件之间通过自定义事件user-follow通信。
  • 页面本身只需要定义一个<user-list>标签,不再关心内部实现。

这种结构在业务中最大的优势是,如果后续需要新增“用户卡片”的展示形态,只需要修改user-card.js一个文件;如果后续要改列表布局,只需要修改user-list.js。模块之间互不干扰。

6. 常见问题与排查思路

问题现象常见原因解决思路
自定义标签显示为空脚本未加载或注册失败检查customElements.define是否执行,确认脚本引入顺序
组件样式被外部影响没有使用 Shadow DOM在构造函数中调用attachShadow,并将内容挂载到 shadow root
组件内部图片不显示图片路径或src属性未正确设置检查avatar属性是否传值,检查图片地址是否可访问
点击事件不生效事件绑定发生在render()之前,元素被重新渲染覆盖connectedCallback中绑定事件,或使用事件委托
属性变化不会触发更新忘记声明observedAttributes在类中定义static get observedAttributes()
Shadow DOM 内部事件外部收不到自定义事件没有设置composed: true创建CustomEvent时传入{ bubbles: true, composed: true }
多个组件实例状态互相影响直接把共享变量挂在类静态属性上使用构造函数constructor中的实例属性保存状态
组件脚本加载顺序不对页面还没准备好就执行脚本<script>放在 body 底部,或使用DOMContentLoaded

逐个说明几个高频问题:

问题一:自定义元素不渲染

最常见原因是脚本没有真正执行。打开浏览器控制台,先确认有没有报错信息。如果customElements.define('user-card', UserCard)运行时报错,通常会提示类名未定义或标签名不合法。自定义标签名必须包含短横线,比如user-carduser-listusercard这种命名会被浏览器拒绝。

问题二:样式没有隔离

如果组件模板是直接this.innerHTML = ...写入的,那它就不在 Shadow DOM 中,样式也必然全局共享。要使用attachShadow并挂载到 shadow root,才能获得样式隔离。

问题三:事件绑定丢失

如果render()方法内部重建了整个 DOM,比如每次渲染都用this.shadow.innerHTML = ...,那么之前绑定的按钮事件会因为 DOM 被替换而丢失。解决方案有两个:

  • 在构造器中用变量保存按钮引用,只更新按钮的textContentclass,不重建整个 DOM。
  • 使用事件委托,在 shadow root 上监听按钮点击。

我推荐第一种,也是本文示例采用的方式,简单直接。

7. 最佳实践与工程建议

7.1 命名规范

自定义元素标签名必须包含短横线,且全小写。推荐采用“业务前缀 + 组件名”的方式:

  • user-card
  • user-list
  • chart-line
  • nav-menu

统一前缀不仅减少命名冲突,还能在代码中一眼看出组件归属哪个业务模块。

7.2 属性与状态边界

外部传入的数据通过 HTML 属性传递,组件内部可变状态保存在this实例属性上。不要把内部状态回写到 HTML 属性上,否则容易混淆数据来源。

如果需要传递对象、数组这类复杂数据,可以把数据直接挂在组件的 JavaScript 属性上,比如list.users = [...]。这是自定义元素支持的,不需要通过属性字符串传递。

7.3 渲染性能

attributeChangedCallback有可能会在高频更新属性时频繁触发。如果组件渲染逻辑较重,可以加一层节流或防抖:

attributeChangedCallback() { clearTimeout(this._renderTimer); this._renderTimer = setTimeout(() => { this.render(); }, 16); }

7.4 组件卸载清理

disconnectedCallback中清理全局事件监听、定时器、异步请求:

disconnectedCallback() { if (this._timer) { clearInterval(this._timer); this._timer = null; } }

这是很容易被忽略的一点,尤其是在长列表或 SPA 场景中,组件反复挂载卸载,如果不清理会造成内存泄漏。

7.5 组件复用与团队协作

给每个组件文件加文件头注释,标明组件职责、使用属性、对外事件,对团队协作很有帮助:

/** * user-card 用户卡片组件 * * 属性: * - name: 用户昵称 * - role: 用户角色 * - avatar: 头像地址 * * 事件: * - user-follow: 点击关注按钮时触发 */

7.6 浏览器兼容性检查

如果项目需要兼容旧版浏览器,可以在入口脚本中统一做能力检测。不支持时给出提示或加载 polyfill,而不是让页面直接白屏。

if (!('customElements' in window)) { document.body.innerHTML = '当前浏览器不支持 Web Components,请升级浏览器'; }

7.7 使用时机

原生 Web Components 不是万能的。如果需要复杂状态管理、数据响应式机制、大型组件树的高性能 diff,Vue、React 这类框架依然是更成熟的选择。但在多页面应用、内容型站点、或者需要长期维护且不想引入构建链路的场景中,这套分层组件系统完全值得一试。

8. 总结与后续学习方向

这篇文章从“如何避免页面重复代码”的问题出发,完整实现了一个基于原生 Web Components 的分层 HTML 组件系统。核心要点可以归结为以下几点:

  • 组件系统天然要分层:模板层管结构、样式层管外观、逻辑层管交互、数据层管输入输出。
  • Shadow DOM 是样式隔离的关键,没有它,组件就谈不上“封装”。
  • Custom Elements 生命周期是组件逻辑的骨架,connectedCallbackattributeChangedCallback最常用。
  • 组件间通信优先使用自定义事件,并记得设置bubbles: truecomposed: true
  • 合理的目录结构和命名规范,比代码本身更能决定组件系统的可维护性。

如果你还不熟悉 HTML 基础语法,建议先补齐 HTML、CSS、JavaScript 的基础知识,再回头看组件的实现;如果你已经掌握了本文内容,下一步可以继续深入:

  • ::part::slotted的用法,它们用来外部控制 Shadow DOM 内部的局部样式。
  • <slot>插槽机制,它能让组件内部插入外部传入的 HTML 结构。
  • Form-associated custom elements,它可以让自定义组件像表单项一样参与表单提交和校验。
  • 浏览器模块化加载方式,例如用 ES Module 的importexport重构组件文件。

动手实践是最好的学习方式。建议你先把本文示例源码敲一遍,然后在此基础上增加一个“删除用户”按钮,再想想删除后列表容器如何感知这条消息。把这个功能实现完,你对整个分层组件系统的理解会上一个台阶。

如果本文对你有帮助,可以收藏备用。后续遇到组件样式泄漏、事件穿透等问题时,回来翻一翻这几节内容,应该能节省不少排查时间。

http://www.cnnetsun.cn/news/4277387.html

相关文章:

  • AI算力分级分配:从模型网关到配额实践的省钱指南
  • 从if-else到状态机:手写实现与工程实践
  • DLMS/COSEM协议栈与HDLC链路层从标准到源码实战解析
  • Python零基础入门:从环境配置到项目实战的学习闭环
  • 基于gplearn的量化因子自动生成系统实践:收益回撤比与5日IC双指标评估
  • FPGA驱动TLC5615 DAC:从时序解析到多通道同步的硬件设计实践
  • Claude母公司发布MHS:让AI突破身体限制,像《超体》一样调用万物!
  • Grok Build v1.0.11:无头会话可浏览与权限优化,让自动化任务可追踪、更安全
  • PCF8591芯片实战指南:从I2C通信到51单片机A/D与D/A转换
  • 写毕业论文踩了十几款AI工具的坑后,我整理出从选题、文献到查重降重的靠谱工具组合
  • MATLAB微分方程建模实战:从SIR模型到数值求解
  • Codex 入门到实战:零基础安装配置与命令行使用教程
  • 美赛Python环境搭建与数据分析建模全流程实战指南
  • PHP支付系统源码安全加固与生产级改造指南
  • Python数学建模模板:从零搭建高效、可复现的建模框架
  • 基于DEAP数据集的情绪识别实战:从EEG信号处理到深度学习模型构建
  • 30V车规级MOSFET量产,EPS电动助力转向迎来新选择
  • Matlab优化工具箱实战:从数学建模到工程优化的高效求解
  • 基于51单片机与状态机的多功能闹钟设计:从JX-TX-1C实验板到实用工具
  • 高校教室管理系统源码拆解:数据库设计、冲突检测与部署实战
  • C++模板实战:从泛型编程到编译期计算的深度解析
  • PBR渲染技术:从物理原理到游戏与影视的实践应用
  • STM32定时器结构体详解:从HAL库配置到PWM、输入捕获实战
  • zip压缩包从报错到跑通:验货、修复、解压与源码运行指南
  • MATLAB数学建模核心技能:从数据预处理到模型求解的完整指南
  • 双节点上线完整指南:从验收标准到回滚预案
  • 医疗数据交换基石:HL7消息解析原理、实战与演进
  • 滴滴2016研发笔试题解析:高并发与LBS场景下的技术考察
  • Redis Geo 实战:深入探索附近的人、LBS 场景与 Geohash 原理
  • Python爬虫实战:构建商品价格监控系统与反爬策略详解