【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 = ` "container"> `; 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。例如,网页的
实际上,控件
影子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字符串,或者使用元素。HTML模板基本上算是一个惰性的HTML片段,我们可先定义以供后续使用。在实际插入DOM结构树之前,它是不可见或不被解析的,这意味着在它内部定义的任何外部资源都不会被获取,任何CSS和JavaScript也都不会被解析。当组件的HTML随着状态发生改变时,我们可以定义多个元素以便根据不同状态来做引入。如此一来,我们可以轻松的对组件HTML进行更改,而无需修改单个DOM节点。
一旦创建了影子根,我们可以像通常作用于document对象的方法一样,对其应用所有的DOM方法,例如,通过this.shadowRoot.querySelector方法来查找元素。组件的CSS都定义在