使用 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 模式:

firstUpdated() {
this.staticNode = this.renderRoot.querySelector('#static-node');
}

get _closeButton() {
return this.renderRoot.querySelector('#close-button');
}

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

@query、@queryAll 和 @queryAsync 装饰器

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

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

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

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

import {LitElement, html} from 'lit';
import {query} from 'lit/decorators/query.js';

class MyElement extends LitElement {
@query('#first')
_first;

render() {
return html`
<div id="first"></div>
<div id="second"></div>
`;
}
}

这个装饰器等同于:

get _first() {
return this.renderRoot?.querySelector('#first') ?? null;
}

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

import {LitElement, html} from 'lit';
import {queryAll} from 'lit/decorators/queryAll.js';

class MyElement extends LitElement {
@queryAll('div')
_divs;

render() {
return html`
<div id="first"></div>
<div id="second"></div>
`;
}
}

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

@queryAll('button')
_buttons!: NodeListOf<HTMLButtonElement>

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

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

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

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

<my-element>
<p>一个子元素</p>
</my-element>

默认情况下,如果一个元素有 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> 中。

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

<slot>我是回退内容</slot>

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

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

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

get _slottedChildren() {
const slot = this.shadowRoot.querySelector('slot');
return slot.assignedElements({flatten: true});
}

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

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

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

handleSlotchange(e) {
const childNodes = e.target.assignedNodes({flatten: true});
// ... 对 childNodes 做一些处理 ...
this.allText = childNodes.map((node) => {
return node.textContent ? node.textContent : ''
}).join('');
}

render() {
return html`<slot @slotchange=${this.handleSlotchange}></slot>`;
}

更多信息,请参见 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 这样的编译器来使用装饰器。详情请参见 使用装饰器

@queryAssignedElements({slot: 'list', selector: '.item'})
_listItems!: Array<HTMLElement>;

@queryAssignedNodes({slot: 'header', flatten: true})
_headerNodes!: Array<Node>;

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

get _listItems() {
const slot = this.shadowRoot.querySelector('slot[name=list]');
return slot.assignedElements().filter((node) => node.matches('.item'));
}

get _headerNodes() {
const slot = this.shadowRoot.querySelector('slot[name=header]');
return slot.assignedNodes({flatten: true});
}

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

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

<my-element>
#shadow-root
<p>子元素 1</p>
<p>子元素 2</p>

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

  • 设置 shadowRootOptions
  • 实现 createRenderRoot 方法。

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

class DelegatesFocus extends LitElement {
static shadowRootOptions = {...LitElement.shadowRootOptions, delegatesFocus: true};
}

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

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

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

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

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