想象你刚租下一间“精装公寓”:水电、墙纸、地板全部就位,拎包入住即可。房间里有一面神奇墙壁——外面的施工队想刷油漆进来,被挡得死死的;你在屋里改造软装,也绝不会弄脏邻居的墙。更妙的是,这房子是标准规格的“活动房”,今天放进北京的写字楼,明天搬进上海的小区,都能无缝适配。 这就是前端世界里 Web Components 想做的事。它不使用任何框架,只靠浏览器原生能力就造出“自带装修、与世隔绝、随处安放”的组件房间。房间里发生什么,外面一概不知;外面的样式洪水,冲不破那堵“影子墙”。本文就用“精装房”这个比喻,带你走进 Custom Elements、Shadow DOM、模板和插槽的奇妙世界,亲手搭建一间属于你的前端样板间。

岗位关联

对 AI 从业者而言,掌握 Web Components 意味着你能把 AI 对话、数据可视化、模型交互面板封装成“通用零件”,脱离框架束缚,在任意技术栈的 AI 产品中即插即用。

目录
  1. 可视化演示
  2. 组件化思想——从毛坯房到精装房
  3. 自定义元素——办下组件的“房产证”
  4. Shadow DOM——那面与世隔绝的魔术墙
  5. 生命周期——从拎包入住到物业维护
  6. Slot 插槽——房间里的“万能插座”
  7. HTML Templates——随取随用的设计蓝图
  8. 跨框架复用——能住进任何大楼的精装房
  9. 应用场景——AI 时代的精装房生态
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Shadow DOM样式隔离、自定义元素生命周期回调与slot内容分发三大核心机制,支持模式切换交互。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Shadow DOM样式隔离 shadowRoot创建封闭边界,外部CSS无法穿透 Light DOM样式与Shadow DOM互不影响 element.attachShadow({mode:"open"}) Shadow DOM实现原生样式封装隔离

组件化思想——从毛坯房到精装房

传统前端开发像在一间毛坯房里装修:想要一个按钮,就在墙角自己画;想复用整个侧边栏,就得把砖一块块复制过来。好不容易装好的客厅,换个户型就全部推倒重来。而 Web Components 提倡的组件化,就像买精装房——开发商把每个房间都封装成独立模块,功能齐全,拎包入住。

Web Components 不是单一技术,而是由 Custom Elements(自定义元素)、Shadow DOM(影子 DOM)、HTML Templates(模板)以及 ES Modules(模块导入)共同构成的浏览器原生规范。它要解决的核心问题是封装与复用:把结构、样式、行为装进一个元素里,让任何人拿来即用,不依赖任何框架。

一个组件就是一个自定义标签,例如 <my-card>,它可以自带样式和逻辑,使用时只需写一行标签。这个模型可以概括为:

就像精装房必须包含“图纸 + 隔墙 + 保修维护”,一个完整的 Web Component 也缺不了这三样。缺了图纸,房子没有骨架;缺了墙,样式互相污染;缺了生命周期,组件无法感知自己活在哪个页面里。

CustomElement=Template+ShadowDOM+Lifecycle\text{CustomElement} = \text{Template} + \text{ShadowDOM} + \text{Lifecycle}
该公式表示:一个自定义组件等于三样东西的相加——模板决定“长什么样”,影子 DOM 决定“怎么隔离”,生命周期决定“什么时候干活”。
  1. Template(模板)模板是组件的骨架图纸,定义了内部 HTML 结构,相当于精装房的户型图。
    Template: HTML 结构蓝色图\text{Template}: \ \text{HTML 结构蓝色图}
  2. ShadowDOM(影子 DOM)影子 DOM 把样式和 DOM 关进独立空间,不让外界干扰,等同于精装房那面“魔术墙”。
    ShadowDOM: 隔离的样式树与结构树\text{ShadowDOM}: \ \text{隔离的样式树与结构树}
  3. Lifecycle(生命周期)生命周期钩子让组件在挂载、更新、卸载时做出响应,相当于物业的入住—维护—搬迁服务。
    Lifecycle: connected→attributeChanged→disconnected\text{Lifecycle}: \ \text{connected} \rightarrow \text{attributeChanged} \rightarrow \text{disconnected}

自定义元素——办下组件的“房产证”

每个精装房在交付前都要在房管局登记备案,拿到房产证后,开发商才能合法出售。自定义元素也有这样一本“房产证”:浏览器通过 customElements.define() 方法,把自定义标签的名字和组件类绑定在一起。一旦注册成功,所有页面都能使用这个“带证”的标签。

定义分组件的关键在于写一个继承 HTMLElement 的类,并在构造函数里完成初始化。类的名字通常与标签名对应,比如标签叫 my-card,类就叫 MyCard。而标签名必须包含连字符(例如 x-tip、ai-chat),这是浏览器用来识别“这是自定义元素”的暗号,避免与未来的原生标签撞名。

它的注册机制可以写成:

拿到了房产证,你的组件就正式成为“合法居民”。在 HTML 里写一行 <my-card>,浏览器就会立即为你盖好一所“精装房”,里面的构造和逻辑,全都封装在那一纸证书背后。

customElements.define(tagName,Class)\text{customElements.define}(\text{tagName}, \text{Class})
这里的 define 相当于给组件类颁发房产证,tagName 是门牌号,Class 是住在里面的人。
  1. define 方法浏览器暴露的全局注册方法,把标签与类关联起来,写一次,全局生效。
    define: customElements→注册表\text{define}: \ \text{customElements} \rightarrow \text{注册表}
  2. tagName 参数标签名强制包含连字符,例如 my-card、ai-chat,作为合法身份的标识。
    tagName: ’my-card’ 含连字符\text{tagName}: \ \text{'my-card'} \ \text{含连字符}
  3. Class 参数继承 HTMLElement 的类,决定元素创建后如何初始化和响应生命周期。
    Class: extends HTMLElement\text{Class}: \ \text{extends} \ \text{HTMLElement}

Shadow DOM——那面与世隔绝的魔术墙

精装房最神奇的部分是那面“魔术墙”:墙外的人看不见屋里的电线和水管,墙内的装修也不会渗透到邻居家。Shadow DOM 就是浏览器提供的一堵代码墙——它给组件内部一棵独立 DOM 树起名为“影子树”,这棵树与主文档隔离,外部 CSS 选择器默认找不到它,内部样式也不会泄漏出去。

创建这面墙的入口是 attachShadow() 方法,它接受一个配置对象,其中的 mode 决定墙的“透明程度”。open 模式允许外部通过 element.shadowRoot 打开门缝观察内部;closed 模式则从机制上关闭了这条访问路径,只能靠内部持有的引用来操作。

这堵魔术墙也不是绝对封死。CSS 自定义属性(变量)能顺管道穿过墙体,:part() 伪元素能精准打开几扇窗,而 ::slotted() 则是留给插槽内容的小门。墙的存在不是拒绝交流,而是让交流变得有秩序、有边界。

shadowRoot=host.attachShadow({mode:’open’ 或 ’closed’})\text{shadowRoot} = \text{host.attachShadow}(\{\text{mode}: \text{'open'} \ \text{或} \ \text{'closed'}\})
attachShadow 把影子树挂到宿主元素上,mode 决定这堵墙是磨砂玻璃(open)还是不透明钢板(closed)。
  1. host 宿主元素调用 attachShadow 的那个自定义元素,是整面墙的承重结构。
    host: 挂载影子树的元素\text{host}: \ \text{挂载影子树的元素}
  2. attachShadow 方法在宿主上创建并挂载影子根节点,开启隔离世界。
    attachShadow: 创建 ShadowRoot\text{attachShadow}: \ \text{创建} \ \text{ShadowRoot}
  3. mode 参数open 允许外部访问 shadowRoot;closed 拒绝外部访问,更严格也更安全。
    mode: ’open’ 或 ’closed’\text{mode}: \ \text{'open'} \ \text{或} \ \text{'closed'}

生命周期——从拎包入住到物业维护

精装房有自己的“人生轨迹”:交房时业主入住、入住期间房子设施更新、最后搬迁离去、整栋楼被移交给新物业。自定义元素同样拥有四个生命周期回调,浏览器会在恰当的时机自动打电话通知组件。

这四个回调分别对应四种时刻:元素被挂载到文档时触发 connectedCallback,适合初始化数据或绑定事件;元素被移除时触发 disconnectedCallback,适合清理定时器与监听器;监听数组 observedAttributes 中声明的属性一旦发生变化,就会触发 attributeChangedCallback;而当元素整体被移动到另一个文档(比如 iframe)时,adoptedCallback 会被调用。

生命周期是组件自我管理的“售后体系”。有了这套体系,定时器不会泄漏,事件监听不会堆积,组件在复杂的单页应用里才能做到“来无影、去无痕”,活得体面、走得干净。

Lifecycle=connected→attributeChanged→disconnected\text{Lifecycle} = \text{connected} \rightarrow \text{attributeChanged} \rightarrow \text{disconnected}
生命周期是一条从入住到搬迁的流水线,每个节点都有对应的“物业电话”通知组件。
  1. connected 挂载元素进入页面时触发,相当于业主入住,物业登记。
    connected: 元素入 document\text{connected}: \ \text{元素入} \ \text{document}
  2. attributeChanged 属性变化observedAttributes 中的属性值改变时触发,相当于更换家电时物业记录。
    attributeChanged: old→new\text{attributeChanged}: \ \text{old} \rightarrow \text{new}
  3. disconnected 卸载元素离开页面时触发,相当于业主搬走,物业清理房间。
    disconnected: 元素 remove\text{disconnected}: \ \text{元素} \ \text{remove}

Slot 插槽——房间里的“万能插座”

精装房的电视墙提前预留了几个插座,业主把什么电器插上去,插座就显示什么电器。自定义元素里的 <slot> 就是这样的“插座”。组件外壳上写着 <slot></slot>,使用者往标签中间塞的任何 HTML 内容,都会被浏览器自动“插进”这个位置——这就是内容分发。

如果房间里有多个插座需要区分,可以给 <slot> 起名:<slot name="header">。使用方只需在内容上写 slot="header" 属性,浏览器就能精准地把内容投递到对应插座。而如果使用者什么都没往插座上插,组件里写的插槽默认内容就会作为“回退内容”显示,就像插座自带的 USB 口——你不接充电线,它也不会空着。

插槽让“自定义元素”不再是封闭的铁盒子,而成为一种可组合的容器。就像精装房里的插座电压、接口都是标准规格,任何品牌的电器都能即插即用——组件写一次,内容由调用方灵活决定。

AssignedContent→slot→Shadow Tree\text{AssignedContent} \rightarrow \text{slot} \rightarrow \text{Shadow Tree}
外部内容被分发到插槽,进而在影子树中完成渲染;slot 是连接外部世界与影子世界的接口。
  1. 分发来源使用者在组件标签中书写的内容,是“待插入的电器”。
    AssignedContent: light DOM 节点\text{AssignedContent}: \ \text{light DOM} \ \text{节点}
  2. 插槽接收<slot> 声明一个接收点,name 属性区分不同插槽。
    slot: 具名 name 匹配\text{slot}: \ \text{具名} \ \text{name} \ \text{匹配}
  3. 影子树渲染分发后的内容在影子树中渲染,完成“接入”。
    Shadow Tree: 最终渲染位置\text{Shadow Tree}: \ \text{最终渲染位置}

HTML Templates——随取随用的设计蓝图

开发商在样板间里展示了一套设计蓝图,从图纸上看客厅、卧室一应俱全,但图纸本身永远不会变成真实的房子。HTML 的 <template> 标签正是这样的“蓝图”:里面的内容不会渲染到页面上,也不会发出任何网络请求,它只是安静地躺在文档里,等待被调用。

当组件需要内容时,用 template.content.cloneNode(true) 把蓝图复制一份,变成真实的 DOM 节点,再挂载到 Shadow DOM 中。由于模板内容不参与渲染,复制操作非常轻量,适合在大量组件实例间共享同一份结构,就像同一个户型可以在不同楼层反复复制。

蓝图与房子的关系,就是模板与组件实例的关系。一套图纸可以盖出无数栋楼,一份模板也能克隆出无数个组件实例。这种“惰性 + 克隆”的组合,让组件的批量生产效率远高于传统 DOM 拼接。

Node=template.content.cloneNode(true)\text{Node} = \text{template.content.cloneNode}(\text{true})
cloneNode(true) 做深克隆,连子节点一起复制;模板成为批量生产组件的“印钞机”。
  1. template 内容<template> 内包裹的 HTML 是惰性的,浏览器不会绘制它。
    template.content: 惰性 DocumentFragment\text{template.content}: \ \text{惰性} \ \text{DocumentFragment}
  2. cloneNode 克隆深克隆复制整棵树,产生一份独立的 DOM 副本。
    cloneNode(true): 深拷贝 节点子树\text{cloneNode}(\text{true}): \ \text{深拷贝} \ \text{节点子树}
  3. 挂载使用把克隆结果插入影子树,完成从蓝图到实体的转化。
    appendChild: Node→ShadowRoot\text{appendChild}: \ \text{Node} \rightarrow \text{ShadowRoot}

跨框架复用——能住进任何大楼的精装房

精装房最诱人的特质是“标准化”:不管这栋楼是万科盖的还是保利盖的,房间尺寸、管线接口、电梯规格都符合国家统一标准,房子搬到哪里都能正常使用。Web Components 生来就是浏览器标准,所以 React、Vue、Angular 都能直接使用它——只要输出 HTML 标签,任何框架都无法拒绝。

主流框架走的是“数据驱动视图”的路线,组件逻辑深度绑定框架运行时;而 Web Components 走的是“平台原生”路线,零依赖零运行时。两者并不矛盾:React 组件内部可以包裹一个 Web Component,反之,Web Component 也可以嵌入 Vue 项目。常用策略包括:用 Web Components 封装完全独立的通用功能(如 AI 对话),再向各框架输出适配层;在微前端架构中,用自定义元素作为跨子应用共享的“通用零件”。

标准化的房间让业主自由选择楼盘,标准化组件让团队自由选择框架。你不需要为了 React 写一套 AI 面板、为 Vue 再写一套——写一次浏览器原生组件,整个前端生态都为你敞开大门。

∀ Framework∈{React,Vue,Angular}:Component works\forall \ \text{Framework} \in \{\text{React}, \text{Vue}, \text{Angular}\}: \quad \text{Component}\ \text{works}
该公式表示:对集合中的任意一个框架,组件都能正常工作——这是平台标准带来的通用性。
  1. 全称量词 ∀对每一个框架成员都成立,强调“无例外”的普适性。
    ∀: 任意 一个 框架\forall: \ \text{任意} \ \text{一个} \ \text{框架}
  2. 框架集合列出的主流框架只是示例,实际范围是“所有能渲染 HTML 的环境”。
    Framework∈{⋅}: 属于 集合\text{Framework} \in \{\cdot\}: \ \text{属于} \ \text{集合}
  3. works 成立组件在各自环境中挂载、运行、交互均正常,即“即插即用”。
    works: 挂载 ∧ 运行正常\text{works}: \ \text{挂载} \ \land \ \text{运行正常}

应用场景——AI 时代的精装房生态

当精装房足够多时,城市里就会出现“拎包入住”的生态:同一个豪华装修的 AI 对话房,可以装进客服系统、装进办公软件、还能装进数据分析台。Web Components 在真实世界的落点,正从“组件”走向“生态”。

在 AI 产品中,这种封装价值尤为明显。AI 应用通常由模型服务、前端交互、业务系统三层构成,而前端交互层往往要在多个产品线复用:一个智能对话组件需要流式输出、消息气泡、可终止生成按钮;一份模型评测报告需要展示 PR 曲线、混淆矩阵、Loss 衰减曲线。这些能力如果封装成 <ai-chat>、<model-report> 这样的自定义元素,那么任何团队——不论用 React、Vue 还是原生 JS 写业务——都能通过一两行标签接入。更长远地,设计系统团队可以把所有 AI 相关组件统一封装成标准件,配合微前端架构在各业务线之间共享。

AI 时代的前端不再只服务“好看”,还要服务“智能”。当模型输出的数据流能通过标准组件直接嵌入任意业务系统,当算法团队的成果能在产品端即插即用,这间“精装房”才算真正完成了从技术规范到产业生态的最后一跃。

Reuse Rate=shared_componentstotal_components×100%\text{Reuse Rate} = \dfrac{\text{shared\_components}}{\text{total\_components}} \times 100\%
复用率越高,单位功能开发成本越低;Web Components 让“一次开发、处处复用”从口号变成可量化的工程指标。
  1. shared_components 共享组件数已经被封装为 Web Components 并跨项目复用的数,分子越大复用越好。
    shared_components: 被复用的 组件数\text{shared\_components}: \ \text{被复用的} \ \text{组件数}
  2. total_components 组件总数企业或项目中的全部组件数量,作为分母基准。
    total_components: 全部 组件数\text{total\_components}: \ \text{全部} \ \text{组件数}
  3. 100% 缩放把比值转化为百分比,便于横向对比团队间的组件复用水平。
    ×100%: 比率 → 百分比\times 100\%: \ \text{比率} \ \rightarrow \ \text{百分比}

AI 应用场景

AI 对话组件封装

阶段:前端应用架构 / 组件开发

解决的问题:不同产品线(React 客服后台、Vue 运营中台、原生内部系统)都需要接入 AI 对话界面。传统做法是每个技术栈各写一套聊天 UI,样式、流式渲染逻辑重复建设,维护成本高。基于 Web Components 封装 <ai-chat> 后,只要一行标签即可接入,彻底告别重复劳动。

具体案例:某电商公司的智能客服团队定义了 <ai-chat api-endpoint="/v1/chat" /> 组件。组件内部用 Shadow DOM 隔离气泡样式,用生命周期管理大模型 SSE 流式连接,用 slot 让业务方插入快捷问题按钮。运营后台(Vue)、商家后台(React)、数据看板(原生)三个系统都以同一组件接入,UI 完全一致,升级只需发布一次。

模型评测可视化卡片

阶段:训练评估 → 结果展示

解决的问题:模型评估产物(PR 曲线、混淆矩阵、Loss 曲线、推理速度指标)需要在 Jupyter 分析页面、内部管理后台、客户汇报系统三处展示。传统方法为每个环境各画一套图表,样式漂移严重。封装 <model-report model-id="..."> 后,组件内部用 Canvas 绘图,Shadow DOM 隔离图表样式,无论嵌在哪里展示效果都一致。

具体案例:一家 AI 医疗影像公司的算法团队交付模型后,自动生成 <model-report> 自定义元素描述体检单。医院管理是 React 系统,算法演示台是 Vue 系统,报告邮件模板用原生 HTML,三者都通过引入同一份 Web Components 脚本库获得完全一致的评测视觉呈现,模型上线报告时间从 3 天缩短到 1 天。

微前端中的 AI 能力共享

阶段:大型 AI 平台架构 / 微前端治理

解决的问题:大型 AI 平台由多个团队共同开发——模型训练团队用 Vue、数据标注团队用 React、监控团队用原生 JS。公共组件(如“推理请求状态指示器”)无法统一维护。Web Components 充当各子应用间的“世界语”,团队之间无需统一框架即可共享 AI 可视化能力。

具体案例:某自动驾驶研发平台将“传感器数据回放面板”封装成 <sensor-player> 组件,内部用 Shadow DOM 隔离复杂样式与 WebGL 绘制逻辑。三个子应用以 <sensor-player> 引入同一数据流,生命周期钩子统一管理 WebGL 资源的创建和销毁,帧率稳定且无内存泄漏,跨团队协作效率大幅提升。

Python 演示

python84 行
class ComponentRegistry:    """模拟浏览器 customElements 注册表"""     def __init__(self):        self._registry = {}     def define(self, tag_name, cls):        """注册一个自定义元素类,对应 customElements.define"""        if tag_name in self._registry:            raise ValueError(f"标签已被占用: {tag_name}")        self._registry[tag_name] = cls        print(f"[注册] {tag_name} -> {cls.__name__}")     def create(self, tag_name):        """通过标签名创建实例"""        cls = self._registry.get(tag_name)        if cls is None:            raise KeyError(f"未注册的标签: {tag_name}")        return cls()  class BaseElement:    """模拟自定义元素基类,公开生命周期钩子"""     tag_name = "base-element"    observed_attributes = []     def __init__(self):        self.attrs = {}        self.connected = False        self.shadow_root = {"mode": "open", "children": []}        print(f"[{self.tag_name}] 实例创建完成")     def set_attribute(self, key, value):        """模拟属性变化,触发属性监听回调"""        old = self.attrs.get(key)        if old != value:            self.attrs[key] = value            self.attribute_changed(key, old, value)     def attribute_changed(self, name, old, new):        """生命周期钩子:observed_attributes 中声明的属性变化时回调"""        if name in self.observed_attributes:            print(f"[{self.tag_name}] 属性 {name} 变化: {old} -> {new}")            self.render()     def connected_callback(self):        """生命周期钩子:元素挂载到文档时调用"""        self.connected = True        print(f"[{self.tag_name}] 已挂载到页面")     def disconnected_callback(self):        """生命周期钩子:元素从文档移除时调用"""        self.connected = False        print(f"[{self.tag_name}] 已从页面卸载")     def render(self):        """子类覆写:负责更新影子树内容"""        pass  class MyCard(BaseElement):    """一个自定义的卡片组件,相当于自定义元素类"""     tag_name = "my-card"    observed_attributes = ["title", "status"]     def render(self):        title = self.attrs.get("title", "未命名")        status = self.attrs.get("status", "idle")        self.shadow_root["children"] = [f"<h2>{title}</h2>", f"<p>{status}</p>"]        print(f"[{self.tag_name}] 影子树更新: {self.shadow_root['children']}")  if __name__ == "__main__":    registry = ComponentRegistry()    registry.define(MyCard.tag_name, MyCard)     card = registry.create("my-card")    card.set_attribute("title", "AI 助手")    card.connected_callback()     card.set_attribute("status", "loading")    card.disconnected_callback()
目的:用 Python 类模拟 Web Components 的注册表、生命周期与属性监听机制,帮助理解浏览器中原生组件的工作节奏。
  1. registry.define(tag_name, cls)把标签名与组件类绑定,相当于 customElements.define('my-card', MyCard)。
  2. class BaseElement(BaseElement)用基类统一收口生命周期钩子,正是自定义元素继承 HTMLElement 的写照。
  3. card.connected_callback()模拟元素挂载到页面,触发“入住”逻辑。
  4. set_attribute("title", "AI 助手")属性值变化时自动走 attribute_changed,对应 observedAttributes 机制。
  5. self.shadow_root["children"]用一个简单字典模拟影子树内容,体现“样式/结构隔离”的容器思想。

常见误区

误区我以为一旦用了 Shadow DOM,外部 CSS 就真的完全进不来,组件内部的任何部分都永远碰不到,就像混凝土浇筑的密室一样。

正解Shadow DOM 提供的是“默认隔离”,而不是“绝对封锁”。你可以用 CSS 自定义属性从外部传入主题色,用 ::part() 暴露组件内部的可定制部位,用 ::slotted() 控制插槽内容的样式。这些“管道”是规范刻意留下的,让封装与可定制并存。就像精装房既有隔音墙,也有交换空气的窗户。

为何常见:“隔离”这个词太有误导性,人们容易把它想象成密封罐头,忘记了规范设计者也考虑了“受控的开放”。

误区浏览器都原生支持组件了,React、Vue 这些框架是不是没用了?我应该马上把项目重写成纯 Web Components。

正解两者是互补关系而非替代关系。Web Components 擅长封装“UI 零件”,而 React/Vue 的核心价值在于声明式数据流、状态管理和庞大生态。实际工程里最常见的做法恰恰是“在 React 里用 Web Component,或用 React 包一层 Web Component”。就像精装房不会取代整栋楼,它只是让楼里的居住体验更好。

为何常见:看到“原生”两个字就容易联想到“颠覆框架”,忽略了框架解决的是应用层状态同步的问题,而组件封装只是其中的一环。

误区我给自己定义的组件起名 `<button>`,结果页面一片混乱;改成 `<Card>` 好像也怪怪的,浏览器报错说不是合法自定义元素。

正解自定义元素名必须包含连字符,例如 <my-card>、<ai-chat>。这是浏览器的硬性规定,用来区分原生元素与自定义元素,也确保未来新增的 HTML 标签不会与你的命名冲突。一个单词或大写开头的名字都无法通过 customElements.define 注册。

为何常见:普通 HTML 标签都是单个单词,初学者按直觉命名,忽略了这个隐藏的语法约束;加上错误提示不够显眼,容易让人困惑。

误区我听说 `attachShadow({ mode: 'closed' })` 之后,外部谁也不能访问影子树,那我就不用担心任何安全隐患了。

正解closed 模式只是关闭了 element.shadowRoot 这个对外入口。但组件内部在 attachShadow 时拿到的 shadowRoot 引用仍然可以操作,外部也可以通过已保存的引用或其他手段访问。它主要防止“意外碰触”,而非“恶意攻破”。就像锁门能挡君子,挡不住持钥匙的专业开锁匠。

为何常见:把“访问不了”理解成“安全”,混淆了浏览器机制层面的符号不可见与真实的安全边界。

误区我以为插槽内容就像水泥浇筑,组件初始化时把内容填进去就固定了,改不了一点。

正解插槽分发是动态的。调用方修改了分配给插槽的节点、改变 slot 属性、或增加新的内容,浏览器都会自动重新分发,无需手动调用。slotchange 事件还能让组件主动感知这种变化并做出响应。就像插座随时可以拔掉旧电器插上新电器,无需重新装修墙面。

为何常见:初次接触时分发过程像“一次到位”的模板填充,忽略了 DOM 本身是活的可变结构。

面试题

Q1. 自定义元素的标签名必须满足哪条规则?

单选题 · easy

  • A.必须包含连字符← 正确答案
  • B.必须全是大写字母
  • C.必须以英文字母 x 开头
  • D.长度不能超过四个字符

答案:A。customElements.define 要求标签名必须包含连字符(如 my-card),这是区分原生标签与自定义元素的语法约定。大写开头、x 前缀和长度都不是规范要求。

Q2. attachShadow() 的 mode 参数可以取哪两个值?

单选题 · mid

  • A.open 或 closed← 正确答案
  • B.true 或 false
  • C.public 或 private
  • D.single 或 repeat

答案:A。mode: 'open' 允许通过 element.shadowRoot 访问影子树;'closed' 则关闭该入口。true/false、public/private、single/repeat 都不是合法的 mode 取值,注意这是字符串而非布尔值。

Q3. 自定义元素被添加到页面文档时触发哪个生命周期回调?

单选题 · easy

  • A.connectedCallback← 正确答案
  • B.disconnectedCallback
  • C.constructor 构造函数
  • D.adoptedCallback

答案:A。元素入文档触发 connectedCallback,离开文档触发 disconnectedCallback,迁移文档触发 adoptedCallback。构造函数在 createElement 时执行,早于任何生命周期回调。

Q4. 下列哪些属于 Shadow DOM 提供的特性?

多选题 · mid

  • A.组件内外部样式相互隔离← 正确答案
  • B.影子树中 DOM 查询与外部隔离← 正确答案
  • C.组件内部无法运行任何 JavaScript
  • D.支持通过 slot 进行内容分发← 正确答案

答案:ABD。Shadow DOM 的核心价值正是样式隔离与 DOM 封装,同时支持 <slot> 实现内容分发。但它不限制 JavaScript,内部脚本照常运行,因此 C 错误。

Q5. 自定义元素被移动到另一个文档(如 iframe)时,会触发哪个回调?

单选题 · mid

  • A.adoptedCallback← 正确答案
  • B.connectedCallback
  • C.attributeChangedCallback
  • D.webStateCallback

答案:A。adoptedCallback 专门用于元素跨文档迁移的场景,提示组件重新初始化上下文。挂载、属性变化分别由 connectedCallback 与 attributeChangedCallback 负责,webStateCallback 不存在。

Q6. 关于 <template> 标签,下列说法正确的是?

多选题 · hard

  • A.标签内部内容不会立即渲染到页面← 正确答案
  • B.可通过 cloneNode 克隆出可用的 DOM 节点← 正确答案
  • C.只能用于包裹表单类元素
  • D.内容属于惰性的 DocumentFragment← 正确答案

答案:ABD。<template> 的核心是惰性——内容不渲染、不请求资源,以 DocumentFragment 形式存放,通过 cloneNode(true) 克隆后插入文档复用之。它可包裹任意 HTML,不限于表单,故 C 错。

Q7. ::part() 伪元素的作用是什么?

单选题 · extreme

  • A.访问组件内显式暴露的自定义部分← 正确答案
  • B.访问 Shadow DOM 中所有隐藏节点
  • C.修改组件宿主元素的全局样式
  • D.拦截插入组件内部的外部脚本

答案:A。组件内部可用 part="xxx" 标记节点,外部再用 ::part(xxx) 精确设置样式——这是 Shadow DOM 刻意留出的“可控之窗”。它不能访问任意隐藏节点,也与全局样式和脚本拦截无关。

Q8. Web Components 的组成规范包含下列哪些?

多选题 · mid

  • A.Custom Elements 自定义元素← 正确答案
  • B.Shadow DOM 影子 DOM← 正确答案
  • C.Cache Storage 缓存存储
  • D.HTML Templates 模板← 正确答案

答案:ABD。Web Components 由 Custom Elements(封装)、Shadow DOM(隔离)和 HTML Templates(蓝图)三驾马车构成,常配合 ES Modules 使用。Cache Storage 是浏览器离线存储 API,不属于组件规范。

Q9. 自定义元素中用于声明要监听属性变化的静态字段是?

单选题 · easy

  • A.observedAttributes← 正确答案
  • B.monitoredAttrs
  • C.watchedFields
  • D.boundProperties

答案:A。observedAttributes 是一个静态数组,列出需要监听变化的属性名。只有出现在该数组中的属性发生变化,attributeChangedCallback 才会被触发。其余三个名称都不是浏览器标准。

Q10. 在 Shadow DOM 中调用 slot.assignedNodes() 的返回值是什么?

单选题 · hard

  • A.插槽分配的节点列表← 正确答案
  • B.一个布尔类型状态值
  • C.组件的类名字符串
  • D.绑定的事件处理回调函数

答案:A。assignedNodes() 返回被分配到该插槽的节点数组,便于组件在脚本中读取外部传入的内容。它不返回布尔值、类名或函数,是 Slot 内容分发机制的核心查询接口。