HTML语义化标签天然定义组件边界,自定义元素需严格遵循attribute/event通信与slot分发原则。语义标签如<header>、<nav>等确立结构契约;Custom Elements须通过observedAttributes响应属性变更、dispatchEvent派发事件、Shadow DOM隔离样式,并仅用<slot>实现内容分发。

HTML 语义化标签本身就是组件边界
纯 HTML 没有“组件”运行时概念,但 <header>、<nav>、<main>、<aside>、<footer> 这些语义化标签天然划定了职责范围。它们不是装饰,而是结构契约——搜索引擎、屏幕阅读器、CSS 选择器都按此识别逻辑区块。
常见错误是用一堆 <div class="header"> 模拟语义,结果样式、JS、测试逻辑全绑死在 class 名上,一改就断。
- 每个语义标签内只放与该角色强相关的子元素(比如
<nav>里只放<a>或<ul>,不塞搜索框或用户头像) - 避免跨语义嵌套,例如不要在
<footer>里写<main> - 用
<section>包裹一组有共同主题的元素,而不是当“万能 div”用
自定义元素(Custom Elements)才是真正的 HTML 组件
浏览器原生支持的 customElements.define() 是让 HTML 具备组件能力的唯一标准路径。它不依赖框架,不污染全局,且可被 SSR 直出。
容易踩的坑:把 custom element 当成 React 组件来写——在 connectedCallback 里直接操作外部 DOM、监听全局事件、或依赖未声明的属性。
立即学习“前端免费学习笔记(深入)”;
- 所有状态必须通过
attributeChangedCallback响应observedAttributes列表里的属性变更 - 内部 DOM 渲染用
Shadow DOM隔离样式和事件流,避免泄漏到父页面 - 对外暴露的接口只通过属性(
this.setAttribute('loading', 'true'))和事件(this.dispatchEvent(new CustomEvent('change'))),不暴露方法 - 不要在
constructor里访问this.shadowRoot—— 此时它还不存在
slot 是 HTML 组件唯一的“插槽”机制
<slot> 不是语法糖,它是浏览器级的 DOM 分发协议。它让父模板决定子组件“长什么样”,而子组件只负责“在哪挖洞”。这正是低耦合的核心体现。
很多人误以为 <slot name="header"> 只是命名占位符,其实它的作用是建立明确的注入契约:父级必须提供匹配 slot="header" 的节点,否则内容不会被分发。
- 默认插槽(无 name)只能有一个;具名插槽可多个,但同名 slot 只会接收第一个匹配节点
- 使用
<slot>fallback content</slot>提供兜底内容,避免空渲染 - 在 Shadow DOM 中,
<slot>是唯一能穿透样式的出口,CSS 选择器无法跨 slot 边界定位 - 动态 slot 内容变更会触发
slotchange事件,可用于响应式更新,但注意它不冒泡
组件间通信只能靠 attribute + event
HTML 组件之间没有 props、context、store 这类抽象层。通信路径只有两条:父 → 子走 attribute,子 → 父走 event。任何试图绕开这条路的方案(比如用全局变量、DOM 查询、或依赖特定框架生命周期)都会导致耦合升高。
典型反例:一个 <user-card> 组件在内部用 document.querySelector('.theme-toggle') 监听主题切换 —— 这等于绑定了具体 class 名,彻底破坏了可移植性。
- 子组件通过
static get observedAttributes()声明关心哪些 attribute 变更 - 父组件修改 attribute 必须用
el.setAttribute('data-status', 'loading'),不能直接赋值el.dataStatus = 'loading' - 子组件派发事件必须用
new CustomEvent('submit', { bubbles: false, composed: true }),composed: true才能穿透 Shadow DOM - 父组件监听事件统一用
el.addEventListener('submit', handler),不要用onsubmit属性,后者无法监听自定义事件



















