我們都有自己不想從事的專案。程式碼變得難以管理,範圍不斷髮展,快速修復應用在其他修復之上,並且結構在義大利麵條式程式碼的重壓下崩潰了。編碼可能是一件很麻煩的事情。
專案受益於使用具有單一職責的簡單、獨立的模組。模組化程式碼被封裝,因此無需擔心實現。只要您知道在給定一組輸入時模組將輸出什麼,您就不一定需要了解它是如何實現該目標的。
將模組化概念應用於單一程式語言很簡單,但Web開發需要多種技術組合。瀏覽器解析HTML 、 CSS和JavaScript以呈現頁面的內容、樣式和功能。
它們並不總是很容易混合,因為:
- 相關程式碼可以拆分為三個或更多檔案,並且
- 全域性樣式和JavaScript物件可能會以意想不到的方式相互干擾。
除了這些問題之外,語言執行時、框架、資料庫和伺服器上使用的其他依賴項也會遇到這些問題。
什麼是Web元件?
Web元件是一種建立可在任何頁面上重用的封裝的、單一職責的程式碼塊的方法。
考慮HTML<video>
標籤。給定URL ,檢視者可以使用諸如播放、暫停、後退、前進和調整音量等控制元件。
雖然您可以使用各種屬性和JavaScript API呼叫進行修改,但仍提供了樣式和功能。任意數量<video>
元素可以放在其他標籤內,它們不會發生衝突。
如果您需要自己的自定義功能怎麼辦?例如,顯示頁面上字數的元素?還沒有HTML<wordcount>
標籤。
React和Vue.js等框架允許開發人員建立Web元件,其中的內容、樣式和功能可以在單個JavaScript檔案中定義。這些解決了許多複雜的程式設計問題,但請記住:
- 您必須學習如何使用該框架並隨著它的發展更新您的程式碼。
- 為一個框架編寫的元件很少與另一個框架相容。
- 框架的流行度起起落落。您將變得依賴於開發團隊和使用者的奇思妙想和優先事項。
- 標準Web元件可以新增瀏覽器功能,這在單獨的JavaScript中是很難實現的(例如Shadow DOM)。
幸運的是,庫和框架中引入的流行概念通常會進入Web標準。這花了一些時間,但Web元件已經到來。
Web元件簡史
在許多特定於供應商的錯誤開始之後, Alex Russell在2011年的Fronteers Conference上首次引入了標準Web元件的概念。谷歌的Polymer庫(一個基於當前提案的polyfill)在兩年後問世,但早期的實現直到2016年才出現在Chrome和Safari中。
瀏覽器供應商花時間協商細節,但Web元件於2018年和20210年分別被新增到Firefox和Edge(當微軟切換到Chromium引擎時)。
可以理解的是,很少有開發人員願意或能夠採用Web元件,但我們最終通過穩定的API達到了良好的瀏覽器支援水平。並非一切都是完美的,但它們是基於框架的元件的日益可行的替代方案。
即使您現在還不願意放棄您最喜歡的,Web元件與每個框架都相容,並且API將在未來幾年內得到支援。
每個人都可以檢視預先構建的Web元件庫:
- WebComponents.org
- The Component Gallery
- generic-components
- web-components-examples
- awesome-standalones
- accessible_components
- Kickstand UI
……但是編寫自己的程式碼更有趣!
本教程完整介紹了在沒有JavaScript框架的情況下編寫的Web元件。您將瞭解它們是什麼以及如何使它們適應您的Web專案。您將需要一些HTML5 、CSS和JavaScript 的知識。
Web元件入門
Web元件是自定義HTML元素,例如<hello-world></hello-world>
。該名稱必須包含一個破折號,以免與HTML規範中正式支援的元素髮生衝突。
您必須定義一個ES2015類來控制元素。它可以命名為任何名稱,但HelloWorld是常見的做法。它必須擴充套件HTMLElement介面,它表示每個HTML元素的預設屬性和方法。
注意: Firefox允許您擴充套件特定的HTML元素,例如HTMLParagraphElement、HTMLImageElement或HTMLButtonElement。這在其他瀏覽器中不受支援,並且不允許您建立Shadow DOM。
為了做任何有用的事情,該類需要一個名為connectedCallback()的方法,該方法在元素新增到文件時呼叫:
class HelloWorld extends HTMLElement { // connect component connectedCallback() { this.textContent = 'Hello World!'; } }
在此示例中,元素的文字設定為“Hello World”。必須使用CustomElementRegistry註冊該類才能將其定義為特定元素的處理程式:
customElements.define( 'hello-world', HelloWorld );
現在,當您的JavaScript載入時,瀏覽器會將<hello-world>
元素與您的HelloWorld <script type="module" src="./helloworld.js"></script>
)。
您現在有一個自定義元素!
這個元件可以像任何其他元素一樣在 CSS 中設定樣式:
hello-world { font-weight: bold; color: red; }
新增屬性
該元件無益,因為無論如何都會輸出相同的文字。像任何其他元素一樣,我們可以新增HTML屬性:
<hello-world name="Craig"></hello-world>
這可能會覆蓋文字,因此“Hello Craig!” 被陳列。為此,您可以向HelloWorld類新增一個constructor()函式,該函式在建立每個物件時執行。它必須:
- 呼叫super()方法來初始化父 HTMLElement,以及
- 進行其他初始化。在這種情況下,我們將定義一個name屬性,該屬性設定為預設值“World”:
class HelloWorld extends HTMLElement {constructor() {super();this.name = 'World';}// more code...class HelloWorld extends HTMLElement { constructor() { super(); this.name = 'World'; } // more code...
class HelloWorld extends HTMLElement { constructor() { super(); this.name = 'World'; } // more code...
您的元件只關心name屬性。靜態observedAttributes()屬性應返回一組要觀察的屬性:
// component attributes static get observedAttributes() { return ['name']; }
當在HTML中定義屬性或使用JavaScript更改屬性時,將呼叫attributeChangedCallback()方法。它傳遞了屬性名稱、舊值和新值:
// attribute change attributeChangedCallback(property, oldValue, newValue) { if (oldValue === newValue) return; this[ property ] = newValue; }
在此示例中,只會更新name屬性,但您可以根據需要新增其他屬性。最後,您需要調整connectedCallback()方法中的訊息:
// connect component connectedCallback() { this.textContent = `Hello ${ this.name }!`; }
生命週期方法
在Web元件狀態的整個生命週期中,瀏覽器會自動呼叫六個方法。此處提供了完整列表,儘管您已經在上面的示例中看到了前四個:
constructor()
它在元件第一次初始化時被呼叫。它必須呼叫super()並且可以設定任何預設值或執行其他預渲染過程。
靜態observedAttributes()
返回瀏覽器將觀察到的一組屬性。
attributeChangedCallback(propertyName, oldValue, newValue)
每當觀察到的屬性更改時呼叫。那些在HTML中定義的會被立即傳遞,但JavaScript可以修改它們:
document.querySelector('hello-world').setAttribute('name', 'Everyone');
發生這種情況時,該方法可能需要觸發重新渲染。
connectedCallback()
當Web元件附加到文件物件模型時,將呼叫此函式。它應該執行任何需要的渲染。
disconnectedCallback()
當從文件物件模型中刪除Web元件時呼叫它。如果您需要清理,例如刪除儲存的狀態或中止Ajax請求,這可能很有用。
adoptedCallback()
當Web元件從一個文件移動到另一個文件時呼叫此函式。
Web元件如何與其他元素互動
Web元件提供了一些您在JavaScript框架中找不到的獨特功能。
Shadow DOM
雖然我們在上面構建的Web元件可以工作,但它不能免受外部干擾,CSS或JavaScript可以對其進行修改。同樣,您為元件定義的樣式可能會洩漏並影響其他元件。
Shadow DOM通過將一個單獨的DOM附加到Web元件來解決這個封裝問題:
const shadow = this.attachShadow({ mode: 'closed' });
模式可以是:
- “open” ——外層頁面的JavaScript可以訪問Shadow DOM(使用Element.shadowRoot ),或者
- “closed” ——Shadow DOM只能在Web元件中訪問。
Shadow DOM可以像任何其他DOM元素一樣進行操作:
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }); shadow.innerHTML = ` <style> p { text-align: center; font-weight: normal; padding: 1em; margin: 0 0 2em 0; background-color: #eee; border: 1px solid #666; } </style> <p>Hello ${ this.name }!</p>`; }
<p>
元素中呈現“Hello”文字併為其設定樣式。它不能被元件外的JavaScript或CSS修改,儘管字型和顏色等一些樣式是從頁面繼承的,因為它們沒有明確定義。
限定在此Web元件範圍內的樣式不能影響頁面上的其他段落,甚至其他<hello-world>
元件。
請注意,CSS :host
選擇器可以從Web元件中設定外部<hello-world>
元素的樣式:
:host { transform: rotate(180deg); }
您還可以設定元素使用特定類時應用的樣式,例如<hello-world class="rotate90">
:
:host(.rotate90) { transform: rotate(90deg); }
HTML模板
對於更復雜的Web元件,在指令碼中定義HTML可能變得不切實際。模板允許您在頁面中定義Web元件可以使用的HTML塊。這有幾個好處:
- 您可以調整HTML程式碼,而無需在JavaScript中重寫字串。
- 無需為每種型別建立單獨的JavaScript類,即可自定義元件。
- 在HTML中定義HTML更容易——並且可以在元件呈現之前在伺服器或客戶端上對其進行修改。
模板是在<template>
標記中定義的,分配一個ID很實用,這樣您就可以在元件類中引用它。此示例使用三個段落顯示“Hello”訊息:
<template id="hello-world"> <style> p { text-align: center; font-weight: normal; padding: 0.5em; margin: 1px 0; background-color: #eee; border: 1px solid #666; } </style> <p class="hw-text"></p> <p class="hw-text"></p> <p class="hw-text"></p> </template>
Web元件類可以訪問此模板、獲取其內容並克隆元素以確保您在任何使用它的地方建立唯一的DOM片段:
const template = document.getElementById('hello-world').content.cloneNode(true);
DOM可以直接修改並新增到Shadow DOM中:
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }), template = document.getElementById('hello-world').content.cloneNode(true), hwMsg = `Hello ${ this.name }`; Array.from( template.querySelectorAll('.hw-text') ) .forEach( n => n.textContent = hwMsg ); shadow.append( template ); }
模板插槽Template Slots
插槽允許您自定義模板。假設您想使用Web元件<hello-world>
,但將訊息放在Shadow DOM中的<h1>標題中。你可以寫這樣的程式碼:
<hello-world name="Craig"> <h1 slot="msgtext">Hello Default!</h1> </hello-world>
(注意slot屬性。)
您可以選擇新增其他元素,例如另一個段落:
<hello-world name="Craig"> <h1 slot="msgtext">Hello Default!</h1> <p>This text will become part of the component.</p> </hello-world>
現在可以在您的模板中實現插槽:
<template id="hello-world"> <slot name="msgtext" class="hw-text"></slot> <slot></slot> </template>
將在名為“msgtext”的 <slot>
位置插入設定為“msgtext”(即<h1>
)的元素slot屬性。 <p>
沒有指定插槽名稱,但在下一個可用的未命名<slot>
中使用。實際上,模板變為:
<template id="hello-world"> <slot name="msgtext" class="hw-text"> <h1 slot="msgtext">Hello Default!</h1> </slot> <slot> <p>This text will become part of the component.</p> </slot> </template>
事實並非如此簡單。Shadow DOM中的<slot>
元素指向插入的元素。只能通過定位<slot>
然後使用.assignedNodes() 方法返回內部子級陣列來訪問它們。更新的connectedCallback()方法:
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }), hwMsg = `Hello ${ this.name }`; // append shadow DOM shadow.append( document.getElementById('hello-world').content.cloneNode(true) ); // find all slots with a hw-text class Array.from( shadow.querySelectorAll('slot.hw-text') ) // update first assignedNode in slot .forEach( n => n.assignedNodes()[0].textContent = hwMsg ); }
此外,您不能直接設定插入元素的樣式,儘管您可以定位Web元件中的特定插槽:
<template id="hello-world"> <style> slot[name="msgtext"] { color: green; } </style> <slot name="msgtext" class="hw-text"></slot> <slot></slot> </template>
模板插槽有點不尋常,但一個好處是,如果JavaScript無法執行,您的內容將被顯示。此程式碼顯示了僅在Web元件類成功執行時才替換的預設標題和段落:
<hello-world name="Craig"> <h1 slot="msgtext">Hello Default!</h1> <p>This text will become part of the component.</p> </hello-world>
因此,您可以實現某種形式的漸進增強——即使它只是一條“You need JavaScript”的訊息!
宣告式Shadow DOM
上面的例子使用JavaScript構建了一個Shadow DOM。這仍然是唯一的選擇,但正在為Chrome開發一個實驗性的宣告性Shadow DOM。這允許伺服器端渲染並避免任何佈局變化或無樣式內容的閃爍。
HTML解析器檢測到以下程式碼,它建立一個與您在上一節中建立的Shadow DOM相同的Shadow DOM(您需要根據需要更新訊息):
<hello-world name="Craig"> <template shadowroot="closed"> <slot name="msgtext" class="hw-text"></slot> <slot></slot> </template> <h1 slot="msgtext">Hello Default!</h1> <p>This text will become part of the component.</p> </hello-world>
該功能在任何瀏覽器中均不可用,並且不能保證它會到達Firefox或Safari。您可以找到有關宣告式Shadow DOM的更多資訊,polyfill很簡單,但請注意實現可能會發生變化。
Shadow DOM事件
您的Web元件可以像在頁面DOM中一樣將事件附加到Shadow DOM中的任何元素,例如偵聽所有內部子級上的單擊事件:
shadow.addEventListener('click', e => { // do something });
除非您stopPropagation ,否則該事件將冒泡到頁面DOM中,但該事件將被重定向。因此,它似乎來自您的自定義元素,而不是其中的元素。
在其他框架中使用Web元件
您建立的任何Web元件都可以在所有JavaScript框架中工作。他們都不知道或關心HTML元素——您的<hello-world>
元件將被視為與<div>
相同,並被放置到DOM中,類將在其中啟用。
custom-elements-everywhere.com提供了框架和Web元件註釋的列表。儘管在React.js有一些問題,但大多數都是完全相容的。可以在JSX中使用<hello-world>
:
import React from 'react'; import ReactDOM from 'react-dom'; import from './hello-world.js'; function MyPage() { return ( <> <hello-world name="Craig"></hello-world> </> ); } ReactDOM.render(<MyPage />, document.getElementById('root'));
…但:
- React只能將原始資料型別傳遞給HTML屬性(而不是陣列或物件)
- React無法偵聽Web元件事件,因此您必須手動附加自己的處理程式。
關於Web元件批評和問題
Web元件有了顯著改進,但有些方面可能難以管理。
樣式和難點
樣式化Web元件會帶來一些挑戰,尤其是當您想覆蓋範圍樣式時。有很多解決方案:
- 避免使用Shadow DOM。您可以將內容直接附加到您的自定義元素,儘管任何其他JavaScript都可能意外或惡意更改它。
- 使用
:host
類。正如我們在上面看到的,當類應用於自定義元素時,作用域CSS可以應用特定的樣式。 - 檢視CSS自定義屬性(變數)。自定義屬性級聯到Web元件中,因此,如果您的元素使用
var(--my-color)
,您可以在外部容器(例如:root
--my-color
,它將被使用。自定義屬性級聯到Web元件中,因此,如果您的元素使用var(--my-color)
,您可以在外部容器(例如::root
)中設定--my-color
,然後使用它。 - 利用陰影部件。新的::part() 選擇器選擇器可以設定具有part屬性的內部元件的樣式,即
<hello-world>
元件內部的<h1 part="heading">
可以使用選擇器hello-world::part(heading)
設定樣式。 - 傳入一串樣式。您可以將它們作為屬性傳遞給
<style>
塊。
沒有一個是理想的,您需要計劃其他使用者如何仔細定製您的Web元件。
忽略輸入
Shadow DOM中的任何<input>
、 <textarea>
或<select>
欄位都不會在包含表單中自動關聯。早期的Web元件採用者會將隱藏欄位新增到頁面DOM中,或者使用FormData介面來更新值。兩者都不是特別實用的,都會破壞Web元件的封裝。
新的ElementInternals介面允許Web元件連線到表單中,以便定義自定義值和有效性。它是用Chrome實現的,但是其他瀏覽器也可以使用polyfill。
為了演示,您將建立一個基本的<input-age name="your-age"></input-age>
元件。該類必須將靜態formAssociated值設定為true,並且可以選擇在外部窗體關聯時呼叫formAssociatedCallback() 方法:
// <input-age> web component class InputAge extends HTMLElement { static formAssociated = true; formAssociatedCallback(form) { console.log('form associated:', form.id); }
建構函式現在必須執行attachInternals()方法,該方法允許元件與表單和其他想要檢查值或驗證的JavaScript程式碼進行通訊:
constructor() { super(); this.internals = this.attachInternals(); this.setValue(''); } // set form value setValue(v) { this.value = v; this.internals.setFormValue(v); }
ElementInternal的setFormValue()方法在此處為使用空字串初始化的父窗體設定元素的值(也可以傳遞具有多個名稱/值對的FormData物件)。其他屬性和方法包括:
- form:父表單
- labels:標記元件的元素陣列
- 約束驗證API選項,例如willValidate、checkValidity和validationMessage
connectedCallback()方法像以前一樣建立Shadow DOM,但還必須監視欄位的更改,以便可以執行setFormValue():
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }); shadow.innerHTML = ` <style>input { width: 4em; }</style> <input type="number" placeholder="age" min="18" max="120" />`; // monitor input values shadow.querySelector('input').addEventListener('input', e => { this.setValue(e.target.value); }); }
您現在可以使用此Web元件建立一個HTML表單,其作用與其他表單欄位類似:
<form id="myform"> <input type="text" name="your-name" placeholder="name" /> <input-age name="your-age"></input-age> <button>submit</button> </form>
它有效,但不可否認,它感覺有點令人費解。在CodePen演示中檢視有關更多資訊,請參閱有關功能更強大的表單控制元件的文章。
小結
在JavaScript框架的地位和能力不斷提高的時候,Web元件很難獲得一致和採用。如果您來自React、Vue或Angular,Web元件可能會顯得複雜而笨拙,尤其是當您缺少資料繫結和狀態管理等功能時。
有很多問題需要解決,但Web元件的未來是光明的。它們是框架無關的、輕量級的、快速的,並且可以實現單獨使用JavaScript無法實現的功能。
十年前,很少有人會在沒有jQuery的情況下解決網站問題,但瀏覽器供應商採用了優秀的部分,並新增了本地替代方案(如querySelector)。JavaScript框架也是如此,Web元件是第一個嘗試性的步驟。
評論留言