分层HTML组件系统:基于Web Components的架构实践
做 Web 前端的人大概都经历过这样一个阶段组件数量不断膨胀样式文件越写越长你只是改了一个按钮的圆角结果发现好几个页面的样式同时变了。很多人第一反应是“CSS 命名规范没做好”于是去加前缀、用 BEM、上 CSS Modules。但真到了项目后期你会发现根本问题不是命名而是组件的职责边界没有在架构层面被划分清楚。命名规范是在“同一层内部”做约束而当一个组件既管样式又管业务逻辑还管接口请求时再严格的命名也救不了代码的可维护性。这篇文章要讲的分层 HTML 组件系统是一种从架构层面解决组件复杂度问题的思路。它把组件按照“基础单元、组合单元、业务页面”三个层级组织每一层只做自己该做的事。上层通过标准接口依赖下层下层不反向依赖上层。这样设计之后样式隔离、逻辑复用、状态管理会清晰很多团队多人协作时也更容易划分边界。读完这篇文章你会明白三件事分层 HTML 组件系统到底在分什么如何用原生 Web ComponentsCustom Elements Shadow DOM Slot从零搭建一个可运行的分层组件系统这套架构在真实项目中会遇到哪些坑以及怎么规避。1. 分层 HTML 组件系统为什么现在值得关注先看一个真实场景。假设项目里只有一个按钮组件初期很简单十几个页面共用大家都开心。后来 A 页面需要一个“次要按钮”你给组件加了一个secondary属性B 页面需要一个禁用态你加了disabledC 页面需要加载状态你加了loading。半年之后这个按钮组件的属性列表越来越长if/else分支越来越多每一个新需求都要来改这个组件。更麻烦的是有人发现页面需要一个“带图标、带气泡提示、带异步请求的按钮”他不敢直接改公共组件于是在自己的页面里复制了一份样式又造了一个半新不旧的组件。这就是无分层组件系统的典型症状组件因为承载了过多职责而腐化团队因为害怕改公共代码而选择复制粘贴最终样式和逻辑同时在项目里扩散。分层 HTML 组件系统的价值就在这里。它不依赖某个人的自觉而是通过明确的分层规则从架构上阻止这种腐化。基础层只提供原子组件组合层负责把原子组件拼成可复用的功能模块页面层只处理业务逻辑和页面编排。每一层都有明确的能力边界跨层调用被规则限制。为什么现在值得关注因为原生 Web Components 技术已经足够成熟。Custom Elements、Shadow DOM、HTML Templates、Slot 这些标准能力让开发者不需要引入任何框架就能实现组件封装、样式隔离和组合复用。换句话说分层组件系统不一定要绑定 React 或 Vue它可以是一套“框架无关”的架构方案。对于团队里同时存在多个前端框架、或者想长期维护一套 UI 组件的项目来说这种方案非常有吸引力。这篇文章的读者适合以下三种情况第一种你正在维护一个长期演进的中后台项目组件越来越多需要一套可落地的组件架构规范第二种你想用原生 HTML/JavaScript 实现一个不依赖框架的组件库并且希望结构经得起推敲第三种你听过 Web Components 但一直没找到一套完整的分层实践案例想看看它在真实项目里怎么组织。2. 核心概念组件系统、分层架构与 Web Components2.1 HTML 组件系统是什么广义地说HTML 组件系统就是把页面拆成若干个独立、可复用、职责清晰的 UI 单元。一个按钮、一个输入框、一张卡片、一个表单都可以是组件。组件系统的核心能力有三个封装内部细节不暴露给外部、复用同一个组件可以出现在多个地方、组合多个小组件能拼成大组件。在原生 HTML 时代页面是“一块一块”静态拼出来的组件概念只存在于后端模板或者前端工程约定里。后来前端框架崛起React 的组件、Vue 的 SFC单文件组件把组件系统推向了主流。但框架组件有一个问题它们和框架运行时深度绑定。你今天用 React 写的组件明天要拿到 Vue 项目里基本不可能直接用。2.2 “分层”到底分什么“分层”这个词在软件工程里并不新鲜三层架构、MVC、MVP都是分层思想的应用。放到 HTML 组件系统里分层的本质是把“结构、样式、行为、数据”这些不同的关注点拆开再把组件按“抽象程度”从低到高组织起来。这里要区分两个容易混淆的概念组件内部的分层单个组件内部HTML 管结构、CSS 管表现、JavaScript 管行为。这是纵向拆分。组件之间的分层整个系统里基础组件在最底层组合组件在中间业务页面组件在最上层。这是横向聚合。很多人说“我会写组件”其实只做到了一半。他能把一段 HTML 封装成一个自定义元素但组件之间没有层级约束组件职责越写越杂。分层 HTML 组件系统真正解决的是横向聚合这一层的问题。2.3 Web Components 四项核心能力要落地分层组件系统先要理解 Web Components 的四项标准能力。Custom Elements自定义元素允许开发者定义新的 HTML 标签比如my-button、my-card。自定义元素必须包含短横线命名这是硬性规范目的是避免与原 HTML 标签冲突。Shadow DOM影子 DOM为组件提供独立的 DOM 树和样式作用域。组件内部的样式默认不会泄漏到外部外部样式也默认无法穿透进入组件内部。这是实现样式隔离的关键能力。HTML Templates模板template标签可以声明一段不会立即渲染的 HTML 片段需要时再克隆使用。它适合定义组件的静态结构避免反复用 JavaScript 拼接字符串。Slot插槽让组件的使用者可以向组件内部“投影”内容。插槽实现了组合能力组件外壳是固定的里面的内容可以按需替换。这几项能力配合起来正好覆盖了组件系统需要的封装、复用和组合。下面这张表可以更直观地看到分层组件系统和传统写法的差异。维度传统 DIV 类名约束原生 Web Components 分层系统样式隔离靠命名规范人工维持Shadow DOM 默认隔离组件定义无标准靠框架或约定Custom Elements 标准定义组合方式嵌套或拼接模板Slot 插槽结构清晰逻辑复用复制粘贴或抽公共函数按层复用职责边界明确框架绑定取决于所选框架框架无关任何项目可直接使用2.4 与现代前端框架的关系分层 HTML 组件系统和 React/Vue 并不互斥。它更像是一种底层组件策略你可以用原生 Web Components 建设底层组件库然后在 React/Vue 项目中直接引用这些组件。Vue 官方文档专门有“Web Components 集成”章节React 18 之后也支持在 JSX 中直接使用自定义元素。这意味着把基础组件层做得足够“框架无关”可以同时服务于多个上层技术栈。当然框架组件在响应式更新、状态管理、生态工具方面仍然有巨大优势。分层架构的关键判断是基础组件层和组合组件层尽量保持框架无关页面业务层可以自由使用框架。这种“混合”策略在实际项目中非常常见。3. 三层架构设计基础层、组合层、页面层这套分层 HTML 组件系统把组件分为三个层级。设计核心是两条规则上层可以依赖下层下层绝对不能依赖上层同层之间尽量不要互相依赖必须协作时通过事件解耦。3.1 基础组件层Base Layer基础组件层是系统的原子单元对应设计系统里的“原子”。这一层的组件通常不包含业务概念只负责具体的 UI 交互。按钮、输入框、复选框、单选按钮、图标、通知提示都属于这一层。基础组件的特征有三个不感知业务数据不发起接口请求。通过属性和事件对外通信。样式完全自包含外部不需要也不能侵入其内部实现。3.2 组合组件层Composite Layer组合组件层把多个基础组件组合成有意义的“功能模块”对应设计系统里的“分子”和“组织”。例如一个搜索框内部由输入框、按钮、下拉列表组合而成一个表单项由标签、输入框、校验提示组合而成。组合组件可以包含少量业务逻辑比如表单校验、状态切换、请求编排但这些逻辑应该与具体业务场景解耦。组合组件面向的是“通用功能”而不是“某个具体页面”。3.3 页面业务层Page Layer页面业务层是分层系统的最上层直接对应用户看到的页面。这一层负责编排组合组件处理具体业务数据管理页面级状态调用接口服务。页面组件可以依赖任意下层组件但下层组件不应该感知页面组件的存在。页面层是唯一允许出现“这个页面专属逻辑”的地方。比如“用户资料管理页面”的数据结构、校验规则、保存回调都应该集中在页面组件里而不是散落到基础组件中。3.4 层间通信规则组件层之间的通信必须遵守方向约束属性向下传递父组件通过 HTML 属性给子组件传值例如my-input label姓名 value张三。事件向上抛出子组件通过自定义事件通知父组件发生了交互父组件监听事件并处理。禁止跨层穿透页面组件不能直接修改基础组件的内部 DOM基础组件也不能反向调用页面组件的业务方法。这条规则的工程意义在于当某一个下级组件需要重构时只要它对外暴露的属性和事件不变上层调用方就不需要改动。层与层之间通过“接口契约”连接而不是通过具体实现连接。4. 环境准备与项目结构4.1 运行环境这个示例使用原生 Web Components不需要安装任何前端框架也不需要 Webpack 或 Vite 构建。需要满足两个条件使用现代浏览器推荐 Chrome、Edge、Firefox 最新版以正常支持 Custom Elements 和 Shadow DOM。版本请以实际浏览器为准本文重点演示通用思路。通过 HTTP 服务访问页面不要直接双击index.html文件。因为示例会使用 ES Module 的import/export语法file://协议下浏览器会拦截模块加载这是安全策略的正常行为。如果你本机安装了 Python可以在项目根目录执行下面的命令启动一个临时静态文件服务python3 -m http.server 8080如果没有 Python也可以用 VS Code 的 Live Server 插件或者任意静态文件服务器。只要能通过 HTTP 访问到index.html即可。4.2 项目目录结构整个项目按“分层”组织目录目录结构本身就是架构的直观表达。layered-html-components/ ├── index.html └── src/ ├── core/ # 核心基础类与工具 │ └── base-component.js ├── components/ │ ├── base/ # 基础组件层 │ │ ├── my-button.js │ │ └── my-input.js │ ├── composite/ # 组合组件层 │ │ └── my-card.js │ └── page/ # 页面业务层 │ └── my-user-profile.js目录命名习惯建议固定base放基础组件composite放组合组件page放页面组件。新组件属于哪一层直接由目录决定不需要开会讨论。这是分层架构最落地的一种约束方式。5. 完整实现从零搭建分层组件系统下面用一个“用户个人信息表单”作为演示场景。它的结构是页面层my-user-profile组合了卡片my-card卡片内部放输入框my-input和按钮my-button。输入框、按钮属于基础层卡片属于组合层用户信息表单属于页面层。5.1 核心基础类BaseComponent先定义一个所有组件都可以继承的基础类。它负责统一的生命周期入口和状态管理约定。注意它是核心层的辅助类本身不是一个可见组件。// 文件路径src/core/base-component.js export class BaseComponent extends HTMLElement { constructor() { super(); // 统一状态对象子组件可以按需使用 this.state {}; } // 组件被挂载到文档后触发 connectedCallback() { if (this.mounted) { this.mounted(); } } // 子组件统一通过 setState 更新状态 setState(nextState) { this.state { ...this.state, ...nextState }; this.afterStateChange this.afterStateChange(); } }有了这个基础类所有组件都拥有统一的state管理入口和connectedCallback生命周期。后面实现具体组件时只需继承BaseComponent覆盖需要的方法即可。5.2 基础组件层my-button基础按钮组件是最典型的原子组件。它具备三种视觉变体默认、次要、禁用态对外抛出一个my-click自定义事件。按钮不关心点击之后要做什么它只负责把“用户点击了”这个消息抛出去。// 文件路径src/components/base/my-button.js import { BaseComponent } from ../../core/base-component.js; export class MyButton extends BaseComponent { static get observedAttributes() { return [variant, disabled, label]; } constructor() { super(); const shadow this.attachShadow({ mode: open }); const style document.createElement(style); style.textContent :host { display: inline-block; } button { border: none; border-radius: 6px; padding: 8px 16px; font-size: 14px; cursor: pointer; background: #2f6fed; color: #ffffff; transition: opacity 0.2s; } button:hover { opacity: 0.85; } button:disabled { opacity: 0.4; cursor: not-allowed; } button.secondary { background: #f0f1f3; color: #333333; } ; const button document.createElement(button); button.addEventListener(click, () { if (this.disabled) { return; } this.dispatchEvent(new CustomEvent(my-click, { detail: { value: this.getAttribute(value) || }, bubbles: true, composed: true })); }); shadow.appendChild(style); shadow.appendChild(button); this._button button; } attributeChangedCallback(name, oldValue, newValue) { if (!this._button) { return; } if (name label) { this._button.textContent newValue; } if (name disabled) { this._button.disabled newValue ! null; } if (name variant) { this._button.classList.toggle(secondary, newValue secondary); } } } customElements.define(my-button, MyButton);注意两个细节。第一自定义事件my-click设置了bubbles: true和composed: true。bubbles让事件可以在普通 DOM 树中向上冒泡composed让事件可以跨越 Shadow DOM 边界继续冒泡。如果漏掉composed父组件在 Shadow DOM 外部可能监听不到这个事件。第二attributeChangedCallback在属性变化时同步更新按钮内部状态这是“属性向下传递”的落地实现。5.3 基础组件层my-input输入框组件同样属于基础层。它封装了label、input、错误提示三部分并支持label、value、placeholder、error四个属性。输入事件通过my-input自定义事件向上抛出。// 文件路径src/components/base/my-input.js import { BaseComponent } from ../../core/base-component.js; export class MyInput extends BaseComponent { static get observedAttributes() { return [label, value, placeholder, error]; } constructor() { super(); const shadow this.attachShadow({ mode: open }); shadow.innerHTML style :host { display: block; margin-bottom: 12px; } label { display: block; font-size: 13px; color: #555555; margin-bottom: 4px; } input { width: 100%; box-sizing: border-box; padding: 8px 10px; border: 1px solid #cccccc; border-radius: 6px; font-size: 14px; } input:focus { outline: none; border-color: #2f6fed; box-shadow: 0 0 0 2px rgba(47, 111, 237, 0.15); } .error-hint { color: #d93025; font-size: 12px; margin-top: 4px; display: none; } /style label/label input / div classerror-hint/div ; this._label shadow.querySelector(label); this._input shadow.querySelector(input); this._errorHint shadow.querySelector(.error-hint); this._input.addEventListener(input, () { this.dispatchEvent(new CustomEvent(my-input, { detail: { value: this._input.value }, bubbles: true, composed: true })); }); } static createAttributeChangedCallback() { return null; } attributeChangedCallback(name, oldValue, newValue) { if (!this._input) { return; } if (name label) { this._label.textContent newValue; } if (name value) { // 仅当外部属性值与当前输入值不一致时才更新避免覆盖用户输入 if (this._input.value ! newValue) { this._input.value newValue; } } if (name placeholder) { this._input.placeholder newValue; } if (name error) { this._errorHint.textContent newValue || ; this._errorHint.style.display newValue ? block : none; this._input.style.borderColor newValue ? #d93025 : ; } } get value() { return this._input.value; } set value(v) { this._input.value v; } } customElements.define(my-input, MyInput);这里有一个新手容易踩的坑在attributeChangedCallback里更新value属性时如果外部设置的值和用户当前输入的值不一致直接覆盖如果一致就不要重复赋值否则可能把光标位置重置到末尾影响输入体验。这个“属性与状态同步”的问题是属性反射设计的核心难点。5.4 组合组件层my-card卡片组件是组合层的一个简单例子。它本身不包含具体的业务逻辑只负责提供统一的视觉容器并通过 Slot 插槽让使用方自由填写标题、操作区和内容区。// 文件路径src/components/composite/my-card.js import { BaseComponent } from ../../core/base-component.js; export class MyCard extends BaseComponent { constructor() { super(); const shadow this.attachShadow({ mode: open }); shadow.innerHTML style :host { display: block; border: 1px solid #e5e6eb; border-radius: 12px; padding: 16px; background: #ffffff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); } .card-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f0f1f3; padding-bottom: 10px; margin-bottom: 12px; } .card-title { font-size: 16px; font-weight: 600; } .card-actions { font-size: 13px; } .card-body { font-size: 14px; color: #444444; } /style div classcard-header span classcard-title slot nametitle默认标题/slot /span span classcard-actions slot nameactions/slot /span /div div classcard-body slot/slot /div ; } } customElements.define(my-card, MyCard);Slot 是组合层实现“组件拼接”的关键。slot nametitle表示使用方可以提供slottitle的内容没有提供时显示插槽内的默认内容“默认标题”。这种机制让组件外壳保持稳定内容可以灵活替换非常符合组合层“通用功能模块”的定位。5.5 页面业务层my-user-profile页面层组件负责真正“接业务”。在这个示例里my-user-profile内部组装了卡片、输入框、按钮并实现了表单校验、保存、重置功能。注意业务数据校验逻辑写在这一层而不是写在my-input里。// 文件路径src/components/page/my-user-profile.js import { BaseComponent } from ../../core/base-component.js; import ../base/my-button.js; import ../base/my-input.js; import ../composite/my-card.js; export class MyUserProfile extends BaseComponent { constructor() { super(); this.state { name: , email: }; const shadow this.attachShadow({ mode: open }); shadow.innerHTML style .profile-form { max-width: 360px; } .form-actions { display: flex; gap: 8px; margin-top: 8px; } .status { font-size: 13px; color: #888888; margin-top: 8px; } /style my-card span slottitle个人信息/span my-input label姓名 placeholder请输入姓名>!-- 文件路径index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title分层 HTML 组件系统示例/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, sans-serif; background: #f7f8fa; padding: 32px; display: flex; justify-content: center; } .page-container { width: 100%; max-width: 420px; } /style /head body div classpage-container my-user-profile/my-user-profile /div script typemodule src./src/components/page/my-user-profile.js/script /body /html页面业务层组件在被引入时会自动导入它依赖的下层组件模块。这种“按需导入、层层依赖”的机制让整个系统的依赖关系清晰可追踪。你在浏览器 DevTools 的 Sources 面板里可以清楚地看到每个模块从哪里来。6. 运行与验证6.1 启动服务在项目根目录执行python3 -m http.server 8080如果本机是 Windows 且没有python3可以使用python -m http.server 8080也可以使用 VS Code 的 Live Server 插件右键index.html选择 “Open with Live Server”。6.2 预期效果浏览器访问http://localhost:8080应该看到一张“个人信息”卡片包含标题“个人信息”。两个输入框标签分别为“姓名”和“邮箱”。一个“保存”按钮和一个“重置”按钮。交互行为预期如下直接点击“保存”姓名输入框下方出现红色提示“姓名不能为空”。输入姓名、不输入邮箱点击“保存”邮箱输入框提示“邮箱不能为空”。输入姓名和格式错误的邮箱点击“保存”邮箱输入框提示“邮箱格式不正确”。输入正确的姓名和邮箱点击“保存”卡片底部显示“保存成功{...}”。点击“重置”所有输入框清空错误提示消失底部状态文本清空。6.3 用 DevTools 验证封装效果这是验证分层组件系统最直观的一步。打开 Chrome DevTools 的 Elements 面板展开my-user-profile元素。你会看到里面有一层#shadow-root (open)继续展开里面又嵌套了my-card再次展开又能看到它自己的#shadow-root。这说明每一层组件的内部 DOM 都被 Shadow DOM 隔离了。把输入框的error属性设置为错误后再在 Elements 面板里查看 Shadow DOM 内部你会发现.error-hint元素的样式完全作用于组件内部没有影响页面其他区域。这就是样式封装的意义。6.4 失败时的排查顺序如果页面空白或者组件没有渲染按下面的顺序检查排查顺序检查项判断方法1是否通过 HTTP 访问地址栏必须是http://localhost:8080不能是file://2浏览器控制台是否有 Module 加载错误F12 打开 Console查看报错路径3自定义元素是否注册成功Console 执行customElements.get(my-button)应返回构造函数4标签名是否全部包含短横线mybutton不会被浏览器识别为自定义元素5是否引入了对应 JS 文件检查index.html中的script路径是否正确7. 常见问题与排查思路分层 HTML 组件系统在实践中会遇到不少问题下面列出最高频的几类。问题现象可能原因排查方式解决方案页面空白无任何组件渲染script模块加载失败或自定义元素未注册打开 Console 查看报错检查typemodule和文件路径统一通过 HTTP 服务访问保证模块路径正确控制台报错 “Identifier already declared”某个组件 JS 文件被重复引入检查模块 import 关系避免一个组件被多个入口重复加载在页面层统一引入子模块只导出不重复注册自定义元素标签名无效标签名没有包含短横线检查customElements.define的第一个参数标签名必须满足xxx-yyy规范样式没有生效或者相互污染Shadow DOM 内部样式被外部覆盖或外部样式穿透检查样式是否写在:host或组件内部style中样式写在 Shadow DOM 内部需要外部定制时使用 CSS 自定义属性而不是全局类名父组件监听不到子组件事件自定义事件缺少composed: true在父组件 Shadow DOM 中尝试监听事件判断是否触发派发事件时设置bubbles: true, composed: true外部设置value属性后输入框光标跳到最后属性回调重复赋值导致光标位置被重置在attributeChangedCallback中打印oldValue/newValue只有当外部值与当前输入值不一致时才更新输入框值Slot 内容不显示插槽名称不匹配或父组件未传对应内容检查slot nametitle与slottitle是否一致插槽名统一小写、用短横线连接上面这些坑大多不是分层本身带来的而是 Web Components 的底层细节。这其实印证了一个观点分层架构解决的是“组织复杂度”但底层标准能力不熟练依然会在细节上翻车。建议在团队推广之前先让核心成员把 Custom Elements 和 Shadow DOM 的边界行为吃透。8. 最佳实践与工程建议8.1 命名规范自定义元素标签名统一采用前缀-组件名格式比如my-button、my-card。前缀建议用公司或团队的缩写避免与第三方组件冲突。属性名使用小写短横线比如label、placeholder、>button { background: var(--my-button-bg, #2f6fed); color: var(--my-button-color, #ffffff); }外部要定制主题时给组件设置my-button { --my-button-bg: #16a34a; --my-button-color: #ffffff; }这样既保持了样式隔离又开放了可控的定制能力。禁止的做法是在全局样式里直接写my-button button { ... }试图强行覆盖 Shadow DOM 内部样式这种做法在封闭 Shadow DOM 下无效在开放模式下也容易失控。8.4 状态归属与通信状态应该尽可能放在离使用场景最近的层级。基础组件只维护内部 UI 状态组合组件维护模块级状态页面组件维护业务级状态。层间通信严格遵循“属性向下、事件向上”。如果业务状态实在复杂可以引入现代的轻量状态库但要注意状态库应该只作用于页面业务层不要渗透到基础组件层。否则基础组件会失去“框架无关”的价值。8.5 性能优化基础组件尽量保持“轻”减少不必要的observedAttributes监听减少 Shadow DOM 内部的 DOM 节点数。大批量渲染场景下优先使用document.createDocumentFragment()或template来批量创建节点避免反复触发重排。组件体积较大时采用动态导入。比如页面组件在connectedCallback里import()按需加载而不是在入口一次性引入所有组件。注意 Shadow DOM 带来的事件开销事件在跨越多个 Shadow 边界时有一定性能损耗高频事件如mousemove、scroll要避免从组件内部频繁向外派发。8.6 可访问性组件系统很容易忽视可访问性Accessibility。基础按钮组件要把内部button的原生语义保留而不是用div模拟。输入框必须绑定label与input的关联关系或者使用aria-label。自定义事件抛出时也应该同步维护aria状态比如禁用态设置aria-disabledtrue。8.7 测试策略分层系统的边界清晰反而更容易写测试。推荐采用两种测试单元测试用 Playwright 或 Web Test Runner 直接挂载单个组件传入属性、触发事件、断言结果。基础组件和组合组件是单元测试的主力。端到端测试用 Playwright 打开index.html模拟用户输入和点击验证表单校验、保存、重置完整链路。测试的关键是把断言写在外部的“公共接口”上。不要断言 Shadow DOM 内部的某个类名而应该断言组件暴露的属性、事件、可见文本。否则内部实现一调整测试就大面积失败维护成本极高。8.8 安全边界用原生 JavaScript 拼接组件内容时最容易犯的安全错误是把用户输入当 HTML 插入。永远不要用innerHTML直接拼接外部数据。示例代码中全部使用textContent或createElement创建节点这是安全做法。如果不得不使用模板字符串渲染也必须经过转义函数处理。组件库作为公共基础设施安全要求比普通业务页面更高因为一个组件可能被几十个页面引用。9. 总结与后续学习方向这篇内容真正讲清楚了几件事。第一分层 HTML 组件系统的本质是把组件按照“基础层、组合层、页面层”划分职责边界用“属性向下、事件向上”的规则约束层间通信。第二原生 Web Components 提供了实现这套架构的标准能力Custom Elements 负责定义组件Shadow DOM 负责样式隔离Slot 负责组合复用。第三分层不是银弹它解决的是组织复杂度但组件内部的属性反射、事件穿透、光标体验这些底层细节仍然需要逐个认真处理。如果你接下来要动手实践建议按这个顺序推进先把本文的示例跑通围绕my-button和my-input扩展更多基础组件然后在真实项目里选择两三个核心页面把现有代码迁移到分层结构对比迁移前后的改动成本和维护体验再逐步补充设计令牌Design Tokens、主题变量和自动化测试。值得继续深入的方向还有几个一是如何用设计令牌统一管理各层组件的颜色、间距、圆角让基础组件层的主题定制更系统化二是如何把分层组件系统和 Vite、Rollup 等构建工具结合实现按需打包和组件文档自动生成三是如何与现有 React/Vue 项目集成让基础组件层真正做到一次封装、多栈复用。分层架构不是宏大理论它是把一个朴素的事实反复落在工程里边界清晰代码才不会腐烂。建议把本文收藏备用下次重构组件代码时按“分层规则”先画边界再动手写。