使用 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 的更多信息:
- Web Fundamentals 上的 Shadow DOM v1: Self-Contained Web Components。
- MDN 上的 使用 shadow DOM。
旧版浏览器。 在不支持原生 shadow DOM 的旧版浏览器中,可以使用 web components polyfills。请注意,Lit 的 polyfill-support
模块必须与 web components polyfills 一起加载。详情请参见 旧版浏览器的要求。
访问 shadow DOM 中的节点
Permalink to "访问 shadow DOM 中的节点"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 这样的编译器来使用装饰器。详情请参见 使用装饰器。
@query
Permalink to "@query"修改类属性,将其转换为从渲染根返回节点的 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;
}
@queryAll
Permalink to "@queryAll"与 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
视为始终被定义,永远不会是 null
或 undefined
。
@queryAsync
Permalink to "@queryAsync"类似于 @query
,但不是直接返回节点,而是返回一个 Promise
,该 Promise 在任何待处理的元素渲染完成后解析为该节点。代码可以使用这个而不是等待 updateComplete
promise。
这在某些情况下很有用,例如,如果由 @queryAsync
返回的节点可能因另一个属性更改而改变。
使用插槽渲染子元素
Permalink to "使用插槽渲染子元素"你的组件可以接受子元素(就像 <ul>
元素可以有 <li>
子元素一样)。
<my-element>
<p>一个子元素</p>
</my-element>
默认情况下,如果一个元素有 shadow tree,它的子元素根本不会渲染。
要渲染子元素,你的模板需要包含一个或多个 <slot>
元素,它们作为子节点的占位符。
使用 slot 元素
Permalink to "使用 slot 元素"要渲染元素的子元素,在元素的模板中为它们创建一个 <slot>
。子元素在 DOM 树中并没有被移动,但它们被渲染得好像它们是 <slot>
的子元素。例如:
使用命名插槽
Permalink to "使用命名插槽"要将子元素分配给特定的插槽,确保子元素的 slot
属性与插槽的 name
属性匹配:
命名插槽只接受具有匹配
slot
属性的子元素。例如,
<slot name="one"></slot>
只接受具有属性slot="one"
的子元素。具有
slot
属性的子元素只会在具有匹配name
属性的插槽中渲染。例如,
<p slot="one">...</p>
只会放在<slot name="one"></slot>
中。
指定插槽回退内容
Permalink to "指定插槽回退内容"你可以为插槽指定回退内容。当没有子元素分配给插槽时,会显示回退内容。
<slot>我是回退内容</slot>
渲染回退内容。 如果有任何子节点分配给插槽,其回退内容就不会渲染。没有名称的默认插槽接受任何子节点。即使唯一分配的节点是包含空白的文本节点,它也不会渲染回退内容,例如 <example-element> </example-element>
。当使用 Lit 表达式作为自定义元素的子元素时,确保在适当时使用非渲染值,以便渲染任何插槽回退内容。更多信息请参见 移除子内容。
访问插槽的子元素
Permalink to "访问插槽的子元素"要访问 shadow root 中插槽分配的子元素,你可以使用标准的 slot.assignedNodes
或 slot.assignedElements
方法和 slotchange
事件。
例如,你可以创建一个 getter 来访问特定插槽的分配元素:
get _slottedChildren() {
const slot = this.shadowRoot.querySelector('slot');
return slot.assignedElements({flatten: true});
}
元素只在插槽渲染后才会被分配。
如果你需要在启动时访问分配的元素,你需要等待 firstUpdated
或 updated
。如果你想在渲染更改时访问分配的元素,你可以使用 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.assignedElements
或 slot.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});
}
自定义渲染根
Permalink to "自定义渲染根"每个 Lit 组件都有一个渲染根——一个作为其内部 DOM 容器的 DOM 节点。
默认情况下,LitElement 创建一个开放的 shadowRoot
并在其中渲染,产生以下 DOM 结构:
<my-element>
#shadow-root
<p>子元素 1</p>
<p>子元素 2</p>
有两种方式可以自定义 LitElement 使用的渲染根:
- 设置
shadowRootOptions
。 - 实现
createRenderRoot
方法。
设置 shadowRootOptions
Permalink to "设置 shadowRootOptions"自定义渲染根最简单的方法是设置 shadowRootOptions
静态属性。createRenderRoot
的默认实现在创建组件的 shadow root 时将 shadowRootOptions
作为选项参数传递给 attachShadow
。它可以设置为自定义 ShadowRootInit 字典中允许的任何选项,例如 mode
和 delegatesFocus
。
class DelegatesFocus extends LitElement {
static shadowRootOptions = {...LitElement.shadowRootOptions, delegatesFocus: true};
}
更多信息请参见 MDN 上的 Element.attachShadow()。
实现 createRenderRoot
Permalink to "实现 createRenderRoot"createRenderRoot
的默认实现创建一个开放的 shadow root,并向其添加在 static styles
类字段中设置的任何样式。关于样式的更多信息,请参见样式。
要自定义组件的渲染根,实现 createRenderRoot
并返回你想要模板渲染到的节点。
例如,要将模板渲染到主 DOM 树中作为你的元素的子元素,实现 createRenderRoot
并返回 this
。
渲染到子元素。 渲染到子元素而不是 shadow DOM 通常不推荐。你的元素将无法访问 DOM 或样式作用域,并且它将无法将元素组合到其内部 DOM 中。