哈Ha!最近,我注意到许多文章批评各种Web组件。有时,这种批评是非常严厉的,甚至带有仇恨的气息。我认为,这里的主要问题是在开发人员社区中缺乏建立与该组标准一起使用的惯例。许多熟悉的模型并不总是有机地融入涉及“ 自定义元素”和“ Shadow DOM”的项目中,许多事情必须从一个新的角度来看,而不是每个人都喜欢。我已经成功地使用了Web组件多年,甚至开发了自己的库基于它们,所以我认为这种情况不是很公平。我会尽我所能,至少部分地修复它。我决定制作一系列紧凑的出版物,我计划在每一个紧凑出版物中提及批评的频繁方面之一,并展示一些尚未决定他应该站在路障哪一侧的人们可能会感兴趣的技术技巧。今天,我想谈谈如何在没有Shadow DOM的情况下创建组件。做什么的?
我这次要传达的主要思想是Web组件和Shadow DOM 不是同一件事。使用Shadow DOM时,有两个主要好处:- 文档的孤立部分,您的样式可以免受外界影响和“漏水”
- 一种组合机制,可让您将文档划分为组件本身及其内容的结构(树中DOM元素的后代)
但是,这种机制也很自然地会给创建和设计隔离空间带来一些成本。在某些情况下(大型列表,带有数据的表格单元等),出于性能优化的原因,我想避免这些成本。现在我们将修复它:const MY_CSS = {
title: 'color: #00f; font-size: 2em',
item: 'color: #f00; font-size: 1.2em',
};
const DATA = [
{text: 'Text 1'},
{text: 'Text 2'},
{text: 'Text 3'},
];
let template = document.createElement('template');
template.innerHTML = `
<div style="${MY_CSS_.title}">List items:</div>
<div class="my-list">
${DATA.map(item => /*html*/ `<div style="${MY_CSS.item}">${item.text}</div>`).join('')}
</div>
`;
class ShadowlessComponent extends HTMLElement {
constructor() {
super();
this._contents = new DocumentFragment();
this._contents.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
this.appendChild(this._contents);
}
}
window.customElements.define('shadowless-component', ShadowlessComponent);
如果您已经熟悉“定制元素”标准,那么您将立即注意到正在发生的事情:我们没有attachShadow
在组件构造函数中调用方法,而是创建了一个DocumentFragment,在其中克隆了预先准备的模板。在此阶段,该组件不会由浏览器呈现,并且可以相对安全地进行修改,例如,绑定/插入数据。下一个重要步骤与“自定义元素”生命周期相关。只有在构造函数完全工作之后,才将组件添加到常规文档中,直到那一刻,DOM API中负责与元素的父代或后代以及属性一起工作的那部分将不可用。因此,要直接向我们的组件添加内容,我们使用connectedCallback
。创建模板时,为了简化起见,我们使用了该方法innerHTML
。创建“模板”元素时,此操作仅执行一次,每次创建组件实例时都不会重复执行此操作。但是,也可以通过强制创建模式来进一步优化这一点。总体而言,在我们的标记中使用自定义标签shadowless-component
,我们在浏览器中获得以下结果:<shadowless-component>
<div id="caption" style="color: #00f; font-size: 2em">List items:</div>
<div class="my-list">
<div style="color: #f00; font-size: 1.2em">Text 1</div>
<div style="color: #f00; font-size: 1.2em">Text 2</div>
<div style="color: #f00; font-size: 1.2em">Text 3</div>
</div>
</shadowless-component>
由于摆脱了ShadowRoot,我们失去了样式的隔离,因此我们使用属性将样式添加到模板中。在这种情况下,它们具有优先权,这可以部分解决问题,并且可以在重要的地方使用。对于所有其他情况,可通过通用样式表获得经典样式,并且自定义标签充当便捷的选择器。显示内容
Web组件是DOM的完整节点。这意味着,除了您可以使用DOM元素的所有标准方法外,而且您的组件始终是一种容器。也就是说,如果您想使用Web组件向DOM中添加元素的任意结构,它们将都是组件的后代,这并不总是很方便。在这种情况下,您可以使用新的CSS规则-display:contents。浏览器支持:caniuse.com/#feat=css-display-contents默认情况下,所有组件都具有display:inline属性。有点恶作剧
但是,如果我们根本不想要任何额外的容器和自定义标签,该怎么办?提供纯HTML!好: constructor() {
super();
this._contents = new DocumentFragment();
this._contents.appendChild(template.content.cloneNode(true));
this._titleEl = this._contents.querySelector('#caption');
window.setInterval(() => {
this._titleEl.textContent = Date.now();
}, 1000);
}
connectedCallback() {
this.parentNode.prepend(this._contents, this);
this.remove();
}
结果,我们得到了:<div id="caption" style="color: #00f; font-size: 2em">1581075598392</div>
<div class="my-list">
<div style="color: #f00; font-size: 1.2em">Text 1</div>
<div style="color: #f00; font-size: 1.2em">Text 2</div>
<div style="color: #f00; font-size: 1.2em">Text 3</div>
</div>
所有事件和绑定将继续起作用,并由我们的组件控制,该组件现在仅存在于内存中。在这种情况下,当您要完全卸下组件时,必须特别注意取消订阅和进行其他垃圾清理。CSS组件
根据标准,自定义标签必须使用强制性的“-”字符来命名。如果在标记中使用标签,但同时不要在JS中创建任何组件并将其构造函数添加到组件注册表中,则浏览器会将您的标签视为“未知元素”(HTMLUnknownElement)。默认情况下,这些元素的行为与span标签相似。如果您需要创建具有简单结构的简单哑组件,而CSS规则:: before,:: after和attr()表达式就足够了,则可以使用此方法。例: my-container {
display: block;
padding: 10px;
border: 1px solid currentColor;
}
my-container::before {
content: attr(caption);
margin-bottom: .6em;
}
在标记中使用:<my-container caption=""></my-container>