Chọn scope cho Angular service
Xác định khi nào nên giữ state trong component, dùng service ở root, hoặc tạo instance riêng tại component và route.
Chốt nhanh trước khi đi sâu
Nếu bạn đang nghĩ “để hết service ở root cho khỏe, ứng dụng vẫn chạy mà?” thì bạn không sai. Root là lựa chọn tốt trong rất nhiều trường hợp. Chỉ khi state, cấu hình hoặc resource thực sự thuộc về một vùng UI nhỏ hơn, chúng ta mới cần kéo scope xuống route hay component.
Mục lục
- Bắt đầu từ hai câu hỏi khác nhau
- Root không có tội
- Khi component tự giữ state là đủ
- Component scoped service đáng dùng ở đâu
- Route scoped service giải quyết chuyện gì
- Mổ xẻ ba cách đặt CheckoutStore
- Những bẫy scope thường gặp
- Scope DI không quyết định chunk
- Quy trình chốt scope
- Kiểm thử đúng instance
- Tóm tắt
- Tài liệu tham khảo
- Đọc tiếp
Bắt đầu từ hai câu hỏi khác nhau
Đây là chỗ rất nhiều dự án Angular bị “rối”. Thấy component dài một chút, chúng ta lập tức tách ra SomethingService. Tạo service xong lại tiện tay thêm providedIn: 'root'. Vậy là một state vốn thuộc một màn hình đã trở thành state dùng chung cho cả ứng dụng.
Muốn tránh chuyện đó, hãy tách bài toán thành hai câu hỏi:
- Logic có cần được tách thành một class service không?
- Nếu đã là service, instance của nó thuộc về ai?
Hai câu hỏi nghe gần giống nhau nhưng thực ra giải quyết hai việc hoàn toàn khác nhau.
Có thể hình dung đơn giản thế này:
- Root service giống bảng thông báo chung của cả công ty.
- Route service giống bảng công việc của một team.
- Component service giống cuốn sổ riêng trên bàn của một người.
Logic có thật sự cần một service
Một component hoàn toàn có thể tự giữ:
- Signal phục vụ template của chính nó.
- Trạng thái mở hoặc đóng popover.
- Item đang được chọn.
- Giá trị form tạm thời.
- Một listener nhỏ được cleanup trong lifecycle.
- Logic hiển thị gắn trực tiếp với UI.
Chỉ nên tách service khi có một lợi ích nhìn thấy được, chẳng hạn:
- Nhiều component cần dùng chung state.
- Logic validation, history hoặc đồng bộ dữ liệu đã đủ lớn.
- Muốn giấu WebSocket, storage hoặc SDK phía sau một API dễ dùng hơn.
- Muốn thay implementation bằng fake trong test.
- Một component làm owner, nhưng nhiều component con đều cần context của nó.
Đừng tách chỉ để file component ngắn hơn
Một component 150 dòng nhưng trách nhiệm rõ ràng vẫn dễ bảo trì hơn ba file service vòng qua vòng lại mà không file nào có owner rõ ràng.
Instance này thuộc về ai
Giả sử service là cần thiết. Câu hỏi tiếp theo không phải “đặt root cho tiện được không?”, mà là:
Khi nào state này hết giá trị, và những ai cần nhìn thấy cùng một instance?
| Owner thật sự | Nơi đặt provider thường hợp lý | Ví dụ |
|---|---|---|
| Cả Angular application | providedIn: 'root' | Auth session, telemetry, repository stateless. |
| Một feature điều hướng | providers của route | Workspace context, checkout context cho guard và các page con. |
| Một cây UI | providers của component | Editor session, dialog state, tabs context. |
| Một component đơn giản | Không cần service | Toggle, selection, state trình bày cục bộ. |
Nói nôm na: scope chính là câu trả lời cho câu hỏi “ai sở hữu instance này?”.
Root không có tội
providedIn: 'root' không phải anti-pattern. Trái lại, đây là lựa chọn rất tốt cho phần lớn service hạ tầng.
Khi root là lựa chọn tốt nhất
Ví dụ một repository chỉ gọi API:
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ProductsRepository {
private readonly http = inject(HttpClient);
getById(id: string) {
return this.http.get<Product>(`/api/products/${id}`);
}
}Service này không giữ tab đang chọn, form đang nhập hay draft của một page. Nó chỉ cung cấp năng lực lấy dữ liệu sản phẩm. Để root vừa gọn, vừa đúng nghĩa.
Root thường hợp với:
- Repository hoặc API client không giữ state màn hình.
- Auth/session thật sự dùng chung toàn app.
- Logger và telemetry.
- Feature flag toàn cục.
- Cache được thiết kế rõ ràng là cache cấp application.
- Store mà mọi consumer phải nhìn cùng một state.
Nếu service của bạn thuộc nhóm này, cứ để root. Không cần “hạ scope” chỉ vì nghe nói local provider tốt hơn.
Cái giá đi kèm một singleton toàn app
Vấn đề chỉ bắt đầu khi service có mutable state hoặc resource cục bộ. Khi chọn root, bạn đang đồng ý với các điều sau:
- Mọi consumer đi tới root provider sẽ dùng cùng instance.
- State sống rất lâu, thường tới lúc app bị hủy.
- Rời một page không tự xóa state.
- Socket, timer hoặc subscription không tự dừng theo page.
- Root service không thể nhìn xuống child injector để lấy config chỉ có ở route/component.
Ví dụ CheckoutStore ở root hoàn toàn có thể chạy ngon. Nhưng lúc đó chính ứng dụng phải nhớ:
- Khi nào gọi
reset()? - Logout có xóa địa chỉ cũ không?
- User mới đăng nhập có nhìn thấy state của user trước không?
- Nếu có hai checkout context, phân biệt chúng bằng ID nào?
Root không gây ra bug. Root chỉ giao toàn bộ trách nhiệm quản lý vòng đời lại cho bạn.
Khi component tự giữ state là đủ
Một ví dụ không cần store
Hãy nhìn một quantity picker rất bình thường:
import { Component, computed, input, signal } from '@angular/core';
@Component({
standalone: true,
selector: 'app-quantity-picker',
template: `
<button type="button" (click)="decrement()">−</button>
<span>{{ quantity() }}</span>
<button type="button" (click)="increment()">+</button>
<p>Tạm tính: {{ subtotal() }}</p>
`,
})
export class QuantityPickerComponent {
readonly unitPrice = input.required<number>();
readonly quantity = signal(1);
readonly subtotal = computed(() => this.unitPrice() * this.quantity());
increment(): void {
this.quantity.update((value) => value + 1);
}
decrement(): void {
this.quantity.update((value) => Math.max(1, value - 1));
}
}quantity chỉ phục vụ template này. Component biến mất thì state cũng hết nhiệm vụ. Tạo thêm QuantityPickerStore ở đây chỉ làm bạn phải mở thêm một file để hiểu cùng một câu chuyện.
Đây chính là trường hợp mà câu hỏi “sao không để luôn trong component?” có đáp án rất rõ: để trong component là đúng rồi.
Khi nào component bắt đầu quá tải
Bây giờ tưởng tượng editor không còn một khung text đơn giản nữa. Nó có:
- Toolbar thay đổi format.
- Canvas chỉnh nội dung.
- Autosave chạy nền.
- Undo/redo history.
- Status bar báo đã lưu hay chưa.
Bạn vẫn có thể nhét tất cả vào EditorComponent, rồi truyền input và output qua từng tầng. Không sai. Nhưng khi ba, bốn component con cùng xoay quanh một draft, service cục bộ bắt đầu “đáng tiền”: nó trở thành context chung cho cả editor mà không biến draft thành state toàn app.
Component scoped service đáng dùng ở đâu
Một editor nhiều mảnh
Trước hết, tạo store không tự provide ở root:
import { Injectable, computed, signal } from '@angular/core';
@Injectable()
export class DraftStore {
private readonly contentState = signal('');
private readonly savedContent = signal('');
readonly content = this.contentState.asReadonly();
readonly dirty = computed(
() => this.contentState() !== this.savedContent(),
);
update(content: string): void {
this.contentState.set(content);
}
markSaved(): void {
this.savedContent.set(this.contentState());
}
}Sau đó để editor shell làm owner:
import { Component } from '@angular/core';
import { DraftStore } from './draft-store';
import { EditorCanvasComponent } from './editor-canvas.component';
import { EditorToolbarComponent } from './editor-toolbar.component';
@Component({
standalone: true,
selector: 'app-editor-shell',
providers: [DraftStore],
imports: [EditorToolbarComponent, EditorCanvasComponent],
template: `
<app-editor-toolbar />
<app-editor-canvas />
`,
})
export class EditorShellComponent {}Toolbar và canvas chỉ việc inject context gần nhất:
@Component({ /* ... */ })
export class EditorToolbarComponent {
readonly draft = inject(DraftStore);
}
@Component({ /* ... */ })
export class EditorCanvasComponent {
readonly draft = inject(DraftStore);
}Điểm hay ở đây không phải “service chuyên nghiệp hơn component”. Điểm hay là owner được nói rõ bằng code: editor shell sinh ra draft, các child dùng chung draft, shell biến mất thì draft cũng hết vòng đời.
Bạn vẫn có thể truyền input/output nếu cây component còn nông. Local service chỉ là một lựa chọn để chia sẻ context, không phải luật bắt buộc.
Hai editor trên cùng màn hình
Đây là lúc khác biệt với root hiện ra rõ nhất:
<app-editor-shell />
<app-editor-shell />Angular tạo hai shell, đồng nghĩa có hai ElementInjector và hai DraftStore:
Gõ ở editor A không làm editor B “nhảy chữ”. Undo của A cũng không động vào history của B.
Nếu dùng root, bạn vẫn giải quyết được bằng Map<editorId, Draft>. Nhưng hãy nhìn phần việc phát sinh:
- Mọi consumer phải cầm đúng
editorId. - Mọi lời gọi phải truyền ID.
- Khi editor đóng, phải nhớ xóa entry.
- ID sai là state chảy sang editor khác.
Component scope không tạo ra năng lực mới. Nó chỉ giúp Angular quản lý phần phân vùng này thay bạn.
Resource cần dừng cùng UI
Một ví dụ khác là live order feed:
import { DestroyRef, Injectable, inject, signal } from '@angular/core';
@Injectable()
export class OrderFeed {
private readonly socket = new WebSocket('wss://example.test/orders');
private readonly messagesState = signal<readonly string[]>([]);
readonly messages = this.messagesState.asReadonly();
constructor() {
this.socket.addEventListener('message', (event) => {
this.messagesState.update((items) => [...items, String(event.data)]);
});
inject(DestroyRef).onDestroy(() => this.socket.close());
}
}Page sở hữu feed:
@Component({
standalone: true,
providers: [OrderFeed],
template: `...`,
})
export class LiveOrdersPage {
readonly feed = inject(OrderFeed);
}Khi page bị destroy, OrderFeed cũng bị destroy và socket được đóng.
Nhưng cần nói thật: nếu chỉ một page dùng socket và logic rất ngắn, đặt socket thẳng trong component rồi đóng ở ngOnDestroy cũng hoàn toàn ổn. Chỉ tách OrderFeed khi bạn có reconnect, parse message, nhiều child cùng dùng hoặc muốn test protocol riêng.
Tách logic và chọn scope là hai việc riêng
Bạn có thể giữ socket trong component. Bạn cũng có thể tách socket thành service nhưng vẫn provide tại component. “Đã là service” không kéo theo “phải là root”.
Route scoped service giải quyết chuyện gì
Component provider rất hợp với UI subtree. Nhưng Router còn có một thời điểm đặc biệt: guard và resolver chạy trước khi component được tạo. Đây là vùng mà route provider phát huy tác dụng.
Context cho cả một feature
Giả sử workspace có ba page: overview, members và settings. Cả ba đều cần cùng context của workspace hiện tại.
import { Routes } from '@angular/router';
import { WorkspaceContext } from './workspace-context';
export const WORKSPACE_ROUTES: Routes = [
{
path: ':workspaceId',
providers: [WorkspaceContext],
children: [
{
path: 'overview',
loadComponent: () =>
import('./overview.page').then((m) => m.OverviewPage),
},
{
path: 'members',
loadComponent: () =>
import('./members.page').then((m) => m.MembersPage),
},
{
path: 'settings',
loadComponent: () =>
import('./settings.page').then((m) => m.SettingsPage),
},
],
},
];Các page con đi lên route injector và gặp cùng một WorkspaceContext. Page ở ngoài workspace không thấy provider này.
Đặt context ở root vẫn có thể chạy. Khi đó service phải tự theo dõi “workspace hiện tại là ai” và tự reset lúc URL đổi. Route scope chỉ giúp ranh giới feature hiện ra ngay trong route config.
Guard và resolver chạy trước component
Giả sử checkout có hai page con là address và payment. Chúng ta muốn chỉ cho vào payment sau khi địa chỉ đã hợp lệ.
Trước hết, CheckoutShellComponent chỉ là component layout cha của flow checkout. Bản thân nó không phải API đặc biệt của Angular:
@Component({
standalone: true,
selector: 'app-checkout-shell',
imports: [RouterOutlet],
providers: [CheckoutContext],
template: `<router-outlet />`,
})
export class CheckoutShellComponent {}Router tạo shell trước, sau đó render AddressPage hoặc PaymentPage vào <router-outlet> của shell:
Nếu chỉ các component đã render cần CheckoutContext, đặt provider ở shell như trên là hợp lý. Nhưng guard lại chạy trước khi Router tạo CheckoutShellComponent. Vì thế guard chưa thể nhìn thấy provider nằm trên component này.
Context và guard của ví dụ:
@Injectable()
export class CheckoutContext {
readonly canContinue = signal(false);
}
const checkoutGuard: CanActivateChildFn = () => {
return inject(CheckoutContext).canContinue();
};Muốn guard dùng được CheckoutContext, hãy chuyển provider lên route. Route injector được tạo trong quá trình Router xử lý điều hướng, nên guard có thể inject context trước khi component xuất hiện:
export const routes: Routes = [
{
path: 'checkout',
providers: [CheckoutContext],
canActivateChild: [checkoutGuard],
component: CheckoutShellComponent,
children: [
{ path: 'address', component: AddressPage },
{ path: 'payment', component: PaymentPage },
],
},
];Lúc này không khai báo lại CheckoutContext trong providers của shell. Nếu cung cấp ở cả hai nơi, shell và các page con có thể nhận instance component-scoped khác với instance mà guard lấy từ route injector.
Đây là lý do rất cụ thể để chọn route scope: dependency phải có mặt trong quá trình điều hướng, không chỉ sau khi UI đã render. Phần Mổ xẻ ba cách đặt CheckoutStore bên dưới sẽ so sánh đầy đủ root, shell component và route.
Mỗi feature một cấu hình
Route provider còn giải quyết một bài toán mà root service không tự làm được: cùng một abstraction nhưng mỗi feature cần config khác nhau.
import { InjectionToken } from '@angular/core';
export const USERS_API_URL = new InjectionToken<string>('users.api-url');
@Injectable()
export class UsersApi {
private readonly http = inject(HttpClient);
private readonly apiUrl = inject(USERS_API_URL);
list() {
return this.http.get<readonly User[]>(`${this.apiUrl}/users`);
}
}Public route dùng endpoint public:
{
path: 'users',
providers: [
UsersApi,
{ provide: USERS_API_URL, useValue: '/api/public' },
],
component: PublicUsersPage,
}Admin route dùng endpoint admin:
{
path: 'admin/users',
providers: [
UsersApi,
{ provide: USERS_API_URL, useValue: '/api/admin' },
],
component: AdminUsersPage,
}Bây giờ mỗi route có một UsersApi được lắp ráp bằng config gần nó nhất.
Nếu UsersApi là một root instance, nó được tạo trong root injector. Root injector chỉ nhìn lên cha, không nhìn xuống route con. Vì thế nó không thể tự “ngửi” ra USERS_API_URL của route hiện tại.
Lưu ý về vòng đời route injector
Đây là chỗ dễ hiểu nhầm nhất:
Route-scoped không đồng nghĩa chắc chắn “mỗi lần quay lại URL sẽ có instance mới”.
Route scope bảo đảm vùng phân giải token. Còn injector có được giữ lại hay không phụ thuộc phiên bản Angular Router, route config và RouteReuseStrategy.
Nếu yêu cầu là “UI biến mất thì instance phải chết ngay”, component provider thường dễ dự đoán hơn. Nếu vẫn cần route provider cho guard/resolver, hãy kiểm tra đúng hành vi Router của dự án và thiết kế reset rõ ràng.
Mổ xẻ ba cách đặt CheckoutStore
Checkout là ví dụ hay vì cả ba đáp án dưới đây đều có thể đúng. Khác biệt nằm ở sản phẩm muốn state sống bao lâu.
Đặt ở root
@Injectable({ providedIn: 'root' })
export class CheckoutStore {
readonly address = signal<Address | null>(null);
readonly payment = signal<Payment | null>(null);
reset(): void {
this.address.set(null);
this.payment.set(null);
}
}Root hợp lý khi:
- Toàn app chỉ có một checkout session.
- Người dùng rời checkout rồi quay lại vẫn cần giữ dữ liệu.
- Header hoặc feature khác cần đọc trạng thái checkout.
- Team đã xác định rõ lúc reset: đặt hàng thành công, hủy flow, logout hoặc đổi account.
Trong bối cảnh này, để root không chỉ “không sao” mà còn đúng yêu cầu.
Cái cần canh là những đường thoát bất ngờ. Ví dụ user A nhập địa chỉ, logout giữa chừng, user B đăng nhập. Nếu luồng logout quên gọi reset(), dữ liệu cũ vẫn nằm nguyên trong singleton.
Đặt ở shell component
Nếu checkout chỉ là một phiên UI và không cần guard inject store, shell component có thể làm owner:
@Injectable()
export class CheckoutStore {
readonly address = signal<Address | null>(null);
readonly payment = signal<Payment | null>(null);
}
@Component({
standalone: true,
selector: 'app-checkout-shell',
providers: [CheckoutStore],
imports: [RouterOutlet],
template: `<router-outlet />`,
})
export class CheckoutShellComponent {}export const routes: Routes = [
{
path: 'checkout',
component: CheckoutShellComponent,
children: [
{ path: 'address', component: AddressPage },
{ path: 'payment', component: PaymentPage },
{ path: 'review', component: ReviewPage },
],
},
];Address, payment và review cùng dùng store của shell. Khi shell bị destroy, store cũng đi theo. Không cần một lệnh reset toàn cục chỉ để dọn state của phiên UI này.
Đặt ở route
Nếu guard, resolver và các page con đều cần context:
export const routes: Routes = [
{
path: 'checkout',
providers: [CheckoutStore],
canActivateChild: [checkoutGuard],
children: [
{ path: 'address', component: AddressPage },
{ path: 'payment', component: PaymentPage },
{ path: 'review', component: ReviewPage },
],
},
];Bảng dưới đây là cách đọc nhanh ba thiết kế:
| Thiết kế | Ai dùng chung | Có chủ đích giữ state khi rời checkout | Guard/resolver dùng được | Owner được diễn đạt |
|---|---|---|---|---|
| Root store | Toàn application | Có, trừ khi gọi reset | Có | Application session |
| Shell component | Shell và UI descendants | Không khi shell bị destroy | Không trước khi shell được tạo | Checkout UI session |
| Route provider | Route subtree | Phụ thuộc lifecycle/reuse của route injector | Có | Route feature context |
Không có “đáp án Angular chuẩn” cho bảng này. Đáp án nằm ở vòng đời mà sản phẩm mong muốn.
Những bẫy scope thường gặp
Scope quá rộng
Để mọi service ở root có thể khiến bạn gặp những lỗi khá khó chịu:
- Hai editor vô tình dùng chung draft.
- Dialog mới mở ra vẫn còn form của lần trước.
- Socket của page cũ tiếp tục chạy âm thầm.
- Checkout phải nhớ reset ở năm đường điều hướng khác nhau.
- Service bắt đầu giữ
Map<id, state>chỉ để mô phỏng nhiều instance. - Feature không liên quan cũng inject được một context vốn chỉ có nghĩa ở một vùng nhỏ.
Nhưng đừng đánh đồng: các lỗi này đến từ state và resource có owner cục bộ, không phải vì root tự thân có vấn đề. Repository stateless ở root vẫn rất ổn.
Scope quá hẹp
Hạ mọi provider xuống component cũng không phải thuốc tiên. Bạn có thể vô tình tạo:
- Nhiều cache giống nhau.
- Cùng một API request bị gọi lặp.
- Mỗi widget mở một WebSocket riêng.
- Nhiều
AuthServicevới trạng thái đăng nhập khác nhau. - Provider con che provider cha khiến test hoặc behavior khó hiểu.
Ví dụ sau đáng nghi nếu không có lý do rất rõ:
@Component({
providers: [AuthService, ProductsRepository],
})
export class ProductCardComponent {}Mỗi product card có thể sinh một auth context và repository riêng. Trong đa số ứng dụng, card chỉ cần state hiển thị cục bộ; auth và repository vẫn nên lấy từ root.
Local service không kéo mọi dependency xuống local
Nhiều người ngại component provider vì tưởng mọi dependency bên dưới cũng bị tạo lại. Thực tế Angular chỉ tạo instance cục bộ cho những token được provide ở scope đó.
DraftStore là local. HttpClient và Logger vẫn dùng instance từ root nếu không có provider gần hơn che chúng.
Scope DI không quyết định chunk
Đây là một đường ray khác hoàn toàn.
@Injectable({ providedIn: 'root' })
export class ReportsRepository {}Nếu ReportsRepository chỉ được import từ lazy feature, code của nó vẫn có thể nằm trong lazy graph. Instance thuộc root không có nghĩa file JavaScript phải nằm trong initial chunk.
Chiều ngược lại cũng đúng: một route-scoped service bị static import từ code eager vẫn có thể đi vào initial graph.
Hãy nhớ hai câu ngắn gọn:
- Scope trả lời: injector nào giữ instance?
- Import graph trả lời: code nằm trong chunk nào?
Nếu muốn đào sâu phần này, xem Lazy loading service và phạm vi provider.
Quy trình chốt scope
Thay vì tranh luận “root hay không root” theo cảm giác, hãy đi lần lượt qua bảy bước sau:
- Chỉ một component dùng, logic vẫn gọn? Để ngay trong component.
- Nhiều child của cùng một owner cần context? Cân nhắc component-scoped service.
- Có thể tồn tại hai owner cùng lúc? Nếu state phải độc lập, đừng dùng một root instance trần.
- Guard, resolver hoặc nhiều route con cần dependency? Cân nhắc route provider.
- Rời feature rồi quay lại có phải giữ state không? Nếu có, root hoặc persistence có chủ đích có thể hợp hơn local provider.
- State phải sống qua cả lần reload trình duyệt? DI scope không giải quyết được; cần storage hoặc server.
- Service stateless hoặc thật sự global?
providedIn: 'root'là lựa chọn gọn nhất.
Một câu để tự kiểm tra
Nếu xóa component hoặc feature này khỏi màn hình, state/service đó còn ý nghĩa không? Nếu câu trả lời là “không”, root có thể đang rộng hơn owner thật.
Kiểm thử đúng instance
Scope đúng ở production nhưng test lấy nhầm injector thì kết quả vẫn gây hiểu lầm.
Với root service, TestBed.inject() thường là đủ:
TestBed.configureTestingModule({
providers: [CheckoutStore],
});
const store = TestBed.inject(CheckoutStore);Với component provider, TestBed.inject(DraftStore) không lấy instance của EditorShellComponent. Cần tạo component rồi đọc đúng injector:
const fixture = TestBed.createComponent(EditorShellComponent);
const store = fixture.debugElement.injector.get(DraftStore);Một bẫy khác: bạn provide fake ở root test nhưng component lại tự provide class thật. Provider gần component hơn sẽ thắng. Khi đó cần override metadata trước khi tạo fixture:
TestBed.overrideComponent(EditorShellComponent, {
set: {
providers: [
{ provide: DraftStore, useClass: FakeDraftStore },
],
},
});Với route provider, hãy test qua Router setup hoặc integration test đi vào đúng route. Inject token thẳng từ root test không tái hiện được route injector.
Tóm tắt
- Root là mặc định tốt, không phải thứ cần né.
- State đơn giản chỉ phục vụ một component thì cứ để trong component.
- Tách thành service và chọn scope là hai quyết định riêng.
- Component-scoped service có giá trị khi một UI owner có nhiều child, nhiều instance độc lập hoặc resource cần cleanup cùng UI.
- Route-scoped service có giá trị khi guard, resolver, route con hoặc config feature cần cùng dependency.
- Root checkout store hoàn toàn đúng nếu sản phẩm muốn một checkout session dùng chung và có reset policy rõ ràng.
- Scope quá rộng tạo shared state ngoài ý muốn. Scope quá hẹp lại nhân bản cache, request và resource.
- Local service vẫn dùng được dependency root; Angular không tạo lại toàn bộ dependency tree.
- Scope DI không quyết định chunk. Import graph mới quyết định code được tải eager hay lazy.
Câu chốt dễ nhớ nhất là:
Đừng hỏi “service nên ở root hay không?”. Hãy hỏi “ai là owner thật sự của instance này?”.
Tài liệu tham khảo
- Hierarchical injectors: cây
EnvironmentInjector,ElementInjectorvà quy tắc provider gần nhất. - Defining dependency providers: các loại provider và vị trí đăng ký dependency.
- Define routes: route-level providers và route tree.
- Angular DevTools Injector Tree: quan sát token được phân giải từ injector nào.
Đọc tiếp
Dependency Injection
Hiểu injector hierarchy, provider resolution và vòng đời instance.
Thiết kế service
Tách UI, use case, facade và repository theo trách nhiệm.
Provider và InjectionToken
Cấu hình và thay implementation theo từng injector scope.
Lazy loading service
Phân biệt provider scope với vị trí service trong bundle.