可重用組件的聖杯:自定義元素、Shadow DOM 和 NPM
已發表: 2022-03-10即使是最簡單的組件,人力成本也可能很高。 UX 團隊進行可用性測試。 一系列利益相關者必須在設計上簽字。
開發人員進行 AB 測試、可訪問性審計、單元測試和跨瀏覽器檢查。 一旦你解決了一個問題,你就不想重複那個努力了。 通過構建一個可重用的組件庫(而不是從頭開始構建所有東西),我們可以不斷利用過去的努力,避免重新審視已經解決的設計和開發挑戰。
建立一個組件庫對於像谷歌這樣擁有大量網站組合的公司特別有用,這些網站都共享一個共同的品牌。 通過將他們的 UI 編碼為可組合的小部件,大公司既可以加快開發時間,又可以實現跨項目的視覺和用戶交互設計的一致性。 在過去的幾年裡,人們對樣式指南和模式庫的興趣有所增加。 鑑於多個開發人員和設計師分佈在多個團隊中,大公司尋求實現一致性。 我們可以做得比簡單的色板更好。 我們需要的是易於分發的代碼。
共享和重用代碼
手動複製和粘貼代碼毫不費力。 然而,讓代碼保持最新是一場維護噩夢。 因此,許多開發人員依賴包管理器來跨項目重用代碼。 儘管它的名字,Node Package Manager 已經成為前端包管理的無與倫比的平台。 目前 NPM 註冊表中有超過 700,000 個包,每月下載數十億個包。 任何帶有 package.json 文件的文件夾都可以作為可共享包上傳到 NPM。 雖然 NPM 主要與 JavaScript 相關聯,但一個包可以包含 CSS 和標記。 NPM 使重用變得容易,更重要的是,更新代碼。 無需在無數地方修改代碼,您只需更改包中的代碼。
標記問題
Sass 和 Javascript 可以通過使用 import 語句輕鬆移植。 模板語言賦予 HTML 相同的能力——模板可以以部分的形式導入 HTML 的其他片段。 例如,您可以為您的頁腳編寫一次標記,然後將其包含在其他模板中。 說存在多種模板語言是輕描淡寫的。 將自己與一個綁定嚴重限制了代碼的潛在可重用性。 另一種方法是複制和粘貼標記並將 NPM 僅用於樣式和 javascript。
這是《金融時報》在其Origami組件庫中採用的方法。 在她的演講“你不能讓它更像 Bootstrap 嗎?” Alice Bartlett 總結道:“沒有讓人們在他們的項目中包含模板的好方法”。 談到他在 Lonely Planet 維護組件庫的經驗時,Ian Feather 重申了這種方法的問題:
“一旦他們複製了該代碼,他們實際上就是在削減一個需要無限期維護的版本。 當他們複製一個工作組件的標記時,它有一個指向 CSS 快照的隱式鏈接。 如果您隨後更新模板或重構 CSS,則需要更新散佈在您網站上的所有模板版本。”
解決方案:Web 組件
Web 組件通過在 JavaScript 中定義標記來解決這個問題。 組件的作者可以自由更改標記、CSS 和 Javascript。 組件的使用者可以從這些升級中受益,而無需手動瀏覽項目更改代碼。 可以通過終端通過簡潔的npm update
與項目範圍內的最新更改同步。 只有組件的名稱及其 API 需要保持一致。
安裝 Web 組件就像在終端中輸入npm install component-name
一樣簡單。 Javascript 可以包含在 import 語句中:
<script type="module"> import './node_modules/component-name/index.js'; </script>
然後,您可以在標記中的任何位置使用該組件。 這是一個將文本複製到剪貼板的簡單示例組件。
以組件為中心的前端開發方法已經無處不在,由 Facebook 的 React 框架引入。 不可避免地,鑑於現代前端工作流程中框架的普遍性,許多公司已經使用他們選擇的框架構建了組件庫。 這些組件只能在該特定框架內重用。
大型公司很少有統一的前端,從一個框架到另一個框架的replatorming 並不少見。 框架來來去去。 為了實現跨項目的最大潛在重用,我們需要與框架無關的組件。
“這些年來,我使用 Dojo、Mootools、Prototype、jQuery、Backbone、Thorax 和 React 構建了 Web 應用程序……我希望能夠將我一直在使用的那個殺手級 Dojo 組件帶到我的 React 中今天的應用程序。”
— Dion Almaer,谷歌工程總監
當我們談論 web 組件時,我們談論的是自定義元素與 shadow DOM 的組合。 自定義元素和影子 DOM 是 W3C DOM 規範和 WHATWG DOM 標準的一部分——這意味著 Web 組件是一種 Web 標準。 自定義元素和 shadow DOM終於在今年實現了跨瀏覽器支持。 通過使用原生 Web 平台的標準部分,我們確保我們的組件能夠經受住前端重組和架構重新思考的快速循環。 Web 組件可以與任何模板語言和任何前端框架一起使用——它們是真正的交叉兼容和互操作的。 它們可以在任何地方使用,從 Wordpress 博客到單頁應用程序。
製作 Web 組件
定義自定義元素
總是可以組成標籤名稱並將其內容顯示在頁面上。
<made-up-tag>Hello World!</made-up-tag>
HTML 被設計為容錯的。 上面將呈現,即使它不是有效的 HTML 元素。 這樣做從來沒有很好的理由——偏離標準化標籤傳統上是一種不好的做法。 然而,通過使用自定義元素 API 定義新標籤,我們可以使用具有內置功能的可重用元素來擴充 HTML。 創建自定義元素很像在 React 中創建組件——但這裡是擴展HTMLElement
。
class ExpandableBox extends HTMLElement { constructor() { super() } }
對super()
的無參數調用必須是構造函數中的第一條語句。 構造函數應該用於設置初始狀態和默認值以及設置任何事件監聽器。 需要定義一個新的自定義元素,為其 HTML 標記和元素對應的類命名:
customElements.define('expandable-box', ExpandableBox)
將類名大寫是一種慣例。 然而,HTML 標記的語法不僅僅是一種約定。 如果瀏覽器想要實現一個新的 HTML 元素並且他們想要將其稱為可擴展框怎麼辦? 為防止命名衝突,新的標準化 HTML 標記不會包含破折號。 相比之下,自定義元素的名稱必須包含破折號。
customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid
自定義元素生命週期
API 提供了四種自定義元素反應——可以在類中定義的函數,這些函數將自動調用以響應自定義元素生命週期中的某些事件。
connectedCallback在自定義元素添加到 DOM 時運行。
connectedCallback() { console.log("custom element is on the page!") }
這包括使用 Javascript 添加元素:
document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”
以及簡單地在頁面中包含帶有 HTML 標記的元素:
<expandable-box></expandable-box> // "custom element is on the page"
任何涉及獲取資源或渲染的工作都應該在這裡。
disconnectedCallback在從 DOM 中刪除自定義元素時運行。
disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"
當自定義元素被採用到新文檔中時, adoptedCallback
會運行。 您可能不需要經常擔心這個問題。
在添加、更改或刪除attributeChangedCallback
時運行 attributeChangedCallback。 它可用於偵聽對標準化原生屬性(如disabled或src )以及我們製作的任何自定義屬性的更改。 這是自定義元素最強大的方面之一,因為它可以創建用戶友好的 API。
自定義元素屬性
有很多 HTML 屬性。 為了讓瀏覽器不會浪費時間在任何屬性更改時調用我們的attributeChangedCallback
,我們需要提供一個我們想要監聽的屬性更改的列表。 對於這個例子,我們只對一個感興趣。
static get observedAttributes() { return ['expanded'] }
所以現在我們的attributeChangedCallback
只會在我們更改自定義元素上擴展屬性的值時被調用,因為它是我們列出的唯一屬性。
HTML 屬性可以有相應的值(想想href、src、alt、value等),而其他屬性要么是真要么是假(例如disabled、selected、required )。 對於具有相應值的屬性,我們將在自定義元素的類定義中包含以下內容。
get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }
對於我們的示例元素,屬性將是 true 或 false,因此定義 getter 和 setter 有點不同。
get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }
現在已經處理了樣板文件,我們可以使用attributeChangedCallback
。
attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }
傳統上,配置 Javascript 組件需要將參數傳遞給init
函數。 通過利用attributeChangedCallback
,可以製作一個僅通過標記即可配置的自定義元素。
Shadow DOM 和自定義元素可以單獨使用,您可能會發現自定義元素本身很有用。 與 shadow DOM 不同,它們可以被填充。 但是,這兩個規範可以很好地結合使用。
使用 Shadow DOM 附加標記和样式
到目前為止,我們已經處理了自定義元素的行為。 然而,關於標記和样式,我們的自定義元素相當於一個空的無樣式<span>
。 要將 HTML 和 CSS 封裝為組件的一部分,我們需要附加一個影子 DOM。 最好在構造函數中執行此操作。
class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }
不要擔心理解模式的含義——你必須包含它的樣板,但你幾乎總是想要open
。 這個簡單的示例組件只會渲染文本“hello world”。 與大多數其他 HTML 元素一樣,自定義元素可以有子元素——但默認情況下不會。 到目前為止,我們定義的上述自定義元素不會將任何子元素呈現到屏幕上。 要顯示標籤之間的任何內容,我們需要使用slot
元素。
shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `
我們可以使用樣式標籤將一些 CSS 應用於組件。
shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`
這些樣式只適用於組件,因此我們可以自由地使用元素選擇器,而這些樣式不會影響頁面的其他任何內容。 這簡化了 CSS 的編寫,不需要像 BEM 這樣的命名約定。
在 NPM 上發布組件
NPM 包通過命令行發布。 打開一個終端窗口並移動到一個您想變成可重用包的目錄。 然後在終端中輸入以下命令:
- 如果您的項目還沒有 package.json,
npm init
將引導您生成一個。 -
npm adduser
將您的機器鏈接到您的 NPM 帳戶。 如果您沒有預先存在的帳戶,它將為您創建一個新帳戶。 -
npm publish
如果一切順利,您現在在 NPM 註冊表中有一個組件,可以在您自己的項目中安裝和使用——並與全世界共享。
Web 組件 API 並不完美。 自定義元素目前無法在表單提交中包含數據。 漸進增強的故事不是很好。 處理可訪問性並不像應有的那麼容易。
儘管最初於 2011 年宣布,但瀏覽器支持仍然不是普遍的。 Firefox 支持將於今年晚些時候到期。 儘管如此,一些知名網站(如 Youtube)已經在使用它們。 儘管它們目前存在缺陷,但對於普遍共享的組件來說,它們是唯一的選擇,並且在未來,我們可以期待它們所提供的令人興奮的補充。