[譯] Web Components 的高階工具

徐二斤發表於2019-04-11

該系列由 5 篇文章構成,我們在前 4 篇文章中對構成 Web Components 標準的技術進行了全面的介紹。首先,我們研究了如何建立 HTML 模板,為接下來的工作做了鋪墊。其次,我們深入瞭解了自定義元素的建立。接著,我們將元素的樣式和選擇器封裝到 shadow DOM 中,這樣我們的元素就完全獨立了。

我們通過建立自己的自定義模態對話方塊來探索這些工具的強大功能,該對話方塊可以忽略底層框架或庫,在大多數現代應用程式上下文中使用。在本文中,我們將介紹如何在各種框架中使用我們的元素,並介紹一些高階工具用來真正提高 Web Component 的技能。

系列文章:

  1. Web Components 簡介
  2. 編寫可以複用的 HTML 模板
  3. 從 0 開始建立自定義元素
  4. 使用 Shadow DOM 封裝樣式和結構
  5. Web Components 的高階工具(本文

框架相容

我們的對話方塊元件幾乎在任何框架中都可以很好地執行。(當然,如果 JavaScript 被禁用,那麼整個事情都是徒勞的。)Angular 和 Vue 將 Web Components 視為一等公民:框架的設計考慮了 Web 標準。React 稍微有點自以為是,但並非不可以整合。

Angular

首先,我們來看看 Angular 如何處理自定義元素。預設情況下,每當 Angular 遇到無法識別的元素(即預設瀏覽器元素或任何 Angular 定義的元件),它就會丟擲模板錯誤。可以通過包含 CUSTOM_ELEMENTS_SCHEMA 來更改這個行為。

...允許 NgModule 包含以下內容:

  • Non-Angular 元素用破折號(-)命名。
  • 元素屬性用破折號(-)命名。破折號是自定義元素的命名約定。

Angular 文件

使用此架構就像在模組中新增它一樣簡單:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@NgModule({
  /** 省略 */
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class MyModuleAllowsCustomElements {}
複製程式碼

就像上面這樣。之後,Angular 將允許我們在任何使用標準屬性和繫結事件的地方使用我們的自定義元素:

<one-dialog [open]="isDialogOpen" (dialog-closed)="dialogClosed($event)">
  <span slot="heading">Heading text</span>
  <div>
    <p>Body copy</p>
  </div>
</one-dialog>
複製程式碼

Vue

Vue 對 Web Components 的相容性甚至比 Angular 更好,因為它不需要任何特殊配置。註冊元素後,它可以與 Vue 的預設模板語法一起使用:

<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 的東西,我們需要建立管道,這個超出了本篇文章的討論範圍。

React

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 的風格:一個從我們的元件中抽象出通用邏輯的基類。其他型別的有 StencilSkateJSAngular ElementsPolymer

下一步

Web Components 標準不斷髮展,越來越多的新功能經過討論並被新增到瀏覽器中。很快,Web Components 的使用者將擁有用於與 Web 表單進行高階互動的 API(包括超出這些介紹性文章範圍的其他元素內部),例如原生 HTML 和 CSS 模組匯入,原生模板例項化和更新控制元件,更多的可以在 GitHub 上的 W3C/web components issues board on GitHub 進行跟蹤。

這些標準已經準備好應用到我們今天的專案中,併為舊版瀏覽器和 Edge 提供適當的 polyfill。雖然它們可能無法取代你選擇的框架,但它們可以一起使用,以增強你和你的團隊的工作流程。

系列文章:

  1. Web Components 簡介
  2. 編寫可以複用的 HTML 模板
  3. 從 0 開始建立自定義元素
  4. 使用 Shadow DOM 封裝樣式和結構
  5. Web Components 的高階工具(本文

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章