使用 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 的更多信息:
- Web Fundamentals 上的 Shadow DOM v1:自包含的 Web 组件。
- MDN 上的使用 Shadow DOM。
旧版浏览器。 在不支持原生 Shadow DOM 的旧版浏览器上,可以使用 Web 组件 polyfill。请注意,Lit 的 polyfill-support 模块必须与 Web 组件 polyfill 一起加载。详见旧版浏览器要求。
访问 Shadow DOM 中的节点
Permalink to "访问 Shadow DOM 中的节点"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 这样的编译器来使用装饰器。详见使用装饰器。
@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 返回的节点可能因另一个属性变化而改变,这很有用。
使用 slot 渲染子元素
Permalink to "使用 slot 渲染子元素"你的组件可能接受子元素(就像 <ul> 元素可以有 <li> 子元素一样)。
<my-element> <p>一个子元素</p></my-element> 默认情况下,如果元素有 shadow 树,它的子元素根本不会渲染。
要渲染子元素,你的模板需要包含一个或多个 <slot> 元素,它们作为子节点的占位符。
使用 slot 元素
Permalink to "使用 slot 元素"要渲染元素的子元素,在元素的模板中为它们创建一个 <slot>。子元素不会在 DOM 树中被_移动_,但它们会_像_是 <slot> 的子元素一样被渲染。例如:
使用命名 slot
Permalink to "使用命名 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 后备内容
Permalink to "指定 slot 后备内容"你可以为 slot 指定后备内容。当没有子元素被分配到 slot 时,会显示后备内容。
<slot>我是后备内容</slot> 渲染后备内容。 如果任何子节点被分配到 slot,其后备内容不会渲染。没有名称的默认 slot 接受任何子节点。即使分配的唯一节点是包含空白的文本节点(例如 <example-element> </example-element>),它也不会渲染后备内容。使用 Lit 表达式作为自定义元素的子元素时,确保在适当时使用非渲染值,以便渲染任何 slot 后备内容。详见移除子内容。
访问 slotted 子元素
Permalink to "访问 slotted 子元素"要访问分配到 shadow root 中 slot 的子元素,你可以使用标准的 slot.assignedNodes 或 slot.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.assignedElements 或 slot.assignedNodes 的结果。 使用这些来查询分配给给定 slot 的元素或节点。
两者都接受一个带有以下属性的可选对象:
| 属性 | 描述 |
|---|---|
flatten | 布尔值,指定是否通过将任何子 <slot> 元素替换为其已分配节点来展平已分配节点。 |
slot | Slot 名称,指定要查询的 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});} 自定义渲染根
Permalink to "自定义渲染根"每个 Lit 组件都有一个渲染根——一个作为其内部 DOM 容器的 DOM 节点。
默认情况下,LitElement 创建一个开放的 shadowRoot 并在其中渲染,产生以下 DOM 结构:
<my-element> #shadow-root <p>child 1</p> <p>child 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 中。