ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Shadow DOM Slots and Composition in Modern JavaScript Tutorial: Building Composable Web Components

Shadow DOM Slots and Composition in Modern JavaScript Tutorial: Building Composable Web Components 文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载这篇指南来自现代 JavaScript 教程Modern JavaScript Tutorial仓库的 Web Components 章节深入讲解 Shadow DOM 中slot插槽的完整机制命名插槽、默认插槽、插槽回退内容、slotchange事件以及配套的插槽 API。你将掌握如何让自定义元素如custom-menu、user-card接收并渲染 light DOM 传入的内容理解composition组合与flattened DOM扁平化 DOM的真实含义并能在实际项目中写出可复用、可动态更新内容的 Web Components。组合问题的本质组件需要内容很多类型的组件——标签页tabs、菜单menus、图片画廊image galleries等——都需要渲染外部传入的内容。这与浏览器内置的select期望收到option子元素是同一类需求custom-tabs可能期望调用方传入实际的标签内容custom-menu可能期望传入菜单项。使用custom-menu的代码可能长这样custom-menu titleCandy menu/title itemLollipop/item itemFruit Toast/item itemCup Cake/item /custom-menu此时组件应当把它渲染成一个带标题和菜单项的漂亮菜单并处理菜单事件等。那么如何实现一种思路是分析元素内容动态复制、重排 DOM 节点。这可行但存在两个问题如果把元素移动到 shadow DOM 中文档里的 CSS 样式在 shadow DOM 内不生效视觉效果可能丢失而且需要额外编写不少代码。幸运的是我们完全不必这么做Shadow DOM 支持slot元素浏览器会自动用 light DOM 的内容将其填充。这正是本仓库 5-slots-composition 章节 的核心主题。命名插槽Named slots先看一个简单示例。下面的user-card在 shadow DOM 中提供了两个插槽由 light DOM 填充script customElements.define(user-card, class extends HTMLElement { connectedCallback() { this.attachShadow({mode: open}); this.shadowRoot.innerHTML divName: slot nameusername/slot /div divBirthday: slot namebirthday/slot /div ; } }); /script user-card span slotusernameJohn Smith/span span slotbirthday01.01.2001/span /user-card在 shadow DOM 中slot nameX定义了一个插入点insertion point——即带有slotX的元素被渲染的位置。浏览器随后执行 composition组合它从 light DOM 中取出元素把它们渲染到 shadow DOM 对应的插槽里。最终我们就得到了想要的组件——一个可以被数据填充的组件。组合前的 DOM 结构脚本执行后、不考虑组合时的 DOM 结构如下user-card #shadow-root divName: slot nameusername/slot /div divBirthday: slot namebirthday/slot /div span slotusernameJohn Smith/span span slotbirthday01.01.2001/span /user-card我们创建了 shadow DOM所以它出现在#shadow-root之下。此时元素同时拥有 light DOM 和 shadow DOM。渲染时对于 shadow DOM 中的每个slot name...浏览器会在 light DOM 中寻找同名的slot...元素并把它们渲染进插槽内扁平化 DOMflattened DOM组合后的结果被称为 flattened DOM扁平化 DOMuser-card #shadow-root divName: slot nameusername !-- slotted element is inserted into the slot -- span slotusernameJohn Smith/span /slot /div divBirthday: slot namebirthday span slotbirthday01.01.2001/span /slot /div /user-card但要注意扁平化 DOM 只用于渲染和事件处理它是某种意义上的虚拟结构。这是内容的显示方式文档中的节点实际上并没有被移动这一点可以很容易验证——运行querySelectorAll时节点仍在原位// light DOM 的 span 节点仍在原处位于 user-card 之下 alert( document.querySelectorAll(user-card span).length ); // 2所以扁平化 DOM 是由 shadow DOM 通过插入插槽推导而来的。浏览器渲染它并基于它进行样式继承和事件传播后文详述。但 JavaScript 看到的仍然是原样的文档即扁平化之前的结构。注意只有顶层子元素可以带slot...属性slot...属性只对 shadow host本例中的user-card元素的直接子元素有效对嵌套元素会被忽略。例如下面代码中第二个span会被忽略因为它不是user-card的顶层子元素user-card span slotusernameJohn Smith/span div !-- 无效的 slot必须是 user-card 的直接子元素 -- span slotbirthday01.01.2001/span /div /user-card如果 light DOM 中有多个同名 slot 的元素它们会一个接一个地被追加进插槽。例如user-card span slotusernameJohn/span span slotusernameSmith/span /user-card会得到如下扁平化 DOM——slot nameusername中包含两个元素user-card #shadow-root divName: slot nameusername span slotusernameJohn/span span slotusernameSmith/span /slot /div divBirthday: slot namebirthday/slot /div /user-card插槽回退内容Slot fallback content如果在slot内部放入内容它就变成回退默认内容。当 light DOM 中没有对应的填充元素时浏览器会显示它。例如下面这段 shadow DOM 中如果 light DOM 里没有slotusername就会渲染AnonymousdivName: slot nameusernameAnonymous/slot /div默认插槽第一个未命名的插槽shadow DOM 中第一个没有 name 的slot是 default 插槽。它接收 light DOM 中所有未被其他插槽收纳的节点。例如给user-card添加一个默认插槽用来展示用户的所有未分类信息script customElements.define(user-card, class extends HTMLElement { connectedCallback() { this.attachShadow({mode: open}); this.shadowRoot.innerHTML divName: slot nameusername/slot /div divBirthday: slot namebirthday/slot /div fieldset legendOther information/legend slot/slot /fieldset ; } }); /script user-card divI like to swim./div span slotusernameJohn Smith/span span slotbirthday01.01.2001/span div...And play volleyball too!/div /user-card所有未被插槽收纳的 light DOM 内容都会进入 Other information 的 fieldset 中。元素是一个接一个被追加进插槽的所以两条未分类信息会一起待在默认插槽里。对应的扁平化 DOM 如下user-card #shadow-root divName: slot nameusername span slotusernameJohn Smith/span /slot /div divBirthday: slot namebirthday span slotbirthday01.01.2001/span /slot /div fieldset legendOther information/legend slot divI like to swim./div div...And play volleyball too!/div /slot /fieldset /user-card实战案例custom-menu菜单组件现在回到本章开头的custom-menu。我们可以用插槽来分发元素。custom-menu的标记如下custom-menu span slottitleCandy menu/span li slotitemLollipop/li li slotitemFruit Toast/li li slotitemCup Cake/li /custom-menu带有合适插槽的 shadow DOM 模板template idtmpl style /* menu styles */ /style div classmenu slot nametitle/slot ulslot nameitem/slot/ul /div /templatespan slottitle进入slot nametitle。custom-menu中有多个li slotitem但模板里只有一个slot nameitem。因此所有这样的li slotitem会被一个接一个追加到slot nameitem中从而形成列表。扁平化 DOM 变成custom-menu #shadow-root style /* menu styles */ /style div classmenu slot nametitle span slottitleCandy menu/span /slot ul slot nameitem li slotitemLollipop/li li slotitemFruit Toast/li li slotitemCup Cake/li /slot /ul /div /custom-menu细心的读者可能注意到在合法 DOM 中li必须是ul的直接子元素。但这里是扁平化 DOM——它描述的是组件的渲染方式所以这种情况自然发生。接下来只需添加一个click处理器来打开/关闭列表custom-menu就完成了customElements.define(custom-menu, class extends HTMLElement { connectedCallback() { this.attachShadow({mode: open}); // tmpl 是上面的 shadow DOM 模板 this.shadowRoot.append( tmpl.content.cloneNode(true) ); // 我们无法选中 light DOM 的节点所以把点击事件挂在插槽上 this.shadowRoot.querySelector(slot[nametitle]).onclick () { // 打开/关闭菜单 this.shadowRoot.querySelector(.menu).classList.toggle(closed); }; } });仓库中的完整可运行示例位于 menu.view/index.html。该示例展示了插槽在实际组件中的完整用法值得逐行研读模板通过template idtmpl定义内含style与结构化的.menu布局样式中使用::slotted([slottitle])选择器直接为从 light DOM 进入插槽的标题元素设置字号、加粗与光标样式::slotted的完整语义在 Shadow DOM 样式章节 有专门讲解通过::slotted([slottitle])::before添加文件夹图标再配合.closed类实现展开/收起状态切换.closed ul { display: none; }控制列表显隐组件类在connectedCallback()中attachShadow({mode: open})克隆模板内容到 shadowRoot并把点击事件绑定在slot[nametitle]上——因为组件代码无法直接选择 light DOM 的节点把事件挂在插槽上正是关键技巧。template idtmpl style ul { margin: 0; list-style: none; padding-left: 20px; } ::slotted([slottitle]) { font-size: 18px; font-weight: bold; cursor: pointer; } ::slotted([slottitle])::before { content: ; font-size: 14px; } .closed ::slotted([slottitle])::before { content: ; } .closed ul { display: none; } /style div classmenu slot nametitle/slot ulslot nameitem/slot/ul /div /template script customElements.define(custom-menu, class extends HTMLElement { connectedCallback() { this.attachShadow({mode: open}); this.shadowRoot.append( tmpl.content.cloneNode(true) ); this.shadowRoot.querySelector(slot[nametitle]).onclick () { this.shadowRoot.querySelector(.menu).classList.toggle(closed); }; } }); /script custom-menu span slottitleCandy menu/span li slotitemLollipop/li li slotitemFruit Toast/li li slotitemCup Cake/li /custom-menu当然我们还可以给它添加更多功能事件、方法等等。动态更新插槽Updating slots如果外部代码想要动态添加/移除菜单项怎么办浏览器会监视插槽当被插槽收纳的元素被添加/移除时自动更新渲染。此外由于 light DOM 节点并没有被复制而只是被渲染进插槽中节点内部的变化也会立即反映出来。所以我们不需要做任何事来更新渲染。但如果组件代码想要感知插槽变化可以使用slotchange事件。例如下面这个示例菜单项在 1 秒后被动态插入标题在 2 秒后被修改custom-menu idmenu span slottitleCandy menu/span /custom-menu script customElements.define(custom-menu, class extends HTMLElement { connectedCallback() { this.attachShadow({mode: open}); this.shadowRoot.innerHTML div classmenu slot nametitle/slot ulslot nameitem/slot/ul /div; // shadowRoot 不能挂事件处理器所以挂在其第一个子元素上 this.shadowRoot.firstElementChild.addEventListener(slotchange, e alert(slotchange: e.target.name) ); } }); setTimeout(() { menu.insertAdjacentHTML(beforeEnd, li slotitemLollipop/li) }, 1000); setTimeout(() { menu.querySelector([slottitle]).innerHTML New menu; }, 2000); /script菜单渲染每次都会在无需干预的情况下自动更新。这里一共发生了两次slotchange事件初始化时slotchange: title立即触发因为 light DOM 中的slottitle进入了对应的插槽。1 秒后新增li slotitem时slotchange: item触发。请注意2 秒后修改slottitle的内容时没有触发slotchange事件。因为插槽本身没有变化——我们修改的是被插槽收纳元素内部的内容这是另一回事。如果想从 JavaScript 追踪 light DOM 内部的修改可以使用更通用的机制MutationObserver详见 MutationObserver 章节。插槽 APISlot API最后介绍与插槽相关的 JavaScript 方法。如前所述JavaScript 看到的是真实的 DOM不做扁平化。但如果 shadow 树是{mode: open}我们就能弄清哪些元素被分配给了某个插槽反过来也能由元素找到它所在的插槽node.assignedSlot—— 返回该node被分配到的slot元素。slot.assignedNodes({flatten: true/false})—— 分配给该插槽的 DOM 节点。flatten选项默认为false若显式设为true则会更深入地查看扁平化 DOM在嵌套组件的情况下返回嵌套的插槽如果没有节点被分配则返回回退内容。slot.assignedElements({flatten: true/false})—— 分配给该插槽的 DOM 元素与上面相同但只返回元素节点。当我们不仅需要显示插槽内容还需要在 JavaScript 中追踪它时这些方法非常有用。例如如果custom-menu组件想知道自己展示了什么它可以监听slotchange并通过slot.assignedElements获取菜单项custom-menu idmenu span slottitleCandy menu/span li slotitemLollipop/li li slotitemFruit Toast/li /custom-menu script customElements.define(custom-menu, class extends HTMLElement { items [] connectedCallback() { this.attachShadow({mode: open}); this.shadowRoot.innerHTML div classmenu slot nametitle/slot ulslot nameitem/slot/ul /div; // 插槽内容变化时触发 this.shadowRoot.firstElementChild.addEventListener(slotchange, e { let slot e.target; if (slot.name item) { this.items slot.assignedElements().map(elem elem.textContent); alert(Items: this.items); } }); } }); // 1 秒后菜单项更新 setTimeout(() { menu.insertAdjacentHTML(beforeEnd, li slotitemCup Cake/li) }, 1000); /script这段代码把slotchange的event.target即触发事件的插槽与assignedElements()结合起来当 item 插槽内容变化时组件同步维护自己的items数组——这是组件内部数据模型与外部传入内容保持同步的典型模式。总结通常如果元素拥有 shadow DOM那么它的 light DOM 不会被显示。插槽允许我们在 shadow DOM 的指定位置展示来自 light DOM 的元素。插槽有两种类型命名插槽slot nameX.../slot—— 收纳带slotX的 light 子元素。默认插槽第一个没有 name 的slot后续未命名插槽会被忽略—— 收纳未被插槽收纳的 light 子元素。如果同一插槽有多个元素它们会被一个接一个地追加。slot元素的内容用作回退内容当插槽没有对应的 light 子元素时显示。将插槽元素渲染进对应插槽的过程称为 composition组合其结果称为 flattened DOM扁平化 DOM。组合并不会真正移动节点——从 JavaScript 的角度看DOM 保持不变。JavaScript 可以通过以下方法访问插槽slot.assignedNodes/Elements()—— 返回插槽内部的节点/元素。node.assignedSlot—— 反向属性由节点返回其所在的插槽。如果想了解组件正在展示什么可以用以下方式追踪插槽内容slotchange事件 —— 在插槽首次被填充时触发并在被插槽收纳元素发生添加/移除/替换操作时触发但不包括其子元素的变化。触发事件的插槽就是event.target。使用MutationObserver深入插槽内容观察其内部变化见 MutationObserver 章节。了解了如何在 shadow DOM 中展示 light DOM 元素后下一步就是如何正确地给它们设置样式。基本规则是shadow 元素在内部设置样式light 元素在外部设置样式但也有值得注意的例外——:host、:host(selector)、::slotted(selector)以及穿透 Shadow DOM 的 CSS 自定义属性等细节详见 Shadow DOM 样式章节。如果你希望进一步夯实基础建议顺带阅读同一 Web Components 系列中的 Shadow DOM 入门章节了解 shadow tree 与 light tree 的基本概念以及 Template 元素章节了解template在组件模板复用中的作用它们与插槽组合一起构成了完整的 Web Components 实现路径。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐MoE架构深度解析128专家与8激活的智能路由MoE架构深度解析128专家与8激活的智能路由 混合专家模型MoE通过将复杂计算任务分配给多个专门网络处理显著提升计算效率。在Qwen3 235B A2教程前端上一篇FlatBuffers 编译器 flatc 完全指南命令行用法、生成器选项与数据转换实战下一篇9Router CLI 完整指南安装、启动参数与终端管理实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表