乐趣区

关于angular:Angular-项目中的可摇树依赖-Treeshakable-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<boolean>。

这容许咱们将布尔标记注入服务、组件等。同时,咱们只对每个模块注入器评估一次 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 服务在编译时被删除,从而产生更小的包。

退出移动版