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<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 服务在编译时被删除,从而产生更小的包。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,706评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,002评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,462评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,375评论 0 216
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,763评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,849评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,033评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,768评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,490评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,734评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,204评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,566评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,227评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,137评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,934评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,926评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,774评论 2 274

推荐阅读更多精彩内容