[Web翻译]Angular + Web Components:一个完整的指南

1,924 阅读10分钟

原文地址:indepth.dev/angular-web…

原文作者:indepth.dev/author/arme…

发布时间:2020年2月5日 - 8分钟阅读

在这篇文章中,你将知道如何使用Angular元素在页面内整合一个单一的Angular组件。我们将详细探讨这如何工作,以及如何使用Web Components调试应用程序。

去年,我的任务是在一个现有的ASP.Net应用程序中加入几个Angular组件。当时已经实现了一个混乱的解决方案(一个巨大的脚本,它加载了一个经典的Angular应用和所有的组件,而不管它们与当前页面的相关性),显然,这在速度上有点 "小 "效率不高。当然,这种方法在软件开发过程中也是一个地狱,我需要运行一个Gulp脚本,重建ASP.Net应用,并且每改一个微小的CSS都要硬生生的重新加载页面。无论是应用性能,还是开发过程,团队都不满意,所以我的目标是

  1. 创建一个新的解决方案,它将允许在一个页面中加入一个单一的Angular组件,而不是整个应用程序。
  2. 工作速度更快,不会加载大量不必要的脚本。
  3. 将允许更快的开发构建时间,而不需要不断地重新运行脚本。

所以,我自然而然地转向了Web Components。

但什么是Web Components呢?

Web Components是来自React或Angular等框架的组件的概念,但整体上是为web而构建的,与框架无关。这意味着什么呢?

我们习惯于在我们的UI中使用我们简单的HTML标签。例如,我们知道divspan等标签。它们有预定义的行为,可能会被用在我们视图的不同地方。

Web Components本质上允许我们使用JavaScript创建新的HTML标签/元素。让我们看一个小例子,看看如何纯粹用JavaScript来实现这个功能。

class SpanWithText extends HTMLSpanElement {
    constructor() {
        super();
        this.innerText = `Hello, I am a Web Component!`;
    }
}
customElements.define('span-with-text', SpanWithText);

在这里,我们创建一个类(就像我们在Angular中为组件写一个类一样),这个类是从HTMLElement(更准确的说是我们的HTMLSpanElement)扩展而来的,每当创建这个元素的时候,它就会有一个值为Hello, I am a Web Component的innerText。所以,每当我们在DOM中使用我们的元素时,它本身就会有一个填充的文本。

<span-with-text></span-with-text>

酷,我可以把它和Angular一起使用吗?

当然,如果能把我们的Angular组件变成Web Components,无论在什么地方使用,无论环境如何,都是一件好事。而事实证明,通过使用@angular/elements,我们恰恰可以做到这一点。

工作原理:

首先,我们要启动一个新的Angular应用。我们要用一个特殊的标志来创建它,这样它就只会创建配置文件,而不会创建模板代码(AppModule/AppComponent等等...)。

ng new web-components --createApplication=false。

我们最终会得到一个空的Angular应用。现在,让我们在新的项目目录下创建我们的第一个Web Components。

ng generate application FirstWebComponent --skipInstall=true。

添加这个标志来跳过重新安装任何依赖关系。

所以,这个命令在我们的根目录下创建了一个projects文件夹,里面会有一个名为FirstWebComponent的文件夹。如果我们看一下里面,我们会看到一个典型的Angular应用:一个main.ts文件,一个app.module,一个app.component和其他东西。现在,我们需要获取@angular/elements库,所以我们运行一下

ng add @angular/elements

这将把库带到我们的node_modules文件夹里面,这样我们就可以用它把我们的组件变成Web Components。

关于Angular组件变成Web Components,最重要的一点是,它们是简单的Angular组件--它们的任何东西都需要不同,就可以作为Web Components使用。你可以在它们里面做任何你可以用普通的Angular组件做的事情,而且会正常工作。为了让你的组件工作,你必须做的任何配置步骤都是在模块级别完成的;组件本身没有任何改变。这意味着你可以将几乎任何现有的Angular组件变成一个Web Components,而不需要太多麻烦。

下一步,让我们在新制作的项目中创建一个Angular组件,它将成为我们的Web Components。

ng generate component UIButton

现在我们要做的是让Angular明白,我们不想让它把这个组件当成一个普通的Angular组件,而是要把它当成一个不同的东西。这是在模块引导层完成的--我们要做的是在AppModule上实现ngDoBootstrap方法,并告诉我们的模块定义一个自定义元素。为此,我们将使用@angular/elements包中的createCustomElement函数。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, DoBootstrap, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

@NgModule({
  declarations: [
    UIButtonComponent,
  ],
  imports: [
    BrowserModule,
  ],
  entryComponents: [UIButtonComponent],
})
export class AppModule implements DoBootstrap {

  constructor(private injector: Injector) {
    const webComponent = createCustomElement(UIButtonComponent, {injector});
    customElements.define('ui-button', webComponent);
  }

  ngDoBootstrap() {}
 }

有几个重要的事情需要注意。

  1. 我们必须手动将injector传递给我们的Web Components。这是为了确保依赖注入在运行时工作。
  2. 我们必须将我们的组件放在 entryComponents 数组中。这是Web Components被引导所需要的。
  3. createCustomElement是一个函数,事实上,它将我们自己的组件变成了Web Components--我们将该函数的结果传递给customElements.define,而不是我们的Angular组件。
  4. 我们Angular组件的选择器是不相关的。如何从其他HTML模板中调用它是由我们传递给customElements.define函数的字符串决定的。在这种情况下,它将被调用为<ui-button></ui-button>
  5. 我们传递给customElements.define函数的选择器必须由两个或多个用破折号分隔的单词组成。这与其说是Angular本身的怪癖,不如说是Custom Elements API的要求(所以它可以将自定义元素与原生HTML标签区分开来)。

我们的下一步是构建应用!

ng build FirstWebComponent

当我们查看dist文件夹内的文件时,我们会看到以下内容。

构建后生成的文件

这些是我们需要在另一个应用程序中包含的文件,以便能够使用我们的Web Components。为了达到这个目的,我们可以将这个文件复制到另一个项目中,并以不同的方式包含它。

  1. 如果我们要在一个React应用中使用它,我们可以安装polyfill,然后使用简单的import语句来包含我们的文件。你可以在Vaadin的官方博文中阅读更多关于它的内容。
  2. 为了在纯HTML应用中使用它,我们必须通过脚本标签包含我们所有生成的文件,然后使用该组件。
<html>
  <head>
    <script src="./built-files/polyfills.js"></script>
    <script src="./built-files/vendor.js"></script>
    <script src="./built-files/runtime.js"></script>
    <script src="./built-files/styles.js"></script>
    <script src="./built-files/scripts.js"></script>
  </head>
  <body>
    <ui-button></ui-button>
  </body>
</html>
  1. 为了在另一个Angular应用中使用它,我们其实并不需要构建组件,由于它是一个简单的Angular组件,我们可以直接导入并使用它。但如果我们只能访问编译文件,我们可以在目标项目中的angular.json文件中的scripts字段中包含它们,并schemas: [CUSTOM_ELEMENTS_SCHEMA],添加到我们的app.module.ts文件中。

所以,这第一部分很简单。有几件事要注意。

  1. 如果我们的web Components里面有输入,当我们使用它时,命名模式就会改变。我们在Angular组件中使用camelCase,但如果要从其他HTML文件中访问该输入,我们就必须使用kebab-case。
@Component({/* metadata */})
export class UIButtonComponent {
  @Input() shouldCountClicks = false;
}
<ui-button should-count-click="true"></ui-button>
  1. 如果我们的Angular组件里面有输出,我们可以通过addEventListener来监听发出的合成事件。没有其他方法,包括React--通常的on<EventName={callback}>是不行的,因为这是一个合成事件。

但是浏览器的兼容性呢?

到目前为止,一切都很好--我们已经构建了我们的应用程序,并成功地在主要的现代浏览器中进行了测试,比如说Chrome。但所有网络开发者的祸根--IE呢?如果我们打开我们的应用程序,在其中我们使用一个Web Components,我们会看到我们的按钮与计数器已经成功地呈现。那么,我们就好了?不尽然。

如果我们有让组件对按钮的点击次数进行计数的功能,那么当我们在Internet Explorer中点击后,我们会看到计数器没有被更新。为了省去你进一步的调查--问题是Angular Web Components中的变化检测在IE上无法工作。那么,我们应该忘记IE上的Web Components吗?不,值得庆幸的是,有一个简单的变通方法。我们必须实现一个新的变更检测区策略,或者导入一个。有一个小包正好可以满足我们的需求。

npm i elements-zone-strategy

并稍微修改我们的app.module.ts文件。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, DoBootstrap, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

@NgModule({
  declarations: [
    UIButtonComponent,
  ],
  imports: [
    BrowserModule,
  ],
  entryComponents: [UIButtonComponent],
})
export class AppModule implements DoBootstrap {

  constructor(private injector: Injector) {
    const strategyFactory = new ElementZoneStrategyFactory(UIButtonComponent, injector);
    const webComponent = createCustomElement(UIButtonComponent, {injector, strategyFactory});
    customElements.define('log-activity', webComponent);
  }

  ngDoBootstrap() {}

在这里,我们只是调用了ElementZoneStrategyFactory构造函数,得到了一个新的strategyFactory,并把它和注入器一起传递给createCustomElement函数。

现在,你可以在IE中打开同样的组件,令人惊讶的是,它可以工作了。变更检测现在在IE上也能正常工作了。

不要忘了polyfills

你会经常访问polyfills.ts文件。我的建议是自定义是,不要一次性加载所有的polyfills,只加载需要的。

我如何调试?

在Angular Web Components中,调试(以及一般的错误信息)是一个有点问题的问题。你没有热重载,而且你在另一个环境中运行你构建的组件,而不是Angular应用,所以你可能会想知道你怎么能得到这些。其实,你可以修改一下index.html文件,把你的Web Components的选择器而不是app-root

然后像往常一样运行你的应用。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>UIButton</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <ui-button></ui-button>
</body>
</html>
ng serve UIButton

这将产生一个只包含你的web Components的Angular应用。这样做的一个好处是,你将有热重载(这是一个大问题)。但也有一个缺点:错误信息经常会被咀嚼:例如,如果你忘记提供你的一个服务,在一个常见的Angular应用程序中,你会得到一个愤怒的StaticInjectorError,详细说明哪个服务没有提供。在我们的情况下,我们只会看到一个空白的屏幕,而不会出现任何错误--这是一个需要调查并感到沮丧的谜团。

结论

这个基本设置为我们提供了构建和部署使用Angular Components和Web Components的应用程序所需的一切。但这个设置还远远不够理想--我们希望有一些脚本来实现

  1. 自动生成新的Web Components
  2. 自动运行我们的构建过程的脚本
  3. 热重装

在我的下一篇文章中,我们将开始深入研究这个脚本,使我们的开发过程更加有趣。


通过www.DeepL.com/Translator (免费版)翻译