【译】 Web Components 的高级工具

该系列由 5 篇文章构成,咱们在前 4 篇文章中对构成 Web Components 标准的技术进行了全面的介绍。首先,咱们研究了如何建立 HTML 模板,为接下来的工做作了铺垫。其次,咱们深刻了解了自定义元素的建立。接着,咱们将元素的样式和选择器封装到 shadow DOM 中,这样咱们的元素就彻底独立了。css

咱们经过建立本身的自定义模态对话框来探索这些工具的强大功能,该对话框能够忽略底层框架或库,在大多数现代应用程序上下文中使用。在本文中,咱们将介绍如何在各类框架中使用咱们的元素,并介绍一些高级工具用来真正提升 Web Component 的技能。html

系列文章:

  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 来更改这个行为。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

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

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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索