所属分类:web前端开发
本文通过实际案例,带大家了解依赖注入
在Angular
中的应用和部分实现原理,其中包括
useFactory
、useClass
、useValue
和 useExisting
不同提供商的应用场景
ModuleInjector
和ElementInjector
不同层次注入器的意义
@Injectable()
和@NgModule()
中定义provider
的区别
@Optional()
、@Self()
、@SkipSelf()
、@Host()
修饰符的使用
muti
(多提供商)的应用场景
【相关教程推荐:《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 Dependency Injection: Complete Guide
Angular 中的依赖注入
更多编程相关知识,请访问:编程教学!!
以上就是深入浅析Angular中怎么使用依赖注入的详细内容,更多请关注zzsucai.com其它相关文章!