Angular 项目中的可摇树依赖 - Tree-shakable dependencies
Tree-shakable dependencies in Angular projects
Tree-shakable 依赖更容易推理和编译成更小的包。
Angular 模块 (NgModules) 曾经是提供应用程序范围依赖项(例如常量、配置、函数和基于类的服务)的主要方式。 从 Angular 版本 6 开始,我们可以创建可摇树的依赖项,甚至可以忽略 Angular 模块。
当我们使用 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 服务在编译时被删除,从而产生更小的包。