当前位置: 首页 > news >正文

深入解析Web Components:Shadow DOM实战指南

Web Components:使用Shadow DOM

Web Components不仅仅是自定义元素。Shadow DOM、HTML模板和自定义元素各自扮演着重要角色。本文中,Russell Beswick展示了Shadow DOM在整体架构中的位置,解释了其重要性、适用场景及有效应用方法。

常见做法是将Web Components直接与框架组件进行比较。但大多数示例实际上特定于自定义元素,这只是Web Components的一部分。人们容易忘记Web Components实际上是一组可独立使用的Web平台API:

  • 自定义元素
  • HTML模板
  • Shadow DOM

换句话说,可以不使用Shadow DOM或HTML模板创建自定义元素,但结合这些功能可以增强稳定性、可重用性、可维护性和安全性。它们是同一功能集的组成部分,可以单独或一起使用。

为什么存在Shadow DOM

大多数现代Web应用由来自不同提供商的库和组件组成。在传统(或“轻量”)DOM中,样式和脚本很容易相互泄漏或冲突。如果使用框架,可能相信所有内容都已编写为无缝协作,但仍需努力确保所有元素具有唯一ID,并且CSS规则尽可能具体地限定范围。这可能导致代码过于冗长,既增加应用加载时间,又降低可维护性。

<!-- div soup -->
<div id="my-custom-app-framework-landingpage-header" class="my-custom-app-framework-foo"><div><div><div><div><div><div>etc...</div></div></div></div></div></div>
</div>

Shadow DOM通过提供隔离每个组件的方法来解决这些问题。<video><details>元素是默认使用Shadow DOM防止全局样式或脚本干扰的本机HTML元素的好例子。利用驱动本机浏览器组件的这种隐藏能力,是Web Components与框架对应物的真正区别。

可托管Shadow Root的元素

最常见的是,影子根与自定义元素关联。但它们也可以与任何HTMLUnknownElement一起使用,并且许多标准元素也支持它们,包括:

  • <aside>
  • <blockquote>
  • <body>
  • <div>
  • <footer>
  • <h1><h6>
  • <header>
  • <main>
  • <nav>
  • <p>
  • <section>
  • <span>

每个元素只能有一个影子根。一些元素,包括<input><select>,已经有一个内置的影子根,无法通过脚本访问。可以通过在开发者工具中启用“显示用户代理Shadow DOM”设置来检查它们,该设置默认“关闭”。

创建Shadow Root

在利用Shadow DOM的好处之前,首先需要在元素上建立影子根。这可以通过命令式或声明式实例化。

命令式实例化

要使用JavaScript创建影子根,请在元素上使用attachShadow({ mode })。模式可以是open(允许通过element.shadowRoot访问)或closed(对外部脚本隐藏影子根)。

const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>Hello from the Shadow DOM!</p>';
document.body.appendChild(host);

在此示例中,我们建立了一个开放的影子根。这意味着元素的内容可以从外部访问,我们可以像查询任何其他DOM节点一样查询它:

host.shadowRoot.querySelector('p'); // 选择段落元素

如果我们想完全防止外部脚本访问我们的内部结构,可以将模式设置为closed。这导致元素的shadowRoot属性返回null。我们仍然可以从创建它的作用域中的影子引用访问它。

shadow.querySelector('p');

这是一个关键的安全功能。使用封闭的影子根,我们可以确信恶意行为者无法从我们的组件中提取私人用户数据。例如,考虑一个显示银行信息的小部件。可能包含用户的帐号。使用开放的影子根,页面上的任何脚本都可以深入我们的组件并解析其内容。在封闭模式下,只有用户可以通过手动复制粘贴或检查元素来执行此类操作。

建议在使用Shadow DOM时采用封闭优先的方法。养成使用封闭模式的习惯,除非正在调试,或仅在无法避免实际限制时绝对必要。如果遵循此方法,会发现实际上需要开放模式的情况很少。

声明式实例化

我们不必使用JavaScript来利用Shadow DOM。可以声明式注册影子根。在任何受支持的元素内嵌套带有shadowrootmode属性的<template>将导致浏览器自动升级该元素带有影子根。以这种方式附加影子根甚至可以在禁用JavaScript的情况下完成。

<my-widget><template shadowrootmode="closed"><p> Declarative Shadow DOM content </p></template>
</my-widget>

同样,这可以是开放的或封闭的。在使用开放模式之前考虑安全影响,但请注意,除非此方法与注册的自定义元素一起使用,否则无法通过任何脚本访问封闭模式内容,在这种情况下,可以使用ElementInternals访问自动附加的影子根:

class MyWidget extends HTMLElement {#internals;#shadowRoot;constructor() {super();this.#internals = this.attachInternals();this.#shadowRoot = this.#internals.shadowRoot;}connectedCallback() {const p = this.#shadowRoot.querySelector('p')console.log(p.textContent); // 这有效}
};
customElements.define('my-widget', MyWidget);
export { MyWidget };

Shadow DOM配置

除了模式之外,我们还可以向Element.attachShadow()传递三个其他选项。

选项1:clonable:true

直到最近,如果标准元素附加了影子根,并尝试使用Node.cloneNode(true)document.importNode(node,true)克隆它,只会得到宿主元素的浅拷贝,而没有影子根内容。我们刚看的示例实际上会返回一个空的<div>。这对于在内部构建自己的影子根的自定义元素从来不是问题。

但对于声明式Shadow DOM,这意味着每个元素都需要自己的模板,并且它们不能被重用。通过这个新添加的功能,可以在需要时有选择地克隆组件:

<div id="original"><template shadowrootmode="closed" shadowrootclonable><p> This is a test  </p></template>
</div><script>const original = document.getElementById('original');const copy = original.cloneNode(true); copy.id = 'copy';document.body.append(copy); // 包括影子根内容
</script>

选项2:serializable:true

启用此选项允许保存元素影子根内内容的字符串表示。在宿主元素上调用Element.getHTML()将返回Shadow DOM当前状态的模板副本,包括所有嵌套的shadowrootserializable实例。这可用于将影子根的副本注入另一个宿主,或缓存以供以后使用。

在Chrome中,这实际上通过封闭的影子根工作,因此要小心意外泄漏用户数据。更安全的替代方法是使用封闭包装器屏蔽内部内容免受外部影响,同时在内部保持开放:

<wrapper-element></wrapper-element><script>class WrapperElement extends HTMLElement {#shadow;constructor() {super();this.#shadow = this.attachShadow({ mode:'closed' });this.#shadow.setHTMLUnsafe(`<nested-element><template shadowrootmode="open" shadowrootserializable><div id="test"><template shadowrootmode="open" shadowrootserializable><p> Deep Shadow DOM Content </p></template></div></template></nested-element>`);this.cloneContent();}cloneContent() {const nested = this.#shadow.querySelector('nested-element');const snapshot = nested.getHTML({ serializableShadowRoots: true });const temp = document.createElement('div');temp.setHTMLUnsafe(`<another-element>${snapshot}</another-element>`);const copy = temp.querySelector('another-element');copy.shadowRoot.querySelector('#test').shadowRoot.querySelector('p').textContent = 'Changed Content!';this.#shadow.append(copy);}}customElements.define('wrapper-element', WrapperElement);const wrapper = document.querySelector('wrapper-element');const test = wrapper.getHTML({ serializableShadowRoots: true });console.log(test); // 由于封闭的影子根,空字符串
</script>

注意setHTMLUnsafe()。这是因为内容包含<template>元素。注入这种性质的可信内容时必须调用此方法。使用innerHTML插入模板不会触发自动初始化为影子根。

选项3:delegatesFocus:true

此选项本质上使我们的宿主元素充当其内部内容的<label>。启用后,单击宿主上的任何位置或对其调用.focus()将把光标移动到影子根中的第一个可聚焦元素。这还将:focus伪类应用于宿主,这在创建旨在参与表单的组件时特别有用。

<custom-input><template shadowrootmode="closed" shadowrootdelegatesfocus><fieldset><legend> Custom Input </legend><p> Click anywhere on this element to focus the input </p><input type="text" placeholder="Enter some text..."></fieldset></template>
</custom-input>

此示例仅演示焦点委托。封装的一个奇怪之处是表单提交不会自动连接。这意味着默认情况下,输入的值不会在表单提交中。表单验证和状态也不会从Shadow DOM中传达出来。可访问性存在类似的连接问题,影子根边界可能会干扰ARIA。这些都是特定于表单的考虑因素,我们可以用ElementInternals解决,这是另一篇文章的主题,并且有理由质疑是否可以依赖轻量DOM表单。

插槽内容

到目前为止,我们只看了完全封装的组件。一个关键的Shadow DOM功能是使用插槽选择性地将内容注入组件的内部结构。每个影子根可以有一个默认(未命名)<slot>;所有其他必须命名。命名插槽允许我们提供内容以填充组件的特定部分,以及回退内容以填充用户省略的任何插槽:

<my-widget><template shadowrootmode="closed"><h2><slot name="title"><span>Fallback Title</span></slot></h2><slot name="description"><p>A placeholder description.</p></slot><ol><slot></slot></ol></template><span slot="title"> A Slotted Title</span><p slot="description">An example of using slots to fill parts of a component.</p><li>Foo</li><li>Bar</li><li>Baz</li>
</my-widget>

默认插槽也支持回退内容,但任何杂散文本节点都会填充它们。因此,这仅在折叠宿主元素标记中的所有空白时才有效:

<my-widget><template shadowrootmode="closed"><slot><span>Fallback Content</span></slot>
</template></my-widget>

当添加或删除assignedNodes()时,插槽元素发出slotchange事件。这些事件不包含对插槽或节点的引用,因此需要将它们传递到事件处理程序中:

class SlottedWidget extends HTMLElement {#internals;#shadow;constructor() {super();this.#internals = this.attachInternals();this.#shadow = this.#internals.shadowRoot;this.configureSlots();}configureSlots() {const slots = this.#shadow.querySelectorAll('slot');console.log({ slots });slots.forEach(slot => {slot.addEventListener('slotchange', () => {console.log({changedSlot: slot.name || 'default',assignedNodes: slot.assignedNodes()});});});}
}
customElements.define('slotted-widget', SlottedWidget);

多个元素可以分配给单个插槽,可以通过slot属性声明式或通过脚本:

const widget = document.querySelector('slotted-widget');
const added = document.createElement('p');
added.textContent = 'A secondary paragraph added using a named slot.';
added.slot = 'description';
widget.append(added);

注意此示例中的段落附加到宿主元素。插槽内容实际上属于“轻量”DOM,而不是Shadow DOM。与到目前为止涵盖的示例不同,这些元素可以直接从文档对象查询:

const widgetTitle = document.querySelector('my-widget [slot=title]');
widgetTitle.textContent = 'A Different Title';

如果想从类定义内部访问这些元素,请使用this.childrenthis.querySelector。只有<slot>元素本身可以通过Shadow DOM查询,而不是它们的内容。

从神秘到掌握

现在知道为什么要使用Shadow DOM,何时应将其纳入工作,以及如何立即使用它。

但Web Components之旅不能在这里结束。本文仅涵盖了标记和脚本。我们甚至没有触及Web Components的另一个主要方面:样式封装。这将是另一篇文章的主题。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码

http://www.wxhsa.cn/company.asp?id=47

相关文章:

  • HCIP回顾— BGP基础
  • 你的测试又慢又不可靠-因为你测错了东西
  • 你的错误处理一团糟-是时候修复它了-️
  • 物理焦距、像素焦距、像元与相机内参(fx, fy)的意义与作用
  • 实时通信的头痛-问题不在WebSocket而是你的框架
  • 文件不只是数据-一份稳健的文件处理指南
  • vue+websocket+Stomp组件实现前端长连接
  • java课前问题列表
  • 多字段排序工具类,支持树形
  • 鸿蒙 HAP 包处理全攻略:从解包到签名,So 库加固一步到位
  • 关于vue在PC端,rem对不同屏幕进行适配
  • GreatSQL分页查询优化案例实战
  • 技术面:Java并发(线程同步、死锁、多线程编排)
  • vue3中两对容易搞混的概念
  • LoadRunner 对 WebTours 实现订票的性能分析
  • mac一键关闭chrome自动更新
  • Python游戏开发:使用Pygame库的全面教程
  • 同城黑卡小程序系统介绍
  • 限行提醒小程序介绍
  • 365 快乐农场小程序介绍
  • AP聚类算法实现三维数据点分类
  • 政务预约系统介绍
  • 23Java基础之File
  • 猜灯谜赢大奖系统介绍
  • Linux GNU 工具集详解
  • 基于MATLAB的多输入多输出空时分组码通信系统仿真
  • 国产DevOps工具链崛起:Gitee如何重塑企业研发效能版图
  • docker部署ruoyi-cloud验证码问题记录
  • 【初赛】ip地址 - Slayer
  • 【初赛】反码 补码 原码 - Slayer