在本五部分系列文章的过去四篇文章中,我们对构成 Web 组件标准的技术进行了 广泛的了解。 首先,我们研究了 如何创建可在以后使用 的 HTML 模板。 其次,我们深入研究了 从头开始创建我们自己的自定义元素。 之后,我们将元素的样式和选择器封装到影子 DOM 中,这样我们的元素就完全独立了。
我们通过创建自己的自定义模态对话框探索了这些工具的强大功能,该元素可以在大多数现代应用程序上下文中使用,无论底层框架或库是什么。 在本文中,我们将介绍如何在各种框架中使用我们的元素,并了解一些高级工具,以真正提高您的 Web 组件技能。
系列文章
- Web 组件简介
- 创建可复用的 HTML 模板
- 从头开始创建自定义元素
- 使用 Shadow DOM 封装样式和结构
- Web 组件的高级工具(本文)
框架无关
我们的对话框组件在几乎任何框架中都能很好地工作,甚至没有框架也能工作。(当然,如果禁用了 JavaScript,那么整个操作就毫无意义。)Angular 和 Vue 将 Web 组件视为一等公民:这些框架的设计考虑到了 Web 标准。React 的观点稍微坚定一些,但并不难以集成。
Angular
首先,让我们看一下 Angular 如何处理自定义元素。 默认情况下,Angular 遇到无法识别的元素(即默认浏览器元素或 Angular 定义的任何组件)时会抛出模板错误。 通过包含 CUSTOM_ELEMENTS_SCHEMA
可以更改此行为。
…允许 NgModule 包含以下内容
- 使用连字符命名法(
-
)命名的非 Angular 元素。- 使用连字符命名法(
-
)命名的元素属性。 连字符命名法是自定义元素的命名约定。
使用此模式就像将其添加到模块一样简单
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
/** Omitted */
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 组件的兼容性比 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 属性绑定到其属性,并且会对更改的 props 响应得很好。
问题出现在我们开始查看对话框关闭时分派的 CustomEvent
时。 React 通过其 合成事件系统 为我们实现了一系列原生事件监听器。 不幸的是,这意味着像 onDialogClosed
这样的控件实际上不会为我们的组件附加事件监听器,因此我们必须找到其他方法。
在 React 中添加自定义事件监听器的最明显方法是使用 DOM refs。 在此模型中,我们可以直接引用我们的 HTML 节点。 语法有点冗长,但效果很好
import React, { Component, createRef } from 'react';
export default class MyComponent extends Component {
constructor(props) {
super(props);
// Create the ref
this.dialog = createRef();
// Bind our method to the instance
this.onDialogClosed = this.onDialogClosed.bind(this);
this.state = {
open: false
};
}
componentDidMount() {
// Once the component mounds, add the event listener
this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
}
componentWillUnmount() {
// When the component unmounts, remove the listener
this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
}
onDialogClosed(event) { /** Omitted **/ }
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);
// Create the ref
this.dialog = createRef();
// Bind our method to the instance
this.onDialogClosed = this.onDialogClosed.bind(this);
}
componentDidMount() {
// Once the component mounds, add the event listener
this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
}
componentWillUnmount() {
// When the component unmounts, remove the listener
this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
}
onDialogClosed(event) {
// Check to make sure the prop is present before calling it
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 组件来说是 LitElement。
LitElement 是一个自定义元素基类,它提供了一系列 API 来执行我们迄今为止讨论的所有操作。 它可以在浏览器中运行,无需构建步骤,但是如果您喜欢使用面向未来的工具(如装饰器),也有一些工具可以满足您的需求。
在深入研究如何使用 lit 或 LitElement 之前,请花点时间熟悉 带标签的模板字面量,这是一种在 JavaScript 中对模板字面量字符串调用的特殊函数。 这些函数接收字符串数组和插值值的集合,并且可以返回您可能想要的任何东西。
function tag(strings, ...values) {
console.log({ strings, values });
return true;
}
const who = 'world';
tag`hello ${who}`;
/** would log out { strings: ['hello ', ''], values: ['world'] } and return true **/
LitElement 为我们提供了传递给该值数组的任何内容的实时动态更新,因此当属性更新时,元素的 render 函数将被调用,并且生成的 DOM 将重新渲染。
import { LitElement, html } from 'lit-element';
class SomeComponent {
static get properties() {
return {
now: { type: String }
};
}
connectedCallback() {
// Be sure to call the 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 上 Caleb Williams (@calebdwilliams) 的
LitElement now 示例。
在 CodePen 上。
您会注意到,我们必须使用 static properties
getter 来定义我们希望 LitElement 监视的任何属性。 使用该 API 告诉基类在组件属性发生更改时调用 render。 render
反过来只会更新需要更改的节点。
因此,对于我们的对话框示例,使用 LitElement 将是这样的
查看 CodePen 上 Caleb Williams (@calebdwilliams) 的
使用 LitElement 的对话框示例。
在 CodePen 上。
lit-html 有几种变体,包括 Haunted,这是一个针对 Web 组件的 React 钩子式库,它还可以使用 lit-html 作为基础来利用虚拟组件。
归根结底,大多数现代 Web 组件工具都是 LitElement
的各种变体:一个从我们的组件中抽象出通用逻辑的基类。 其他变体包括 Stencil、SkateJS、Angular Elements 和 Polymer。
下一步
Web Components 标准仍在不断发展,新功能正在不断讨论并添加到浏览器中。 很快,Web Component 作者将拥有与 Web 表单进行高级交互的 API(包括这些入门文章范围之外的其他元素内部信息),例如原生 HTML 和 CSS 模块导入、原生模板实例化和更新控件,以及更多可以跟踪在 GitHub 上的 W3C/web components 问题看板 上。
这些标准已准备好使用适用于传统浏览器和 Edge 的适当 polyfill 集成到我们今天的项目中。 虽然它们可能无法取代您选择的框架,但它们可以与它们一起使用以增强您和您组织的工作流程。
您可能想看看 https://github.com/elmsln/WCFactory
“一个生产 Web Components 的工厂,与库无关,并具有统一的开发、测试和构建到生产的管道。”
我真的很感谢这一系列写得很好且易于理解的文章。 我以前对 Web Components 和自定义元素知之甚少,现在学到了很多东西。 谢谢!
嘿,Sean,感谢您的反馈,我理解您的想法。 但事实是,您实际上不需要任何特殊工具来创建 Web Components。 它们可以用 JS、TypeScript、CoffeeScript 或您想要的任何其他语言编写。 我将倾向于在我的 Web Component 库中使用 Rollup,因为它比 Webpack 轻便得多,但 Webpack 也能正常工作。 如果您想了解有关 Web Components 构建工具的深入信息,可以看看 Open WC,我通常同意那里的内容,它绝对是一个不错的起点。
当我想到高级工具时,我想到了 Webpack 设置。 但在这里没有找到。
好的? 您是否感到失望? 您是否有一些相关的经验可以分享?
我想要这个的原因是,如果没有像 Webpack 这样的工具,你就无法非常轻松地在组件内部导入 npm 模块。 所以,您的应用程序的大部分都是由组件组成的,如果没有某种构建工具,您就无法真正导入任何地方。 或者使用 Stencil 或类似的东西。
您多次提到
无状态函数组件
,这些组件确实是用函数编写的。 但是,如果这些组件使用钩子,它们几乎肯定不是无状态的。 函数也必须理解为不属于函数式编程,而是使用函数语法。 React 钩子不幸地导致一些开发人员混淆了语法和含义。您所说的无状态函数组件的一个简单例子是一个
计数器
组件,它不接受任何输入,使用useState
钩子,并返回一个<span>${count}</span>
,其中count
在每次调用时递增。 如果该函数是纯函数(或如您所说无状态函数),考虑到它不接受任何参数,它将始终返回相同的 HTML,而事实并非如此。总之,这些函数确实是 React 组件,它们确实遵循函数语法,但它们不是无状态的,也不是纯函数(一般而言)。
还有 Stencil。