Angular 项目中的可摇树依赖 - Tree-shakable dependencies
Tree-shakable dependencies in Angular projects
Tree-shakable 依赖更容易推理和编译成更小的包。
Angular 模块 (NgModules) 曾经是提供应用程序范围依赖项(例如常量、配置、函数和基于类的服务)的主要方式。 从 Angular 版本 6 开始,我们可以创建可摇树的依赖项,甚至可以忽略 Angular 模块。
Angular module providers create hard dependencies
当我们使用 NgModule 装饰器工厂的 providers 选项提供依赖项时,Angular 模块文件顶部的 import 语句引用了依赖项文件。
这意味着 Angular 模块中提供的所有服务都成为包的一部分,即使是那些不被 declarable 或其他依赖项使用的服务。 让我们称这些为硬依赖,因为它们不能被我们的构建过程摇树。
相反,我们可以通过让依赖文件引用 Angular 模块文件来反转依赖关系。 这意味着即使应用程序导入了 Angular 模块,它也不会引用依赖项,直到它在例如组件中使用依赖项。
Providing singleton services
许多基于类的服务被称为应用程序范围的单例服务——或者简称为单例服务,因为我们很少在平台注入器级别使用它们。
Pre-Angular 6 singleton service providers
在 Angular 版本 2 到 5 中,我们必须向 NgModule 的 providers 选项添加单例服务。 然后我们必须注意,只有急切加载的 Angular 模块才会导入提供的 Angular 模块——按照惯例,这是我们应用程序的 CoreModule。
// pre-six-singleton.service.ts import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; @Injectable() export class PreSixSingletonService { constructor(private http: HttpClient) {} } // pre-six.module.ts import { NgModule } from '@angular/core'; import { PreSixSingletonService } from './pre-six-singleton.service'; @NgModule({ providers: [PreSixSingletonService], }) export class PreSixModule {} // core.module.ts import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { PreSixModule } from './pre-six.module.ts'; @NgModule({ imports: [HttpClientModule, PreSixModule], }) export class CoreModule {}
以上是 Pre-Angular 6 singleton service.
如果我们在延迟加载的功能模块中导入提供 Angular 的模块,我们将获得不同的服务实例。
Providing services in mixed Angular modules
当在带有可声明的 Angular 模块中提供服务时,我们应该使用 forRoot 模式来表明它是一个混合的 Angular 模块——它同时提供了可声明和依赖项。
这很重要,因为在延迟加载的 Angular 模块中导入具有依赖项提供程序的 Angular 模块将为该模块注入器创建新的服务实例。 即使已经在根模块注入器中创建了一个实例,也会发生这种情况。
// pre-six-mixed.module.ts import { ModuleWithProviders, NgModule } from '@angular/core'; import { MyComponent } from './my.component'; import { PreSixSingletonService } from './pre-six-singleton.service'; @NgModule({ declarations: [MyComponent], exports: [MyComponent], }) export class PreSixMixedModule { static forRoot(): ModuleWithProviders { return { ngModule: PreSixMixedModule, providers: [PreSixSingletonService], }; } }
以上是 The forRoot pattern for singleton services.
静态 forRoot 方法用于我们的 CoreModule,它成为根模块注入器的一部分。
Tree-shakable singleton service providers
幸运的是,Angular 6 向 Injectable 装饰器工厂添加了 providedIn 选项。 这是声明应用程序范围的单例服务的一种更简单的方法。
// modern-singleton.service.ts import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class ModernSingletonService { constructor(private http: HttpClient) {} }
以上是 Modern singleton service.
单例服务是在第一次构建依赖它的任何组件时创建的。
始终使用 Injectable 装饰基于类的服务被认为是最佳实践。 它配置 Angular 以通过服务构造函数注入依赖项。
在 Angular 版本 6 之前,如果我们的服务没有依赖项,则 Injectable 装饰器在技术上是不必要的。 尽管如此,添加它仍然被认为是最佳实践,以便我们在以后添加依赖项时不会忘记这样做。
现在我们有了 providedIn 选项,我们还有另一个理由总是将 Injectable 装饰器添加到我们的单例服务中。
这个经验法则的一个例外是,如果我们创建的服务总是打算由工厂提供者构建(使用 useFactory 选项)。 如果是这种情况,我们不应指示 Angular 将依赖项注入其构造函数。
providedIn: 'root'
该选项将在根模块注入器中提供单例服务。 这是为引导的 Angular 模块创建的注入器——按照惯例是 AppModule.事实上,这个注入器用于所有急切加载的 Angular 模块。
或者,我们可以将 providedIn 选项引用到一个 Angular 模块,这类似于我们过去对混合 Angular 模块使用 forRoot 模式所做的事情,但有一些例外。
// modern-singleton.service.ts import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ModernMixedModule } from './modern-mixed.module'; @Injectable({ providedIn: ModernMixedModule, }) export class ModernSingletonService { constructor(private http: HttpClient) {} } // modern-mixed.module.ts import { NgModule } from '@angular/core'; import { MyComponent } from './my.component'; @NgModule({ declarations: [MyComponent], exports: [MyComponent], }) export class ModernMixedModule {}
单例服务的现代 forRoot 替代方案。
与 'root' 选项值相比,使用此方法有两个不同之处:
除非已导入提供的 Angular 模块,否则无法注入单例服务。
由于单独的模块注入器,延迟加载的 Angular 模块和 AppModule 会创建自己的实例。
Providing primitive values
假设我们的任务是向 Internet Explorer 11 用户显示弃用通知。 我们将创建一个 InjectionToken。
这允许我们将布尔标志注入服务、组件等。 同时,我们只对每个模块注入器评估一次 Internet Explorer 11 检测表达式。 这意味着根模块注入器一次,延迟加载模块注入器一次。
在 Angular 版本 4 和 5 中,我们必须使用 Angular 模块为注入令牌提供值。
首先新建一个 token 实例:
// is-internet-explorer.token.ts import { InjectionToken } from '@angular/core'; export const isInternetExplorer11Token: InjectionToken<boolean> = new InjectionToken('Internet Explorer 11 flag');
然后新建一个 module,通过 factory 为该 token 指定运行时应该注入什么样的值:
// internet-explorer.module.ts import { NgModule } from '@angular/core'; import { isInternetExplorer11Token } from './is-internet-explorer-11.token'; @NgModule({ providers: [ { provide: isInternetExplorer11Token, useFactory: (): boolean => /Trident\/7\.0.+rv:11\.0/.test(navigator.userAgent), }, ], }) export class InternetExplorerModule {}
以上是:Angular 4–5 dependency injection token with factory provider.
Angular 6 的改进:
从 Angular 版本 6 开始,我们可以将工厂传递给 InjectionToken 构造函数,从而不再需要 Angular 模块。
// is-internet-explorer-11.token.ts import { InjectionToken } from '@angular/core'; export const isInternetExplorer11Token: InjectionToken<boolean> = new InjectionToken('Internet Explorer 11 flag', { factory: (): boolean => /Trident\/7\.0.+rv:11\.0/.test(navigator.userAgent), providedIn: 'root', });
使用工厂提供程序时,providedIn 默认为“root”,但让我们通过保留它来明确。 它也与使用 Injectable 装饰器工厂声明提供者的方式更加一致。
Value factories with dependencies
我们决定将 user agent 字符串提取到它自己的依赖注入令牌中,我们可以在多个地方使用它,并且每个模块注入器只从浏览器读取一次。
在 Angular 版本 4 和 5 中,我们必须使用 deps 选项(依赖项的缩写)来声明工厂依赖项。
// user-agent.token.ts import { InjectionToken } from '@angular/core'; export const userAgentToken: InjectionToken<string> = new InjectionToken('User agent string'); // is-internet-explorer.token.ts import { InjectionToken } from '@angular/core'; export const isInternetExplorer11Token: InjectionToken<boolean> = new InjectionToken('Internet Explorer 11 flag'); // internet-explorer.module.ts,在一个 module 里同时提供两个 token 的值 import { Inject, NgModule } from '@angular/core'; import { isInternetExplorer11Token } from './is-internet-explorer.token'; import { userAgentToken } from './user-agent.token'; @NgModule({ providers: [ { provide: userAgentToken, useFactory: () => navigator.userAgent }, { deps: [[new Inject(userAgentToken)]], provide: isInternetExplorer11Token, useFactory: (userAgent: string): boolean => /Trident\/7\.0.+rv:11\.0/.test(userAgent), }, ], }) export class InternetExplorerModule {}
不幸的是,依赖注入令牌构造函数目前不允许我们声明工厂提供程序依赖项。 相反,我们必须使用来自@angular/core 的注入函数。
// user-agent.token.ts import { InjectionToken } from '@angular/core'; export const userAgentToken: InjectionToken<string> = new InjectionToken('User agent string', { factory: (): string => navigator.userAgent, providedIn: 'root', }); // is-internet-explorer-11.token.ts import { inject, InjectionToken } from '@angular/core'; import { userAgentToken } from './user-agent.token'; export const isInternetExplorer11Token: InjectionToken<boolean> = new InjectionToken('Internet Explorer 11 flag', { factory: (): boolean => /Trident\/7\.0.+rv:11\.0/.test(inject(userAgentToken)), providedIn: 'root', });
以上是 Angular 6 之后,如何实例化具有依赖关系的 injection token 的代码示例。
注入函数从提供它的模块注入器中注入依赖项——在这个例子中是根模块注入器。 它可以被 tree-shakable 提供者中的工厂使用。 Tree-shakable 基于类的服务也可以在它们的构造函数和属性初始化器中使用它。
Providing platform-specific APIs
为了利用特定于平台的 API 并确保高水平的可测试性,我们可以使用依赖注入令牌来提供 API。
让我们看一个 Location 的例子。 在浏览器中,它可用作全局变量 location,另外在 document.location 中。 它在 TypeScript 中具有 Location 类型。 如果你在你的一个服务中通过类型注入它,你可能没有意识到 Location 是一个接口。
接口是 TypeScript 中的编译时工件,Angular 无法将其用作依赖注入令牌。 Angular 在运行时解决依赖关系,因此我们必须使用在运行时可用的软件工件。 很像 Map 或 WeakMap 的键。
相反,我们创建了一个依赖注入令牌并使用它来将 Location 注入到例如服务中。
// location.token.ts import { InjectionToken } from '@angular/core'; export const locationToken: InjectionToken<Location> = new InjectionToken('Location API'); // browser.module.ts import { NgModule } from '@angular/core'; import { locationToken } from './location.token'; @NgModule({ providers: [{ provide: locationToken, useFactory: (): Location => document.location }], }) export class BrowserModule {}
以上是 Angular 4 - 5 的老式写法。
Angular 6 的新式写法:
// location.token.ts import { InjectionToken } from '@angular/core'; export const locationToken: InjectionToken<Location> = new InjectionToken('Location API', { factory: (): Location => document.location, providedIn: 'root', });
在 API 工厂中,我们使用全局变量 document
. 这是在工厂中解析 Location API 的依赖项。 我们可以创建另一个依赖注入令牌,但事实证明 Angular 已经为这个特定于平台的 API 公开了一个——由@angular/common 包导出的 DOCUMENT 依赖注入令牌。
在 Angular 版本 4 和 5 中,我们将通过将其添加到 deps 选项来声明工厂提供程序中的依赖项。
// location.token.ts import { InjectionToken } from '@angular/core'; export const locationToken: InjectionToken<Location> = new InjectionToken('Location API'); // browser.module.ts import { DOCUMENT } from '@angular/common'; import { Inject, NgModule } from '@angular/core'; import { locationToken } from './location.token'; @NgModule({ providers: [ { deps: [[new Inject(DOCUMENT)]], provide: locationToken, useFactory: (document: Document): Location => document.location, }, ], }) export class BrowserModule {}
下面是新式写法:
和以前一样,我们可以通过将工厂传递给依赖注入令牌构造函数来摆脱 Angular 模块。 请记住,我们必须将工厂依赖项转换为对注入的调用。
// location.token.ts import { DOCUMENT } from '@angular/common'; import { inject, InjectionToken } from '@angular/core'; export const locationToken: InjectionToken<Location> = new InjectionToken('Location API', { factory: (): Location => inject(DOCUMENT).location, providedIn: 'root', });
现在我们有了一种为特定于平台的 API 创建通用访问器的方法。 这在测试依赖它们的 declarable 和服务时将证明是有用的。
Testing tree-shakable dependencies
在测试 tree-shakable 依赖项时,重要的是要注意依赖项默认由工厂提供,作为选项传递给 Injectable 和 InjectionToken。
为了覆盖可摇树依赖,我们使用 TestBed.overrideProvider,例如 TestBed.overrideProvider(userAgentToken, { useValue: 'TestBrowser' })。
Angular 模块中的提供者仅在将 Angular 模块添加到 Angular 测试模块导入时才用于测试,例如 TestBed.configureTestingModule({imports: [InternetExplorerModule] })。
Do tree-shakable dependencies matter?
Tree-shakable 依赖对于小型应用程序没有多大意义,我们应该能够很容易地判断一个服务是否在实际使用中。
相反,假设我们创建了一个供多个应用程序使用的共享服务库。 应用程序包现在可以忽略在该特定应用程序中未使用的服务。 这对于具有共享库的 monorepo 工作区和 multirepo 项目都很有用。
Tree-shakable 依赖项对于 Angular 库也很重要。 例如,假设我们在应用程序中导入了所有 Angular Material 模块,但仅使用了部分组件及其相关的基于类的服务。 因为 Angular Material 提供了摇树服务,所以我们的应用程序包中只包含我们使用的服务。
Summary
我们已经研究了使用 tree-shakable 提供程序配置注入器的现代选项。 与前 Angular 6 时代的提供者相比,可摇动树的依赖项通常更容易推理且不易出错。
来自共享库和 Angular 库的未使用的 tree-shakable 服务在编译时被删除,从而产生更小的包。