Angular 应用里的摇树优化 - tree shaking
Tree Shakeable Providers and Services in Angular
Angular 最近推出了一项新功能,Tree Shakeable Providers。 Tree Shakeable Providers 是一种定义服务和其他东西的方式,以一种可以提高 Angular 应用程序性能的方式被 Angular 的依赖注入系统使用。
首先,在我们深入挖掘之前,让我们先定义一下摇树。摇树是构建过程中的一个步骤,它从代码库中删除未使用的代码。删除未使用的代码可以被认为是“摇树”,或者您可以想象一棵树的物理摇晃和剩余的枯叶从树上掉下来。通过使用摇树,我们可以确保我们的应用程序只包含我们的应用程序运行所需的代码。
例如,假设我们有一个实用程序库,其中包含函数 a()、b() 和 c()。在我们的应用程序中,我们导入并使用函数 a () 和 c () 但不使用 b ()。我们希望 b() 的代码不会被捆绑并部署给我们的用户。摇树是从我们发送到用户浏览器的已部署生产代码中删除函数 b() 的机制。
为什么过去版本的 Angular 中,服务已经不能被摇树优化?这其实又回到了我们如何在 Angular 的早期版本中注册 Service 的问题。让我们看一个示例,说明我们如何在以前的 Angular 版本中注册一个用于依赖注入的服务。
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { SharedService } from './shared.service'; @NgModule({ imports: [BrowserModule, FormsModule], declarations: [AppComponent], bootstrap: [AppComponent], providers: [SharedService] }) export class AppModule {}
如您所见,我们导入了服务并将其添加到我们的 Angular AppModule。 这会将服务注册到 Angular 的依赖注入系统。 每当组件请求使用此服务时,Angular 的 DI 将确保创建 Service 及其任何依赖项并将其传递给组件的构造函数。
此注册系统的问题在于,构建工具和编译器很难确定我们的应用程序中是否使用了此代码。
摇树系统删除代码的主要方式之一是查看我们定义的导入路径。 如果类或函数未导入,则不会包含在我们提供给用户的生产代码包中。如果它是导入的,则摇树器假定它正在应用程序中使用。在我们上面的示例中,我们在 AppModule 中导入和引用我们的服务,导致显式依赖项不能被摇树优化掉。
Angular Tree Shaking Providers
使用 Tree Shaking Providers (TSP),我们可以使用不同的机制来注册我们的服务。 使用这种新的 TSP 机制将提供摇树性能和依赖注入的好处。 我们有一个带有特定代码的演示应用程序来演示我们如何注册这些服务的不同性能特征。 让我们来看看新的 TSP 语法是什么样的。
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class SharedService { constructor() {} }
在@Injectable 装饰器中,我们有一个名为providedIn 的新属性。有了这个属性,我们可以告诉 Angular 将我们的服务注册到哪个模块,而不必导入模块并将其注册到 NgModule 的提供者。也就是说,不需要像旧版本的 Angular 那样,在 AppModule 里显式 import 服务,并添加到 NgModule 的 providers 数组里。
默认情况下,此语法将其注册到根注入器,这将使我们的服务成为应用程序范围的单例。 对于大多数用例,根提供程序是合理的默认值。 如果您仍然需要控制服务实例的数量,Angular 模块和组件上的常规提供程序 API 仍然可用。
使用这个新 API,您可以看到,由于我们不必将服务导入 NgModule 进行注册,因此我们没有明确依赖。 因为没有 import 语句,构建工具可以确保该服务仅在组件使用时才捆绑在我们的应用程序中。 让我们看一个示例应用程序。
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { HelloComponent } from './hello.component'; import { Shared3Service } from './shared3.service'; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot([ { path: '', component: HelloComponent }, { path: 'feature-1', loadChildren: () => import('./feature-1/feature-1.module').then(m => m.Feature1Module) }, { path: 'feature-2', loadChildren: () => import('./feature-2/feature-2.module').then(m => m.Feature2Module) } } ]) ], declarations: [AppComponent, HelloComponent], bootstrap: [AppComponent], providers: [Shared3Service] }) export class AppModule {}
在这个示例应用程序中,我们有三个组件; 两个是延迟加载的模块,而一个是我们的着陆主页组件。 我们还将在应用程序中使用三种不同的服务。 让我们从第一个服务开始,看看它是如何使用的。
import { Injectable } from '@angular/core'; console.log('SharedService bundled because two components use it'); @Injectable({ providedIn: 'root' }) export class SharedService { constructor() { console.log('SharedService instantiated'); } }
我们的第一个服务使用 tree shakable providers API。 我们在每个延迟加载的功能模块中导入此服务两次,如下所示。
import { Component, OnInit } from '@angular/core'; import { SharedService } from './../shared.service'; @Component({ selector: 'app-feature-1', templateUrl: './feature-1.component.html', styleUrls: ['./feature-1.component.css'] }) export class Feature1Component implements OnInit { constructor(private sharedService: SharedService) {} ngOnInit() {} }
因为我们的两个组件中都使用了服务 1,所以代码被加载并捆绑到我们的应用程序中。 如果我们检查控制台,我们会看到以下消息:
SharedService bundled because two components use it
第二个服务:
import { Injectable } from '@angular/core'; console.log('Shared2Service is not bundled because it not used'); @Injectable({ providedIn: 'root' }) export class Shared2Service { constructor() {} }
如果我们检查控制台,我们不会看到日志消息。 这是因为我们的功能模块或组件中均未使用此服务。 由于它没有被使用,所以没有捆绑和加载代码。
最后,我们类似于前两个服务的第三个服务如下所示:
import { Injectable } from '@angular/core'; console.log('Shared3Service bundled even though not used'); @Injectable() export class Shared3Service { constructor() {} }
console 信息:
Shared3Service bundled even though not used
因为 Shared3Service 是使用较旧的提供程序 API 注册的,所以由于需要注册的 import 语句,它会创建显式依赖项。即使没有组件使用它,import 语句也会导致构建系统包含并加载此代码。
在这三个服务之间,我们可以看到摇树系统如何在我们的应用程序中包含或删除代码的特征。使用 TSP API,我们的服务仍然是单例的,即使对于我们示例中的延迟加载模块中使用的服务。如果我们加载示例应用程序,我们会注意到,如果我们在功能一和功能二之间路由,则 SharedService 中的控制台日志只会被调用一次。一旦请求模块,Angular 将实例化并确保该实例在应用程序的剩余生命周期中使用。
Angular Tree Shakeable Providers 为我们的应用程序提供了更好的性能,并减少了创建可注入服务所需的样板代码量。