1. Lời giới thiệu
Angular 6 đã giới thiệu đến chúng ta một features mới, đó là Tree Shakable Providers. Tree Shakable Providers là cách để định nghĩa các services được sử dụng trong hệ thống Angular’s Dependency Injection nhằm cải thiện performance của ứng dụng Angular.
2. Tree Shaking là gì?
Tree Shaking là một bước trong quá trình build app nhằm xóa bỏ unused codes trong project. Bạn có thể xem việc loại bỏ unused codes giống như việc “rung cây”, rằng chúng ta sẽ thực hiện hành động rung cây để loại bỏ đi những lá chết và chỉ giữ lại những lá còn tươi mới. Bằng việc sử dụng Tree Shaking, chúng ta có thể đảm bảo rằng ứng dụng chỉ gồm những đoạn code thực sự cần thiết để ứng dụng có thể chạy.
Lấy ví dụ, giả sử như chúng ta có một utility library có chứa functions a(), b() and c(). Trong project, chúng ta import và sử dụng function a() và c() nhưng không sử dụng function b(). Chúng ta mong muốn rằng code của function b() sẽ không được bundle và deploy tới người dùng. Tree Shaking chính là cơ chế để loại bỏ function b() ra khỏi code ở production mà chúng ta deploy để đưa tới người dùng.
3. Tại sao Services trong Angular ở các phiên bản trước lại không tree-shakable?
Ta hãy cùng nhìn lại cách chúng ta đăng ký Service trong các phiên bản trước của Angular. Hãy cùng xem một ví dụ sau:
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 {}
Như chúng ta có thể thấy, chúng ta import SharedService và thêm nó vào AppModule. Nhờ đó, ShareService sẽ được đăng ký trong hệ thống Angular’s Dependency Injection. Bất cứ khi nào một component tạo 1 request để sử dụng serivce này, Angular’s DI sẽ đảm bảo rằng service đó và các dependencies của nó sẽ được tạo và truyền vào constructor của component đó. Nhưng vấn đề của việc này là, các build tool và compilers rất khó để xác định các service này có được sử dụng trong ứng dụng của ta hay không.
Một cách cơ bản mà hệ thống tree-shaking dùng để xóa bỏ code, đó là dựa vào phần import mà chúng ta sử dụng trong project. Nếu một class hay function không được import vào bất cứ đâu, thì nó sẽ được không bundle ra code ở production. Nếu nó được import, hệ thống tree-shaking sẽ giả định rằng nó được sử dụng trong ứng dụng của chúng ta. Ở ví dụ trên, chúng ta import và add SharedService vào AppModule, và do đó, các bundled tool sẽ giả định là SharedService đã được sử dụng trong ứng dụng của chúng ta và bundled nó vào code ở production, cho dù service có được dùng hay không.
4. Angular Tree Shaking Providers
Với Tree Shaking Providers (TSP), chúng ta có thể sử dụng một cơ chế khác để đăng ký các services. Sử dụng cơ chế TSP sẽ giúp chúng ta có được lợi ích của cả Tree-shaking và DI. Ta hãy cùng lấy ví dụ về syntax của TSP:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class SharedService {
constructor() {}
}
Trong phần khai báo Injector, ta có một thuộc tính mới là “providedIn”. Với thuộc tính này, ta có thể cho Angular biết rằng ta module nào sẽ đăng ký service này, thay vì import module đó và đăng ký nó vào phần provider của NgModule. Mặc định. cú pháp này sẽ đăng ký service vào root injector và sẽ chỉ tạo 1 instance duy nhất cho service trong toàn bộ application. “root” provider là giá trị mặc định mà ta có thể dùng trong hầu hết các trường hợp. Tuy nhiên, nếu bạn muốn kiểm soát số lượng các instance của service thì bạn vẫn có thể dụng cú pháp cũ (import service vào phần provider của NgModule).
Với cú pháp mới này, bạn có thể thấy rằng, chúng ta đã không import service vào NgModule để đăng ký. Và vì không có câu lệnh import nào cho service này, bundled tools có thể đảm bảo rằng service này sẽ chỉ được bundled nếu có một component sử dụng nó. Chúng ta hãy cùng nhau khám phá ví dụ dưới đây:
4.1. App Module
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 {}
Trong ví dụ trên, chúng ta có 3 components: 2 components sử dụng lazy load và 1 component là trang chủ. Bên cạnh đó, chúng ta có 3 services được sử dụng trong ví dụ của chúng ta.
4.2. Service đầu tiên: SharedService
Chúng ta hãy cùng xem xét service đầu tiên và cách mà nó được sử dụng:
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');
}
}
Service đầu tiên này sử dụng Tree shakable providers API. Chúng ta import service này vào cả Feature1Component và Feature2Component:
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() {}
}
Bởi vì service này được sử dụng ở cả 2 component, nên nó sẽ được bundled vào app của chúng ta. Nếu ta mở tab Console ra thì ta sẽ thấy dòng chữ:
SharedService bundled because two components use it
4.3. Service thứ hai: Shared2Service
Service thứ hai của chúng ta sẽ như sau:
import { Injectable } from '@angular/core';
console.log('Shared2Service is not bundled because it not used');
@Injectable({
providedIn: 'root'
})
export class Shared2Service {
constructor() {}
}
Nếu chúng ta mở tab Console, sẽ không có message nào được hiện ra. Đó là bởi vì service trên không được sử dụng ở bất kỳ component nào. Vì thế nó được không bundled ra app.
4.4. Service thứ ba: Shared3Service
Cuối cùng chúng ta có service thứ ba như sau:
import { Injectable } from '@angular/core';
console.log('Shared3Service bundled even though not used');
@Injectable()
export class Shared3Service {
constructor() {}
}
Sau đó ta import nó vào AppModule. Nếu chúng ta mở tab Console, ta sẽ thấy message như sau:
Shared3Service bundled even though not used
Bởi vì service này sử dụng cú pháp cũ, nên nó cần câu lệnh import vào AppModule để đăng ký. Câu lệnh import này sẽ làm cho các bundled tools gộp cả code của service này vào mặc dù không có component nào sử dụng nó.
Như vậy, khi nhìn vào 3 services ví dụ ở trên, ta có thể thấy được nét đặc trưng khi hệ thống tree-shaking quyết định gộp hay loại bỏ code trong ứng dụng của chúng ta. Với TSP API, các services của ta vẫn là các phiên bản độc lập, kể cả nó được sử dụng trong các lazy load module như trong ví dụ trên. Nếu ta di chuyển giữa 2 trang feature1 và feature2, message trong console log của SharedService sẽ chỉ hiện ra 1 lần. Khi một service được request, Angular sẽ khởi tạo 1 instance của nó và đảm bảo rằng instance đó sẽ được sử dụng trong phần còn lại của toàn bộ dự án.
Kết luận
Angular Tree Shakable Providers sẽ giúp chúng ta cải thiện performance cho ứng dụng cũng như là loại bỏ unused code và chỉ bundled những đoạn code thực sự cần thiết.
Bài viết có tham khảo một số nguồn sau:
https://coryrylan.com/blog/tree-shakeable-providers-and-services-in-angular