11.29 未来前端框架真的会被取代吗?

提起前端开发,不少开发者首先会对主流技术框架如 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组件的主要特性吧。

01.自定义元素

自定义元素单纯是用户定义的HTML元素。其可通过CustomElementRegistry来定义。要定义新元素,先通过window.customElements来获取注册实例,再调用它的define方法:

window.customElements.define('my-element', MyElement);

上述define方法中的第一个参数是新建元素的标签名。可这样来实现简单的添加:

<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。

02.生命周期方法的执行顺序

生命周期方法的执行顺序是:

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
})

03.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属性就是其中一个例子:



const input = document.querySelector('input');
input.disabled = true;

将input的属性disabled设置为true后,此更改将映射到相应的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 = `



`;

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。

04.影子DOM

影子DOM可以将自定义元素的HTML和CSS完全封装在组件中。这意味着,元素以单个HTML标签的形式呈现在文件的DOM结构树中,其内部HTML结构放在#shadow-root中。

实际上,一些本地HTML元素也使用了影子DOM。例如,网页的<video>元素,它显示为一个单独的标签,但它也是显示播放和暂停视频的控件,但在浏览器的开发工具中是找不到<video>元素的。/<video>/<video>

实际上,控件<video>元素是影子DOM的一部分,因此在默认情况下它们时不可见的。要在Chrome中显示出影子DOM,可进入开发工具设置中的“首选项”,选中“显示用户代理影子DOM”复选框。此时在开发工具中查找<video>元素,我们可以找到并检查该元素的影子DOM。/<video>/<video>

影子DOM还可看出CSS的真正作用域。所有在组件内部定义的CSS只作用于组件本身。元素将从组件外部定义的CSS中继承最小数量的属性,甚至可以将这些属性配置为不从外层的CSS中继承任何值。不过,我们可以露出CSS属性,以便用户对组件进行样式设置。这解决了当前许多CSS问题,同时仍然支持给组件自定义样式。

定义一个影子根:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `

Hello world

`;

这里用mode:’open’定义了一个影子根,这意味着,可以在开发工具检出它并做交互,也可以发请求、配置共用CSS属性或监听事件。也可以用mode:’close’来定义影子根,但不建议这样定义,因为它不支持任何方式的交互,甚至不能监听其抛出的事件。

要将HTM添加到影子根中,我们可以给innerHTML属性分配HTML字符串,或者使用<template>元素。HTML模板基本上算是一个惰性的HTML片段,我们可先定义以供后续使用。在实际插入DOM结构树之前,它是不可见或不被解析的,这意味着在它内部定义的任何外部资源都不会被获取,任何CSS和JavaScript也都不会被解析。当组件的HTML随着状态发生改变时,我们可以定义多个<template>元素以便根据不同状态来做引入。如此一来,我们可以轻松的对组件HTML进行更改,而无需修改单个DOM节点。/<template>/<template>

一旦创建了影子根,我们可以像通常作用于document对象的方法一样,对其应用所有的DOM方法,例如,通过this.shadowRoot.querySelector方法来查找元素。组件的CSS都定义在


分享到:


相關文章: