乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      Web 組件勢必取代前端?

       黃元章3355 2019-04-24

      在現(xiàn)代Web API的發(fā)展下,創(chuàng)建可重用的前端組件終于不再需要框架了。

      作者 | Danny Moerkerke,JavaScript程序員,自由職業(yè)者

      譯者 | 彎月

      責(zé)編 | 郭芮

      出品 | CSDN(ID:CSDNnews)

      以下為譯文:

      還記得document.querySelector第一次獲得瀏覽器的廣泛支持,終結(jié)了jQuery一統(tǒng)天下的局面的時(shí)刻嗎?我們終于擁有了一個(gè)原生的方法來代替多年來一直需要通過jQuery來提供的功能:簡單地選擇DOM元素的方法。我相信,同樣的情況也會(huì)發(fā)生在前端框架上,比如Angular和React。

      有了這些框架,我們就能完成一些一直想做但一直沒辦法實(shí)現(xiàn)的事情——?jiǎng)?chuàng)建可重用的自動(dòng)化前端組件。然而,這些框架會(huì)增加復(fù)雜性,增加專有的語法,還會(huì)增大負(fù)擔(dān)。

      一切終將變化。

      在現(xiàn)代Web API的發(fā)展下,創(chuàng)建可重用的前端組件終于不再需要框架了。有了自定義元素和影子DOM,我們就可以創(chuàng)建能夠隨意復(fù)用的組件。

      Web組件(Web Component)的概念最初于2011年提出,組件包括一系列功能,可以僅通過HTML、CSS和JavaScript就能創(chuàng)建可重用的組件。也就是說,創(chuàng)建組件不需要再使用React或Angular之類的框架。更妙的是,這些組件還能夠無縫地集成到這些框架中。

      有史以來我們第一次能夠僅通過HTML、CSS和JavaScript創(chuàng)建組件并在任何現(xiàn)代瀏覽器上運(yùn)行?,F(xiàn)在,最新版本的Chrome、Safari、Firefox和Opera桌面版,以及Safari的iOS版、Chrome的Android版都支持Web組件。

      Edge將在下一個(gè)版本(版本19)中支持Web組件。舊版本瀏覽器還可使用polyfill(https://github.com/webcomponents/webcomponentsjs),最低能在IE11上實(shí)現(xiàn)Web組件。

      也就是說,現(xiàn)在幾乎能在任何瀏覽器(包括移動(dòng)瀏覽器)上使用Web組件。

      你可以創(chuàng)建自定義的HTMl標(biāo)簽,它能夠從被擴(kuò)展的HTML元素那里繼承所有的屬性,然后只需要簡單地導(dǎo)入一段腳本,就可以在任何支持Web組件的瀏覽器中使用。組件中定義的所有HTML、CSS和JavaScript的定義域都僅限于組件內(nèi)部。

      在瀏覽器的開發(fā)者工具中,組件將顯示為單個(gè)HTML標(biāo)簽,所有的樣式和行為都完全被封裝,不需要任何額外的技巧,不需要框架,也不需要編譯。

      我們來看看Web組件的主要功能。

      自定義元素

      自定義元素(Custom Elements)就是用戶自定義的HTML元素,可以使用CustomElementRegistry定義自定義元素。如果你想注冊新的元素,只需通過window.customElements獲得registry的實(shí)例,然后調(diào)用其define方法:

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

      define方法的第一個(gè)參數(shù)是要?jiǎng)?chuàng)建的新元素的標(biāo)簽名稱。接下來,你只需要下面的代碼就可以使用該元素:

      <my-element></my-element> 

      名稱中的橫線(-)是必須的,這是為了避免與原生HTML元素的命名沖突。

      MyElement構(gòu)造函數(shù)必須是ES6類,然而很不幸的是,由于Javascript類不同于傳統(tǒng)的OOP語言的類,這很容易造成混亂。而且,因?yàn)檫@里可以使用Object,所以Proxy也是可行的,這樣就能在自定義元素上實(shí)現(xiàn)簡單的數(shù)據(jù)綁定。但是,如果想實(shí)現(xiàn)對原生HTML元素的擴(kuò)展,這個(gè)限制是必須的,這樣才能保證你的元素能夠繼承整個(gè)DOM API。

      下面我們來為自定義元素寫一個(gè)類:

      class MyElement extends HTMLElement { constructor() { super(); } connectedCallback() { // here the element has been inserted into the DOM }}

      我們自定義元素的類只是普通的JavaScript類,它擴(kuò)展了原生的HTMLElement。除了構(gòu)造函數(shù)之外,它還有個(gè)方法叫做connectedCallback,當(dāng)元素被插入到DOM樹之后該方法會(huì)被調(diào)用。你可以認(rèn)為它相當(dāng)于React的componentDidMount方法。

      一般來說,組件的設(shè)置應(yīng)當(dāng)盡可能低推遲到connectdedCallback中進(jìn)行,因?yàn)檫@是唯一一個(gè)能夠確保所有屬性和子元素都存在的地方。一般來說,構(gòu)造函數(shù)應(yīng)該僅初始化狀態(tài),以及設(shè)置影子DOM(Shadow DOM)。

      元素的constructor和connectedCallback的區(qū)別在于,constructor在元素被創(chuàng)建時(shí)調(diào)用(例如通過調(diào)用document.createElement創(chuàng)建),而connectedCallback是在元素真正被插入到DOM中時(shí)調(diào)用,例如當(dāng)元素所在的文檔被解析時(shí),或者通過document.body.appendChild添加元素時(shí)。

      你也可以通過customElements.get('my-element')來獲取自定義元素的構(gòu)造函數(shù)的引用,通過該方法來創(chuàng)建元素,假設(shè)該元素已經(jīng)通過customElements.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,當(dāng)元素從DOM中移除時(shí)會(huì)調(diào)用該方法。在這個(gè)方法中可以進(jìn)行必要的清理工作,但要記住這個(gè)方法不一定會(huì)被調(diào)用,比如用戶關(guān)閉瀏覽器或關(guān)閉瀏覽器標(biāo)簽頁的時(shí)候。

      還有個(gè)adoptedCallback方法,當(dāng)通過document.adoptNode(element)將元素收養(yǎng)至文檔中時(shí)會(huì)調(diào)用該方法。到目前為止,我從來沒遇到過需要使用該回調(diào)函數(shù)的情況。

      另一個(gè)常用的生命周期方法是attributeChangedCallback。當(dāng)屬性被添加到observedAttributes數(shù)組時(shí)該方法會(huì)被調(diào)用。該方法調(diào)用時(shí)的參數(shù)為屬性的名稱、屬性的舊值和新值:

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

      該回調(diào)函數(shù)僅在屬性存在于observedAttributes數(shù)組中時(shí)才會(huì)被調(diào)用,在上例中為foo和bar。任何其他屬性的變化不會(huì)調(diào)用該回調(diào)函數(shù)。

      屬性主要用于定義元素的初始配置和初始狀態(tài)。理論上通過序列化的方式給屬性傳遞復(fù)雜的值,但這會(huì)對性能造成很大影響,而且由于你能夠訪問組件的方法,所以這樣做是沒有必要的。但如果確實(shí)希望像React、Angular等框架提供的功能那樣,在屬性上實(shí)現(xiàn)數(shù)據(jù)綁定,可以看看Ploymer(https://polymer-library./)。

      生命周期方法的順序

      生命周期方法的執(zhí)行順序?yàn)椋?/p>

      constructor -> attributeChangedCallback -> connectedCallback

      為什么attributeChangedCallback會(huì)在connectedCallback之前被調(diào)用?

      回憶一下,Web組件的屬性的主要目的是初始化配置。也就是說,當(dāng)組件被插入到DOM中時(shí),配置應(yīng)當(dāng)已經(jīng)被初始化過了,所以attributeChangedCallback應(yīng)當(dāng)在connectedCallback之前被調(diào)用。

      也就是說,如果想根據(jù)特定屬性的值,在影子DOM中配置任何結(jié)點(diǎn),那就需要在constructor中引用屬性,而不能在connectedCallback中進(jìn)行。

      例如,如果組件中有個(gè)id='container',而你需要在屬性disabled發(fā)生改變時(shí),將這個(gè)元素設(shè)置為灰色背景,那么需要在constructor中引用該屬性,這樣它才能出現(xiàn)在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中才能創(chuàng)建this.container,那么可能在第一次attributeChangedCallback被調(diào)用時(shí),this.container不存在。所以,盡管你應(yīng)當(dāng)盡量將組件的設(shè)置推遲到connectedCallback中進(jìn)行,但這是個(gè)例外情況。

      另一點(diǎn)很重要的是,要意識(shí)到你可以在通過customElements.define()注冊Web組件之前就使用它。當(dāng)元素存在于DOM中,或者被插入到DOM中時(shí),如果它還沒有被注冊,那么它將成為HTMLUnknownElement的實(shí)例。瀏覽器會(huì)對于任何它不認(rèn)識(shí)的HTML元素的處理方法是,你依然可以像使用其他元素那樣使用它,只是它沒有任何方法,也沒有默認(rèn)的樣式。

      在通過customElements.define()注冊之后,該元素就會(huì)通過類定義得到增強(qiáng)。該過程稱為“升級”(upgrading)。可以在元素被升級時(shí)通過customElements.whenDefined調(diào)用一個(gè)回調(diào)函數(shù),該方法返回一個(gè)Promise,在元素被升級時(shí)該P(yáng)romise得到解決:

      customElements.whenDefined('my-element').then(() => { // my-element is now defined})

      Web組件的公共API

      除了生命周期方法之外,你還可以在元素上定義方法,這些方法可以從外部調(diào)用。這個(gè)功能是React和Angular等框架無法實(shí)現(xiàn)的。例如,你可以定義一個(gè)名為doSomething的方法:

      class MyElement extends HTMLElement { ... doSomething() { // do something in this method }}

      然后在組件外部像這樣調(diào)用它:

      const element = document.querySelector('my-element');element.doSomething();

      任何在元素上定義的屬性都會(huì)成為它的公開JavaScript API的一部分。這樣,只需給元素的屬性提供setter,就可以實(shí)現(xiàn)數(shù)據(jù)綁定,從而實(shí)現(xiàn)類似于在元素的HTML里渲染屬性值等功能。因?yàn)樵腍TML屬性(attribute)值僅支持字符串,因此對象等復(fù)雜的值應(yīng)該作為自定義元素的屬性(properties)。

      除了定義Web組件的初始狀態(tài)之外,HTML屬性(attribute)還用來反映相應(yīng)的組件屬性(property)的值,因此元素的JavaScript狀態(tài)可以反映到其DOM表示中。下面的例子演示了input元素的disabled屬性:

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

      在將input的disabled屬性(property)設(shè)置為true后,這個(gè)改動(dòng)會(huì)反映到相應(yīng)的disabled HTML屬性(attribute)中:

      <input name='name' disabled>

      用setter可以很容易實(shí)現(xiàn)從屬性(property)到HTML屬性(attribute)的映射:

      class MyElement extends HTMLElement { ... set disabled(isDisabled) { if(isDisabled) { this.setAttribute('disabled', ''); } else { this.removeAttribute('disabled'); } } get disabled() { return this.hasAttribute('disabled'); }}

      如果需要在HTML屬性(attribute)發(fā)生變化時(shí)執(zhí)行一些動(dòng)作,那么可以將其加入到observedAttributes數(shù)組中。為了保證性能,只有加入到這個(gè)數(shù)組中的屬性(attribute)才會(huì)被監(jiān)視。當(dāng)HTML屬性(attribute)的值發(fā)生變化時(shí),attributeChangedCallback就會(huì)被調(diào)用,同時(shí)傳入HTML屬性的名稱、當(dāng)前值和新值:

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

      這樣,每當(dāng)disabled屬性(attribute)改變,this.container(即元素的影子DOM中的div元素)上的“disabled”就會(huì)隨之改變。

      影子DOM

      使用影子DOM,自定義元素的HTML和CSS可以完全封裝在組件內(nèi)部。這意味著在文檔的DOM樹中,元素會(huì)顯示為單一的HTML標(biāo)簽,其實(shí)際內(nèi)部HTML結(jié)構(gòu)會(huì)出現(xiàn)在#shadow-root中。

      實(shí)際上,好幾個(gè)原生HTML元素也在使用影子DOM。例如,如果在網(wǎng)頁上放置一個(gè)<video>元素,它會(huì)顯示為單一的標(biāo)簽,但同時(shí)顯示的播放、暫停按鈕等在開發(fā)者工具中查看<video>元素時(shí)是看不到的。

      這些元素實(shí)際上是<video>元素的影子DOM的一部分,因此默認(rèn)是隱藏的。要在Chrome中顯示影子DOM,可以在“偏好設(shè)置”中的開發(fā)者工具中找到設(shè)置,勾選“顯示用戶代理的影子DOM”。在開發(fā)者工具中重新檢查<video>元素,就能看到元素的影子DOM。

      影子DOM還支持真正的CSS范圍(scope)。所有定義在組件內(nèi)部的CSS只對組件本身有效。元素僅從組件外部定義的CSS中繼承最小量的屬性,甚至,連這些屬性都可以配置為不繼承。但是,你可以暴露一些CSS屬性,允許組件的使用者給組件添加樣式。這種機(jī)制解決了現(xiàn)有的CSS的許多問題,同時(shí)依然支持自定義組件的樣式。

      定義影子root的方式如下:

      const shadowRoot = this.attachShadow({mode: 'open'});shadowRoot.innerHTML = `<p>Hello world</p>`;

      這段代碼在定義影子root時(shí)使用了mode: 'open',其含義是它可以通過開發(fā)者工具進(jìn)行查看和操作,可以查詢,也可以配置任何暴露的CSS屬性,也可以監(jiān)聽它拋出的事件。影子root的另一個(gè)模式是mode: 'closed',但這個(gè)選項(xiàng)不推薦使用,因?yàn)槭褂谜邔o法與組件進(jìn)行人和交互,甚至都不能監(jiān)聽其拋出的事件。

      要給影子root添加HTML,可以將HTML字符串賦值給影子root的innerHTML屬性,也可以使用<template>元素。HTML模板基本上是一段HTML片段,供以后使用。在插入到DOM樹中之前,它不可見,也不會(huì)被解析,也就是說其內(nèi)部定義的任何外部資源都不會(huì)被下載,任何CSS和JavaScript在插入到DOM之前也不會(huì)被解析。例如,你可以定義多個(gè)<template>元素,當(dāng)組件的HTML根據(jù)組件狀態(tài)而發(fā)生變化時(shí),將相應(yīng)的模板插入到DOM中。這樣就可以很容易地改變組件的大量HTML,而不需要逐個(gè)處理DOM結(jié)點(diǎn)。

      創(chuàng)建影子root之后,就可以在上面使用所有DOM的方法,就像平常處理document對象那樣,如使用this.shadowRoot.querySelector來查找元素。組件的所有CSS都可以定義在<style>標(biāo)簽中,但也可以通過通常的<link rel='stylesheet'>來讀取外部樣式表。除了一般的CSS之外,還可以使用:host選擇器給組件自己定義樣式。例如,自定義元素默認(rèn)使用display: inline,使用下面的CSS可以將其定義為塊元素:

      :host { display: block;}

      這還可以實(shí)現(xiàn)上下文樣式。例如,如果想在組件定義了disabled屬性時(shí)灰掉,可以這樣做:

      :host([disabled]) { opacity: 0.5;}

      默認(rèn)情況下,自定義元素會(huì)從周圍的CSS繼承一些屬性,如color、font等。但是如果你希望從全新的狀態(tài)開始,使組件的所有CSS屬性重置到默認(rèn)值,可以這樣做:

      :host { all: initial;}

      有一點(diǎn)很重要:外部定義在組件上的樣式的優(yōu)先級要高于在影子DOM中使用:host定義的樣式。因此,如果定義了:

      my-element { display: inline-block;}

      它將會(huì)覆蓋:

      :host { display: block;}

      外部不可能給自定義元素內(nèi)部的任何元素定義樣式。但如果希望使用者能夠給組件(中的部分元素)定義樣式,那么可以通過暴露CSS變量來實(shí)現(xiàn)。例如,如果希望使用者能選擇組件的背景顏色,那么可以暴露名為--background-color的CSS變量。

      假設(shè)組件的影子DOM的根節(jié)點(diǎn)的元素為<div id='container'>:

      #container { background-color: var(--background-color);}

      那么,組件的使用者可以從外部定義其背景色:

      my-element { --background-color: #ff0000;}

      組件內(nèi)部應(yīng)該為其定義默認(rèn)值,以備使用者不定義背景色的情況:

      :host { --background-color: #ffffff;}#container { background-color: var(--background-color);}

      當(dāng)然,CSS變量的名字可以任意選擇,唯一的要求是必須以“--”開始。

      通過對CSS和HTML范圍(scope)的支持,影子DOM解決了CSS的全局性帶來的問題——會(huì)導(dǎo)致巨大的、只能添加的樣式表,其中的選擇器的規(guī)則越來越具體,充滿了各種覆蓋。影子DOM使得開發(fā)者可以將標(biāo)記語言和樣式打包到組件內(nèi)部,而不需要任何工具或命名規(guī)則。這樣就不用擔(dān)心新的class或id會(huì)與已有的沖突。

      除了能夠通過CSS變量給Web組件內(nèi)部設(shè)置樣式之外,還可以給Web組件注入HTML。

      通過slot進(jìn)行組合

      組合就是將影子DOM樹與使用者提供的標(biāo)記語言組合在一起。<slot>元素可以實(shí)現(xiàn)這一過程,可以認(rèn)為它是影子DOM中的一個(gè)占位符,使用者提供的標(biāo)記語言將在此處渲染。使用者提供的標(biāo)記語言稱為“輕量DOM”(light DOM)。組合過程將輕量DOM和影子DOM結(jié)合在一起,形成新的DOM樹。

      例如,你可以創(chuàng)建一個(gè)<image-gallery>組件,使用該組件時(shí),提供兩個(gè)標(biāo)準(zhǔn)的<img>標(biāo)簽供組件渲染用:

      <image-gallery> <img src='foo.jpg' slot='image'> <img src='bar.jpg' slot='image'></image-gallery>

      該組件將接受兩個(gè)圖像,并在組件的影子DOM內(nèi)部渲染。注意圖像上的slot='image'屬性。該屬性告訴組件圖像在影子DOM中渲染的位置。影子DOM的樣子可能如下:

      <div id='container'> <div class='images'> <slot name='image'></slot> </div></div>

      當(dāng)輕量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元素內(nèi)部,slot元素的name屬性值必須匹配相應(yīng)的slot屬性的值。

      <select>元素就使用了這種方式,你可以在Chrome的開發(fā)者工具中查看(如果你勾選了“顯示用戶代理的影子DOM”選項(xiàng),如上文所示):

      Web 組件勢必取代前端?

      它接受用戶提供的<option>元素,將其渲染成下拉菜單。

      帶有name屬性的slot元素稱為命名slot,但該屬性并不是必須的。name屬性只是用來將內(nèi)容渲染到特定的位置。如果一個(gè)或多個(gè)slot沒有name屬性,內(nèi)容將會(huì)按照使用者提供的順序進(jìn)行渲染。如果使用者提供的內(nèi)容少于slot的個(gè)數(shù),slot還可以提供默認(rèn)內(nèi)容。

      假設(shè)<image-gallery>的影子DOM如下所示:

      <div id='container'> <div class='images'> <slot></slot> <slot></slot> <slot> <strong>No image here!</strong> <-- fallback content --> </slot> </div></div>

      提供上文中的兩個(gè)圖像時(shí),產(chǎn)生的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>

      影子DOM內(nèi)部通過slot渲染的元素稱為分配結(jié)點(diǎn)。這些結(jié)點(diǎn)的樣式會(huì)在渲染到組件內(nèi)部的影子DOM(即“分配”)后依然有效。在影子DOM內(nèi)部,分配結(jié)點(diǎn)還可以通過::slotted()選擇器獲得額外的樣式:

      ::slotted(img) { float: left;}

      ::slotted()可以接受任何有效的CSS選擇器,但只能選擇頂層結(jié)點(diǎn)。例如,::slot(section img)在這種情況下無法使用:

      <image-gallery> <section slot='image'> <img src='foo.jpg'> </section></image-gallery>

      用JavaScript處理slot

      JavaScript也可以處理slot,可以查看某個(gè)slot被分配了什么結(jié)點(diǎn),查看某個(gè)元素被分配到了哪個(gè)slot,還可以使用slotchange事件。

      調(diào)用slot.assignedNodes()可以訪問slot分配到的結(jié)點(diǎn)。如果想獲取任何默認(rèn)內(nèi)容,可以調(diào)用slot.assignedNodes({flatten: true})。

      查看element被分配到的slot,可以訪問element.assignedSlot。

      每當(dāng)slot內(nèi)部的結(jié)點(diǎn)發(fā)生變化(結(jié)點(diǎn)被添加或刪除)時(shí)會(huì)產(chǎn)生slotChange事件。注意該事件僅在slot結(jié)點(diǎn)本身上觸發(fā),而不會(huì)在slot結(jié)點(diǎn)的子元素上觸發(fā)。

      slot.addEventListener('slotchange', e => { const changedSlot = e.target; console.log(changedSlot.assignedNodes());});

      Chrome會(huì)在元素首次初始化時(shí)觸發(fā)slotchange事件,而Safari和Firefox在此情況下不會(huì)。

      影子DOM中的事件

      自定義元素產(chǎn)生的標(biāo)準(zhǔn)事件(如鼠標(biāo)和鍵盤事件等)默認(rèn)情況下會(huì)從影子DOM中冒泡出來。如果事件從影子DOM內(nèi)部的結(jié)點(diǎn)產(chǎn)生,那么它的目標(biāo)會(huì)被重新設(shè)置,使之看起來像是從自定義元素本身產(chǎn)生的。如果想知道事件到底產(chǎn)生于影子DOM中的哪個(gè)元素,可以調(diào)用event.composedPath()來獲取該事件經(jīng)過的一系列結(jié)點(diǎn)。但是,事件的target屬性永遠(yuǎn)指向自定義元素本身。

      從自定義元素中可以通過CustomEvent拋出任何事件。

      class MyElement extends HTMLElement { ... connectedCallback() { this.dispatchEvent(new CustomEvent('custom', { detail: {message: 'a custom event'} })); }}// on the outsidedocument.querySelector('my-element').addEventListener('custom', e => console.log('message from event:', e.detail.message));

      但是,任何影子DOM內(nèi)部的結(jié)點(diǎn)拋出的事件則不會(huì)冒泡到影子DOM外面,除非它是使用composed: true創(chuàng)建的:

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

      template元素

      除了使用this.shadowRoot.innerHTML給影子root中的元素添加HTML之外,還可以使用<template>來實(shí)現(xiàn)這一點(diǎn)。模板用來提供一小段代碼供以后使用。模板中的代碼不會(huì)被渲染,初始化時(shí)它的內(nèi)容會(huì)被解析,但僅僅用來保證其內(nèi)容是正確的。模板內(nèi)部的JavaScript不會(huì)被執(zhí)行,任何外部資源也不會(huì)被獲取。默認(rèn)情況下它是隱藏的。

      如果Web組件需要根據(jù)不同的情況渲染完全不同的標(biāo)記,那么可以使用不同的模板來實(shí)現(xiàn)這一點(diǎn):

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

      這里兩個(gè)模板都通過innerHTML放到了影子root內(nèi)。一開始時(shí)兩個(gè)模板都是隱藏的,只有容器被渲染。在connectedCallback內(nèi)我們調(diào)用this.shadowRoot.querySelector('#view1').content.cloneNode(true)獲取了#view1的內(nèi)容。模板的content屬性返回的模板內(nèi)容為DocumentFragment實(shí)例,該實(shí)例可以通過appendChild添加到另一個(gè)元素中。由于appendChild在元素已存在于DOM中的情況下會(huì)移動(dòng)元素,所以我們首先需要使用cloneNode(true)來復(fù)制它。否則,模板的內(nèi)容將會(huì)被移動(dòng)而不會(huì)被添加,意味著我們只能使用其內(nèi)容一次。

      模板在需要快速改變一大片HTML或重用HTML的情況下非常有用。模板也不限于Web組件,可以用在DOM中的任何地方。

      擴(kuò)展原生元素

      到目前為止,我們一直在擴(kuò)展HTMLElement來創(chuàng)建全新的HTML元素。自定義元素還可以用來擴(kuò)展內(nèi)置的原生元素,從而實(shí)現(xiàn)對圖像、按鈕等已有HTML元素的增強(qiáng)。在撰寫本文時(shí),該功能僅Chrome和Firefox支持。

      擴(kuò)展已有HTML元素的好處是,它能繼承所有的屬性和方法。這樣就可以漸進(jìn)式增強(qiáng)已有元素,因此即使瀏覽器不支持自定義元素,該元素也是可用的,它只不過是采用默認(rèn)的內(nèi)置行為。而如果撰寫全新的HTML標(biāo)記,在不支持自定義元素的瀏覽器中就完全無法使用了。

      舉個(gè)例子,假設(shè)我們要增強(qiáng)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組件沒有擴(kuò)展更通用的HTMLElement,而是擴(kuò)展了HTMLButtonElement?,F(xiàn)在調(diào)用customElements.define時(shí)還帶了另一個(gè)參數(shù){extends: 'button'},來指明我們的類擴(kuò)展了<button>元素。這看起來有點(diǎn)多余,因?yàn)槲覀円呀?jīng)指明過要擴(kuò)展HTMLButtonElement了,但這是必要的,因?yàn)橛锌赡苡衅渌厥褂昧送粋€(gè)DOM接口。例如,<q>和<blockquote>都使用同一個(gè)HTMLQuoteElement接口。

      增強(qiáng)后的按鈕可以使用is屬性了:

      <button is='my-button'>

      該按鈕被我們的MyElement類增強(qiáng)。如果它加載到不支持自定義元素的瀏覽器中,它就會(huì)變成普通的按鈕。這是真正的漸進(jìn)式增強(qiáng)!

      注意,在擴(kuò)展已有元素時(shí)不能使用影子DOM。這僅僅是通過繼承所有屬性、方法和事件并提供額外的功能來擴(kuò)展原生HTML的方法。當(dāng)然,在組件內(nèi)部修改元素的DOM和CSS是可能的,但試圖創(chuàng)建影子root則會(huì)拋出錯(cuò)誤。

      擴(kuò)展內(nèi)置元素的另一個(gè)好處就是,它可以用于元素限制父子關(guān)系的情況。例如,<thead>元素僅允許<tr>元素作為子結(jié)點(diǎn),那么使用<awesome-tr>元素將被視為非法標(biāo)記。這種情況下我們可以擴(kuò)展內(nèi)置的<tr>元素,并這樣使用:

      <table> <thead> <tr is='awesome-tr'></tr> </thead></table>

      這樣使用Web組件可以帶來非常好的漸進(jìn)式增強(qiáng),但正如前面所說,目前只有Chrome和Firefox支持。Edge將來也會(huì)支持,但在本文撰寫之時(shí),Safari并不支持。

      測試Web組件

      測試Web組件非常容易、直接,與Angular、React等框架相比,測試Web組件簡直是小菜一碟。不需要任何編譯,也不需要復(fù)雜的設(shè)置。只需創(chuàng)建元素,添加到DOM中,然后運(yùn)行測試即可。

      下面是使用Mocha進(jìn)行測試的例子:

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

      這里,第一行導(dǎo)入了my-element.js,該文件將Web組件暴露為ES6模塊。這就是說,測試文件也需要作為ES6組件加載到瀏覽器中。因此,需要在瀏覽器中使用下面的html文件來運(yùn)行測試。除了Mocha之外,我們還加載了WebcomponentsJS polyfill,還有Chai用于測試斷言,還有Sinon用于監(jiān)視(spy)和模擬(mock):

      <!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()進(jìn)行斷言,并設(shè)置Mocha使用BDD接口。接下來加載測試文件(本例中只有一個(gè)),然后調(diào)用mocha.run()運(yùn)行測試。

      注意,在使用ES6模塊時(shí),還需要將mocha.run()放在帶有type='module'的script內(nèi)。這是因?yàn)槟J(rèn)情況下ES6模塊是被延遲加載的,如果mocha.run()在正常的script標(biāo)簽內(nèi),它將在my-element.test.js加載之前被執(zhí)行。

      在舊的瀏覽器中使用Polyfill

      目前,最新版本的Chrome、Firefox、Safari和Opera桌面版都支持自定義元素,Edge 19也將支持。iOS和Android上的Safari、Chrome和Firefox也支持。

      對于舊的瀏覽器,可以使用WebcomponentsJS這個(gè)polyfill:

      npm install --save @webcomponents/webcomponentsjs

      可以使用webcomponents-loader.js,該文件會(huì)進(jìn)行功能檢測,只有在必要時(shí)才會(huì)加載polyfill。使用polyfill就可以使用自定義元素,而不需要改動(dòng)源代碼。但是,它并不能提供真正的CSS范圍,意味著如果不同的Web組件中的元素?fù)碛型瑯拥腸lass名和id,它們將會(huì)沖突。而且,影子DOM的CSS選擇器:host()和:slotted()可能無法正確工作。

      想要讓這兩個(gè)選擇器正確工作,你需要加載Shady CSS polyfill,還需要(少量)修改源代碼。我個(gè)人不喜歡這一點(diǎn),所以我寫了個(gè)Webpack加載器來幫你實(shí)現(xiàn)這一點(diǎn)。這意味著你需要編譯代碼,但不再需要修改源代碼了。

      Webpack加載器完成三項(xiàng)工作:它給Web組件的影子DOM中的所有不是以::host或::slotted開頭的CSS規(guī)則添加前綴,前綴為元素的名稱,從而提供正確的范圍。之后,它會(huì)解析所有::host和::slotted規(guī)則,保證它們正確工作。

      示例#1:lazy-img

      我創(chuàng)建了一個(gè)Web組件,可以懶加載圖像,只有圖像完全出現(xiàn)在瀏覽器的窗口中時(shí)才進(jìn)行加載。代碼在Github(https://github.com/DannyMoerkerke/lazy-img)上。

      組件的正式版本是將原生的<img>標(biāo)簽包裹在<lazy-img>自定義元素內(nèi):

      <lazy-img src='path/to/image.jpg' width='480' height='320' delay='500' margin='0px'></lazy-img>

      代碼倉庫還有個(gè)extend-native分支,它利用is屬性擴(kuò)展原生的<img>為lazy-img:

      <img is='lazy-img' src='path/to/img.jpg' width='480' height='320' delay='500' margin='0px'>

      這是個(gè)用來演示原生Web組件的非常好的例子:只需要導(dǎo)入JavaScript文件,添加HTML標(biāo)簽或利用is擴(kuò)展已有的原生標(biāo)簽就可以了!

      示例#2:material-webcomponents

      我利用自定義元素實(shí)現(xiàn)了Google的Material Design,代碼也在Github(https://dannymoerkerke./material-webcomponents)上。

      該庫也演示了CSS自定義屬性(https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)的強(qiáng)大功能。

      那么,我還應(yīng)該使用框架嗎?

      還是那句話,需要視情況而定。

      當(dāng)前的前端框架通過數(shù)據(jù)綁定、狀態(tài)管理和非常標(biāo)準(zhǔn)化的代碼帶來了很多額外的價(jià)值。問題就是你的應(yīng)用程序是否需要這些。

      如果你不清楚應(yīng)用程序是否真的需要Redux等狀態(tài)管理,那么很大可能你并不需要。需要時(shí)你肯定會(huì)感受到。

      數(shù)據(jù)綁定也許會(huì)給你帶來好處,但Web組件已經(jīng)支持直接將屬性設(shè)置為數(shù)組、對象等非簡單值了。簡單值可以通過HTML屬性(attribute)來設(shè)置,屬性的改變可以通過atributeChangedCallback來監(jiān)視。

      盡管這一流程完全有道理,但會(huì)讓更新一小部分DOM的操作變得很麻煩,而React和Angular的描述性方式更容易。這些框架可以定義一個(gè)包含表達(dá)式的視圖,在表達(dá)式發(fā)生變化時(shí)進(jìn)行更新。

      原生的Web組件(還)不提供這樣的功能,盡管已經(jīng)有提案建議擴(kuò)展<template>元素以支持使用數(shù)據(jù)進(jìn)行初始化和更新:

      <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);//updateinstance.update({title: 'A new title', text: 'Hi there'});

      目前可用的庫中,能夠有效地更新DOM的是lit-html(https://lit-html./)。

      前端框架的另一個(gè)經(jīng)常被提及的好處就是,它們提供了標(biāo)準(zhǔn)的代碼,團(tuán)隊(duì)中的每個(gè)新成員都能從一開始就很熟悉。我相信這是正確的做法,但我也認(rèn)為這個(gè)好處非常有限。

      我曾在多個(gè)項(xiàng)目中使用過Angular、React和Polymer,盡管它們之間有相似性,但即使是使用同一個(gè)框架,代碼結(jié)構(gòu)也會(huì)大相徑庭。一個(gè)清晰的工作方式和樣式指南,為代碼提供的一致性遠(yuǎn)遠(yuǎn)好于僅依賴框架??蚣芤矔?huì)帶來額外的復(fù)雜性,所以應(yīng)該問問自己這樣做是否值得。

      現(xiàn)在,Web組件已經(jīng)得到了廣泛的支持,你也許可以看出,原生代碼可以提供與框架媲美的功能,但性能更好,代碼量更小,復(fù)雜度更低。

      原生Web組件的優(yōu)勢很明顯:

      • 原生,不需要框架
      • 很容易集成,不需要編譯
      • 真正的CSS范圍
      • 標(biāo)準(zhǔn)化,僅使用HTML、CSS和JavaScript

      jQuery及其優(yōu)異的遺產(chǎn)依然會(huì)繼續(xù)存在一段時(shí)間,但現(xiàn)在很少有新項(xiàng)目再使用它們,因?yàn)槲覀冇辛烁玫倪x擇。我并不認(rèn)為現(xiàn)在的框架會(huì)很快消失,但作為更好的選擇,原生Web組件已經(jīng)出現(xiàn),而且迅速得到了關(guān)注。我認(rèn)為,這些前端框架的角色也會(huì)改變,它們會(huì)在原生Web組件的基礎(chǔ)上提供一個(gè)簡單的層。

      我對于原生Web組件的未來非常樂觀,而且我還會(huì)繼續(xù)發(fā)表有關(guān)這方面技術(shù)的文章。如果你利用Web組件做了有意思的東西,請?jiān)谙路搅粞愿嬖V我們!

      原文:https://www./blog/web-components-will-replace-your-frontend-framework,本文由CSDN翻譯,轉(zhuǎn)載請注明來源出處。

        本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多