首页 >web前端 >js教程 >使用 Web 组件时可能面临的注意事项

使用 Web 组件时可能面临的注意事项

Susan Sarandon
Susan Sarandon原创
2024-12-09 03:19:11760浏览

Caveats You May Face While Working With Web Components

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: '<some-web-component [someProperty]="someClassProperty"></some-web-component>
})
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',
  },
});

用法

<MyElementComponent
  active={isActive}
  onactivate={(e) => setIsActive(e.active)}
  onchange={handleChange}
/>

幸运的是,有了 React v19,你就不再需要创建包装组件了。耶!

在微前端中使用 Web 组件揭示了一个有趣的挑战:

2. 全局注册问题

一个重要的问题是自定义元素注册表的全局性质:

如果您使用微前端并计划使用 Web 组件在每个应用程序中重用 UI 元素,您很可能会遇到此错误。

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class MyModule {}

尝试使用已使用的名称注册自定义元素时会发生此错误。这在微前端中很常见,因为微前端中的每个应用程序共享相同的 index.html 文件,并且每个应用程序都尝试定义自定义元素。

有一项提案可以解决这个问题,称为“范围自定义元素注册表”,但没有预计到达时间,因此不幸的是,您需要使用填充。

如果您不使用polyfill,一种解决方法是使用前缀手动注册自定义元素,以避免命名冲突。

要在 Lit 中执行此操作,您可以避免使用自动注册自定义元素的 @customElement 装饰器。然后为 tagName 添加静态属性。

之前

@Component({
  selector: 'some-web-component-wrapper',
  template: '<some-web-component [someProperty]="someClassProperty"></some-web-component>
})
export class SomeWebComponentWrapper {
  @Input() someClassProperty: string;
}

@NgModule({
    declarations: [SomeWebComponentWrapper],
    exports: [SomeWebComponentWrapper],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class WrapperModule {}

之后

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',
  },
});

然后在每个应用程序中,您可以使用应用程序名称的前缀定义自定义元素。

<MyElementComponent
  active={isActive}
  onactivate={(e) => setIsActive(e.active)}
  onchange={handleChange}
/>

然后要使用自定义元素,您可以将其与新前缀一起使用。

Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry':
the name "foo-bar" has already been used with this registry

这是一个快速的短期解决方案,但是您可能会注意到这不是最好的开发人员体验,因此建议使用范围自定义元素注册表polyfill。

3. 继承风格

Shadow DOM 在提供封装的同时,也带来了自己的一系列挑战:

Shadow dom 通过提供封装来工作。它可以防止样式从组件中泄漏。它还可以防止全局样式定位组件的 Shadow dom 内的元素。但是,如果继承了组件外部的样式,这些样式仍然可能会泄漏。

这是一个例子。

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  render() {
    return html`<p>Hello world!</p>`;
  }
}
export class SimpleGreeting extends LitElement {
  static tagName = 'simple-greeting';

  render() {
    return html`<p>Hello world!</p>`;
  }
}

当我们点击时该按钮会发出一个冒泡的组合事件。

组件-a

[SimpleGreeting].forEach((component) => {
  const newTag = `app1-${component.tagName}`;
  if (!customElements.get(newTag)) {
    customElements.define(newTag, SimpleGreeting);
  }
});

由于事件来自组件 b,您可能会认为目标是组件 b 或按钮。然而,事件被重新定位,因此目标成为组件 a。

因此,如果您需要知道事件是否来自

6. 整页重新加载

如果链接在 Shadow dom 中使用,如本例所示,它将在您的应用程序中触发整个页面重新加载。

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class MyModule {}

这是因为路由是由浏览器而不是您的框架处理的。框架需要干预这些事件并在框架级别处理路由。然而,由于事件在 Shadow dom 中被重新定位,这使得框架这样做更具挑战性,因为它们无法轻松访问锚元素。

要解决此问题,我们可以在 上设置一个事件处理程序这将停止事件的传播并发出一个新事件。新事件需要冒泡并组合。此外,我们还需要访问详细信息
我们可以从 e.currentTarget 获取实例。

@Component({
  selector: 'some-web-component-wrapper',
  template: '<some-web-component [someProperty]="someClassProperty"></some-web-component>
})
export class SomeWebComponentWrapper {
  @Input() someClassProperty: string;
}

@NgModule({
    declarations: [SomeWebComponentWrapper],
    exports: [SomeWebComponentWrapper],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class WrapperModule {}

在消费端,您可以设置一个全局事件监听器来监听此事件并通过调用框架特定的路由函数来处理路由。

7. 嵌套影子dom

构建 Web 组件时。您可以决定将其他 Web 组件放入插槽或将它们嵌套在另一个 Web 组件中。这是一个例子。

开槽图标

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',
  },
});

嵌套图标

<MyElementComponent
  active={isActive}
  onactivate={(e) => setIsActive(e.active)}
  onchange={handleChange}
/>

如果您决定嵌套组件,这可能会使查询嵌套组件变得更加困难。特别是如果您有一个 QA 团队需要创建端到端测试,因为他们需要针对页面上的特定元素。
例如,要访问 some-icon,我们需要首先通过获取 some-banner 的shadowRoot 来访问它,然后在该影子根内创建一个新查询。

Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry':
the name "foo-bar" has already been used with this registry

这可能看起来很简单,但组件嵌套得越深,它就会变得越来越困难。此外,如果您的组件是嵌套的,这可能会使工具提示的使用变得更加困难。特别是如果您需要定位深层嵌套的元素,以便可以在其下方显示工具提示。

我发现使用插槽使我们的组件更小、更灵活,也更易于维护。所以更喜欢插槽,避免嵌套 Shadow dom。

8. 有限::slotted Selector

插槽提供了一种组合 UI 元素的方法,但它们在 Web 组件中存在局限性。

::slotted 选择器仅适用于插槽的直接子级,限制了它在更复杂场景中的用处。

这是一个例子。

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  render() {
    return html`<p>Hello world!</p>`;
  }
}
export class SimpleGreeting extends LitElement {
  static tagName = 'simple-greeting';

  render() {
    return html`<p>Hello world!</p>`;
  }
}
[SimpleGreeting].forEach((component) => {
  const newTag = `app1-${component.tagName}`;
  if (!customElements.get(newTag)) {
    customElements.define(newTag, SimpleGreeting);
  }
});

10. 功能采用速度较慢

Web 组件在采用新功能和最佳实践方面通常落后于 Vue、React、Svelte 和 Solid 等流行框架。
这可能是由于 Web 组件依赖于浏览器实现和标准,与现代 JavaScript 框架的快速开发周期相比,这可能需要更长的时间才能发展。
因此,开发人员可能会发现自己正在等待某些功能,或者必须实现其他框架中随时可用的解决方法。

Lit 的一些例子是在 JS 中使用 CSS 作为样式的默认选项。人们很早就知道 JS 框架中的 CSS 存在性能问题
因为它们经常引入额外的运行时开销。因此,我们开始在 JS 框架中看到更新的 CSS,这些框架切换到基于零运行时的解决方案。
Lit 的 CSS in JS 解决方案仍然是基于运行时的。

另一个例子是信号。目前 Lit 中的默认行为是我们通过添加 @property 装饰器来向类属性添加反应性。
但是,当属性发生更改时,它将触发整个组件重新渲染。使用信号,只有依赖信号的组件的一部分才会更新。
这对于使用 UI 来说更加高效。如此高效,以至于有一个新提案(TC39)将其添加到 JavaScript 中。
现在 Lit 确实提供了一个使用 Signals 的包,但它并不是默认的反应性,而 Vue 和 Solid 等其他框架已经这样做了很多年。
在信号成为网络标准之前,我们很可能在几年内不会将信号视为默认反应性。

另一个例子与我之前的警告“9. 开槽元素总是在 dom 中”相关。 Svelte 的创始人 Rich Harris 谈到了这个
在他 5 年前题为“为什么我不使用 Web 组件”的博客文章中。
他谈到了他们如何采用 Web 标准方法来在 Svelte v2 中快速呈现分槽内容。然而,他们不得不远离它
在 Svelte 3 中,因为这对开发人员来说是一个很大的挫败点。他们注意到大多数时候您希望开槽内容延迟渲染。

我可以举出更多示例,例如在 Web 组件中,当 Vuejs 等其他框架已经支持此操作时,就没有简单的方法将数据传递到插槽。但这里的主要要点是
Web 组件由于依赖于 Web 标准,因此采用的功能比不依赖于 Web 标准的框架慢得多。
通过不依赖网络标准,我们可以创新并提出更好的解决方案。

结论

Web 组件提供了一种强大的方法来创建可重用和封装的自定义元素。然而,正如我们所探讨的,开发人员在使用它们时可能会面临一些注意事项和挑战。例如框架不兼容、微前端的使用、Shadow DOM 的限制、事件重定向问题、插槽以及缓慢的功能采用都是需要仔细考虑的领域。

尽管存在这些挑战,Web 组件的优点(例如真正的封装、可移植性和框架独立性)使它们成为现代 Web 开发中的宝贵工具。随着生态系统的不断发展,我们预计会看到解决这些问题的改进和新的解决方案。

对于考虑 Web Components 的开发人员来说,权衡这些利弊并随时了解该领域的最新进展至关重要。通过正确的方法和理解,Web 组件可以成为您的开发工具包的强大补充。

以上是使用 Web 组件时可能面临的注意事项的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn