這篇文章為大家介紹一下依賴注入在 Angular 中的應用,希望對大家有幫助!
本文透過實際案例,帶大家了解依賴注入
在Angular
中的應用與部分實作原理,其中包含
useFactory
、useClass
、useValue
和useExisting
不同提供者的應用程式場景
ModuleInjector
和ElementInjector
不同層次注入器的意義
##@Injectable() 與
@NgModule()中定義
provider的差異
、@Self()
、@SkipSelf()
、@Host()
修飾符的使用
(多提供者)的應用場景
下面,我們透過實際例子,來對幾個提供者的使用場景進行說明。 的功能,並將其注入
到Angular
應用程式中,使其可以在系統中全域使用首先編寫服務類別
,實作其儲存功能<pre class="brush:js;toolbar:false;">// storage.service.ts
export class StorageService {
get(key: string) {
return JSON.parse(localStorage.getItem(key) || &#39;{}&#39;) || {};
}
set(key: string, value: ITokenModel | null): boolean {
localStorage.setItem(key, JSON.stringify(value));
return true;
}
remove(key: string) {
localStorage.removeItem(key);
}
}</pre>
如果你馬上在
中嘗試使用<pre class="brush:js;toolbar:false;">// 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) {
...
}
...
}</pre>
你應該會看到這樣的一個錯誤:
NullInjectorError: No provider for StorageService!
顯而易見,我們並沒有將
StorageService加入到Angular的依賴注入系統
。 Angular
無法取得StorageService
依賴項的Provider
,也就無法實例化這個類,更沒法呼叫類別中的方法。 接下來,我們本著缺撒補撒的理念,手動添加一個
。修改storage.service.ts
檔案如下<pre class="brush:js;toolbar:false;">// storage.service.ts
export class StorageService {
get(key: string) {
return JSON.parse(localStorage.getItem(key) || &#39;{}&#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>
透過上述程式碼,我們已經有了
。那麼接下來的問題,就是如果讓Angular
每次掃描到StorageService
這個依賴項的時候,讓其去執行storageServiceProviderFactory
方法,來建立實例這就引出來了下一個知識點
在一個服務類別中,我們常常需要加入多個依賴項,來確保服務的可用。而
是各個依賴項的唯一標識,它讓Angular
的依賴注入系統能準確的找到各個依賴項的Provider
。 接下來,我們手動新增一個
<pre class="brush:js;toolbar:false;">// storage.service.ts
import { InjectionToken } from &#39;@angular/core&#39;;
export class StorageService {
get(key: string) {
return JSON.parse(localStorage.getItem(key) || &#39;{}&#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<StorageService>(&#39;AUTH_STORE_TOKEN&#39;);</pre>
ok,我們已經有了
的Provider
和InjectionToken
。 接下來,我們需要一個配置,讓
的依賴注入系統
能夠對其進行識別,在掃描到StorageService
(Dependency )的時候,根據STORAGE_SERVICE_TOKEN
(InjectionToken)去找到對應的storageServiceProviderFactory
(Provider),然後建立這個依賴項的實例。如下,我們在module
中的@NgModule()
裝飾器中進行配置:<pre class="brush:js;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:js;toolbar:false;">// 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(@Inject(STORAGE_SERVICE_TOKEN) private storageService: StorageService) {
...
}
...
}</pre>
到此,我們便可以在
呼叫# StorageService
裡面的方法了
和InjectionToken
。如下:<pre class="brush:js;toolbar:false;">// 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) {
...
}
...
}
// storage.service.ts
@Injectable({ providedIn: 'root' })
export class StorageService {}
// user.module.ts
@NgModule({
imports: [
...
],
declarations: [
...
],
providers: [StorageService]
})
export class UserModule { }</pre>
下面,我們來對上述的這種
進行剖析。 在
,我們捨棄了@Inject
裝飾器,直接加入依賴項private storageService: StorageService
,這得益於Angular
對InjectionToken
的設計。 <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) { ... } ...
这也就是我们平常开发中,最常见的一种写法。
完成本地存储服务
的实现后,我们又收到了一个新需求,研发老大希望提供一个配置文件,来存储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'); 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 Dependency Injection: Complete Guide
更多编程相关知识,请访问:编程教学!!
以上是深入淺析Angular怎麼使用依賴注入的詳細內容。更多資訊請關注PHP中文網其他相關文章!