Angular 17+ 高级教程 – NgModule

前言

NgModule 在 Angular v14 以前是一门必修课。然而,自 Angular v14 推出 Standalone Component 以后,它的地位变得越来越边缘化。

本教程从开篇到本篇,所有例子使用的都是 Standalone Component,一点 NgModule 的影子也没有😔。

但是!NgModule 还是有它的价值的,而且在越复杂的项目中你越可以体会到它的价值。

本篇,就让我们一起学习这个被遗忘了但其实很强大的 NgModule 吧🚀。

 

NgModule 有啥用?

NgModule 主要是用于 (组件 / 指令 / Pipe) 的管理。

是的,你没听错,就是管理。

一个项目即使完全不使用 NgModule 也不会有什么功能做不出来。

用与不用只在管理上有区别,在功能上是没有区别的。

这也是为什么 NgModule 会越来越不被重视,毕竟项目只有复杂到一定程度才需要管理的丫。

 

组件 / 指令 / Pipe の 管理

Standalone Component 对组件 / 指令 / Pipe (简称组件) 的管理方式是这样的:

  1. 当一个组件想使用另一个组件时,就 import 它。
  2. 每一个组件都是公开的,意思就是任何一个组件都可以 import 任何一个组件。

Standalone Component 的管理方式简单明了,但在复杂项目中经常会遇到二个烦人问题。

A lot of component imports

比如说,我们有一个封装好的 table 组件,长这样

它不是简简单单的一个组件,要使用它还需要搭配一些相关的指令。

每一次要使用这个 table 组件,就需要 import 一个组件 +  5 个指令,总共 6 个 importable。

这 6 个 importable 是有密切关系的,但是从 imports 代码上完全看不出来。

有关联的东西,却没有被 group 在一起,这在管理上就是扣分的。

或许你会想,为什么一个 table 组件需要配那么多指令,不可以一个 table 组件完事吗?

理论上是可以,但是管理上不行,因为 table 是一个复杂的组件,如果把所有逻辑都塞在一个地方,管理就会很乱。

软件工程的奥义就是把大的逻辑拆小,通过组合和关联把小的逻辑组装大。这样只要确保小的可运行,慢慢推导到大的也可以运行,软件就稳定了。

再看一个例子:

select 和 table 都遇到了相同的问题。

Private 组件

Standalone Component 的第二条规则:

每一个组件都是公开的,意思就是任何一个组件都可以 import 任何一个组件

表面上,select 组件是由 select, option, optgroup 组件组装而成。但或许 select 组件内还使用了其它的组件,只是对于使用者来说它被封装起来了而且。

如果说 select 组件内部所使用的组件也是公开的 (因为 Standalone 组件一定是公开的),这样就会对使用者造成一些混乱,更正确的做法是引入 private 概念。

也就是说,某些组件应该只能被某些组件使用,而不是所有组件都可以使用。

 

使用 NgModule 管理组件 / 指令 / Pipe

创建一个项目

ng new module --routing=false --ssr=false --skip-tests --style=scss

Create NgModule

创建一个 NgModule 和一些组件

ng generate module dialog;
ng generate component dialog/dialog --standalone=false --module=dialog;
ng generate component dialog/dialog-public --standalone=false --module=dialog;
ng generate component dialog/dialog-private --standalone=false --module=dialog;

shortform 版本

ng g m dialog;
ng g c dialog/dialog --standalone false -m dialog;
ng g c dialog/dialog-public --standalone false -m dialog;
ng g c dialog/dialog-private --standalone false -m dialog;
View Code

这里有几个知识点:

  1. dialog/dialog 是 path 的概念
    folder 结构是这样的


    dialog 组件要放进 dialog folder

  2.  --standalone=false

    组件要使用 NgModule 做管理,就不可以是 Standalone Component。
    Standalone 的意思是独立,也就是没人管。

  3. --module=dialog
    这句的意思是,这个组件交给 dialog module 管理。 

NgModule Definition

这是一个完整的 NgModule Definition

import { NgModule } from '@angular/core';
import { DialogComponent } from './dialog/dialog.component';
import { DialogPublicComponent } from './dialog-public/dialog-public.component';
import { DialogPrivateComponent } from './dialog-private/dialog-private.component';
import { OtherModule } from '../other/other.module';              // 其它 NgModule
import { StandaloneComponent } from '../standalone/standalone.component'; // 其它 Standalone Component

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent],
  imports: [OtherModule, StandaloneComponent],
  exports: [DialogComponent, DialogPublicComponent],
})
export class DialogModule {}

语法上看,NgModule 是一个 class with @NgModule Decorator,class 本身是空的,Definition 都写在 @NgModule Decorator 的参数对象里。

它有好几个规则,我们一个一个看:

  1. declarations
    declarations 表示这个 NgModule 负责管理的组件集合。
    集合内的组件们可以相互使用,不需要在 @Component 做 imports。
    举例:
    declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent]
    在 Dialog Template 使用 DialogPrivate 组件


    Dialog 组件不需要 import DialogPrivate 组件。

  2. exports

    declarations 的组件集合只是自己跟自己玩,要让外面的人也能使用到 declarations 组件集合,我们需要 exports 它们。
    没有 export 的组件,我们称为 private 组件 (在 NgModule 内跟自己人玩),有 export 的称为 public 组件。
    举例:

    exports: [DialogComponent, DialogPublicComponent]

    外人 (App 组件) import 了 DialogModule。App Template 可以使用 Dialog 组件 和 DialogPublic 组件,但是无法使用 DialogPrivate 组件。
    因为 DialogPrivate 组件没有在 DialogModule 的 exports list 里,它是 private 的,只有 declarations 的组件可以用它。

  3. imports
    imports 表示这个 NgModule 依赖的其它 NgModule 或者组件 (Standalone Component),
    所有 declarations 的组件都可以使用 imports 的 NgModule (内的 public 组件) 和 Standalone 组件。

  4. re-export
    exports list 不仅仅可以放 declarations 的组件,也可以放 imports 的 NgModule 和 组件。
    import 了又 export 就叫 re-export。
    举例:
    imports: [OtherModule, StandaloneComponent],
    exports: [DialogComponent, DialogPublicComponent, OtherModule],

    假如 App 组件 import 了 DialogModule,App Template 可以使用 Dialog 组件,DialogPublic 组件 和 OtherModule export 的组件和 NgModule。

通过以上几个规则,NgModule 就解决了 Standalone Component 的 2 大问题:

  1. A lot of component imports
    App Template 要使用 Dialog 组件集合,不需要一个一个组件 import,只要 import 一个 DialogModule 就可以了。
  2. Private 组件
    DialogPrivate 组件不是 Standalone Component,App 组件无法直接 import 它。
    DialogPrivate 也不在 DialogModule 的 exports list 里,即使 App 组件 import DialogModule 依然无法使用 DialogPrivate。
    只有 DialogModule declarations 组件集合才可以使用到 DialogPrivate。

 

NgModule 源码逛一逛

上面我们提到的各种 NgModule 对组件管理的规则检测,并不是发生在 runtime,而是在 compilation 之前 Angular 就已经通过 Angular Language Service 做静态分析 (Static Program Analysis) 了。

规则 1:NgModule declarations 的组件集合可以相互使用。

假设 DialogModule 没有 declare DialogPublic 和 DialogPrivate。

@NgModule({
  declarations: [
    DialogComponent,
    // DialogPublicComponent,
    // DialogPrivateComponent
  ]
})
export class DialogModule {}

当我们在 Dialog Template 尝试使用 DialogPublic 或 DialogPrivate 时,IDE 就会报错。

declare DialogPublic 和 DialogPrivate 组件

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent],
})
export class DialogModule {}

declare 后 error 就没有了

规则 2:NgModule 有 export 的才是 public 组件,没有 export 就是 private 组件

假设 DialogModule 没有 exports

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent],
  exports: [
    // DialogComponent,
    // DialogPublicComponent
  ],
})
export class DialogModule {}

即便 App 组件 import 了 DialogModule 也无法使用到 Dialog 组件。

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  imports: [DialogModule], // imported DialogModule
})
export class AppComponent {
  constructor() {}
}

error on App Template

三个组件都报错了。

export Dialog 和 DialogPublic 组件。但不要 export DialogPrivate 组件。

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent],
  exports: [DialogComponent, DialogPublicComponent],
})
export class DialogModule {}

效果

只剩下 DialogPrivate 报错,因为它是 private 组件。

规则 3:NgModule imports 的组件 / NgModule,declarations 都可以使用

假设有一个 Standalone 组件和一个 OtherModule

Standalone 组件

OtherModule

Dialog Template 想使用 Standalone 组件和 Other 组件

在 DialogModule import Standalone 组件和 OtherModule (Other 组件不是 Standalone Component,所以不可以直接被 import,只可以 import 它的管理负责人 -- OtherModule)

import 后,Dialog Template 就可以使用 Standalone 组件和 Other 组件了。

规则 4:NgModule re-export

假设 App 组件 import 了 DialogModule,App Template 想使用 Other 组件

DialogModule re-export OtherModule

re-export 后,App Template 就可以使用 Other 组件了。

NgModule After Compilation

我们来看看 compile 后的 NgModule 长啥样。

这是 DialogModule,它 export 了很多组件。

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent,
  ],
  exports: [DialogComponent, DialogPublicComponent, OtherModule],
  imports: [StandaloneComponent, OtherModule],
})
export class DialogModule {}

App 组件 import 了 DialogModule

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  imports: [DialogModule],
})
export class AppComponent {
  constructor() {}
}

但是 App Template 只用到了一个 Dialog 组件

<h1>Hello World</h1>
<app-dialog />

运行 compile

yarn run ngc -p tsconfig.json

app.component.js

App Definition 的 dependencies 有一个 DialogModule 和 DialogComponent。

dialog.module.js

可以看到,虽然 DialogModule export 了很多组件,但由于 App Template 只用了其中的 Dialog 组件,所以 App Definition 只依赖了一个 Dialog 组件 (这一点和 import Standalone Component 的规则是完全一样的) 和 DialogModule。

而 DialogModule 里并没有任何 declarations 组件的信息。也就是说那些信息只用于 Angular Language Service 做静态分析而已,compile 后就不需要了。

两个结论:

  1. NgModule 管理组件只限于 Angular Language Service 静态分析,compile 以后它的行为和 Standalone Component 是完全一样的。
  2. 既然和 Standalone Component 一样,那为什么 compile 后还需要生成 DialogModule?而且 App Definition 的 dependencies 也包含了 DialogModule 丫?
    这是因为 NgModule 除了可以拿来管理组件,它还可以拿来搞 Dependency Injection 依赖注入 (为什么我用 "搞" 而不是 "管理"?因为我觉得它把东西越搞越乱就有😵)。
    好,下一 part 我们就继续讲解这个 NgModule の Dependency Injection,Let's check it out 🚀

 

NgModule の Dependency Injection

不熟悉 Dependency Injection (DI) 的朋友,可以先看这两篇 Dependency Injection 依赖注入 和 Component 组件 の Dependency Injection & NodeInjector

我们学习过 3 种提供 Provider 的方式:

  1. 通过 @Injectable 的 providedIn


    或者 InjectionToken 的 providedIn

  2. 通过 ApplicationConfig.providers

  3. 通过组件 / 指令 Decorator 的 providers

使用 NgModule 提供 Provder 是我们将学习到的四种方式。

虽然它是第四种方式,但其实它诞生的比 providedIn 和 ApplicationConfig.providers 都要早,只是它落伍了,所以我们才一直不需要去学它。

NgModule providers 长这样

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

目录

上一篇 Angular 17+ 高级教程 – HttpClient

下一篇 TODO

想查看目录,请移步 Angular 17+ 高级教程 – 目录

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章