使用 Web Components 时可能遇到的 10 个注意事项
Web 组件已经存在了一段时间,有望提供一种创建可重复使用的自定义元素的标准化方法。显然,尽管 Web 组件取得了长足的进步,但开发人员在使用它们时仍可能面临一些注意事项。本博客将探讨其中的 10 个注意事项。
1. 框架特定问题
如果您正在决定是否在项目中使用 Web 组件。重要的是要考虑您选择的框架是否完全支持 Web 组件,否则您可能会遇到一些不愉快的警告。
角度
例如,要在 Angular 中使用 Web 组件,需要在模块导入中添加“CUSTOM_ELEMENTS_SCHEMA”。
@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class MyModule {}
使用 `CUSTOM_ELEMENTS_SCHEMA` 的问题在于 Angular 将退出模板中自定义元素的类型检查和智能感知。(参见问题)
为了解决这个问题,您可以创建一个 Angular 包装器组件。
下面是其样子的一个例子。
@Component({ selector: 'some-web-component-wrapper', template: '}) export class SomeWebComponentWrapper { @Input() someClassProperty: string; } @NgModule({ declarations: [SomeWebComponentWrapper], exports: [SomeWebComponentWrapper], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class WrapperModule {}
这可行,但手动创建这些组件并不是一个好主意。因为这会产生大量维护工作,而且我们可能会遇到 API 不同步的问题。为了让这个过程不那么繁琐。Lit(参见此处)和 Stencil(参见此处)都提供了一个 CLI 来自动创建这些组件。但是,首先需要创建这些包装器组件会带来额外的开销。如果您选择的框架正确支持 Web 组件,则您不必创建包装器组件。
反应
另一个例子是 React。现在 React v19 刚刚发布,解决了这些问题。但是,如果您仍在使用 v18,请注意 v18 不完全支持 Web 组件。因此,以下是您在 React v18 中使用 Web 组件时可能遇到的几个问题。这直接取自 Lit 文档。
“React 假定所有 JSX 属性都映射到 HTML 元素属性,并且不提供设置属性的方法。这使得将复杂数据(如对象、数组或函数)传递给 Web 组件变得很困难。”
“React 还假设所有 DOM 事件都有相应的“事件属性”(onclick、onmousemove 等),并使用这些属性而不是调用 addEventListener()。这意味着要正确使用更复杂的 Web 组件,您通常必须使用 ref() 和命令式代码。”
对于 React v18,Lit 建议使用其包装器组件,因为它们可以解决设置属性和监听事件的问题。
下面是使用 Lit 的 React 包装器组件的示例。
import React from 'react'; import { createComponent } from '@lit/react'; import { MyElement } from './my-element.js'; export const MyElementComponent = createComponent({ tagName: 'my-element', elementClass: MyElement, react: React, events: { onactivate: 'activate', onchange: 'change', }, });
用法
setIsActive(e.active)} onchange={handleChange} />
幸运的是,有了 React v19,您不再需要创建包装器组件。耶!
在微前端中使用 Web 组件揭示了一个有趣的挑战:
2. 全局注册问题
一个重大问题是自定义元素注册表的全局性:
如果您正在使用微前端并计划使用 Web 组件在每个应用程序中重用 UI 元素,则很可能会遇到此错误。
Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': the name "foo-bar" has already been used with this registry
尝试注册名称已被使用的自定义元素时会出现此错误。这在微前端中很常见,因为微前端中的每个应用都共享同一个 index.html 文件,并且每个应用都会尝试定义自定义元素。
有一项提案旨在解决此问题,称为“Scoped Custom Element Registries”,但是没有预计到达的时间,因此很遗憾您需要使用 polyfill。
如果您不使用 polyfill,一种解决方法是手动使用前缀注册自定义元素,以避免命名冲突。
要在 Lit 中执行此操作,您可以避免使用自动注册自定义元素的 `@customElement` 装饰器。然后为 `tagName` 添加一个静态属性。
前
@customElement('simple-greeting') export class SimpleGreeting extends LitElement { render() { return html`Hello world!
`; } }
后
export class SimpleGreeting extends LitElement { static tagName = 'simple-greeting'; render() { return html`Hello world!
`; } }
然后在每个应用程序中使用应用程序名称的前缀定义自定义元素。
[SimpleGreeting].forEach((component) => { const newTag = `app1-${component.tagName}`; if (!customElements.get(newTag)) { customElements.define(newTag, SimpleGreeting); } });
然后,要使用自定义元素,您需要将其与新前缀一起使用。
这是一种快速的短期解决方案,但是您可能会注意到它不是最佳的开发人员体验,因此建议使用 Scoped Custom Element Registry polyfill。
3. 继承的样式
Shadow DOM 在提供封装的同时,也带来了一系列挑战:
Shadow dom 通过提供封装来工作。它可以防止样式从组件中泄漏出来。它还可以防止全局样式定位组件 shadow dom 内的元素。但是,如果继承了这些样式,组件外部的样式仍然可能泄漏进来。
这是一个例子。
import { html, css, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @customElement('simple-greeting') export class SimpleGreeting extends LitElement { static styles = css``; @property() name = 'Somebody'; render() { return html`Hello, ${this.name}!
`; } }
文本颜色应用于 light dom 中的 Web 组件外部,但由于“color”是继承的 css 属性,因此组件 shadow dom 内的文本也将继承该颜色。现在这只是文本颜色的一个示例,但还有更多继承的 css 属性。以下是常见属性的列表。
4. 实用程序类的限制
之前我们讨论了 shadow dom 如何阻止 light dom 的样式定位 shadow dom 内的元素。这也意味着我们不能使用像 tailwindcss 这样的实用程序类框架。
如今,像 tailwindcss 这样的实用类 CSS 框架非常流行,这是有原因的。我不会在这篇博客中讨论这个问题。但不幸的是,在 Web 组件中,至少在 Lit 中,我们只能在 JS 中使用 CSS。这不仅效率较低,而且我们还需要确保设置我们的构建系统来处理最小化 JS 模板字符串中的 CSS。如果不这样做,它会降低应用程序的性能,因为它会增加应用程序的 JS 包大小。这是 CSS in JS 框架遇到的主要问题,因此他们不得不提出零基于运行时的解决方案。
5. 事件重定向
通常,如果没有 shadow dom,您可能会习惯于“target”是对事件被分派到的对象的引用的事件。而“currentTarget”是附加事件处理程序的元素。
然而,这在 shadow dom 中工作方式不同。当在 shadow dom 中发出 `composed` 事件时。该事件将被重新定位,因此 `target` 和 `currentTarget` 将是具有事件侦听器的 Lit 组件。
这是一个例子。
组件-b
import { html, css, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @customElement('component-b') export class ComponentB extends LitElement { override render() { return html``; } }
当我们点击“` 按钮发出一个 `composed` 事件并 `bubbles`。
组件-a
import { html, css, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import './component-b.js'; @customElement('component-a') export class ComponentA extends LitElement { override connectedCallback() { super.connectedCallback(); this.addEventListener('click', (e) => { console.log(e); }); } override render() { return html``; } }
由于事件来自“component-b”,您可能会认为“目标”将是“component-b”或“按钮”。然而,事件被重新定位,因此“目标”变成了“component-a”。
因此,如果您需要知道某个事件是否来自`` 或 `` 您需要检查事件的组合路径。
6. 整个页面重新加载
如果在影子 dom 中使用 ` ` 链接(如本例所示),它将触发应用中的整页重新加载。
import { html, css, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @customElement('some-link') export class SomeLink extends LitElement { static styles = css``; @property() href = ''; render() { return html``; } }
这是因为路由是由浏览器而不是框架处理的。框架需要干预这些事件并在框架级别处理路由。但是,由于事件在 shadow dom 中重新定位,这使得框架执行此操作更具挑战性,因为它们无法轻松访问锚元素。
为了解决这个问题,我们可以在 ` ` 上设置一个事件处理程序,它将停止事件的传播并发出一个新事件。新事件将需要 `` 冒泡 '' 并 `` 组合 ''。此外,在细节上我们需要访问
到我们可以从 `e.currentTarget` 获得的 ` ` 实例。
import { html, css, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @customElement('some-link') export class SomeLink extends LitElement { static styles = css``; @property() href = ''; render() { return html``; } private _handleClick(e: Event) { e.stopPropagation(); this.dispatchEvent( new CustomEvent('some-link-clicked', { bubbles: true, composed: true, detail: e.currentTarget, }) ); } }
在使用方面,您可以设置一个全局事件监听器来监听此事件,并通过调用框架特定的路由函数来处理路由。
7. 嵌套阴影 DOM
构建 Web 组件时。您可以决定插入其他 Web 组件或将它们嵌套在另一个组件中。以下是示例。
有槽的图标
嵌套图标
如果您决定嵌套组件,则查询嵌套组件会变得更加困难。特别是如果您有一个需要创建端到端测试的 QA 团队,因为他们需要针对页面上的特定元素。
例如,为了访问“some-icon”,我们首先需要通过获取它的“shadowRoot”来访问“some-banner”,然后在该影子根内创建一个新的查询。
someBannerEl.shadowRoot?.queryElement('some-icon');
这看起来很简单,但组件嵌套得越深,难度就越大。此外,如果组件是嵌套的,则使用工具提示会更加困难。特别是如果您需要定位一个嵌套很深的元素,以便在其下方显示工具提示时。
我发现使用插槽可以让我们的组件更小、更灵活,也更易于维护。因此,请优先使用插槽,避免嵌套 shadow dom。
8. 有限的 ::slotted 选择器
插槽提供了一种组合 UI 元素的方法,但它们在 Web 组件中有局限性。
`::slotted` 选择器仅适用于插槽的直接子项,限制了其在更复杂场景中的实用性。
这是一个例子。
/* ✅ works */ ::slotted(.first) { background: red; } /* ❌ does not work */ ::slotted(.second) { background: orange; }
FirstSecondFirst
这里使用 `::slotted`,您只能定位直接子元素(具有 `.first` 类的 div)。在 Web 组件中,这是出于性能原因而设计的
但需要记住的是,当开发人员首次使用 Web 组件时,我经常会发现这是一个陷阱。
9. 开槽元素始终位于 dom 中
Web 组件使用 `` 来渲染子元素。但是,无论 `` 存在或不存在。
通过将子项保留在 dom 中,这可能会产生一种不必要的行为,即在内容再次显示后状态仍可继续存在。
一种常见的情况是放置一个“` 在模态框内。我们通常不希望输入在模态框关闭并重新打开时保持其状态。(见示例)
import { html, css, LitElement, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; @customElement('some-component') export class SomeComponent extends LitElement { @state() private _isOpen = true; render() { return html` ${this._isOpen ? html`` : nothing} `; } private _toggle() { this._isOpen = !this._isOpen; } }
10. 功能采用速度较慢
在采用新功能和最佳实践方面,Web 组件通常落后于 Vue、React、Svelte 和 Solid 等流行框架。
这可能是因为 Web 组件依赖于浏览器的实现和标准,与现代 JavaScript 框架的快速开发周期相比,它们可能需要更长的时间来发展。
因此,开发人员可能会发现自己正在等待某些功能或必须实施其他框架中现有的解决方法。
例如,Lit 使用 JS 中的 CSS 作为样式的默认选项。众所周知,JS 框架中的 CSS 存在性能问题
因为它们经常会引入额外的运行时开销。所以我们开始看到较新的 CSS in JS 框架转向基于零运行时的解决方案。
Lit 的 CSS in JS 解决方案仍然基于运行时。
另一个例子是信号。目前 Lit 中的默认行为是我们通过添加 `@property` 装饰器来增加类属性的反应性。
但是,当属性发生变化时,它将触发整个组件重新渲染。使用信号时,只有依赖信号的部分组件才会更新。
这对于处理 UI 来说更加高效。如此高效以至于有一项新提案 (TC39) 将其添加到 JavaScript 中。
现在 Lit 确实提供了一个使用 Signals 的包,但是当 Vue 和 Solid 等其他框架已经这样做多年时,它并不是默认的反应性。
在信号成为网络标准的一部分之前,我们很可能在未来几年内不会将信号视为默认的反应方式。
还有一个例子与我之前的警告“9. 开槽元素始终在 dom 中”有关。Svelte 的创建者 Rich Harris 谈到了这一点
在他五年前的博客文章“为什么我不使用 Web 组件”中。
他谈到了他们如何在 Svelte v2 中采用 Web 标准方法来快速呈现插槽内容。然而,他们不得不放弃这种方法
在 Svelte 3 中,因为这是开发人员的一大挫败点。他们注意到,大多数时候你都希望插槽内容能够延迟渲染。
我可以举出更多的例子,例如在 Web 组件中,没有简单的方法将数据传递给插槽,而其他框架(如 Vuejs)已经支持这一点。但这里的主要结论是
由于依赖于 Web 标准,因此 Web 组件采用功能的速度比不依赖于 Web 标准的框架慢得多。
通过不依赖网络标准,我们可以创新并提出更好的解决方案。
结论
Web 组件提供了一种创建可重复使用和封装的自定义元素的强大方法。但是,正如我们所探讨的,开发人员在使用它们时可能会面临一些注意事项和挑战。例如框架不兼容、在微前端中的使用、Shadow DOM 的限制、事件重定向问题、插槽和功能采用缓慢都是需要仔细考虑的领域。
尽管存在这些挑战,但 Web 组件的优势(例如真正的封装、可移植性和框架独立性)使其成为现代 Web 开发中的重要工具。随着生态系统的不断发展,我们可以期待看到解决这些问题的改进和新解决方案。
对于考虑使用 Web 组件的开发人员来说,权衡这些利弊并了解该领域的最新进展至关重要。通过正确的方法和理解,Web 组件可以成为您开发工具包的强大补充。