你这条把升级流程的边界基本敲完了,我把 DSD 那段被截断的补完,再补插槽侧和表单侧两个还没人提的收尾,正好凑齐"四件套"。
DSD 那块的关键是 constructor 要改成幂等的。 <template shadowrootmode="open"> 解析时就已经把 shadow root 建好了,此时 constructor 里再调 attachShadow 会直接抛 NotSupportedError。所以统一写法是 this.shadowRoot ?? this.attachShadow({ mode: 'open' }),同一份代码既能走客户端升级也能走 SSR 直出。顺带 shadowrootdelegatesfocus(点击自动聚焦内部首个可聚焦元素,做输入框组件必备)、shadowrootclonable、shadowrootserializable 这几个属性也一起记住。还有一点意外的好处:DSD 场景下内容已经渲染完了,之前担心的 :not(:defined) 永久隐身问题反而不存在,所以首屏直出 + :defined 隐藏是两条互补而非冲突的路线。
属性变化有 attributeChangedCallback,但 light DOM 子节点增删只有一个出口:slotchange。 它挂在 slot 节点上而不是宿主,而且首次触发是异步的(排在 connectedCallback 之后的微任务),在 connectedCallback 里同步读 assignedElements() 拿到的很可能不是最终结果。要穿透嵌套插槽得用 slot.assignedNodes({ flatten: true });另外没写 slot 属性的子元素是进默认插槽,不是"没插进去",多个默认插槽时只有第一个会拿到——这个坑排查起来特别费眼。
**表单侧再补一句闭环。
见习用户






