1. Service trong Angular
- Service bao gồm bất kỳ giá trị, chức năng hoặc tính năng nào mà ứng dụng cần.
- Angular phân tách các component với các service để tăng tính module và khả năng tái sử dụng. Bằng cách tách chức năng liên quan đến view của một component khỏi các loại xử lý khác, bạn có thể làm cho các lớp component của mình gọn gàng và hiệu quả.
- Lý tưởng nhất, công việc của một component chỉ nên là kích hoạt trải nghiệm người dùng. Một component phải trình bày các thuộc tính và phương thức (biến và function) để liên kết dữ liệu (binding data), để làm trung gian giữa view (được hiển thị bởi template - file .html) và logic ứng dụng (thường bao gồm một số khái niệm về model - file .ts).
- Một component có thể ủy quyền các tác vụ nhất định cho các service, chẳng hạn như tìm nạp dữ liệu từ máy chủ, xác thực đầu vào của người dùng hoặc ghi trực tiếp vào console.
- Sau khi định nghĩa các tác vụ nhất định trong một lớp service, bạn có thể cung cấp các service này cho các component thông qua việc dependency injection.
2. Dependency là gì?
class AuthApiService { message: string = 'internal'; constructor() { } login() { this.message = 'Login success!'; }}class LoginComponent { private authService: AuthApiService; constructor() {}}
Ở ví dụ trên ta thấy: Trong lớp LoginComponent có sự tồn tại của lớp AuthApiService và LoginComponent dùng AuthApiService để thực hiện chức năng login. Do đó, ta nói lớp LoginComponent đang phụ thuộc vào lớp AuthApiService.Vậy nếu muốn sử dung property và method của class AuthApiService thì chúng ta phải khởi tạo ở đâu, vì nếu không khởi tạo thì chúng ta sẽ không thể sử dụng được property message và method login()? 👉 Chúng ta sẽ có 2 cách để khởi tạo:
Cách 1: Khởi tạo đối tượng (instance) của class AuthApiService trong hàm constructor của class LoginComponent:
Với cách sẽ phát sinh vấn đề khi chúng ta muốn thay instance AuthApiService bằng một instance của một class khác - có cùng các phương thức và method đã sử dụng nhưng có thể các về logic xử lý - (và tệ hơn nữa nếu các instance của AuthApiService đang được sử dụng ở nhiều component khác thuộc nhiều module khác nhau nhưng chúng ta chỉ muốn thay đổi instance của class AuthApiService ở các component thuộc một module trong các module đó). Lúc nào bắt buộc chúng ta phải vào từng component và khởi tạo lại instance (tệ hơn là có thể phải viết lại component đó). Ngoài ra, việc test cũng trở nên khó khăn vì chúng ta khó thay đổi instance đó cho việc mock dữ liệu test.
3. Dependency Injection trong Angular
- Injector là một object có chứa các API để chúng ta có thể lấy về các instances đã tạo hoặc tạo các instances của các phụ thuộc. Angular tạo một injector có phạm vi sử dụng trong toàn ứng dụng cho bạn trong quá trình khởi động và bổ sung các injector khi cần thiết. Bạn không cần phải tạo injector.
- Injector tạo ra các phụ thuộc và duy trì một vùng chứa các instance phụ thuộc mà nó sẽ sử dụng lại nếu có thể.
- Injector có trách nhiệm tạo đối tượng cung cấp service và inject chúng vào Consumer(Component, service...)
- DI Token là đinh danh duy nhất cho một Dependency . Chúng ta sử dụng DI Token khi chúng ta đăng ký dependency.
- Provider là một đối tượng quản lý danh sách các dependencies và token của nó, cung cấp cho injector biết cách để tạo ra một instance của dependency.
- Dependency là một object (có thể là function, một value thông thường hoặc một class) của một kiểu dữ liệu cần phải khởi tạo - được truyền vào như một tham số của hàm constructor trong component.
Một phần phụ thuộc không nhất thiết phải là một service. Ví dụ: nó có thể là một hàm hoặc một giá trị.
constructor(private authApiService: AuthApiService) {}
- Khi Angular phát hiện ra rằng một component phụ thuộc vào một service, trước tiên nó sẽ kiểm tra xem injector có bất kỳ instance nào hiện có của service đó (AuthApiService) hay không.
- Nếu một instance của service được yêu cầu chưa tồn tại, injector tạo một instance bằng cách sử dụng provider đã đăng ký và thêm nó vào injector trước khi trả lại instance của service cho Angular.
- Khi tất cả các service được yêu cầu đã được khởi tạo và trả về, Angular có thể gọi hàm khởi tạo của component với instance của các service đó làm đối số.
- Quá trình inject service sẽ giống như thế này:
Lưu ý: Class Decorator sẽ được call sau khi mà bạn đã tạo xong class.
Decorator @Injectable
Lưu ý: Tất cả các class có phụ thuộc đến các thành phần khác – như class LoginComponent ở trên – sẽ phải decorate bằng @Injectable() decorator.
- Kiểm chứng trong tương lai (Future proofing): Không cần nhớ @Injectable () khi chúng ta thêm phụ thuộc sau này.
- Nhất quán (Consistency): Tất cả các service đều tuân theo các quy tắc giống nhau và chúng ta không phải thắc mắc tại sao lại thiếu decorator.
Cách sử dụng Dependency Injection trong Angular
- Tạo một dependency (service, hoặc component, ...) - ví dụ: AuthApiService
- Đăng ký dependency đó với provider (xem bên dưới)
- Truyền service vào phương thức khởi tạo (constructor) của component như một đối số.
constructor(private authApiService: AuthApiService) {}
hoặc
// Trường hợp này có thể bỏ qua nếu phụ vào một class tự định nghĩa import { Inject } from '@angular/core'; constructor(@Inject(AuthApiService) private authApiService: AuthApiService) {}
Lưu ý:
Đăng ký dependencies với provider
Khi bạn cung cấp service ở level này, Angular sẽ tạo một instance duy nhất (singleton) cho suốt toàn bộ app (dùng chung một instance AuthApiService duy nhất và đưa nó vào bất kỳ lớp nào yêu cầu nó). Đăng ký provider trong siêu dữ liệu của @Injectable() cũng cho phép Angular tối ưu hóa ứng dụng bằng cách xóa service khỏi ứng dụng đã biên dịch nếu nó không được sử dụng, quá trình này được gọi là tree-shaking.
- Siêu dữ liệu (metadata), providedIn: 'root' có nghĩa là chỉ có duy nhất 1 instance AuthApiService hiển thị trong toàn bộ ứng dụng. Điều này tương tự như việc bạn add AuthApiService vào providers của AppModule.
- Trên thực tế, sử dụng providedIn là cách ưu tiên để cung cấp service trong một module.
2. Khi đăng ký một provider với một NgModule cụ thể thì tất cả các component trong NgModule đó sẽ sử dụng cùng một instance của service. Để đăng ký ở level này, hãy sử dụng thuộc tính providers của decorator @NgModule():Trong thực tế, chúng ta thường khai báo các services ở cấp độ Module để sử dụng xuyên suốt trong chương trình.
3. Khi bạn đăng ký một provider ở level component, bạn sẽ nhận được một instance mới của service với mỗi instance mới của component đó. Ở level component, hãy đăng ký một provider của service trong thuộc tính providers của siêu dữ liệu @Component():
Ghi đè provider (Override Provider)
Angular mở rộng giá trị providers của class provider syntax trên thành một object provider đầy đủ như sau:
providers: [{provide: AuthApiService, useClass: AuthApiService}]
- Thuộc tính thứ nhất là provide đóng vai trò là một key để định vị giá trị phụ thuộc và định cấu hình injector. Đây là token để DI system map với token mà @Inject/@Injectable đã mô tả. Khi có token, DI system sẽ đọc key tiếp theo (thuộc tính thứ hai). Giá trị của thuộc tính này có thể là string hoặc một kiểu dữ liệu (có thể là class tự định nghĩa, ...).
Bạn có thể xem nó là một cái tên provider để định danh cho instance service sẽ được truyền vào component, để angular biết được với tên đó thì sẽ truyền instance của class dependency nào cho component
- Thuộc tính thứ hai là đối tượng định nghĩa một provider, đối tượng này nói cho injector biết cách tạo giá trị phụ thuộc. Khóa định nghĩa nhà cung cấp (provider-definition key) có thể là useClass, như trong ví dụ. Nó cũng có thể là useExisting, useValue hoặc useFactory. Mỗi khóa này cung cấp một loại phụ thuộc khác nhau.
Điều này giúp chúng ta thay đổi một class khác mà vẫn sử dụng token trên, chúng ta chỉ cần bảo DI class chúng ta cần mà không phải sửa token ở class cần phụ thuộc.
providers: [{provide: AuthApiService, useClass: AuthApiExtService}]
Do đó, khi muốn thay đổi logic xử lý của service như chuyển từ tính toán thông tin ở client sang call đến một external datasource (chẳng hạn như API, ...). Để tránh việc phải đi vào từng component và thay đổi service lần lượt thì ta sẽ tạo 1 service mới để override service trước đó, ta làm như sau:
Tạo service AuthApiExtService:
Bạn chỉ cần có thế và có thể tiến hành override, mà không cần sửa đổi lại code của các component đã sử dụng AuthApiService như sau:
Hoặc có thể override vào @Injectable của service:
Inject parent component to child component
Angular application là một component tree có dạng như sau.
Do Angular support DI đến tận level của từng Component, nên chúng ta hoàn toàn có thể inject parent component vào child component.constructor(private parent: ParentComponent) { }Override provider:
@Component({ selector: 'app-bs-parent', templateUrl: './bs-parent.component.html', styleUrls: ['./bs-parent.component.css'], providers: [ { provide: ParentComponent, useExisting: BsParentComponent } ] }) export class BsParentComponent extends ParentComponent { }
Provider in-depth
{ provide: AuthApiService, useClass: AuthApiService
}
Sử dụng value
{
provide: 'API_ENDPOINT',
useValue: 'https://sanhangsalegiare.herokuapp.com'
}
Sử dụng alias
{ provide: AuthApiService, useClass: AuthApiService },
{ provide: Server, useExisting: AuthApiService }
- useClass: sẽ tạo ra một instance mới (tạo instance AuthApiService cho provider AuthApiService)
- useExisting: giống như bí danh (alias) cho một provider đã đăng ký. Trường hợp sử dụng là cung cấp cùng một instance của provider với các khóa khác nhau. (Không tạo instance mới mà sử dụng lại instance AuthApiService đã được tạo trước đó với key 'Server').
Sử dụng factory
{
provide: AuthApiService,
useFactory: () => {
return isExternal ? new AuthApiExtService() : new AuthApiService();
}
}
{
provide: LoginComponent,
useFactory: (authService) => {
return new LoginComponent(authService);
},
deps: [AuthApiService]
}
{ provide: 'API_ENDPOINT', useValue: 'https://sanhangsalegiare.herokuapp.com' },
{ provide: 'API_ENDPOINT', useValue: 'https://sanhangsalegiare.herokuapp.com/posts' }
Multiple Provider
{
provide: 'API_ENDPOINT',
useValue: 'https://sanhangsalegiare.herokuapp.com',
multi: true
},
{
provide: 'API_ENDPOINT',
useValue: 'https://sanhangsalegiare.herokuapp.com/posts',
multi: true
},
Forward References
Forward References cho phép tham chiếu đến các tham chiếu chưa được xác định.
ERROR in src/app/app.component.ts:9:5 - error TS2449: Class 'NameService' used before its declaration.
export const REQUIRED_VALIDATOR: Provider = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RequiredValidator),
// hoặc useClass: forwardRef(() => CustomClass),
multi: true
};
@Directive({
selector: '…',
providers: [REQUIRED_VALIDATOR],
host: {'[attr.required]': 'required ? "" : null'}
})
export class RequiredValidator implements Validator {
}
Optional Dependencies
import { Optional } from '@angular/core';
export class OptionalClass {
public log: LogService;
constructor(@Optional() log: LogService) {
// do something if log exist
if (log) {
this.log = log;
}
}
}