题目与范围
页面从服务端流式输出商品卡片。每张卡片要在没有 JavaScript 时展示有意义内容,组件样式要隔离,客户端脚本到达后再接入行为。请使用声明式 shadow root,并说明不支持该属性的浏览器,以及自定义元素在 HTML 已存在后才升级的情况。
核心能力是浏览器渲染、组件边界与 hydration 正确性,因此归入 frontend。
面试官考察什么
第一,是否知道支持时,带 shadowrootmode="open" 或 "closed" 的 template 会由 HTML 解析器转换为 shadow root?
第二,能否说明同一 host 只有第一个声明式 shadow root 会附着,后续实例仍是 template,需要明确降级策略?
第三,能否在服务端 HTML 与客户端升级间保持 slot、样式、可访问性和表单行为?
第四,能否区分 open 与 closed?closed 只隐藏 shadowRoot 引用,不是安全边界。
第五,能否避免 hydration 或流式传输中的重复渲染、重复监听和事件丢失?
先澄清的问题
- 支持哪些浏览器与服务端响应模式?
- 自定义元素会立即注册,还是可能延迟升级?
- 哪些子节点使用 slot,是否需要消费者通过
::slotted样式化? - 测试和集成是否需要 open,还是产品要求 closed?
- 页面会流式发送嵌套组件,还是一次发送完整 root?
- 不支持声明式解析时采用什么降级?
30 秒回答框架
“我会服务端输出带声明式 shadow root 的组件和语义化 light DOM 降级内容,custom element 升级时复用已解析的 root,不重新生成树。公开内容使用 slot,样式放在 root 内;open 或 closed 是集成决策,不是安全控制。通过特性检测处理不支持的浏览器,hydration 保证幂等,事件只绑定一次,测试兼容性、流式顺序、slot、可访问性和升级时机。”
分步作答
第一步:输出语义化 root
服务端输出 host 和声明式 template。保持文本与控件有意义,让响应在脚本前仍可用。工具或集成需要检查时选择 open;只有组件契约需要隐藏引用时才选 closed。
<product-card>
<template shadowrootmode="open">
<style>:host { display: block }</style>
<article><slot name="title"></slot><button>购买</button></article>
</template>
<span slot="title">键盘</span>
</product-card>slot 分配属于公开契约。除非同时控制可访问性语义,不要在 shadow 树和降级内容中重复标题。
第二步:定义升级契约
custom element 注册时先检测是否已有 shadow root,不能再次调用 attachShadow。只初始化一次、只绑定一次事件,并保留服务端节点。若浏览器没有创建 root,再从 template 创建或保留兼容的 light DOM 降级。
第三步:特性检测与降级
用小型解析探针或相关 template 属性检测支持情况。不支持的浏览器仍应收到语义化 light DOM;客户端可以稍后升级,但过渡期间不能隐藏内容。
第四步:保持 slot 与样式
slot 把 light DOM 子节点投影到 shadow 树。文档要说明具名 slot、默认 slot 和 ::slotted 的样式边界。组件样式放在 root 内,通过自定义属性或 parts 暴露刻意的外部接口。
第五步:处理流式与嵌套
流式传输可能先到 host,再到嵌套 slot 子节点或 custom element 定义。把解析顺序当成正常状态:子节点到达后 slot 应更新,升级发生在流结束前后都要安全,不能替换 host 生成第二棵树。
第六步:无重复地接入行为
只绑定一次事件,重复卡片可在 root 层委托。初始化状态放在私有字段或 WeakMap,不要放在消费者能覆盖的公开属性。接入行为时保留表单状态与焦点。
第七步:测试浏览器与可访问性边界
测试支持与不支持的解析器、open 与 closed、一个与多个声明式 root、slot 延迟到达、升级先于或晚于解析、键盘焦点、标签、表单提交和网络中断后的 hydration,断言只有一棵交互树且事件不重复。
示例回答
“我会流式输出语义化 host 和一个声明式 shadow root,用具名 slot 放置公开内容,样式留在 root 内。custom element 升级时先检查已有 root,复用服务端标记而不是替换。open 便于检查,closed 只隐藏引用,并非安全边界。
我会做解析支持检测并保留 light DOM 降级。流式和延迟升级都是正常状态,因此 slot 投影和初始化必须幂等。测试覆盖不支持的浏览器、嵌套 root、晚到的 slot、焦点、表单、可访问性和重复事件。”
常见错误
- 每次升级都调用
attachShadow→ root 或状态丢失 → 复用已解析 root。 - 把 closed 当安全机制 → 只是隐藏引用 → 按封装能力说明。
- 重复降级与 shadow 内容 → 读屏可能播报两次 → 定义唯一可访问来源。
- 假设 slot 一定先到 → 流式子节点模型错误 → 测试晚到投影。
- 用内部选择器样式化 slot 内容 → 规则不能穿越边界 → 使用
::slotted、parts 或自定义属性。 - 每次渲染都绑定事件 → 点击触发多次 → 保证 hydration 幂等。
- 删除 light DOM 降级 → 不支持浏览器出现空白 → 保留语义化服务端标记。
追问
追问 1:声明式 shadow root 一定会附着吗?
不一定。取决于解析器支持和合法 mode。不支持时 template 仍是普通内容,需要降级或客户端升级。
追问 2:为什么每个 host 只有一个 root?
解析器只附着第一个声明式 root,后续 template 留待明确处理,避免竞争 root。
追问 3:何时选择 open?
测试、集成或受控扩展需要 root 引用时选择 open;它不授予或撤销安全权限。
追问 4:流式传输中 slot 如何工作?
host 可以先存在,slot 子节点随后到达,投影会更新;测试两种到达顺序。
追问 5:如何避免 hydration 不匹配?
保持服务端与客户端契约稳定,复用现有节点,让初始化幂等,不生成第二棵树。
追问 6:如何测试 closed root?
通过公开契约测试可见行为、焦点、事件和可访问性,不直接读取 shadowRoot。