使用 Shadow DOM

Lit 组件使用 shadow DOM 来封装它们的 DOM。Shadow DOM 提供了一种为元素添加独立且封装的 DOM 树的方法。DOM 封装是实现与页面上运行的任何其他代码(包括其他 Web Components 或 Lit 组件)互操作性的关键。

Shadow DOM 提供三个好处:

  • DOM 作用域。像 document.querySelector 这样的 DOM API 不会在组件的 shadow DOM 中找到元素,这使得全局脚本更难意外破坏你的组件。
  • 样式作用域。你可以为你的 shadow DOM 编写封装的样式,这些样式不会影响 DOM 树的其他部分。
  • 组合。组件的 shadow root(包含其内部 DOM)与组件的子元素是分开的。你可以选择如何在组件的内部 DOM 中渲染子元素。

关于 shadow DOM 的更多信息:

旧版浏览器。 在不支持原生 shadow DOM 的旧版浏览器中,可以使用 web components polyfills。请注意,Lit 的 polyfill-support 模块必须与 web components polyfills 一起加载。详情请参见 旧版浏览器的要求

Lit 将组件渲染到其 renderRoot,默认情况下这是一个 shadow root。要查找内部元素,你可以使用 DOM 查询 API,比如 this.renderRoot.querySelector()

renderRoot 应该始终是 shadow root 或元素,它们共享像 .querySelectorAll().children 这样的 API。

你可以在组件初始渲染后查询内部 DOM(例如,在 firstUpdated 中),或使用 getter 模式:

LitElement 提供了一组装饰器,提供了定义这样的 getter 的简写方式。

@query、@queryAll 和 @queryAsync 装饰器

Permalink to "@query、@queryAll 和 @queryAsync 装饰器"

@query@queryAll@queryAsync 装饰器都提供了一种方便的方式来访问内部组件 DOM 中的节点。

使用装饰器。 装饰器是一个提议的 JavaScript 特性,所以你需要使用像 Babel 或 TypeScript 这样的编译器来使用装饰器。详情请参见 使用装饰器

修改类属性,将其转换为从渲染根返回节点的 getter。当可选的第二个参数为 true 时,只执行一次 DOM 查询并缓存结果。这可以用作性能优化,适用于被查询的节点不会改变的情况。

这个装饰器等同于:

query 相同,只是它返回所有匹配的节点,而不是单个节点。它相当于调用 querySelectorAll

这里,_divs 会返回模板中的两个 <div> 元素。对于 TypeScript,@queryAll 属性的类型是 NodeListOf<HTMLElement>。如果你确切知道你将检索什么类型的节点,类型可以更具体:

buttons 后面的感叹号(!)是 TypeScript 的非空断言运算符。它告诉编译器将 buttons 视为始终被定义,永远不会是 nullundefined

类似于 @query,但不是直接返回节点,而是返回一个 Promise,该 Promise 在任何待处理的元素渲染完成后解析为该节点。代码可以使用这个而不是等待 updateComplete promise。

这在某些情况下很有用,例如,如果由 @queryAsync 返回的节点可能因另一个属性更改而改变。

你的组件可以接受子元素(就像 <ul> 元素可以有 <li> 子元素一样)。

默认情况下,如果一个元素有 shadow tree,它的子元素根本不会渲染。

要渲染子元素,你的模板需要包含一个或多个 <slot> 元素,它们作为子节点的占位符。

要渲染元素的子元素,在元素的模板中为它们创建一个 <slot>。子元素在 DOM 树中并没有被移动,但它们被渲染得好像它们是 <slot> 的子元素。例如:

要将子元素分配给特定的插槽,确保子元素的 slot 属性与插槽的 name 属性匹配:

  • 命名插槽只接受具有匹配 slot 属性的子元素。

    例如,<slot name="one"></slot> 只接受具有属性 slot="one" 的子元素。

  • 具有 slot 属性的子元素只会在具有匹配 name 属性的插槽中渲染。

    例如,<p slot="one">...</p> 只会放在 <slot name="one"></slot> 中。

你可以为插槽指定回退内容。当没有子元素分配给插槽时,会显示回退内容。

渲染回退内容。 如果有任何子节点分配给插槽,其回退内容就不会渲染。没有名称的默认插槽接受任何子节点。即使唯一分配的节点是包含空白的文本节点,它也不会渲染回退内容,例如 <example-element> </example-element>。当使用 Lit 表达式作为自定义元素的子元素时,确保在适当时使用非渲染值,以便渲染任何插槽回退内容。更多信息请参见 移除子内容

要访问 shadow root 中插槽分配的子元素,你可以使用标准的 slot.assignedNodesslot.assignedElements 方法和 slotchange 事件。

例如,你可以创建一个 getter 来访问特定插槽的分配元素:

元素只在插槽渲染后才会被分配。

如果你需要在启动时访问分配的元素,你需要等待 firstUpdatedupdated。如果你想在渲染更改时访问分配的元素,你可以使用 slotchange

你可以使用 slotchange 事件在节点首次分配或更改时采取行动。 以下示例提取所有插槽子元素的文本内容。

更多信息,请参见 MDN 上的 HTMLSlotElement

@queryAssignedElements 和 @queryAssignedNodes 装饰器

Permalink to "@queryAssignedElements 和 @queryAssignedNodes 装饰器"

@queryAssignedElements@queryAssignedNodes 将类属性转换为 getter,该 getter 返回在组件的 shadow tree 中对给定插槽调用 slot.assignedElementsslot.assignedNodes 的结果。 使用这些来查询分配给给定插槽的元素或节点。

两者都接受一个可选对象,具有以下属性:

属性描述
flatten布尔值,指定是否通过将任何子 <slot> 元素替换为其分配的节点来扁平化分配的节点。
slot指定要查询的插槽的插槽名称。未定义则选择默认插槽。
selector (仅 queryAssignedElements如果指定,只返回匹配此 CSS 选择器的分配元素。

选择使用哪个装饰器取决于你是想查询分配给插槽的文本节点,还是只查询元素节点。这个决定取决于你的具体用例。

使用装饰器。 装饰器是一个提议的 JavaScript 特性,所以你需要使用像 Babel 或 TypeScript 这样的编译器来使用装饰器。详情请参见 使用装饰器

上面的示例等同于以下代码:

每个 Lit 组件都有一个渲染根——一个作为其内部 DOM 容器的 DOM 节点。

默认情况下,LitElement 创建一个开放的 shadowRoot 并在其中渲染,产生以下 DOM 结构:

有两种方式可以自定义 LitElement 使用的渲染根:

  • 设置 shadowRootOptions
  • 实现 createRenderRoot 方法。

自定义渲染根最简单的方法是设置 shadowRootOptions 静态属性。createRenderRoot 的默认实现在创建组件的 shadow root 时将 shadowRootOptions 作为选项参数传递给 attachShadow。它可以设置为自定义 ShadowRootInit 字典中允许的任何选项,例如 modedelegatesFocus

更多信息请参见 MDN 上的 Element.attachShadow()

createRenderRoot 的默认实现创建一个开放的 shadow root,并向其添加在 static styles 类字段中设置的任何样式。关于样式的更多信息,请参见样式

要自定义组件的渲染根,实现 createRenderRoot 并返回你想要模板渲染到的节点。

例如,要将模板渲染到主 DOM 树中作为你的元素的子元素,实现 createRenderRoot 并返回 this

渲染到子元素。 渲染到子元素而不是 shadow DOM 通常不推荐。你的元素将无法访问 DOM 或样式作用域,并且它将无法将元素组合到其内部 DOM 中。