Tree Shakeable Providers and Services in Angular
Angular recently introduced a new feature, Tree Shakeable Providers. Tree Shakeable Providers are a way to define services and other things, and are used by Angular's dependency injection system in a way that can improve the performance of Angular applications.
First, before we dig deeper, let's define tree shaking. Tree shaking is a step in the build process that removes unused code from the code base. Deleting unused code can be considered a "tree shaking", or you can imagine the physical shaking of a tree and the remaining dead leaves falling from the tree. By using Shake the tree, we can ensure that our application only contains the code necessary for our application to run.
For example, suppose we have a utility library that contains functions a(), b(), and c(). In our application, we import and use functions a () and c () but not b (). We hope that the code of b() will not be bundled and deployed to our users. Tree shaking is a mechanism to remove the function b() from the deployed production code that we send to the user's browser.
Why in the past version of Angular, the service has not been optimized by tree shaking? This actually goes back to the question of how we register the Service in the earlier version of Angular. Let us look at an example of how we can register a service for dependency injection in a previous version of 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 {}
As you can see, we imported the service and added it to our Angular AppModule. This will register the service to Angular's dependency injection system. Whenever a component requests to use this service, Angular's DI will ensure that the Service and any dependencies are created and passed to the component's constructor.
The problem with this registration system is that it is difficult for build tools and compilers to determine whether this code is used in our application.
One of the main ways to delete code in the Shaking system is to check the import path we defined. If the class or function is not imported, it will not be included in the production code package we provide to users. If it is imported, the tree shaker assumes that it is being used in the application. In our example above, we imported and referenced our service in AppModule, resulting in explicit dependencies that could not be optimized by Shaker.
Angular Tree Shaking Providers
Using Tree Shaking Providers (TSP), we can use different mechanisms to register our services. Using this new TSP mechanism will provide the benefits of tree shake performance and dependency injection. We have a demo application with specific code to demonstrate how we can register different performance characteristics of these services. Let's take a look at what the new TSP syntax looks like.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class SharedService {
constructor() {}
}
In the @Injectable decorator, we have a new attribute called providedIn. With this attribute, we can tell Angular which module to register our service without having to import the module and register it with the provider of NgModule. In other words, there is no need to explicitly import the service in AppModule and add it to the providers array of NgModule like in the old version of Angular.
By default, this syntax registers it to the root injector, which will make our service an application-wide singleton. For most use cases, the root provider is a reasonable default. If you still need to control the number of service instances, the regular provider APIs on Angular modules and components are still available.
Using this new API, you can see that since we don't have to import the service into NgModule for registration, we have no explicit dependency. Because there is no import statement, the build tool can ensure that the service is only bundled in our application when the component is used. Let's look at a sample application.
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 {}
In this sample application, we have three components; two are lazy loaded modules, and one is our landing home page component. We will also use three different services in the application. Let's start with the first service and see how it is used.
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');
}
}
Our first service uses the tree shakable providers API. We import this service twice in each lazily loaded function module, as shown below.
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() {}
}
Because service 1 is used in both of our components, the code is loaded and bundled into our application. If we check the console, we will see the following message:
SharedService bundled because two components use it
The second service:
import { Injectable } from '@angular/core';
console.log('Shared2Service is not bundled because it not used');
@Injectable({
providedIn: 'root'
})
export class Shared2Service {
constructor() {}
}
If we check the console, we will not see log messages. This is because this service is not used in our functional modules or components. Since it is not used, there is no binding and loading code.
Finally, our third service, which is similar to the first two services, looks like this:
import { Injectable } from '@angular/core';
console.log('Shared3Service bundled even though not used');
@Injectable()
export class Shared3Service {
constructor() {}
}
console information:
Shared3Service bundled even though not used
Because Shared3Service is registered with an older provider API, it creates an explicit dependency due to the import statement that needs to be registered. Even if no component uses it, the import statement will cause the build system to include and load this code.
Among these three services, we can see how the tree shake system includes or deletes the characteristics of the code in our application. Using TSP API, our service is still singleton, even for the service used in the lazy loading module in our example. If we load the sample application, we will notice that if we route between function one and function two, the console log in SharedService will only be called once. Once the module is requested, Angular will instantiate and ensure that the instance is used for the rest of the application lifecycle.
Angular Tree Shakeable Providers provide better performance for our applications and reduce the amount of boilerplate code required to create injectable services.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。