溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Angular中如何使用依賴注入

發(fā)布時間:2021-09-29 11:43:31 來源:億速云 閱讀:175 作者:小新 欄目:web開發(fā)

這篇文章主要介紹了Angular中如何使用依賴注入,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

useFactory、useClass、useValue 和 useExisting 不同類型provider的應用場景

下面,我們通過實際例子,來對幾個提供商的使用場景進行說明。

useFactory 工廠提供商

某天,咱們接到一個需求:實現(xiàn)一個本地存儲的功能,并將其注入Angular應用中,使其可以在系統(tǒng)中全局使用

首先編寫服務類storage.service.ts,實現(xiàn)其存儲功能

// 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的依賴注入系統(tǒng)中。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);
  }
}

// 添加工廠函數(shù),實例化StorageService
export storageServiceProviderFactory(): StorageService {
  return new StorageService();
}

通過上述代碼,我們已經有了Provider。那么接下來的問題,就是如果讓Angular每次掃描到StorageService這個依賴項的時候,讓其去執(zhí)行storageServiceProviderFactory方法,來創(chuàng)建實例

這就引出來了下一個知識點InjectionToken

在一個服務類中,我們常常需要添加多個依賴項,來保證服務的可用。而InjectionToken是各個依賴項的唯一標識,它讓Angular的依賴注入系統(tǒng)能準確的找到各個依賴項的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,我們已經有了StorageServiceProviderInjectionToken。

接下來,我們需要一個配置,讓Angular依賴注入系統(tǒng)能夠對其進行識別,在掃描到StorageService(Dependency)的時候,根據(jù)STORAGE_SERVICE_TOKEN(InjectionToken)去找到對應的storageServiceProviderFactory(Provider),然后創(chuàng)建這個依賴項的實例。如下,我們在module中的@NgModule()裝飾器中進行配置:

// user.module.ts
@NgModule({
  imports: [
    ...
  ],
  declarations: [
    ...
  ],
  providers: [
    {
      provide: STORAGE_SERVICE_TOKEN, // 與依賴項關聯(lián)的InjectionToken,用于控制工廠函數(shù)的調用
      useFactory: storageServiceProviderFactory, // 當需要創(chuàng)建并注入依賴項時,調用該工廠函數(shù)
      deps: [] // 如果StorageService還有其他依賴項,這里添加
    }
  ]
})
export class UserModule { }

到這里,我們完成了依賴的實現(xiàn)。最后,還需要讓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里面的方法了

useClass 類提供商

emm...你是否覺得上述的寫法過于復雜了,而在實際開發(fā)中,我們大多數(shù)場景是無需手動創(chuàng)建ProviderInjectionToken的。如下:

// 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,這得益于AngularInjectionToken的設計。

InjectionToken不一定必須是一個InjectionToken object,只要保證它在運行時環(huán)境中能夠識別對應的唯一依賴項即可。所以,在這里,你可以用類名即運行時中的構造函數(shù)名稱來作為依賴項InjectionToken。省略創(chuàng)建InjectionToken這一步驟。

// user.module.ts
@NgModule({
  imports: [
    ...
  ],
  declarations: [
    ...
  ],
  providers: [{
    provide: StorageService, // 使用構造函數(shù)名作為InjectionToken
    useFactory: storageServiceProviderFactory,
    deps: []
  }]
})
export class UserModule { }

注意:由于Angular依賴注入系統(tǒng)是在運行時環(huán)境中根據(jù)InjectionToken識別依賴項,進行依賴注入的。所以這里不能使用interface名稱作為InjectionToken,因為其只存在于Typescript語言的編譯期,并不存在于運行時中。而對于類名來說,其在運行時環(huán)境中以構造函數(shù)名體現(xiàn),可以使用。

接下來,我們可以使用useClass替換useFactory,其實也能達到創(chuàng)建實例的效果,如下:

...
providers: [{
  provide: StorageService,
  useClass: StorageService,
  deps: []
}]
...

當使用useClass時,Angular會將后面的值當作一個構造函數(shù),在運行時環(huán)境中,直接執(zhí)行new指令進行實例化,這也無需我們再手動創(chuàng)建 Provider

當然,基于Angular依賴注入設計,我們可以寫得更簡單

...
providers: [StorageService]
...

直接寫入類名providers數(shù)組中,Angular會識別其是一個構造函數(shù),然后檢查函數(shù)內部,創(chuàng)建一個工廠函數(shù)去查找其構造函數(shù)中的依賴項,最后再實例化

useClass還有一個特性是,Angular會根據(jù)依賴項Typescript中的類型定義,作為其運行時InjectionToken去自動查找Provider。所以,我們也無需使用@Inject裝飾器來告訴Angular在哪里注入了

你可以簡寫如下

  ...
  // 無需手動注入 :constructor(@Inject(StorageService) private storageService: StorageService)
  constructor(private storageService: StorageService) {
    ...
  }
  ...

這也就是我們平常開發(fā)中,最常見的一種寫法。

useValue 值提供商

完成本地存儲服務的實現(xiàn)后,我們又收到了一個新需求,研發(fā)老大希望提供一個配置文件,來存儲StorageService的一些默認行為

我們先創(chuàng)建一個配置

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'));
  }
}
useExisting 別名提供商

如果我們需要基于一個已存在的provider來創(chuàng)建一個新的provider,或需要重命名一個已存在的provider時,可以用useExisting屬性來處理。比如:創(chuàng)建一個angular的表單控件,其在一個表單中會存在多個,每個表單控件存儲不同的值。我們可以基于已有的表單控件provider來創(chuàng)建

// 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可以創(chuàng)建一個間接引用,Angular在后續(xù)在解析該引用
      multi: true
    }
  ]
})
export class NewInputComponent implements ControlValueAccessor {
  ...
}

ModuleInjector 和 ElementInjector 層級注入器的意義

Angular中有兩個注入器層次結構

  • ModuleInjector —— 使用 @NgModule() 或 @Injectable() 的方式在模塊中注入

  • ElementInjector —— 在 @Directive() 或 @Component() 的 providers 屬性中進行配置

我們通過一個實際例子來解釋兩種注入器的應用場景,比如:設計一個展示用戶信息的卡片組件

ModuleInjector 模塊注入器

我們使用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 {
  ...
}

如果你足夠細心,會發(fā)現(xiàn)上述例子中,我們既可以通過在當前service文件中的@Injectable({ provideIn: xxx })定義provider,也可以在其所屬module中的@NgModule({ providers: [xxx] })定義。那么,他們有什么區(qū)別呢?

@Injectable()@NgModule()除了使用方式不同外,還有一個很大的區(qū)別是:

使用 @Injectable() 的 providedIn 屬性優(yōu)于 @NgModule() 的 providers 數(shù)組,因為使用 @Injectable() 的 providedIn 時,優(yōu)化工具可以進行搖樹優(yōu)化 Tree Shaking,從而刪除你的應用程序中未使用的服務,以減小捆綁包尺寸。

我們通過一個例子來解釋上面的概述。隨著業(yè)務的增長,我們擴展了UserService1UserService2兩個服務,但由于某些原因,UserService2一直未被使用。

如果通過@NgModule()providers引入依賴項,我們需要在user.module.ts文件中引入對應的user1.service.tsuser2.service.ts文件,然后在providers數(shù)組中添加UserService1UserService2引用。而由于UserService2所在文件在module文件中被引用,導致Angular中的tree shaker錯誤的認為這個UserService2已經被使用了。無法進行搖樹優(yōu)化。代碼示例如下:

// 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沒有被依賴時,即可被優(yōu)化掉。代碼示例如下:

// user2.service.ts
import UserModule from './user.module.ts';
@Injectable({
  providedIn: UserModule
})
export class UserService2 {
  ...
}
ElementInjector 組件注入器

在了解完ModuleInjector后,我們繼續(xù)通過剛才的例子講述ElementInjector。

最初,我們系統(tǒng)中的用戶只有一個,我們也只需要一個組件和一個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被添加到根模塊中,它僅會被實例化一次。

如果這時候系統(tǒng)中有多個用戶,每個用戶卡片組件里的UserService需存取對應用戶的信息。如果還是按照上述的方法,UserService只會生成一個實例。那么就可能出現(xiàn),張三存了數(shù)據(jù)后,李四去取數(shù)據(jù),取到的是張三的結果。

那么,我們有辦法實例化多個UserService,讓每個用戶的數(shù)據(jù)存取操作隔離開么?

答案是有的。我們需要在user.component.ts文件中使用ElementInjector,將UserServiceprovider添加即可。如下:

// 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,來存取各自的用戶信息。

如果要解釋上述的現(xiàn)象,就需要說到AngularComponents and Module Hierarchical Dependency Injection。

在組件中使用依賴項時,Angular會優(yōu)先在該組件的providers中尋找,判斷該依賴項是否有匹配的provider。如果有,則直接實例化。如果沒有,則查找父組件的providers,如果還是沒有,則繼續(xù)找父級的父級,直到根組件(app.component.ts)。如果在根組件中找到了匹配的provider,會先判斷其是否有存在的實例,如果有,則直接返回該實例。如果沒有,則執(zhí)行實例化操作。如果根組件仍未找到,則開始從原組件所在的module開始查找,如果原組件所在module不存在,則繼續(xù)查找父級module,直到根模塊(app.module.ts)。最后,仍未找到則報錯No provider for xxx。

@Optional()、@Self()、@SkipSelf()、@Host() 修飾符的使用

Angular應用中,當依賴項尋找provider時,我們可以通過一些修飾符來對搜索結果進行容錯處理限制搜索的范圍

@Optional()

通過@Optional()裝飾服務,表明讓該服務可選。即如果在程序中,沒有找到服務匹配的provider,也不會程序崩潰,報錯No provider for xxx,而是返回null。

export class UserCardComponent {
  constructor(@Optional private userService: UserService) {}
}
@Self()

使用@Self()Angular僅查看當前組件或指令的ElementInjector。

如下,Angular只會在當前UserCardComponentproviders中搜索匹配的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()

@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()

@Host()使你可以在搜索provider時將當前組件指定為注入器樹的最后一站。這和@Self()類似,即使樹的更上級有一個服務實例,Angular也不會繼續(xù)尋找。

multi 多服務提供商

某些場景下,我們需要一個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關聯(lián)。最后可以通過HTTP_INTERCEPTORS獲取所有provider的值

感謝你能夠認真閱讀完這篇文章,希望小編分享的“Angular中如何使用依賴注入”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業(yè)資訊頻道,更多相關知識等著你來學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI