Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。
其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫"依赖查找"(Dependency Lookup)。
通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
在传统的开发模式中,调用者负责管理所有对象的依赖,循环依赖一直是梦魇,而在依赖注入模式中,这个管理权交给了注入器(Injector),它在软件运行时负责依赖对象的替换,而不是在编译时。
这种控制反转,运行注入的特点即是依赖注入的精华所在。
依赖注入(DI)是一种重要的应用设计模式。 Angular 有自己的 DI 框架,在设计应用时常会用到它,以提升它们的开发效率和模块化程度。
依赖注入就是不通过 new 这种方式来在类的内部创建所依赖类的对象,而是在外部创建好需要依赖的类对象之后通过构造函数等方式注入进来就可以了;
如果一个类附加了 @Injectable
装饰器,就表示是 Angular 的服务,并且可以注入到 Angular 的组件或其他服务中。
与组件一样,服务也必须进行注册,才能使用。@Injectable
装饰器的 providedIn
属性值是一个对象,表示服务的注入器。 上面代码中的 root
,表示根注入器,也是默认注入器
// 不使用依赖注入
class NotificationComponent {
msg: MessageService;
constructor() {
this.msg = new MessageService();
}
sendMsg(msgType: string, info: string) {
this.msg.send(msgType, info);
}
}
// 使用依赖注入
class NotificationComponent {
constructor(msg: MessageService) {} // Angular 中注入依赖的方式
sendMsg(msgType: string, info: string) {
this.msg.send(msgType, info);
}
}
在 NotificationComponent 的构造函数中引入了 MessageService 并手动实例化,
在第二个例子中,并没有对实例化这部分做操作,或者说我们把实例化这部分流程交给了外层框架。
angular中常见的依赖注入有
服务的概念:服务是指封装单一功能,类似工具库,常被引用于组件内部,作为组件的功能扩展。可以简单理解为就是一个工具库,或者功能函数
服务的组成可以是以下几种
组件中注入服务的步骤
1、import引入被依赖的服务 2、在组件的providers中配置注入器 3、在组件的构造函数声明需要注入的依赖
注入到派生组件 创建组件是export class 组件名称...那么我们组件就是一个类,既然是类,那么就有类的:封装、继承、多态,所谓的派生组件就是一个继承父组件的子组件
注意点:
1、派生组件不能继承父组件的注入器 2、父组件与派生组件之间没有任何关联 3、父组件注入的服务,那么派生组件也必须注入父组件依赖的服务 4、在派生组件中的constructor中使用super(...)往组件中传递
为什么要用 @Injectable()?
@Injectable()
标识一个类可以被注入器实例化。
建议:为每个服务类都添加 @Injectable()。
为什么不标记 Component
为 @Injectable()
呢?
我们可以添加它,但是没有必要。因为 HerosComponent
已经有 @Component
装饰器了,@Component
(@Directive
以及 @Pipe
)是 Injectable 的子类型。
Angular 中的依赖注入使用方式
@Optional()
表示该服务是可选的,有时候我们引入的服务是不一定存在的,或者说用户不一定会在提供商中配置注入器
@Injectable(
// 注释这段代码,这样在通知组件中就无法找到 MessageService
// { providedIn: 'root' }
)
export class MessageService {
constructor() {}
send() {
console.log('msg');
}
}
@Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.less']
})
export class NotificationComponent implements OnInit {
constructor(@Optional() private msg: MessageService) {}
ngOnInit() {
this.msg.send(); //
}
}
@Optional() 允许 Angular 将您注入的服务视为可选服务。如果无法在运行时解析它,Angular 只会将服务解析为 null,而不会抛出错误;
@Self
使用 @Self 让 Angular 仅查看当前组件或指令的 ElementInjector
@Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.less']
})
export class NotificationComponent implements OnInit {
constructor(@Self() private msg: MessageService) {}
ngOnInit() {
this.msg.send();
}
}
// 报错查找不到依赖
@Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.less'],
providers: [
{
provide: MessageService,
useClass: NewMessageService
}
]
})
export class NotificationComponent implements OnInit {
constructor(@Self() private msg: MessageService) {}
ngOnInit() {
this.msg.send();
}
}
@SkipSelf
使用 @SkipSelf(),Angular 在父 ElementInjector 中而不是当前 ElementInjector 中开始搜索服务
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ParentMessageService {
constructor() {}
send() {
console.log('come from parent');
}
}
@Component({
selector: 'app-container',
templateUrl: './container.component.html',
styleUrls: ['./container.component.less'],
providers: [
{
provide: MessageService,
useClass: ParentMessageService
}
]
})
export class ContainerComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
// 使用的还是父级的service
@Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.less'],
providers: [
{
provide: MessageService,
useClass: NewMessageService
}
]
})
export class NotificationComponent implements OnInit {
constructor(@SkipSelf() private msg: MessageService) {}
ngOnInit() {
this.msg.send();
}
}
@Host
ClassProvider(类提供商)
providers: [Logger]
// 等效的明细写法
[{ provide: Logger, useClass: Logger }]
// 第一个是令牌,它作为键值 (key) 使用,用于定位依赖值和注册提供商。
// 第二个是一个提供商定义对象。 可以把它看做是指导如何创建依赖值的配方。
// 为提供商使用不同的类
[{ provide: Logger, useClass: BetterLogger }]
// 带依赖的类提供商
@Injectable()
class EvenBetterLogger extends Logger {
constructor(private userService: UserService) { super(); }
log(message: string) {
let name = this.userService.user.name;
super.log(`Message to ${name}: ${message}`);
}
}
// 注册提供商:
[ UserService,{ provide: Logger, useClass: EvenBetterLogger }]
[ NewLogger, // Not aliased! Creates two instances of NewLogger
{ provide: OldLogger, useClass: NewLogger}]
// 会创建两个实例
// 使用 useExisting 选项指定别名
[ NewLogger,
// Alias OldLogger w/ reference to NewLogger
{ provide: OldLogger, useExisting: NewLogger}]
ValueProvider(值提供商)
let silentLogger = {
logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
log: () => {}
};
通过 useValue
选项来注册提供商
[{ provide: Logger, useValue: silentLogger }]
FactoryProvider(工厂提供商)
工厂就是指工厂函数,既然是函数方式创建对象,那么就拥有了在运行期动态创建的能力。
需要动态创建这个依赖值,因为它所需要的信息直到最后一刻才能确定。 还假设这个可注入的服务没法通过独立的源访问此信息。这种情况下可调用工厂提供商。
// src/app/heroes/hero.service.ts
constructor(
private logger: Logger,
private isAuthorized: boolean) { }
getHeroes() {
let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}
let heroServiceFactory = (logger: Logger, userService: UserService) => {
return new HeroService(logger, userService.user.isAuthorized);
};
export let heroServiceProvider =
{ provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService]
};
HeroService
不能访问 UserService
,但是工厂方法可以。
useFactory
字段告诉 Angular:这个提供商是一个工厂方法,它的实现是 heroServiceFactory
deps
属性是提供商令牌数组。 Logger
和 UserService
类作为它们自身类提供商的令牌。 注入器解析这些令牌,把相应的服务注入到工厂函数中相应的参数中去。
import { Component } from '@angular/core';
import { heroServiceProvider } from './hero.service.provider';
@Component({
selector: 'my-heroes',
template: `
<h2>Heroes</h2>
<hero-list></hero-list>
`,
providers: [heroServiceProvider]
})
export class HeroesComponent { }
InjectionToken(预定义的 token 与多提供商(multi 参数))
可以理解为 Angular 应用某些操作的回调
多提供商机制可以使用一个令牌初始化多个提供商,方法就是设置 multi 参数为 true 即可。
多提供商其实很多人都会用到,比如在设置 HTTP 拦截器时,除了使用默认拦截器之外,还希望再添加上 JWT 拦截器时,多提供商就可以很好的组织服务提供方式:
**const** INTERCEPTOR_PROVIDES **=** [
{ provide**:** HTTP_INTERCEPTORS, useClass**:** DefaultInterceptor, multi**:** **true** },
{ provide**:** HTTP_INTERCEPTORS, useClass**:** JWTInterceptor, multi**:** **true** }
];
export interface AppConfig {
apiEndpoint: string;
title: string;
}
export const HERO_DI_CONFIG: AppConfig = {
apiEndpoint: 'api.heroes.com',
title: 'Dependency Injection'
};
TypeScript 接口不是一个有效的令牌
// FAIL! Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }
接口只是 TypeScript 设计时 (design-time) 的概念。JavaScript 没有接口。
TypeScript 接口不会出现在生成的 JavaScript 代码中。 在运行期,没有接口类型信息可供 Angular 查找。
解决方案是为非类依赖定义和使用 InjectionToken 作为提供商令牌。
import { InjectionToken } from '@angular/core';
export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
使用这个 InjectionToken
对象注册依赖的提供商
providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]
// 配置对象可以注入到任何需要它的构造函数中
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}
// 在 ngModule 中提供并注入这个配置对象,如 AppModule:
providers: [
UserService,
{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],
Angular DI 是一个分层的依赖注入系统,这意味着嵌套的注入器可以创建它们自己的服务实例。
多层级注入器 ModuleInjector 与 ElementInjector
转存失败重新上传取消
ModuleInjector
通过 @NgModule()
或者 @Injectable()
配置
Injectable()
的 providedIn 属性是要高于 @NgModule()
的 providers 数组providedIn: 'root'
ElementInjector
通过 @Directive()
或 @Component()
中的 providers 属性中配置
转存失败重新上传取消
用到了 InjectionToken 来生成 token,在 Angular 官方文档中,还为我们介绍了如何使用 InjectionToken 来封装浏览器内置的 API,比如 localStorage
首先将 localStorage
改为可注入的 BROWSER_STORAGE
token
const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
providedIn: 'root',
factory: () => localStorage
})
之后声明 BrowserStorageService
,并注入 BROWSER_STORAGE
@Injectable({
providedIn: 'root'
})
export class BrowserStorageService {
constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}
get(key: string) {
this.storage.getItem(key);
}
set(key: string, value: string) {
this.storage.setItem(key, value);
}
remove(key: string) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
}
使用 @Self() 和 @SkipSelf() 来修改提供商的搜索方式
@Self()
修饰符的很多作用都被 @Host()
修饰符所替代了,这里我们说一说 @SkipSelf()
在 ng-zorro 中的妙用
使用 ng-zorro 模态框组件 nz-modal 的同学应该都知道 nz-modal 可以通过调用 NzModalService
的 closeAll()
方法来关闭所有的模态框,那这在 ng-zorro 中是如何做到的呢?
我们首先看到 NzModalService 是调用了 NzModalControlService 的 closeAll()
// Closes all of the currently-open dialogs
closeAll(): void {
this.modalControl.closeAll();
}
NzModalControlService 会去获取当前所有打开的模态框并依次关闭,这并不是通过某个全局变量来存储的,而是通过查找 injection 树来获取的
// Track singleton openModals array through over the injection tree
get openModals(): NzModalRef[] {
return this.parentService ? this.parentService.openModals : this.rootOpenModals!;
}
来瞧瞧 NzModalControlService 的构造函数中 parentService 的注入方法:
constructor(@Optional() @SkipSelf() private parentService: NzModalControlService) {}
这里就是 @SkipSelf()
的使用方式,与 @Optional()
修饰符搭配可以查找到 Injection 树上所有的注入实例。