搜尋
首頁web前端js教程深入淺析Angular怎麼使用依賴注入

深入淺析Angular怎麼使用依賴注入

Sep 29, 2021 am 11:23 AM
angular依賴注入

這篇文章為大家介紹一下依賴注入在 Angular 中的應用,希望對大家有幫助!

深入淺析Angular怎麼使用依賴注入

本文透過實際案例,帶大家了解依賴注入Angular中的應用與部分實作原理,其中包含

  • useFactoryuseClassuseValueuseExisting 不同提供者的應用程式場景

  • ModuleInjectorElementInjector不同層次注入器的意義

  • ##@Injectable() @NgModule()中定義provider的差異

  • ##@Optional()

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

  • muti

    (多提供者)的應用場景

  • 【相關教學推薦:《
angular教學

》】如果你還不清楚什麼是

依賴注入

,可以先看這篇文章詳解依賴注入

useFactory、useClass、useValue 和useExisting 不同型別

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

useFactory 工廠提供者

某天,咱們接到一個需求:實作一個

本地儲存

的功能,並將其注入Angular應用程式中,使其可以在系統中全域使用首先編寫服務類別

storage.service.ts

,實作其儲存功能<pre class='brush:php;toolbar:false;'>// storage.service.ts export class StorageService { get(key: string) { return JSON.parse(localStorage.getItem(key) || &amp;#39;{}&amp;#39;) || {}; } set(key: string, value: ITokenModel | null): boolean { localStorage.setItem(key, JSON.stringify(value)); return true; } remove(key: string) { localStorage.removeItem(key); } }</pre>如果你馬上在

user.component.ts

中嘗試使用<pre class='brush:php;toolbar:false;'>// user.component.ts @Component({ selector: &amp;#39;app-user&amp;#39;, templateUrl: &amp;#39;./user.component.html&amp;#39;, styleUrls: [&amp;#39;./user.component.css&amp;#39;] }) export class CourseCardComponent { constructor(private storageService: StorageService) { ... } ... }</pre>你應該會看到這樣的一個錯誤:

NullInjectorError: No provider for StorageService!

顯而易見,我們並沒有將

StorageService

加入到Angular的依賴注入系統Angular無法取得StorageService依賴項的Provider,也就無法實例化這個類,更沒法呼叫類別中的方法。 接下來,我們本著缺撒補撒的理念,手動添加一個

Provider

。修改storage.service.ts檔案如下<pre class='brush:php;toolbar:false;'>// storage.service.ts export class StorageService { get(key: string) { return JSON.parse(localStorage.getItem(key) || &amp;#39;{}&amp;#39;) || {}; } set(key: string, value: any) { localStorage.setItem(key, JSON.stringify(value)); } remove(key: string) { localStorage.removeItem(key); } } // 添加工厂函数,实例化StorageService export storageServiceProviderFactory(): StorageService { return new StorageService(); }</pre>透過上述程式碼,我們已經有了

Provider

。那麼接下來的問題,就是如果讓Angular每次掃描到StorageService這個依賴項的時候,讓其去執行storageServiceProviderFactory方法,來建立實例這就引出來了下一個知識點

InjectionToken

在一個服務類別中,我們常常需要加入多個依賴項,來確保服務的可用。而

InjectionToken

是各個依賴項的唯一標識,它讓Angular的依賴注入系統能準確的找到各個依賴項的Provider接下來,我們手動新增一個

InjectionToken

<pre class='brush:php;toolbar:false;'>// storage.service.ts import { InjectionToken } from &amp;#39;@angular/core&amp;#39;; export class StorageService { get(key: string) { return JSON.parse(localStorage.getItem(key) || &amp;#39;{}&amp;#39;) || {}; } 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&lt;StorageService&gt;(&amp;#39;AUTH_STORE_TOKEN&amp;#39;);</pre>ok,我們已經有了

StorageService

ProviderInjectionToken接下來,我們需要一個配置,讓

Angular

依賴注入系統能夠對其進行識別,在掃描到StorageService(Dependency )的時候,根據STORAGE_SERVICE_TOKEN(InjectionToken)去找到對應的storageServiceProviderFactory(Provider),然後建立這個依賴項的實例。如下,我們在module中的@NgModule()裝飾器中進行配置:<pre class='brush:php;toolbar:false;'>// user.module.ts @NgModule({ imports: [ ... ], declarations: [ ... ], providers: [ { provide: STORAGE_SERVICE_TOKEN, // 与依赖项关联的InjectionToken,用于控制工厂函数的调用 useFactory: storageServiceProviderFactory, // 当需要创建并注入依赖项时,调用该工厂函数 deps: [] // 如果StorageService还有其他依赖项,这里添加 } ] }) export class UserModule { }</pre>到這裡,我們完成了

依賴

#的實現。最後,還需要讓Angular知道在哪裡注入Angular提供了@Inject裝飾器來識別<pre class='brush:php;toolbar:false;'>// user.component.ts @Component({ selector: &amp;#39;app-user&amp;#39;, templateUrl: &amp;#39;./user.component.html&amp;#39;, styleUrls: [&amp;#39;./user.component.css&amp;#39;] }) export class CourseCardComponent { constructor(@Inject(STORAGE_SERVICE_TOKEN) private storageService: StorageService) { ... } ... }</pre>到此,我們便可以在

user.component.ts

呼叫# StorageService裡面的方法了

useClass 類別提供者

emm...你是否覺得上述的寫法過於複雜了,而在實際開發中,我們大多數的場景是無需手動建立的

Provider

InjectionToken。如下:<pre class='brush:php;toolbar:false;'>// user.component.ts @Component({ selector: &amp;#39;app-user&amp;#39;, templateUrl: &amp;#39;./user.component.html&amp;#39;, styleUrls: [&amp;#39;./user.component.css&amp;#39;] }) export class CourseCardComponent { constructor(private storageService: StorageService) { ... } ... } // storage.service.ts @Injectable({ providedIn: &#39;root&#39; }) export class StorageService {} // user.module.ts @NgModule({ imports: [ ... ], declarations: [ ... ], providers: [StorageService] }) export class UserModule { }</pre>下面,我們來對上述的這種

簡寫模式

進行剖析。

user.component.ts

,我們捨棄了@Inject裝飾器,直接加入依賴項private storageService: StorageService,這得益於AngularInjectionToken的設計。 <blockquote><p><code>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) {
    ...
  }
  ...

这也就是我们平常开发中,最常见的一种写法。

useValue 值提供商

完成本地存储服务的实现后,我们又收到了一个新需求,研发老大希望提供一个配置文件,来存储StorageService的一些默认行为

我们先创建一个配置

const storageConfig = {
  suffix: &#39;app_&#39; // 添加一个存储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');
export const storageConfig = {
  suffix: &#39;app_&#39; // 添加一个存储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: &#39;app-user&#39;,
  templateUrl: &#39;./user.component.html&#39;,
  styleUrls: [&#39;./user.component.css&#39;]
})
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 + &#39;demo&#39;));
  }
}

useExisting 别名提供商

如果我们需要基于一个已存在的provider来创建一个新的provider,或需要重命名一个已存在的provider时,可以用useExisting属性来处理。比如:创建一个angular的表单控件,其在一个表单中会存在多个,每个表单控件存储不同的值。我们可以基于已有的表单控件provider来创建

// new-input.component.ts
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from &#39;@angular/forms&#39;;

@Component({
  selector: &#39;new-input&#39;,
  exportAs: &#39;newInput&#39;,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => NewInputComponent), // 这里的NewInputComponent已经声明了,但还没有被定义。无法直接使用,使用forwardRef可以创建一个间接引用,Angular在后续在解析该引用
      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: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;]
})
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,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。

我们通过一个例子来解释上面的概述。随着业务的增长,我们扩展了UserService1UserService2两个服务,但由于某些原因,UserService2一直未被使用。

如果通过@NgModule()providers引入依赖项,我们需要在user.module.ts文件中引入对应的user1.service.tsuser2.service.ts文件,然后在providers数组中添加UserService1UserService2引用。而由于UserService2所在文件在module文件中被引用,导致Angular中的tree shaker错误的认为这个UserService2已经被使用了。无法进行摇树优化。代码示例如下:

// user.module.ts
import UserService1 from &#39;./user1.service.ts&#39;;
import UserService2 from &#39;./user2.service.ts&#39;;
@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 &#39;./user.module.ts&#39;;
@Injectable({
  providedIn: UserModule
})
export class UserService2 {
  ...
}

ElementInjector 组件注入器

在了解完ModuleInjector后,我们继续通过刚才的例子讲述ElementInjector

最初,我们系统中的用户只有一个,我们也只需要一个组件和一个UserService来存取这个用户的信息即可

// user-card.component.ts
@Component({
  selector: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;]
})
export class UserCardComponent {
  ...
}

// user.service.ts
@Injectable({
  providedIn: "root"
})
export class UserService {
  ...
}

注意:上述代码将UserService被添加到根模块中,它仅会被实例化一次。

如果这时候系统中有多个用户,每个用户卡片组件里的UserService需存取对应用户的信息。如果还是按照上述的方法,UserService只会生成一个实例。那么就可能出现,张三存了数据后,李四去取数据,取到的是张三的结果。

那么,我们有办法实例化多个UserService,让每个用户的数据存取操作隔离开么?

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

// user-card.component.ts
@Component({
  selector: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;],
  providers: [UserService]
})
export class UserCardComponent {
  ...
}

通过上述代码,每个用户卡片组件都会实例化一个UserService,来存取各自的用户信息。

如果要解释上述的现象,就需要说到AngularComponents and Module Hierarchical Dependency Injection

在组件中使用依赖项时,Angular会优先在该组件的providers中寻找,判断该依赖项是否有匹配的provider。如果有,则直接实例化。如果没有,则查找父组件的providers,如果还是没有,则继续找父级的父级,直到根组件(app.component.ts)。如果在根组件中找到了匹配的provider,会先判断其是否有存在的实例,如果有,则直接返回该实例。如果没有,则执行实例化操作。如果根组件仍未找到,则开始从原组件所在的module开始查找,如果原组件所在module不存在,则继续查找父级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: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;],
  providers: [UserService],
})
export class UserCardComponent {
  constructor(@Self() private userService?: UserService) {}
}

@SkipSelf()

@SkipSelf()@Self()相反。使用@SkipSelf()Angular在父ElementInjector中而不是当前ElementInjector中开始搜索服务.

// 子组件 user-card.component.ts
@Component({
  selector: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;],
  providers: [UserService], // not work
})
export class UserCardComponent {
  constructor(@SkipSelf() private userService?: UserService) {}
}

// 父组件 parent-card.component.ts
@Component({
  selector: &#39;parent-card.component.ts&#39;,
  templateUrl: &#39;./parent-card.component.html&#39;,
  styleUrls: [&#39;./parent-card.component.less&#39;],
  providers: [
    {
      provide: UserService,
      useClass: ParentUserService, // work
    },
  ],
})
export class ParentCardComponent {
  constructor() {}
}

@Host()

@Host()使你可以在搜索provider时将当前组件指定为注入器树的最后一站。这和@Self()类似,即使树的更上级有一个服务实例,Angular也不会继续寻找。

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关联。最后可以通过HTTP_INTERCEPTORS获取所有provider的值

参考链接

Angular Dependency Injection: Complete Guide

Angular 中的依赖注入

更多编程相关知识,请访问:编程教学!!

以上是深入淺析Angular怎麼使用依賴注入的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:掘金社区。如有侵權,請聯絡admin@php.cn刪除
JavaScript,C和瀏覽器之間的關係JavaScript,C和瀏覽器之間的關係May 01, 2025 am 12:06 AM

引言我知道你可能會覺得奇怪,JavaScript、C 和瀏覽器之間到底有什麼關係?它們之間看似毫無關聯,但實際上,它們在現代網絡開發中扮演著非常重要的角色。今天我們就來深入探討一下這三者之間的緊密聯繫。通過這篇文章,你將了解到JavaScript如何在瀏覽器中運行,C 在瀏覽器引擎中的作用,以及它們如何共同推動網頁的渲染和交互。 JavaScript與瀏覽器的關係我們都知道,JavaScript是前端開發的核心語言,它直接在瀏覽器中運行,讓網頁變得生動有趣。你是否曾經想過,為什麼JavaScr

node.js流帶打字稿node.js流帶打字稿Apr 30, 2025 am 08:22 AM

Node.js擅長於高效I/O,這在很大程度上要歸功於流。 流媒體匯總處理數據,避免內存過載 - 大型文件,網絡任務和實時應用程序的理想。將流與打字稿的類型安全結合起來創建POWE

Python vs. JavaScript:性能和效率注意事項Python vs. JavaScript:性能和效率注意事項Apr 30, 2025 am 12:08 AM

Python和JavaScript在性能和效率方面的差異主要體現在:1)Python作為解釋型語言,運行速度較慢,但開發效率高,適合快速原型開發;2)JavaScript在瀏覽器中受限於單線程,但在Node.js中可利用多線程和異步I/O提升性能,兩者在實際項目中各有優勢。

JavaScript的起源:探索其實施語言JavaScript的起源:探索其實施語言Apr 29, 2025 am 12:51 AM

JavaScript起源於1995年,由布蘭登·艾克創造,實現語言為C語言。 1.C語言為JavaScript提供了高性能和系統級編程能力。 2.JavaScript的內存管理和性能優化依賴於C語言。 3.C語言的跨平台特性幫助JavaScript在不同操作系統上高效運行。

幕後:什麼語言能力JavaScript?幕後:什麼語言能力JavaScript?Apr 28, 2025 am 12:01 AM

JavaScript在瀏覽器和Node.js環境中運行,依賴JavaScript引擎解析和執行代碼。 1)解析階段生成抽象語法樹(AST);2)編譯階段將AST轉換為字節碼或機器碼;3)執行階段執行編譯後的代碼。

Python和JavaScript的未來:趨勢和預測Python和JavaScript的未來:趨勢和預測Apr 27, 2025 am 12:21 AM

Python和JavaScript的未來趨勢包括:1.Python將鞏固在科學計算和AI領域的地位,2.JavaScript將推動Web技術發展,3.跨平台開發將成為熱門,4.性能優化將是重點。兩者都將繼續在各自領域擴展應用場景,並在性能上有更多突破。

Python vs. JavaScript:開發環境和工具Python vs. JavaScript:開發環境和工具Apr 26, 2025 am 12:09 AM

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。

JavaScript是用C編寫的嗎?檢查證據JavaScript是用C編寫的嗎?檢查證據Apr 25, 2025 am 12:15 AM

是的,JavaScript的引擎核心是用C語言編寫的。 1)C語言提供了高效性能和底層控制,適合JavaScript引擎的開發。 2)以V8引擎為例,其核心用C 編寫,結合了C的效率和麵向對象特性。 3)JavaScript引擎的工作原理包括解析、編譯和執行,C語言在這些過程中發揮關鍵作用。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。