- 原文地址:Advanced Tooling for Web Components
- 原文做者:Caleb Williams
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Xuyuey
- 校對者:Long Xiong, Ziyin Feng
該系列由 5 篇文章構成,咱們在前 4 篇文章中對構成 Web Components 標準的技術進行了全面的介紹。首先,咱們研究了如何建立 HTML 模板,爲接下來的工做作了鋪墊。其次,咱們深刻了解了自定義元素的建立。接着,咱們將元素的樣式和選擇器封裝到 shadow DOM 中,這樣咱們的元素就徹底獨立了。css
咱們經過建立本身的自定義模態對話框來探索這些工具的強大功能,該對話框能夠忽略底層框架或庫,在大多數現代應用程序上下文中使用。在本文中,咱們將介紹如何在各類框架中使用咱們的元素,並介紹一些高級工具用來真正提升 Web Component 的技能。html
咱們的對話框組件幾乎在任何框架中均可以很好地運行。(固然,若是 JavaScript 被禁用,那麼整個事情都是徒勞的。)Angular 和 Vue 將 Web Components 視爲一等公民:框架的設計考慮了 Web 標準。React 稍微有點自覺得是,但並不是不能夠整合。前端
首先,咱們來看看 Angular 如何處理自定義元素。默認狀況下,每當 Angular 遇到沒法識別的元素(即默認瀏覽器元素或任何 Angular 定義的組件),它就會拋出模板錯誤。能夠經過包含 CUSTOM_ELEMENTS_SCHEMA
來更改這個行爲。node
...容許 NgModule 包含如下內容:react
- Non-Angular 元素用破折號(
-
)命名。- 元素屬性用破折號(
-
)命名。破折號是自定義元素的命名約定。— Angular 文檔android
使用此架構就像在模塊中添加它同樣簡單:ios
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
/** 省略 */
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class MyModuleAllowsCustomElements {}
複製代碼
就像上面這樣。以後,Angular 將容許咱們在任何使用標準屬性和綁定事件的地方使用咱們的自定義元素:git
<one-dialog [open]="isDialogOpen" (dialog-closed)="dialogClosed($event)">
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</one-dialog>
複製代碼
Vue 對 Web Components 的兼容性甚至比 Angular 更好,由於它不須要任何特殊配置。註冊元素後,它能夠與 Vue 的默認模板語法一塊兒使用:github
<one-dialog v-bind:open="isDialogOpen" v-on:dialog-closed="dialogClosed">
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</one-dialog>
複製代碼
然而,Angular 和 Vue 都須要注意的是它們的默認表單控件。若是咱們但願使用一個相似於可響應的表單或者 Angular 的 [(ng-model)]
或者 Vue 中的 v-model
的東西,咱們須要創建管道,這個超出了本篇文章的討論範圍。web
React 比 Angular 稍微複雜一點。React 的虛擬 DOM 有效地獲取了一個 JSX 樹並將其渲染爲一個大對象。所以,React 不是像 Angular 或 Vue 同樣,直接修改 HTML 元素上的屬性,而是使用對象語法來跟蹤須要對 DOM 進行的更改並批量更新它們。在大多數狀況下這很好用。咱們將對話框的 open 屬性綁定到對象的屬性上,在改變屬性時響應很是好。
當咱們關閉對話框,開始調度 CustomEvent
時,會出現問題。React 使用合成事件系統爲咱們實現了一系列原生事件監聽器。不幸的是,這意味着像 onDialogClosed
這樣的控制方法實際上不會將事件監聽器附加到咱們的組件上,所以咱們必須找到其餘方法。
在 React 中添加自定義事件監聽器的最著名的方法是使用 DOM refs。在這個模型中,咱們能夠直接引用咱們的 HTML 節點。語法有點冗長,但效果很好:
import React, { Component, createRef } from 'react';
export default class MyComponent extends Component {
constructor(props) {
super(props);
// 建立引用
this.dialog = createRef();
// 在實例上綁定咱們的方法
this.onDialogClosed = this.onDialogClosed.bind(this);
this.state = {
open: false
};
}
componentDidMount() {
// 組件構建完成後,添加事件監聽器
this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
}
componentWillUnmount() {
// 卸載組件時,刪除監聽器
this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
}
onDialogClosed(event) { /** 省略 **/ }
render() {
return <div>
<one-dialog open={this.state.open} ref={this.dialog}>
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</one-dialog>
</div>
}
}
複製代碼
或者,咱們可使用無狀態函數組件和鉤子:
import React, { useState, useEffect, useRef } from 'react';
export default function MyComponent(props) {
const [ dialogOpen, setDialogOpen ] = useState(false);
const oneDialog = useRef(null);
const onDialogClosed = event => console.log(event);
useEffect(() => {
oneDialog.current.addEventListener('dialog-closed', onDialogClosed);
return () => oneDialog.current.removeEventListener('dialog-closed', onDialogClosed)
});
return <div>
<button onClick={() => setDialogOpen(true)}>Open dialog</button>
<one-dialog ref={oneDialog} open={dialogOpen}>
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</one-dialog>
</div>
}
複製代碼
這個還不錯,但你能夠看到重用這個組件很快會變得很麻煩。幸運的是,咱們能夠導出一個默認的 React 組件,它使用相同的工具包裹咱們的自定義元素。
import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';
export default class OneDialog extends Component {
constructor(props) {
super(props);
// 建立引用
this.dialog = createRef();
// 在實例上綁定咱們的方法
this.onDialogClosed = this.onDialogClosed.bind(this);
}
componentDidMount() {
// 組件構建完成後,添加事件監聽器
this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
}
componentWillUnmount() {
// 卸載組件時,刪除監聽器
this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
}
onDialogClosed(event) {
// 在調用屬性以前進行檢查以確保它是存在的
if (this.props.onDialogClosed) {
this.props.onDialogClosed(event);
}
}
render() {
const { children, onDialogClosed, ...props } = this.props;
return <one-dialog {...props} ref={this.dialog}>
{children}
</one-dialog>
}
}
OneDialog.propTypes = {
children: children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
onDialogClosed: PropTypes.func
};
複製代碼
...或者,再次使用無狀態函數組件和鉤子:
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
export default function OneDialog(props) {
const { children, onDialogClosed, ...restProps } = props;
const oneDialog = useRef(null);
useEffect(() => {
onDialogClosed ? oneDialog.current.addEventListener('dialog-closed', onDialogClosed) : null;
return () => {
onDialogClosed ? oneDialog.current.removeEventListener('dialog-closed', onDialogClosed) : null;
};
});
return <one-dialog ref={oneDialog} {...restProps}>{children}</one-dialog>
}
複製代碼
如今咱們能夠在 React 中使用咱們的對話框,並且能夠在咱們全部的應用程序中保持相同的 API(若是你喜歡的話,還能夠不使用類)。
import React, { useState } from 'react';
import OneDialog from './OneDialog';
export default function MyComponent(props) {
const [open, setOpen] = useState(false);
return <div>
<button onClick={() => setOpen(true)}>Open dialog</button>
<OneDialog open={open} onDialogClosed={() => setOpen(false)}>
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</OneDialog>
</div>
}
複製代碼
有不少很是棒的工具能夠用來編寫你的自定義元素。在 npm 上進行搜索,你能找到許多用於建立高響應性自定義元素的工具(包括我本身的寵物項目),但到目前爲止最流行的是來自 Polymer 團隊的 lit-html,對 Web Components 來講更具體的是指,LitElement。
LitElement 是一個自定義元素基類,它提供了一系列 API,能夠用於完成咱們迄今爲止所作的全部事情。不用構建它也能夠在瀏覽器中運行,但若是你喜歡使用更前沿的工具,如裝飾器,那麼也可使用它。
在深刻了解如何使用 lit 或 LitElement 以前,請花一點時間熟悉 帶標籤的模板字符串(tagged template literals),這是一種特殊的函數,能夠在 JavaScript 中調用模板字符串。這些函數接受一個字符串數組和一組內插值,並能夠返回你可能想要的任何內容。
function tag(strings, ...values) {
console.log({ strings, values });
return true;
}
const who = 'world';
tag`hello ${who}`;
/** 會打印出 { strings: ['hello ', ''], values: ['world'] },而且返回 true **/
複製代碼
LitElement 爲咱們提供的是對傳遞給該值數組的任何內容的實時動態更新,所以當屬性更新時,將調用元素的 render 函數並從新渲染呈現 DOM。
import { LitElement, html } from 'lit-element';
class SomeComponent {
static get properties() {
return {
now: { type: String }
};
}
connectedCallback() {
// 必定要調用 super
super.connectedCallback();
this.interval = window.setInterval(() => {
this.now = Date.now();
});
}
disconnectedCallback() {
super.disconnectedCallback();
window.clearInterval(this.interval);
}
render() {
return html`<h1>It is ${this.now}</h1>`;
}
}
customElements.define('some-component', SomeComponent);
複製代碼
在 CodePen 查看 LitElement 示例。
你會注意到咱們必須使用 static properties
getter 定義任何咱們想要 LitElement 監視的屬性。使用該 API 會告訴基類每當對組件的屬性進行更改時都要調用 render
函數。反過來,render
將僅更新須要更改的節點。
所以,對於咱們的對話框示例,它使用 LitElement 時看起來像這樣:
在 CodePen 查看 使用 LitElement 的對話框示例。
有幾種可用的 lit-html 的變體,包括 Haunted,一個用於 Web Components 的 React 鉤子庫,也可使用 lit-html 做爲基礎來使用虛擬組件。
目前,大多數現代 Web Components 工具都是 LitElement
的風格:一個從咱們的組件中抽象出通用邏輯的基類。其餘類型的有 Stencil、SkateJS、Angular Elements 和 Polymer。
Web Components 標準不斷髮展,愈來愈多的新功能通過討論並被添加到瀏覽器中。很快,Web Components 的使用者將擁有用於與 Web 表單進行高級交互的 API(包括超出這些介紹性文章範圍的其餘元素內部),例如原生 HTML 和 CSS 模塊導入,原生模板實例化和更新控件,更多的能夠在 GitHub 上的 W3C/web components issues board on GitHub 進行跟蹤。
這些標準已經準備好應用到咱們今天的項目中,併爲舊版瀏覽器和 Edge 提供適當的 polyfill。雖然它們可能沒法取代你選擇的框架,但它們能夠一塊兒使用,以加強你和你的團隊的工做流程。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。