您好,登錄后才能下訂單哦!
這篇文章主要介紹了Angular中如何使用依賴注入,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
provider
的應用場景下面,我們通過實際例子,來對幾個提供商的使用場景進行說明。
某天,咱們接到一個需求:實現一個本地存儲
的功能,并將其注入
到Angular
應用中,使其可以在系統中全局使用
首先編寫服務類storage.service.ts
,實現其存儲功能
// storage.service.ts export class StorageService { get(key: string) { return JSON.parse(localStorage.getItem(key) || '{}') || {}; } set(key: string, value: ITokenModel | null): boolean { localStorage.setItem(key, JSON.stringify(value)); return true; } remove(key: string) { localStorage.removeItem(key); } }
如果你馬上在user.component.ts
中嘗試使用
// user.component.ts @Component({ selector: 'app-user', templateUrl: './user.component.html', styleUrls: ['./user.component.css'] }) export class CourseCardComponent { constructor(private storageService: StorageService) { ... } ... }
你應該會看到這樣的一個錯誤:
NullInjectorError: No provider for StorageService!
顯而易見,我們并沒有將StorageService
添加到 Angular的依賴注入系統
中。Angular
無法獲取StorageService
依賴項的Provider
,也就無法實例化這個類,更沒法調用類中的方法。
接下來,我們本著缺撒補撒的理念,手動添加一個Provider
。修改storage.service.ts
文件如下
// storage.service.ts export class StorageService { get(key: string) { return JSON.parse(localStorage.getItem(key) || '{}') || {}; } set(key: string, value: any) { localStorage.setItem(key, JSON.stringify(value)); } remove(key: string) { localStorage.removeItem(key); } } // 添加工廠函數,實例化StorageService export storageServiceProviderFactory(): StorageService { return new StorageService(); }
通過上述代碼,我們已經有了Provider
。那么接下來的問題,就是如果讓Angular
每次掃描到StorageService
這個依賴項的時候,讓其去執行storageServiceProviderFactory
方法,來創建實例
這就引出來了下一個知識點InjectionToken
在一個服務類中,我們常常需要添加多個依賴項,來保證服務的可用。而InjectionToken
是各個依賴項的唯一標識,它讓Angular
的依賴注入系統能準確的找到各個依賴項的Provider
。
接下來,我們手動添加一個InjectionToken
// storage.service.ts import { InjectionToken } from '@angular/core'; export class StorageService { get(key: string) { return JSON.parse(localStorage.getItem(key) || '{}') || {}; } set(key: string, value: any) { localStorage.setItem(key, JSON.stringify(value)); } remove(key: string) { localStorage.removeItem(key); } } export storageServiceProviderFactory(): StorageService { return new StorageService(); } // 添加StorageServiced的InjectionToken export const STORAGE_SERVICE_TOKEN = new InjectionToken<StorageService>('AUTH_STORE_TOKEN');
ok,我們已經有了StorageService
的Provider
和InjectionToken
。
接下來,我們需要一個配置,讓Angular
的依賴注入系統
能夠對其進行識別,在掃描到StorageService
(Dependency)的時候,根據STORAGE_SERVICE_TOKEN
(InjectionToken)去找到對應的storageServiceProviderFactory
(Provider),然后創建這個依賴項的實例。如下,我們在module
中的@NgModule()
裝飾器中進行配置:
// user.module.ts @NgModule({ imports: [ ... ], declarations: [ ... ], providers: [ { provide: STORAGE_SERVICE_TOKEN, // 與依賴項關聯的InjectionToken,用于控制工廠函數的調用 useFactory: storageServiceProviderFactory, // 當需要創建并注入依賴項時,調用該工廠函數 deps: [] // 如果StorageService還有其他依賴項,這里添加 } ] }) export class UserModule { }
到這里,我們完成了依賴
的實現。最后,還需要讓Angular
知道在哪里注入
。Angular
提供了 @Inject
裝飾器來識別
// user.component.ts @Component({ selector: 'app-user', templateUrl: './user.component.html', styleUrls: ['./user.component.css'] }) export class CourseCardComponent { constructor(@Inject(STORAGE_SERVICE_TOKEN) private storageService: StorageService) { ... } ... }
到此,我們便可以在user.component.ts
調用StorageService
里面的方法了
emm...你是否覺得上述的寫法過于復雜了,而在實際開發中,我們大多數場景是無需手動創建Provider
和InjectionToken
的。如下:
// user.component.ts @Component({ selector: 'app-user', templateUrl: './user.component.html', styleUrls: ['./user.component.css'] }) export class CourseCardComponent { constructor(private storageService: StorageService) { ... } ... } // storage.service.ts @Injectable({ providedIn: 'root' }) export class StorageService {} // user.module.ts @NgModule({ imports: [ ... ], declarations: [ ... ], providers: [StorageService] }) export class UserModule { }
下面,我們來對上述的這種簡寫模式
進行剖析。
在user.component.ts
,我們舍棄了@Inject
裝飾器,直接添加依賴項private storageService: StorageService
,這得益于Angular
對InjectionToken
的設計。
InjectionToken
不一定必須是一個InjectionToken object
,只要保證它在運行時環境中能夠識別對應的唯一依賴項
即可。所以,在這里,你可以用類名
即運行時中的構造函數名稱
來作為依賴項
的InjectionToken
。省略創建InjectionToken
這一步驟。
// user.module.ts @NgModule({ imports: [ ... ], declarations: [ ... ], providers: [{ provide: StorageService, // 使用構造函數名作為InjectionToken useFactory: storageServiceProviderFactory, deps: [] }] }) export class UserModule { }
注意:由于Angular
的依賴注入系統
是在運行時環境
中根據InjectionToken
識別依賴項,進行依賴注入的。所以這里不能使用interface
名稱作為InjectionToken
,因為其只存在于Typescript
語言的編譯期,并不存在于運行時中。而對于類名
來說,其在運行時環境中以構造函數名
體現,可以使用。
接下來,我們可以使用useClass
替換useFactory
,其實也能達到創建實例的效果,如下:
... providers: [{ provide: StorageService, useClass: StorageService, deps: [] }] ...
當使用useClass
時,Angular
會將后面的值當作一個構造函數
,在運行時環境中,直接執行new
指令進行實例化,這也無需我們再手動創建 Provider
了
當然,基于Angular
的依賴注入設計
,我們可以寫得更簡單
... providers: [StorageService] ...
直接寫入類名
到providers
數組中,Angular
會識別其是一個構造函數
,然后檢查函數內部,創建一個工廠函數去查找其構造函數
中的依賴項
,最后再實例化
useClass
還有一個特性是,Angular
會根據依賴項
在Typescript
中的類型定義,作為其運行時
的InjectionToken
去自動查找Provider
。所以,我們也無需使用@Inject
裝飾器來告訴Angular
在哪里注入了
你可以簡寫如下
... // 無需手動注入 :constructor(@Inject(StorageService) private storageService: StorageService) constructor(private storageService: StorageService) { ... } ...
這也就是我們平常開發中,最常見的一種寫法。
完成本地存儲服務
的實現后,我們又收到了一個新需求,研發老大希望提供一個配置文件,來存儲StorageService
的一些默認行為
我們先創建一個配置
const storageConfig = { suffix: 'app_' // 添加一個存儲key的前綴 expires: 24 * 3600 * 100 // 過期時間,毫秒戳 }
而useValue
則可以 cover 住這種場景。其可以是一個普通變量,也可以是一個對象形式。
配置方法如下:
// config.ts export interface STORAGE_CONFIG = { suffix: string; expires: number; } export const STORAGE_CONFIG_TOKEN = new InjectionToken<STORAGE_CONFIG>('storage-config'); export const storageConfig = { suffix: 'app_' // 添加一個存儲key的前綴 expires: 24 * 3600 * 100 // 過期時間,毫秒戳 } // user.module.ts @NgModule({ ... providers: [ StorageService, { provide: STORAGE_CONFIG_TOKEN, useValue: storageConfig } ], ... }) export class UserModule {}
在user.component.ts
組件中,直接使用配置對象
:
// user.component.ts @Component({ selector: 'app-user', templateUrl: './user.component.html', styleUrls: ['./user.component.css'] }) export class CourseCardComponent { constructor(private storageService: StorageService, @Inject(STORAGE_CONFIG_TOKEN) private storageConfig: StorageConfig) { ... } getKey(): void { const { suffix } = this.storageConfig; console.log(this.storageService.get(suffix + 'demo')); } }
如果我們需要基于一個已存在的provider
來創建一個新的provider
,或需要重命名一個已存在的provider
時,可以用useExisting
屬性來處理。比如:創建一個angular
的表單控件,其在一個表單中會存在多個,每個表單控件存儲不同的值。我們可以基于已有的表單控件provider
來創建
// new-input.component.ts import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'new-input', exportAs: 'newInput', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NewInputComponent), // 這里的NewInputComponent已經聲明了,但還沒有被定義。無法直接使用,使用forwardRef可以創建一個間接引用,Angular在后續在解析該引用 multi: true } ] }) export class NewInputComponent implements ControlValueAccessor { ... }
在Angular
中有兩個注入器層次結構
ModuleInjector —— 使用 @NgModule() 或 @Injectable() 的方式在模塊中注入
ElementInjector —— 在 @Directive() 或 @Component() 的 providers 屬性中進行配置
我們通過一個實際例子來解釋兩種注入器的應用場景,比如:設計一個展示用戶信息的卡片組件
我們使用user-card.component.ts
來顯示組件,用UserService
來存取該用戶的信息
// user-card.component.ts @Component({ selector: 'user-card.component.ts', templateUrl: './user-card.component.html', styleUrls: ['./user-card.component.less'] }) export class UserCardComponent { ... } // user.service.ts @Injectable({ providedIn: "root" }) export class UserService { ... }
上述代碼是通過@Injectable
添加到根模塊
中,root
即根模塊的別名。其等價于下面的代碼
// user.service.ts export class UserService { ... } // app.module.ts @NgModule({ ... providers: [UserService], // 通過providers添加 }) export class AppModule {}
當然,如果你覺得UserService
只會在UserModule
模塊下使用的話,你大可不必將其添加到根模塊
中,添加到所在模塊即可
// user.service.ts @Injectable({ providedIn: UserModule }) export class UserService { ... }
如果你足夠細心,會發現上述例子中,我們既可以通過在當前service
文件中的@Injectable({ provideIn: xxx })
定義provider
,也可以在其所屬module
中的@NgModule({ providers: [xxx] })
定義。那么,他們有什么區別呢?
@Injectable()
和@NgModule()
除了使用方式不同外,還有一個很大的區別是:
使用 @Injectable() 的 providedIn 屬性優于 @NgModule() 的 providers 數組,因為使用 @Injectable() 的 providedIn 時,優化工具可以進行
搖樹優化 Tree Shaking
,從而刪除你的應用程序中未使用的服務,以減小捆綁包尺寸。
我們通過一個例子來解釋上面的概述。隨著業務的增長,我們擴展了UserService1
和UserService2
兩個服務,但由于某些原因,UserService2
一直未被使用。
如果通過@NgModule()
的providers
引入依賴項,我們需要在user.module.ts
文件中引入對應的user1.service.ts
和user2.service.ts
文件,然后在providers
數組中添加UserService1
和UserService2
引用。而由于UserService2
所在文件在module
文件中被引用,導致Angular
中的tree shaker
錯誤的認為這個UserService2
已經被使用了。無法進行搖樹優化
。代碼示例如下:
// user.module.ts import UserService1 from './user1.service.ts'; import UserService2 from './user2.service.ts'; @NgModule({ ... providers: [UserService1, UserService2], // 通過providers添加 }) export class UserModule {}
那么,如果通過@Injectable({providedIn: UserModule})
這種方式,我們實際是在服務類
自身文件中引用了use.module.ts
,并為其定義了一個provider
。無需在UserModule
中在重復定義,也就不需要在引入user2.service.ts
文件了。所以,當UserService2
沒有被依賴時,即可被優化掉。代碼示例如下:
// user2.service.ts import UserModule from './user.module.ts'; @Injectable({ providedIn: UserModule }) export class UserService2 { ... }
在了解完ModuleInjector
后,我們繼續通過剛才的例子講述ElementInjector
。
最初,我們系統中的用戶只有一個,我們也只需要一個組件和一個UserService
來存取這個用戶的信息即可
// user-card.component.ts @Component({ selector: 'user-card.component.ts', templateUrl: './user-card.component.html', styleUrls: ['./user-card.component.less'] }) export class UserCardComponent { ... } // user.service.ts @Injectable({ providedIn: "root" }) export class UserService { ... }
注意:上述代碼將UserService
被添加到根模塊
中,它僅會被實例化一次。
如果這時候系統中有多個用戶,每個用戶卡片組件
里的UserService
需存取對應用戶的信息。如果還是按照上述的方法,UserService
只會生成一個實例。那么就可能出現,張三存了數據后,李四去取數據,取到的是張三的結果。
那么,我們有辦法實例化多個UserService
,讓每個用戶的數據存取操作隔離開么?
答案是有的。我們需要在user.component.ts
文件中使用ElementInjector
,將UserService
的provider
添加即可。如下:
// user-card.component.ts @Component({ selector: 'user-card.component.ts', templateUrl: './user-card.component.html', styleUrls: ['./user-card.component.less'], providers: [UserService] }) export class UserCardComponent { ... }
通過上述代碼,每個用戶卡片組件
都會實例化一個UserService
,來存取各自的用戶信息。
如果要解釋上述的現象,就需要說到Angular
的Components and Module Hierarchical Dependency Injection
。
在組件中使用依賴項時,
Angular
會優先在該組件的providers
中尋找,判斷該依賴項是否有匹配的provider
。如果有,則直接實例化。如果沒有,則查找父組件的providers
,如果還是沒有,則繼續找父級的父級,直到根組件
(app.component.ts)。如果在根組件
中找到了匹配的provider
,會先判斷其是否有存在的實例,如果有,則直接返回該實例。如果沒有,則執行實例化操作。如果根組件
仍未找到,則開始從原組件
所在的module
開始查找,如果原組件
所在module
不存在,則繼續查找父級module
,直到根模塊
(app.module.ts)。最后,仍未找到則報錯No provider for xxx
。
在Angular
應用中,當依賴項
尋找provider
時,我們可以通過一些修飾符來對搜索結果進行容錯處理或限制搜索的范圍。
通過
@Optional()
裝飾服務,表明讓該服務可選。即如果在程序中,沒有找到服務匹配的provider
,也不會程序崩潰,報錯No provider for xxx
,而是返回null
。
export class UserCardComponent { constructor(@Optional private userService: UserService) {} }
使用
@Self()
讓Angular
僅查看當前組件或指令的ElementInjector
。
如下,Angular
只會在當前UserCardComponent
的providers
中搜索匹配的provider
,如果未匹配,則直接報錯。No provider for UserService
。
// user-card.component.ts @Component({ selector: 'user-card.component.ts', templateUrl: './user-card.component.html', styleUrls: ['./user-card.component.less'], providers: [UserService], }) export class UserCardComponent { constructor(@Self() private userService?: UserService) {} }
@SkipSelf()
與@Self()
相反。使用@SkipSelf()
,Angular
在父ElementInjector
中而不是當前ElementInjector
中開始搜索服務.
// 子組件 user-card.component.ts @Component({ selector: 'user-card.component.ts', templateUrl: './user-card.component.html', styleUrls: ['./user-card.component.less'], providers: [UserService], // not work }) export class UserCardComponent { constructor(@SkipSelf() private userService?: UserService) {} } // 父組件 parent-card.component.ts @Component({ selector: 'parent-card.component.ts', templateUrl: './parent-card.component.html', styleUrls: ['./parent-card.component.less'], providers: [ { provide: UserService, useClass: ParentUserService, // work }, ], }) export class ParentCardComponent { constructor() {} }
@Host()
使你可以在搜索provider
時將當前組件指定為注入器樹的最后一站。這和@Self()
類似,即使樹的更上級有一個服務實例,Angular
也不會繼續尋找。
某些場景下,我們需要一個InjectionToken
初始化多個provider
。比如:在使用攔截器的時候,我們希望在default.interceptor.ts
之前添加一個 用于 token 校驗的JWTInterceptor
... const NET_PROVIDES = [ { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true } ]; ...
multi: 為false
時,provider
的值會被覆蓋;設置為true
,將生成多個provider
并與唯一InjectionToken
HTTP_INTERCEPTORS
關聯。最后可以通過HTTP_INTERCEPTORS
獲取所有provider
的值
感謝你能夠認真閱讀完這篇文章,希望小編分享的“Angular中如何使用依賴注入”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。