关于 Angular 中定义公共组件的一些概念和误区

44 阅读6分钟

本文介绍关于 Angular 中定义公共组件的一些概念和误区,希望对您有所启发。

1. 澄清误区

在 Angular 中,AppModule 中通过 declarations 声明的组件不能直接在其他子模块中使用。‌

在 Angular 中,NgModule 的 declarations 属性用于声明当前模块中的组件、指令和管道。这些元素只能在当前 NgModule 中使用,而不能直接在其他子模块中使用。

例如,如果一个组件在 AppModule 的 declarations 中声明,那么它只能在 AppModule 中的其他组件模板中使用,而不能在子模块中使用。

如果需要在其他子模块中使用某个组件,可以通过以下几种方式:

  • 导出和导入 ‌:在 AppModule 中通过 exports 属性将组件导出,然后在需要使用该组件的子模块中通过 imports 属性导入该模块。这样,子模块就可以使用 AppModule 中导出的组件了。

  • 共享模块 ‌:创建一个共享模块,将需要共享的组件、指令和管道声明在该模块中,并通过 exports 属性导出。然后,在其他需要使用这些元素的模块中导入这个共享模块即可。

通过这些方法,可以在 Angular 项目中灵活地管理和使用组件、指令和管道等元素。

2. 澄清误区 2

当我们创建一个公共组件时(注意这里说的是非 standalone 组件),我们的目标是使其能够在不同的上下文和模块中重用。在这个过程中,我们的公共组件的 HTML 模板可能会依赖于 Angular 的一些指令(如[routerLink])或者其他组件,这些依赖通常是由包含该公共组件的 NgModule 提供的。由于公共组件本身并不属于任何一个特定的 NgModule,它不会拥有自己的依赖注入环境,这意味着它的 HTML 模板中的指令和组件引用必须能够在未来的任何宿主模块中正常工作。

在 Angular 中,指令(如[routerLink])是与特定的 NgModule(如RouterModule)相关联的。当你在公共组件的 HTML 模板中使用这些指令时,Angular 的编译器并不会立即检查这些指令是否可用,因为公共组件可能会被用在不同的模块中,而这些模块可能会在不同的编译条件和编译步骤中提供必要的依赖。

因此,当你在公共组件中使用[routerLink]这样的指令时,Angular 编译器在静态分析阶段不会报错,因为它假设最终使用该公共组件的 NgModule 会导入RouterModule。这种假设是基于 Angular 的模块化和懒加载特性,即具体的依赖关系是在运行时根据模块的导入来解析的。

然而,如果在运行时,宿主 NgModule 没有导入RouterModule,那么尝试使用[routerLink]指令时就会出现错误。这是因为[routerLink]指令依赖于RouterModule提供的服务,如果没有导入RouterModule,这些服务就不可用,导致运行时错误。

为了避免这种情况,我们需要确保在【使用】公共组件的 NgModule 中正确导入了所有必要的模块。例如,如果你的公共组件使用了[routerLink],那么在使用该组件的 NgModule 中,你需要确保已经导入了RouterModule

import { RouterModule } from "@angular/router";

@NgModule({
  imports: [
    RouterModule,
    // 其他模块
  ],
  // ...
})
export class SomeFeatureModule {}

通过这种方式,你可以确保在运行时,所有必要的依赖都已经就绪,从而避免因缺少依赖而导致的错误。这也是为什么在开发公共组件时,我们需要密切关注组件的宿主环境,确保它们提供了所有必要的依赖。

3. 通过 web-component 实现全局组件

在 Angular 中,要将一个组件定义为 web component,你需要使用 Angular 的@angular/platform-browser 库中的 customElements API。以下是一个简单的例子:

首先,确保你的 Angular 项目支持 web components。在 tsconfig.json 中,确保你有以下配置:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "lib": ["es2015", "dom"]
  }
}

然后,创建一个服务来帮助我们创建 web component:

import { NgModule, Injector, NgZone, ApplicationRef } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { YourComponent } from './your.component';

@NgModule({
  declarations: [YourComponent],
  entryComponents: [YourComponent],
  imports: [BrowserModule]
})

export class AppModule {
  constructor(private injector: Injector) {
    const customElement = createCustomElement(YourComponent, { injector: this.injector });
    customElements.define('your-component', customElement);
  }

  ngDoBootstrap() {}
}

在这个例子中,YourComponent 是你想要定义为 web component 的组件,AppModule 是包含这个组件的模块。

需要额外说明的是,如果我们的组件是 standalone 的,则将声明改成引入即可:

import { NgModule, Injector, NgZone, ApplicationRef } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { YourStandaloneComponent } from './your-standalone.component';

@NgModule({
  declarations: [],
  imports: [BrowserModule, YourStandaloneComponent]
})

export class AppModule {
  constructor(private injector: Injector) {
    const customElement = createCustomElement(YourStandaloneComponent, { injector: this.injector });
    customElements.define('your-standalone-component', customElement);
  }

  ngDoBootstrap() {}
}

最后,在主模块的 imports 数组中引入 AppModule,并在 ngDoBootstrap 方法中调用 createCustomElement 和 customElements.define:

import { AppModule } from "./app.module";

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .then(() => {
    // 现在你可以在 HTML 中使用 `<your-component>` 标签了
  })
  .catch((err) => console.error(err));

在 HTML 中使用这个 web component:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Web Component Example</title>
    <script src="bundle.js"></script>
    <!-- 假设这是你打包后的JS文件 -->
  </head>
  <body>
    <your-component></your-component>
    <your-standalone-component></your-standalone-component>
  </body>
</html>

确保你的构建系统能够处理这个 JavaScript 文件,并且在你的 HTML 文件中正确引入它。这样,你的 Angular 组件就成为了一个可以在任何支持 web components 的环境中使用的 web component。

示例

对于上面的两种 web component 组件,此 app 分别给一个示例,一个是 common/icons(普通组件) 一个是 common/breadcrumb(standalone 组件)

4. 双管齐下

使用 Web Components 技术的初衷是为了实现组件的重用和封装,避免在每个子模块中重复引入相同的公共组件。无论是独立的(standalone)组件还是非独立的,我们都希望通过一种方式来简化它们的引入和使用过程。通过使用 element 库将公共组件封装成 Web Component,我们可以像使用原生 HTML 标签一样在任何地方直接使用这些组件,这是 Web Components 技术为我们解决的核心问题。

然而,即使我们已经将公共组件封装成了 Web Components,我们仍然可以选择按照 Angular 的传统方式引入和使用这些组件。这意味着我们可以为同一个公共组件定义两个“身份”:一个是 Angular 组件的selector,例如common-breadcrumb;另一个是通过 Web Components 技术定义的自定义 HTML 标签,例如the-common-breadcrumb。这种方法被称为“双管齐下”,它允许我们在不同的场景下灵活选择使用哪种方式来引入和使用组件。

这种双重策略的主要目的是为了提供一种回退机制。在某些情况下,如果 Web Components 的使用遇到了兼容性问题或者出现了其他潜在的错误,我们可以轻松地切换回传统的 Angular 组件引入方式,以确保应用的稳定性和可靠性。这种灵活性使得我们能够在享受 Web Components 带来的便利性的同时,也保持了对传统方法的兼容性,从而在不同的开发和部署环境中都能够确保组件的正常工作。

总结来说,通过 Web Components 技术,我们不仅简化了公共组件的引入和使用,还通过保留传统的 Angular 组件引入方式,为应用的稳定性和兼容性提供了额外的保障。这种策略使得我们可以在不同的开发阶段和不同的项目需求中,灵活地选择最适合的组件引入和使用方式。