【CSDN 编者按】提起前端开发,不少开发者首先会对主流技术框架如 Vue、React、Angular 进行一番对比之后,选择相应的技术架构。
在此,随着前端框架的不断升级,其也变得越来越臃肿与复杂,那么,未来前端框架是否会被取而代之?
作者 | Danny Moerkerke
译者 | 谭开朗
责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下为译文:
还记得吗?当时document.querySelector首次被浏览器广泛采用,就此终结了jQuery的通用时代。对于多年来一直沿用jQuery来实现的功能,document.querySelector提供了原生方法:轻松的选取DOM元素。我相信,类似的情况也将会发生在诸如Angular和React的前端框架上。
前端框架为我们一直想做却未能做到的事情提供了可能:创建可复用的自动化前端组件,但它复杂性较高,还有专属语法和较高的有效负载。
前端框架正渐渐被取代。
现代Web API发展至今,我们已无需依赖框架来创建可复用的前端组件。我们所需创建的自定义组件就是自定义元素和影子DOM,这可以在任意地方复用。
2011年开始推出的Web组件,它支持仅通过HTML、CSS和JavaScript就能创建出可复用组件。这意味着,我们无需使用诸如React或Angular的框架就能构建出组件。更锦上添花的是,这些组件可以无缝集成到框架中。
有史以来第一次,我们仅通过HTML,CSS和JavaScript就成功构建了可兼容任意浏览器的可复用组件。Web组件现在可以兼容桌面最新的Chrome、Safari、Firefox和Opera浏览器,还有iOS的Safari浏览器和Android的Chrome浏览器。
Edge浏览器即将发布的第19版中也将得到兼容。对于较老的浏览器,它将会通过polyfill将其引入到IE 11浏览器中。
这意味着,目前基本任一浏览器都可以使用Web组件,包括移动端浏览器。
只需简单的引入一个脚本,我们就可以创建自定义HTML标签,它不仅继承了HTML元素扩展出的所有属性,还能在其支持的任意浏览器中使用。组件中定义的所有HTML,CSS和JavaScript都完全限定在组件的作用域内。
组件在浏览器的开发工具中显示为单个HTML标签,样式和逻辑是完全封装好的,无需方法转换,框架或换位。
一起来学习Web组件的主要特性吧。
自定义元素
自定义元素单纯是用户定义的HTML元素。其可通过CustomElementRegistry来定义。要定义新元素,先通过window.customElements来获取注册实例,再调用它的define方法:
window.customElements.define('my-element', MyElement);
上述define方法中的第一个参数是新建元素的标签名。可这样来实现简单的添加:
<my-element></my-element>
名称中的破折号(-)是必不可少的,它可以避免与原生HTML元素发生重名。
MyElement构造函数必须是一个ES6类,因为Javascript类(暂时还)不像传统的OOP类,而且容易混淆。此外,如果它支持使用object,那么也可以使用Proxy为自定义元素启用简单的数据绑定。但是需要对此作出限制,以便启用原生HTML的拓展属性并确保元素继承了整个DOM API。
为自定义元素编写类:
class MyElement extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
// here the element has been inserted into the DOM
}
}
自定义元素的类只是一个扩展了原生HTMLElement的常规JavaScript类。除了它的构造函数之外,还有一个名为connectedCallback的方法,当有元素插入到DOM中时,这个方法就会被调用。我们可以将其与React的componentDidMount方法进行比较。
通常情况下,组件的设置尽量放在connectedCallback中,因为connectedCallback是唯一能拿到稳定属性且子元素可用的地方。构造函数通常只用于初始化状态和设置影子DOM。
元素的constructor函数和connectedCallback函数之间的区别是,创建元素时调用构造函数(例如调用document.createElement),而connectedCallback函数是在元素已插入DOM后调用,比如声明文件被解析或已通过document.body.appendChild添加。
也可通过引用构造函数customElements.get('my-element')来创建元素,前提是它已经注册了customelelements.define()。然后用new element()方法实例化元素,而非document.createElement()方法:
customElements.define('my-element', class extends HTMLElement {...});
...
const el = customElements.get('my-element');
const myElement = new el(); // same as document.createElement('my-element');
document.body.appendChild(myElement);
connectedCallback的对应函数是disconnectedCallback,当元素从DOM中删除时会调用它。此方法支持执行任何必要的清除工作,但请记住,用户关闭浏览器或关闭浏览器选项卡并不会调用此方法。
还有adoptedCallback函数,它是在通过调用document. adoptnode(element)来将元素引入到文档中时被调用。到目前为止,我还从未遇到过使用该回调的情况。
另一个有用的生命周期函数是attributeChangedCallback函数。它在observedAttributes数组的属性发生变更时被调用。调用时需传入属性名、旧值和新值:
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['foo', 'bar'];
}
attributeChangedCallback(attr, oldVal, newVal) {
switch(attr) {
case 'foo':
// do something with 'foo' attribute
case 'bar':
// do something with 'bar' attribute
}
}
}
这个回调只会在observedAttributes数组包含的属性中发生调用,在本例中是foo和bar。如果是其他任意的属性变化,此回调不会发生调用。
属性主要用于声明元素的初始配置/状态。理论上,可以通过序列化将复杂的值传递给属性,但这可能会影响性能,因为我们可以通过访问组件的方法来替代。但是,如果确实希望通过像React和Angular这样的框架提供的属性来绑定,那么可以试试Polymer。
生命周期方法的执行顺序
生命周期方法的执行顺序是:
constructor -> attributeChangedCallback -> connectedCallback
为什么attributeChangedCallback方法会在connectedCallback方法之前执行?
回想一下,web组件属性的主要用途是初始化配置。这意味着,当组件引入到DOM中时此配置必须是可用的,因此需要在connectedCallback方法之前调用attributeChangedCallback方法。
这也意味着,如果需要基于某特定属性在影子DOM中配置节点,我们需要在constructor函数中引入该节点,而非在connectedCallback中。
例如,组件中有一个id=”container”的元素,我们需要在其可见属性禁用的情况下给它添加一个灰色背景,我们可以在constructor函数中引入这个元素,以便在attributeChangedCallback函数中可以调用:
constructor() {
this.container = this.shadowRoot.querySelector('#container');
}
attributeChangedCallback(attr, oldVal, newVal) {
if(attr === 'disabled') {
if(this.hasAttribute('disabled') {
this.container.style.background = '#808080';
}
else {
this.container.style.background = '#ffffff';
}
}
}
如果等到connectedCallback方法中才创建this.container,那么在首次调用attributeChangedCallback方法时,它还是不可用的。因此,尽管我们尽可能的将组件的设置放在connectedCallback中,但在这种情况下也是不可行的。
同样重要的是,在通过customelelements.define()注册之前,我们就可以使用该web组件。当该元素出现在DOM中或插入到DOM中,且尚未注册时,它将是HTMLUnknownElement的一个实例。浏览器将会处理这类它不认识的HTML元素,我们可以和其他元素一样为它设置交互逻辑,但除此之外,它没有任何方法或默认的样式。
当通过customelelements .define()注册它时,可通过定义类来增强它。这个过程称为升级。当使用customElements.whenDefined升级元素时,可以调用回调,它会返回元素升级后的Promise方法:
customElements.whenDefined('my-element')
.then(() => {
// my-element is now defined
})
Web组件的公共API
除了上述生命周期方法,我们可以给元素定义外部访问的方法,这是使用React或Angular等框架定义元素所实现不了的。例如,我们定义一个名为doSomething的方法:
class MyElement extends HTMLElement {
...
doSomething() {
// do something in this method
}
}
并从组件外部来调用它:
const element = document.querySelector('my-element');
element.doSomething();
在元素上定义的方法都将是其公共JavaScript API的一部分。通过这种方式,我们可以通过为元素的属性提供setter来实现数据绑定,例如,设置setter将在元素的HTML中呈现属性值。由于在本质上不可能向属性提供字符串以外的任何其他值,所以应该将像对象这样的复杂值作为属性传递给自定义元素。
除了声明web组件的初始状态外,属性还用于反映相应属性的值,以便将元素的JavaScript状态反映到它的DOM表示。input元素中的disabled属性就是其中一个例子:
<input name="name">
const input = document.querySelector('input');
input.disabled = true;
将input的属性disabled设置为true后,此更改将映射到相应的disabled 属性中:
<input name="name" disabled>
通过setter可以很容易地实现属性到属性的反射:
class MyElement extends HTMLElement {
...
set disabled(isDisabled) {
if(isDisabled) {
this.setAttribute('disabled', '');
}
else {
this.removeAttribute('disabled');
}
}
get disabled() {
return this.hasAttribute('disabled');
}
}
当属性发生改变而需要执行某些操作时,可将其添加到observedAttributes数组中。作为一种性能优化,只能监听该数组列出的属性。当属性的值发生变化时,attributeChangedCallback将通过传入属性名及其当前值和新值来调用:
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['disabled'];
}
constructor() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
.disabled {
opacity: 0.4;
}
</style>
<div id="container"></div>
`;
this.container = this.shadowRoot('#container');
}
attributeChangedCallback(attr, oldVal, newVal) {
if(attr === 'disabled') {
if(this.disabled) {
this.container.classList.add('disabled');
}
else {
this.container.classList.remove('disabled')
}
}
}
}
现在,disabled属性一旦发生改变,this.container中的“disabled”类就会发生来回切换,它是这个元素影子DOM的div元素。
下面详细探讨一下影子DOM。
影子DOM
影子DOM可以将自定义元素的HTML和CSS完全封装在组件中。这意味着,元素以单个HTML标签的形式呈现在文件的DOM结构树中,其内部HTML结构放在#shadow-root中。
实际上,一些本地HTML元素也使用了影子DOM。例如,网页的<video>元素,它显示为一个单独的标签,但它也是显示播放和暂停视频的控件,但在浏览器的开发工具中是找不到<video>元素的。
实际上,控件<video>元素是影子DOM的一部分,因此在默认情况下它们时不可见的。要在Chrome中显示出影子DOM,可进入开发工具设置中的“首选项”,选中“显示用户代理影子DOM”复选框。此时在开发工具中查找<video>元素,我们可以找到并检查该元素的影子DOM。
影子DOM还可看出CSS的真正作用域。所有在组件内部定义的CSS只作用于组件本身。元素将从组件外部定义的CSS中继承最小数量的属性,甚至可以将这些属性配置为不从外层的CSS中继承任何值。不过,我们可以露出CSS属性,以便用户对组件进行样式设置。这解决了当前许多CSS问题,同时仍然支持给组件自定义样式。
定义一个影子根:
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `<p>Hello world</p>`;
这里用mode:’open’定义了一个影子根,这意味着,可以在开发工具检出它并做交互,也可以发请求、配置共用CSS属性或监听事件。也可以用mode:’close’来定义影子根,但不建议这样定义,因为它不支持任何方式的交互,甚至不能监听其抛出的事件。
要将HTM添加到影子根中,我们可以给innerHTML属性分配HTML字符串,或者使用<template>元素。HTML模板基本上算是一个惰性的HTML片段,我们可先定义以供后续使用。在实际插入DOM结构树之前,它是不可见或不被解析的,这意味着在它内部定义的任何外部资源都不会被获取,任何CSS和JavaScript也都不会被解析。当组件的HTML随着状态发生改变时,我们可以定义多个<template>元素以便根据不同状态来做引入。如此一来,我们可以轻松的对组件HTML进行更改,而无需修改单个DOM节点。
一旦创建了影子根,我们可以像通常作用于document对象的方法一样,对其应用所有的DOM方法,例如,通过this.shadowRoot.querySelector方法来查找元素。组件的CSS都定义在<style>标签中,但如果希望使用常规的<link rel=”stylesheet”>标签,也可以获取到外部样式表。除了常规CSS之外,还可以使用:host选择器对组件本身进行样式设置。例如,自定义元素默认使用display: inline而将组件显示为块元素,可以写为:
:host {
display: block;
}
它也支持为上下文设置样式。例如,如果希望在组件定义了disabled属性后显示为灰色,写为:
:host([disabled]) {
opacity: 0.5;
}
默认情况下,自定义元素会继承一些外层CSS样式,比如颜色和字体等。但如果希望清空样式,将组件中所有的CSS样式重置为默认状态,可以写为:
:host {
all: initial;
}
值得注意的是,外部定义于组件本身上的样式优先于使用:host在影子DOM中定义的样式。因此,如果定义:
my-element {
display: inline-block;
}
它将覆盖:
:host {
display: block;
}
我们不能从外部对自定义元素内的任何节点设置样式。如果想要支持用户对组件样式(某部分)进行设置,我们可以通过露出CSS变量来实现。例如,为便于用户选择组件的背景颜色,我们可以露出background-color这一CSS变量。
假设组件中影子DOM的根节点是<div id="container">:
#container {
background-color: var(--background-color);
}
现在用户从外部设置组件的背景颜色:
my-element {
--background-color: #ff0000;
}
我们要在组件内部为它设置一个默认值以防用户不对其设置:
:host {
--background-color: #ffffff;
}
#container {
background-color: var(--background-color);
}
当然,CSS变量的名称可以是任意的。它的唯一限制是要以“--”开头。
通过限定CSS和HTML的作用域,影子DOM解决了CSS全局特性带来的问题,即通常是只添加样式表而其中包含越来越多的特定选择器和覆盖问题。影子DOM支持在自包含组件中捆绑标记和样式,而无需任何工具或命名约定。因此,我们不必担心新的类或id会与现有类发生冲突。
我们可以通过CSS变量来给web组件设置内部样式,除此之外,还可以将HTML注入到web组件中。
组成元素slot
组成是将影子DOM结构树和用户标记组合在一起的过程。它可以通过<slot>元素实现,<slot>元素是在影子DOM中用来呈现用户所做标记的基本占位符。用户所做的标记被称为自然DOM。组合将自然DOM和影子DOM合成为新的DOM结构树。
例如,我们可以创建一个<image-gallery>组件,并提供标准的<image>标签用以渲染呈现:
<image-gallery>
<img src="foo.jpg" slot="image">
<img src="bar.jpg" slot="image">
</image-gallery>
组件将会获取这两个图像,并通过slot在组件的影子DOM中渲染出来。注意图像上的slot="image"属性。该属性告诉组件应该在影子DOM中的哪个位置呈现,比如可以是这样的:
<div id="container">
<div class="images">
<slot name="image"></slot>
</div>
</div>
当自然DOM中的节点被分布到元素的影子DOM中时,得到的DOM树看起来是这样的:
<div id="container">
<div class="images">
<slot name="image">
<img src="foo.jpg" slot="image">
<img src="bar.jpg" slot="image">
</slot>
</div>
</div>
正如我们看到的,用户提供的元素都具有slot属性,它们都将在slot元素中呈现,而slot元素具有name属性,其值与slot属性值一致。
<select>元素的运行方式与在Chrome开发工具中查看它时所看到的完全一致(用户代理拿到用户选择的影子DOM时,如图):
它获取用户提供的<option>元素并将它们呈现到下拉菜单中。
带有name属性的slot元素被称为命名slot,但此属性不是必须的。它仅用于在特定位置呈现内容。当一个或多个slot没有name属性时,其中内容的渲染顺序将遵循用户提供的顺序。当用户提供的内容少于slot时,slot甚至可以提供回退内容。
假设<image-gallery>的影子DOM是这样的:
<div id="container">
<div class="images">
<slot></slot>
<slot></slot>
<slot>
<strong>No image here!</strong> <-- fallback content -->
</slot>
</div>
</div>
同样提供两个图像,得到的影子DOM树将是:
<div id="container">
<div class="images">
<slot>
<img src="foo.jpg">
</slot>
<slot>
<img src="bar.jpg">
</slot>
<slot>
<strong>No image here!</strong>
</slot>
</div>
</div>
通过slot在影子DOM中呈现的元素称为分布式节点。这些节点在组件的影子DOM (分布式)中呈现之前,应用于它们的所有样式也将在分布之后得到应用。在影子DOM中,分布式节点可以通过::sloated()选择器获得额外的样式:
::slotted(img) {
float: left;
}
:: sloated()可以接受任何有效的CSS选择器,但是它只能选择最高级的节点。例如::sloated (section img)将无法处理以下内容:
<image-gallery>
<section slot="image">
<img src="foo.jpg">
</section>
</image-gallery>
JavaScript作用于slot
我们可以通过JavaScript与slot进行交互,通过查看节点分配到哪个slot,该slot对应哪个元素以及slotchange事件。
调用slot.assignedNodes()方法可以找出该slot对应的元素。如果还需要检索回调内容,可以调用slot.assignedNodes({flatten: true})。
查找slot中是否包含某个特定元素可以使用element.assignedSlot。
当slot节点发生变更时,即添加或删除节点将会触发slotchange事件。这里要注意,该事件会触发仅针对slot节点本身,而非slot节点的子节点。
slot.addEventListener('slotchange', e => {
const changedSlot = e.target;
console.log(changedSlot.assignedNodes());
});
当元素是首次初始化时,Chrome会触发slotchange事件,而Safari和Firefox则不会。
影子DOM事件
默认情况下,自定义元素中诸如鼠标事件和键盘事件等标准事件会从影子DOM中弹出。影子DOM节点触发的事件会被重定向,所以该事件看起来像是源自自定义元素本身。如果想找出是影子DOM中由哪个元素触发了此事件,我们可以调用event.composedPath()来检索事件经过的节点数组。事件的target属性始终指向自定义元素本身。
我们可以使用CustomEvent从自定义元素中抛出任意事件。
class MyElement extends HTMLElement {
...
connectedCallback() {
this.dispatchEvent(new CustomEvent('custom', {
detail: {message: 'a custom event'}
}));
}
}
// on the outside
document.querySelector('my-element').addEventListener('custom', e => console.log('message from event:', e.detail.message));
但是,当事件是从影子DOM中的节点抛出而不是其自定义元素本身时,它不会从影子DOM中弹出,除非它是用composition: true创建的:
class MyElement extends HTMLElement {
...
connectedCallback() {
this.container = this.shadowRoot.querySelector('#container');
// dispatchEvent is now called on this.container instead of this
this.container.dispatchEvent(new CustomEvent('custom', {
detail: {message: 'a custom event'},
composed: true // without composed: true this event will not bubble out of Shadow DOM
}));
}
}
模块元素
除了可以使用this.shadowRoot.innerHTML将HTML添加到影子根中,我们也可以用<template>元素。模板可保存HTML以供后续使用。它并未渲染出来,而最初的解析是为了确保其内容的有效性。模板中的JavaScript不执行,也不会获取任何外部资源。默认情况下它是隐性的。
当一个web组件需要根据不同的情况呈现完全不同的标记时,可以使用不同的模板来完成:
class MyElement extends HTMLElement {
...
constructor() {
const shadowRoot = this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<template id="view1">
<p>This is view 1</p>
</template>
<template id="view1">
<p>This is view 1</p>
</template>
<div id="container">
<p>This is the container</p>
</div>
`;
}
connectedCallback() {
const content = this.shadowRoot.querySelector('#view1').content.clondeNode(true);
this.container = this.shadowRoot.querySelector('#container');
this.container.appendChild(content);
}
}
在这里,两个模板都通过innerHTML置于元素的影子根中。起初,这两个模板都是不可见的,只看得到容器。connectedCallback方法中,我们通过this.shadowRoot.querySelector('#view1').content.clondeNode(true)获取#view1的内容。模板的content属性以DocumentFragment返回模板的内容,可以使用appendChild将其添加到另一个元素中。当某元素已存在于DOM中时,使用appendChild将会移动它,所以我们要先使用cloneNode(true)克隆它。否则,模板的内容将会被移动而不是附加,这就意味着我们只能使用一次了。
模板对于快速更改大多数HTML或可复用的标签非常有用。它们不仅限于web组件,而且可以在DOM中的任何地方使用。
本地元素的扩展
到目前为止,我们一直在扩展HTMLElement来创建一个全新的HTML元素。自定义元素还支持扩展原生内置元素,支持增强已经存在的HTML元素,例如图像和按钮。在撰写本文时,此功能仅有Chrome和Firefox浏览器支持。
扩展现有HTML元素的好处是其继承了元素的所有属性和方法。这允许对现有元素进行逐步增强,这意味着即使在不支持自定义元素的浏览器中加载元素,它仍然是可用的。它只会退回到默认的内置行为,而如果它是一个全新的HTML标记,那么它根本就不能使用。
例如,我们希望增强HTML中的<button>元素:
class MyButton extends HTMLButtonElement {
...
constructor() {
super(); // always call super() to run the parent's constructor as well
}
connectedCallback() {
...
}
someMethod() {
...
}
}
customElements.define('my-button', MyButton, {extends: 'button'});
我们的web组件不再扩展更通用的HTMLElement,而是扩展HTMLButtonElement。对customElements.define的调用还需要一个额外的参数{extends: 'button'},以此表示我们的类扩展了<button>元素。这看起来可能有些多余,因为我们已经表明了我们想要扩展HTMLButtonElement,但是这是必要的,因为有一些元素共享相同的DOM接口。例如,<q>和<blockquote>共用HTMLQuoteElement接口。
增强后的按钮可以与is属性一起使用:
<button is="my-button">
现在它将被我们的MyElement类增强,如果它加载在一个不兼容自定义元素的浏览器中,它将简单地退回到一个标准按钮,真正意义上的渐进增强!
注意,在扩展现有元素时,不能使用影子DOM。这只是一种扩展原生HTML元素的方法,它继承了所有现有的属性、方法和事件,并提供了额外的功能。当然,可以在组件中修改元素的DOM和CSS,但是试图创建一个影子根的话,它将会抛出一个错误。
扩展内置元素的另一个好处是,这些元素也可以用于应用元素子元素限制的地方。例如,<thead> 元素只允许将<tr> 元素作为其子元素,因此<awesome-tr>元素将呈现无效的标记。在这种情况下,我们可以扩展内置的<tr> 元素,并像这样使用它:
<table>
<thead>
<tr is="awesome-tr"></tr>
</thead>
</table>
这种创建web组件的方式带来了巨大的渐进式增强,但正如前面提到的,目前这仅有Chrome和Firefox支持实现。Edge也将能兼容它,但不幸的是,在Safari中不能支持实现。
测试web组件
与为Angular和React之类的框架编写测试用例相比,测试web组件既简单又直接,而且坦率地说,这是一件轻而易举的事情。这无需换位或进行复杂的设置。只需创建元素,将其附加到DOM中并运行测试。
下面是一个使用Mocha测试的例子:
import 'path/to/my-element.js';
describe('my-element', () => {
let element;
beforeEach(() => {
element = document.createElement('my-element');
document.body.appendChild(element);
});
afterEach(() => {
document.body.removeChild(element);
});
it('should test my-element', () => {
// run your test here
});
});
在这里,第一行导入my-element.js文件,该文件将我们的web组件露出为一个ES6模块。这意味着测试文件本身也需要作为一个ES6模块加载到浏览器中。这需要以下index.html能够在浏览器中运行测试。除了Mocha,这个设置还加载WebcomponentsJS polyfill和Chai用于测试判断,以及Sinon用来检测和模拟:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../node_modules/mocha/mocha.css">
<script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script src="../node_modules/sinon/pkg/sinon.js"></script>
<script src="../node_modules/chai/chai.js"></script>
<script src="../node_modules/mocha/mocha.js"></script>
<script>
window.assert = chai.assert;
mocha.setup('bdd');
</script>
<script type="module" src="path/to/my-element.test.js"></script>
<script type="module">
mocha.run();
</script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>
加载所需的脚本后,我们将露出chai.assert作为一个全局变量,因此我们可以在测试中简单地使用assert(),并设置Mocha来使用BDD接口。然后加载测试文件(在本例中只有一个),并通过调用mocha.run()运行测试。
请注意,在使用ES6模块时,还需要将mocha.run()放在type="module"的脚本中。这是因为ES6模块在默认情况下是延迟的,如果mocha.run()放在一个正则脚本标记中,它将在加载my-element.test.js之前执行。
Polyfill老式浏览器
目前,Chrome、Firefox、Safari和Opera的最新版本都支持自定义元素,即将推出的Edge 19也将支持自定义元素。在iOS和Android上,Safari、Chrome和Firefox都能兼容它们。
旧版本浏览器的WebcomponentsJS polyfill可通过以下途径安装:
npm install --save @webcomponents/webcomponentsjs
我们可以引入webcomponents-loader.js文件,该文件将进行特性检测,只加载必要的填充内容。使用此方法,我们可以使用自定义元素,而不需要向源代码添加任何东西。但是,它没有提供真正的CSS作用域,这意味着如果在不同的web组件中包含有相同的类名和id,并将它们加载到相同的文档中,它们将发生冲突。此外,影子DOM的 CSS选择器:host()和:sloated()可能无法正常工作。
为了使其正常运行,我们需要使用Shady CSS polyfill,这也意味着必须(稍微)修改源代码才能使用它。我个人认为这是不可取的,所以我创建了一个webpack加载器,它可以处理这个问题。这表明必须进行转置,但是我们可以保持代码不变。
webpack加载器会做这三件事:它在web组件的影子DOM中为所有CSS规则加上前缀,这些CSS规则不是以元素名加::host或::slotted 开头的,他们提供适当的范围。然后,它解析所有::host和::slot规则,以确保它们也能正确工作。
工作示例1:lazy-img
我创建了一个web组件,当图像在浏览器的视图中完全可见时,它就会惰性地加载图像。具体请参考Github。
组件的主体将本地<img> 标签封装在<lazy-img> 自定义元素中:
<lazy-img
src="path/to/image.jpg"
width="480"
height="320"
delay="500"
margin="0px"></lazy-img>
下例还包含了extend-native分支,其中包含lazy-img,它使用is属性扩展原生<img>标签:
<img
is="lazy-img"
src="path/to/img.jpg"
width="480"
height="320"
delay="500"
margin="0px">
这是一个说明原生web组件强大功能的很好的例子:只需导入JavaScript文件、添加HTML标签或使用is属性扩展原生web组件,大功告成!
工作示例2:material-webcomponents
我使用自定义元素实现了谷歌的材质设计,具体也请参考Github。
这个库还展示了CSS自定义属性的强大功能,其中包含大量的自定义属性。
那么,应该抛弃框架吗?
当然,这要视情况而定。
目前的前端框架因具有数据绑定、状态管理和相当标准化的代码库等功能而带来了额外的价值。但问题是你的应用程序是否真的需要它。
如果还需要问自己,我的应用程序是否真的需要Redux之类的状态管理,那么你可能就是不需要它的。你渐渐就明白了。
使用数据绑定可能有好处,但是对于数组和对象等非基本值,原生web组件已经支持用户直接设置属性。我们可以在属性上设置基本值,并且可以通过attributeChangedCallback观察这些属性的更改。
虽然这种方法看起来非常合理,但是与React和Angular的声明式方法相比,它会使得更新DOM的某一部分变得非常麻烦和冗长。这些框架支持定义一个视图,它包含当表达式变更时更新的表达式。
原生web组件(还)不提供这样的功能,尽管有人建议扩展<template>元素,使其可以用数据实例化和更新:
<template id="example">
<h1>{{title}}</h1>
<p>{{text}}</p>
</template>
const template = document.querySelector('#example');
const instance = template.createInstance({title: 'The title', text: 'Hello world'});
shadowRoot.appendChild(instance.content);
//update
instance.update({title: 'A new title', text: 'Hi there'});
当前可以支持有效DOM更新的库是lit -html。
另一个经常提到前端框架的好处是,它们提供了一个标准的代码基,团队中的每个新开发人员都将从一开始就熟悉这个代码基。虽然我相信这是真的,但我也认为这种好处是相当有限的。
我曾经用Angular、React和Polymer做过很多项目,虽然大家对它们都很熟悉,但是尽管使用了相同的框架,这些代码库还是有很大的不同。清晰定义的工作方式和样式指南比简单地使用框架更有助于代码库的一致性。框架也带来了额外的复杂性,所以问问自己这是否真的值得
现在web组件得到了广泛的支持,我们可能会得出这样的结论:原生代码可以提供与框架相同的功能,但是性能更好、代码更少、复杂度更低。
本地web组件的好处很明显:
-
原生,不需要框架;
-
易于集成,不需要移位;
-
CSS有真正的作用域;
-
规范,只有HTML, CSS和JavaScript。
jQuery及其尤为出色的资源将会继续存在一段时间,但是没有太多人会使用它构建的新项目,因为现在有了更好的选择。我不认为目前的框架会很快消失,但是以原生web组件形式出现的更优选择正在出现,并且迅速获得关注。我确实期望这些前端框架的角色会发生巨大变化,届时它们将仅给本地web组件提供一个薄外层。
原文:https://www.dannymoerkerke.com/blog/web-components-will-replace-your-frontend-framework