Angular依赖注入

一、什么是依赖注入?

说到依赖注入,不能不从控制反转开始说起,控制反转(IOC),是针对面向对象设计不断复杂化而提出的一种设计原则,是一种利用面向对象编程法则来降低应用程序耦合的设计模式。IoC强调的对代码引用的控制权由调用方法转移到外部容器,在运行时通过某种方式注入进来,实现控制的反转,这大大降低了服务类之间的耦合度。依赖注入是一种最常用的实现IoC的方式。

说通俗一点:

依赖注入(DI)是指当模块A需要模块B才能运行,也就是B是A的依赖,这种情况下,不需要实例化B对象,而是在模块中导入一个文件,为模块A提供模块B依赖。而控制反转(IOC)是指由原来的控制权在内部(实例化)变化为控制权在外部(导入的文件内部),这时,依赖的控制权就由原来的内部变为了外部进行控制,达到控制反转的效果。而控制反转与依赖注入是一体两念,是一个思想。控制反转侧重于描述目的,是将依赖的控制权从代码的内部转移到代码的外部;依赖注入侧重于描述手段,就是如何实现控制反转?在Angular中通过依赖注入这个手段去实现控制反转。

二、依赖注入带来的好处

  • 解耦合与可重用性

模块内部所依赖的模块发送改变时,不需要修改内部代码,只需导入其他文件即可。

  • 提高可测试性

测试必要时,可以导入虚拟文件进行测试,提高测试效率。

三、如何实现依赖注入

1、注入器(Inject)

每一个组件都有一个注入器实例,负责注入组件需要的对象,注入器是Angular提供的一个服务类,一般情况下不需要直接调用注入器的方法,注入器会自动通过组件的构造函数将组件所需的对象注入进组件。

//此组件的的构造函数声明了一个productService属性,他的类型是ProductService
constructor(private productService:ProductService){...}

Angular的注入器在看到这样一个构造函数声明时,其会在整个Angular应用中去寻找ProductService的实例,如果能找到这个实例,那么就会将ProductService注入到productService里去。

2、提供器(Provider)

用于配置注入器,注入器通过它来创建被依赖对象的实例,Provider把标识映射到工厂方法中,被依赖的对象就是通过该方法创建的。

providers:[ProductService]
//等同于 providers:[{provide:ProductService, useClass:ProductService}]
//provider指定了提供器的token, useClass表示实例化属性为new。
providers:[{provider:ProductService, useClass:AnotherProductService}]
providers:[{provide:ProductService, useFactory:()=>{...}]

3、提供器的作用域规则

  • 当一个提供器声明在模块中时,作用于所有组件;
  • 当一个提供器声明在组件内时,只作用于当前组件及其子组件内;
  • 当声明在模块中的提供器和声明在组件中的提供器具有相同的token时,组件中声明的会覆盖模块中声明的。
  • 一般情况下,应将提供器首先声明在模块中,只有在服务必须于某个组件相对于其他组件不可见时才声明在组件中,而这种情况十分少见。

四、模块中注入服务

在根组件中注入这个服务,所有子组件都能共享这个服务。
在模块中注入服务和之前的注入场景稍有不同。Angular在启动程序时会启动一个根模块,并加载它所依赖的其他模块,此时会生成一个全局的根注入器,由该注入器创建的依赖注入对象在整个应用程序级别可见,并共享一个实例。同时根模块会指定一个根组件并启动,由该根组件添加的依赖注入对象是组件树级别可见,在根组件以及子组件中共享一个实例。

现在我们来实践一下在根组件中注入服务。

1、新建一个组件:product1,在shared文件夹下新建一个服务:product,命令:

ng g service shared/product

2、在 product 服务中 新建商品信息类,并实例化一个商品:

//当服务有装饰器标识时,才意味着别的服务可以通过构造函数注入到此服务中。
@Injectable()
export class ProductService{
  constructor(){}  
  getProduct():Product{
    //实例化一个商品
    return new Product(0,"iphone11",5899,"最新款苹果手机")
  }
}

// 定义一个商品信息类
export class Product {
  constructor(
    public id:number,
    public title:string,
    public price:number,
    public desc:string
  ) {
  }
}

3、在模块层的提供器中添加 product 服务。

providers:[ProductService]
//此写法还可以写成:providers:[{provide:ProductService, useClass:ProductService}]
//provider指定提供器的token与useClass的属性值是相同时,可以简写为第一种方式

4、修改 product1 组件,获取实例化商品信息

@Component({
  selector: 'app-product1',
  templateUrl: './product1.component.html',
  styleUrls: ['./product1.component.css']
})
export class Product1Component implements OnInit {
  product:Product;
  constructor(private productService:ProductService) { }

  ngOnInit() {
    this.product=this.productService.getProduct();
  }
}

5、修改 product1 组件模板和 根组件模板

//product1 组件模板
<div>
	<h1>商品详情</h1>
	<h2>名称:{{product.title}}</h2>
	<h2>价格:{{product.price}}</h2>
	<h2>描述:{{product.desc}}</h2>
</div>

//根组件模板
<div>
	<div>
		<h1>基本的依赖注入样例</h1>
	</div>
	<div>
		<app-product1></app-product1>
	</div>
</div>

6、展示 在根组件中注入服务的 效果
在这里插入图片描述
五、子组件中注入服务

1、新建组件:product2,并在shared目录下新建 :another-product 服务
2、在another-product 服务中实例化商品信息

export class AnotherProductService implements ProductService{
	getProduct():Product{
		return new Product(1,"iphone11pro",9899,"最新款苹果pro手机")
	}
	constructor(){}
}

3、在 product2 子组件的提供器注入 AnotherProduct 服务,

@Component({
  selector: 'app-product2',
  templateUrl: './product2.component.html',
  styleUrls: ['./product2.component.css'],
  providers:[{
    provide:ProductService,useClass:AnotherProductService
  }]
})
export class Product2Component implements OnInit {

  product:Product;
  constructor(private productService:ProductService) { }

  ngOnInit() {
    this.product=this.productService.getProduct();
  }
}

tips:

  • 只有声明了@Injectable()这个装饰器的服务才可以被注入其他服务。
  • 组件中没有@Injectable()也可在构造函数中注入服务的原因是组件中@Component是@Injectable()的子类。

4、修改 product2 组件模板和 根组件模板

//product2 组件模板
<div>
	<h1>商品详情</h1>
	<h2>名称:{{product.title}}</h2>
	<h2>价格:{{product.price}}</h2>
	<h2>描述:{{product.desc}}</h2>
</div>

//根组件模板
<div>
	<div>
		<h1>基本的依赖注入样例</h1>
	</div>
	<div>
		<app-product1></app-product1>
		<app-product2></app-product2>
	</div>
</div>

5、展示子组件中注入服务的效果

在这里插入图片描述
六、服务中注入服务

现在我们来模拟在一个服务中依赖另一个服务的情况,这里是将 LoggerService 服务注入到 Product 服务中。

1、在shared目录下新建 logger 服务,并新建 log方法

export class LoggerService{
	constructor(){}
	log(message:string){
		console.log(message);
	}
}

2、将 LoggerSevice 注入到 Product 服务中

export class ProductService{
	constructor(private logger:LoggerService){}   //通过构造函数将LoggerService依赖注入到 product服务中
	getProduct():Product{
		//实例化一个商品
		this.logger.log("getProduct方法被调用");	//调用LoggerService的Log方法
		return new Product(0,"iphone11",5899,"最新款苹果手机")
	}
}

3、将 LoggerService 添加到模块中

providers:[ProductService,LoggerService]

4、展示服务中注入服务的效果
在这里插入图片描述

七、使用工厂方法

1、把 product2 子组件内声明的 providers 去掉。
2、两个组件统一使用根组件注入服务,对根组件的 providers 进行修改

providers:[{
	provide:ProductService,
	useFactory:()=>{
		let logger=new LoggerService();	//实例化 logger 服务
		let dev=Math.random()>0.5;	//生成随机数,大于0.5为true,注入product服务,反之注入AnotherProduct服务,在这只是模拟一种情况
		if(dev){
			return new ProductService(logger);
		}else{
			return new AnotherProductService(logger);
		}
	}
},LoggerService]

3、给 AnotherProduct 服务的构造函数添加参数

@Injectable({
  providedIn: 'root'
})
export class AnotherProductService implements ProductService{
  getProduct(): Product {
    return new Product(1,"iphone11 pro",9899,"最新款苹果pro手机")
  }
  //构造函数添加 logger参数
  constructor(public logger:LoggerService) { }
}

4、使用工厂函数,结果展示
在这里插入图片描述

这样我们根据生成随机数的不同而调用不同的服务,注意Product1Component和Product2Component两个组件使用的服务永远是相同的,这意味着工厂方法创建的对象是一个单例对像,工厂方法只在创建第一个需要被注入的对象时调用一次,然后在整个应用中所有被注入的ProductService的实例都是同一个对象。

现在有两个问题:

一是我们手动实例化LoggerService,这样会造成我们的工厂方法会跟LoggerService紧密的耦合在一起,而实际上我们是有声明LoggerService这个提供器的,接下来我们将在工厂方法里使用LoggerService提供器。

@NgModule({
 declarations: [
   AppComponent,
   Product1Component,
   Product2Component
 ],
 imports: [
   BrowserModule
 ],
 providers: [
   {
     provide: ProductService,
     useFactory: (logger: LoggerService) => {
       let dev = Math.random() > 0.5;
       if (dev) {
         return new ProductService(logger);
       } else {
         return new AnotherProductService(logger);
       }
     },
     deps: [LoggerService]		//使用deps 属性声明工厂方法需要的参数。
   }
   , LoggerService],
 bootstrap: [AppComponent]
})
export class AppModule {
}

第二个问题,在我们的例子中实例化哪个对象是由一个随机数决定的,在真实情况中我们可能会通过某个变量来控制。这时我们声明一个新的提供器。

@NgModule({
  declarations: [
    AppComponent,
    Product1Component,
    Product2Component
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {
      provide: ProductService,
      useFactory: (logger: LoggerService, isDev) => {
        if (isDev) {
          return new ProductService(logger);
        } else {
          return new AnotherProductService(logger);
        }
      },
      deps: [LoggerService, 'IS_DEV_ENV']	//用deps 属性声明工厂方法需要的参数LoggerService和isDev
    }	
    , LoggerService,
    {
      provide: 'IS_DEV_ENV', useValue: false
    }],
  bootstrap: [AppComponent]
})
export class AppModule {
}

//最终两个组件都注入 AnotherProduct 服务

也可以使用值对象来控制。

@NgModule({
  declarations: [
    AppComponent,
    Product1Component,
    Product2Component
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {
      provide: ProductService,
      useFactory: (logger: LoggerService, appConfig) => {
        if (appConfig.isDev) {
          return new ProductService(logger);
        } else {
          return new AnotherProductService(logger);
        }
      },
      deps: [LoggerService, 'APP_CONFIG']
    }
    , LoggerService,
    {
      provide: 'APP_CONFIG', useValue: {isDev:false}
    }],
  bootstrap: [AppComponent]
})
export class AppModule {
}
//最终两个组件都注入 AnotherProduct 服务

八、注入器的层级关系

一个图片可以清晰的展示注入器的层级关系:
其中Angular 中最顶级的注入器是应用级的,也就是模块 app.module,在应用级注入器里初始化 主组件注入器,也就是 app.component,最后是子组件注入器,子组件也就是自定义的组件。
在这里插入图片描述

TIPS:注入器的原理

以上述例子为例,在 product1 组件中注入了 ProductService服务,product1组件的注入器首先会检查自身是否注册了 token 类型为 ProductService 的提供器,如没有;则会查找它的父组件 app.component.ts上是否有合适的提供器,如没有,会向上级应用级模块 寻找符合条件的提供器。它会根据这个提供器的配置,实例化一个ProductService的实例,并将其注入到 product1 的组件中。如果应用级都没有发现符合条件的提供器,则抛出异常。

在Angular中不需要手动写代码去调用提供器的方法,它会根据构造函数的参数自动注入所需依赖。而angular中只有一个依赖注入点,就是构造函数,如果看到一个组件中构造函数没有参数,就可以断定此组件没有注入依赖。


感谢阅读,如有错误,欢迎指正!
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值