代表性面试主题

前端面试:如何设计声明式 Shadow DOM 与渐进式 hydration?

前端困难
Offer.cc 编辑团队发布 更新

题干

服务端渲染的商品卡片必须在 JavaScript 加载前可用,保持样式隔离,并在 hydration 后交互。请设计声明式 Shadow DOM 方案,解释 open 与 closed、slot、降级、升级、流式传输和测试。

题目与范围

页面从服务端流式输出商品卡片。每张卡片要在没有 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。

html
<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

公开来源

同类题目