你正在查看 Lit 的旧版本文档。点击 这里查看最新版本。

使用 Shadow DOM

Lit 组件使用 Shadow DOM 来封装其 DOM。Shadow DOM 提供了一种方式来为元素添加一个独立的、隔离的 DOM 树。DOM 封装是在页面上与其他任何代码(包括其他 Web 组件或 Lit 组件)实现互操作的关键。

Shadow DOM 提供三个好处:

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

有关 Shadow DOM 的更多信息:

旧版浏览器。 在不支持原生 Shadow DOM 的旧版浏览器上,可以使用 Web 组件 polyfill。请注意,Lit 的 polyfill-support 模块必须与 Web 组件 polyfill 一起加载。详见旧版浏览器要求

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

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

你可以在组件初始渲染后(例如在 firstUpdated 中)查询内部 DOM,或使用 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 树,它的子元素根本不会渲染。

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

要渲染元素的子元素,在元素的模板中为它们创建一个 <slot>。子元素不会在 DOM 树中被_移动_,但它们会_像_是 <slot> 的子元素一样被渲染。例如:

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

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

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

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

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

你可以为 slot 指定后备内容。当没有子元素被分配到 slot 时,会显示后备内容。

<slot>我是后备内容</slot>

渲染后备内容。 如果任何子节点被分配到 slot,其后备内容不会渲染。没有名称的默认 slot 接受任何子节点。即使分配的唯一节点是包含空白的文本节点(例如 <example-element> </example-element>),它也不会渲染后备内容。使用 Lit 表达式作为自定义元素的子元素时,确保在适当时使用非渲染值,以便渲染任何 slot 后备内容。详见移除子内容

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

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

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

你也可以使用 slotchange 事件在已分配节点发生变化时采取行动。 以下示例提取所有 slotted 子元素的文本内容。

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,分别返回在组件 shadow 树中的给定 slot 上调用 slot.assignedElementsslot.assignedNodes 的结果。 使用这些来查询分配给给定 slot 的元素或节点。

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

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

决定使用哪个装饰器取决于你是要查询分配给 slot 的文本节点,还是仅查询元素节点。这个决定取决于你的具体用例。

使用装饰器。 装饰器是一个 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>child 1</p>
<p>child 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 中。