一、概述
-
Angular 为了解决数据共享和逻辑复用问题,引入了服务的概念,服务简单理解就是一个带有特性功能的类,Angular 提倡把与视图无关的逻辑抽取到服务中,这样可以让组件类更加精简、高效,组件的工作只管用户体验,把业务逻辑相关功能(比如:从服务器获取数据,验证用户输入或直接往控制台中写日志等)委托给各种服务,最后通过 Angular 的依赖注入,这些带有特定功能的服务类可以被任何组件注入并使用。
-
依赖注入 (dependency injection,DI) 是这样一个系统: 它让程序中的某部分可以访问其他部分,而且我们可以配置它们的访问方式。(可以把注入器看作new操作符的替代品)
-
Angular 的依赖注入,需要先了解下面这几个概念:
-
@Injectable() 装饰器来提供元数据,表示一个服务可以被注入的
-
注入器(Injector):注入器是Angular 自己的类,不需要用户创建,它负责提供依赖注入功能Angular 中的注入器是树状结构,按照层级划分,有根注入器、模块注入器(Module Injector)、组件注入器及元素注入器(Element Injector)。Angular 会在Web 应用程序启动过程中创建注入器。注入器是一个容器,它会创建依赖,并管理这些依赖,使这些依赖在 Web 应用程序中的其他地方也可使用
-
@Inject() 装饰器表示要在组件或者服务中注入一个服务
-
提供者(Provider):提供者是一个类,用来告诉注入器应该如何获取或创建依赖。对要用到的任何服务,Angular 都要求必须至少注册一个提供者。
-
依赖:依赖描述的是一个类从外部源接收它需要的对象实例,这些对象实例作为依赖已经在注入器中创建好了,该类仅需要注入它即可使用,不需要自己创建这些依赖。依赖可以是服务类函数、对象、接口或值等。
-
-
最常见的情况是提供一个服务或值,它将在整个应用中保持一致。在我们的应用中,99%的场景可能都属于这种情况。
-
通过依赖注入管理和分发的对象被称为服务(service),一般将类定义在名为
xxx.service.ts
的文件中。
二、创建可注入的服务类
-
在Angular 中创建服务类与创建模块、组件、指令类似。默认情况下,使用Angular CLI的
ng generate service
命令创建服务,它会生成一个用@Iniectable()
装饰器声明的服务类,如创建一个日志服务。# ng generate service log 的缩写,log是服务类的文件名 ng g service log
-
上述命令会生成服务类文件
src/app/log.service.ts
,文件初始内容如下import { Injectable } from '@angular/core';@Injectable({providedIn: 'root' }) export class LogService {constructor() { } }
-
Angular 的服务类用
@lnjectable()
装饰器声明。@lnjectable()
装饰器是一个标记性装饰器,它声明的类可由注入器创建并可以作为依赖项注入。它仅包含一个 providedIn 属性的元数据providedIn 属性用于指定注入器,可接收 3 种字符串: root、platform 和any。它们分别代表着3种不同级别的注入器。
三、选择注入器
- Angular 提供的注入器有多种,Angular 在启动过程中会自动为每个模块创建一个注入器,注入器是一个树结构。
- (1) Angular 为根模块 (AppModule )创建的是根注入器。
- (2) 根注入器会提供依赖的一个单例对象,可以把这个单例对象注入多个组件中。
- (3) 模块和组件级别的注入器可以为它们的组件及其子组件提供同一个依赖的不同实例。
- (4) 可以为同一个依赖使用不同的提供者来配置这些注入器,这些提供者可以为同一个依赖提供不同的实现方式。
- (5) 注入器是可继承的,这意味着如果指定的注入器无法解析某个依赖,它就会请求父注入器来解析它。组件可以从它自己的注入器中获取服务、从其祖先组件的注入器中获取服务、从其父模块(NgModule 类)级别的注入器中获取服务,或从根注入器中获取服务。
- 服务有作用域,表示该服务在 Web 应用程序中的可见性。不同级别的注入器创建的依赖服务对应着 Web 应用程序中的不同可见性。
@Iniectable()
装饰器提供了选择注入器的一种方式,即通过配置元数据providedIn
属性的值,可以选择不同级别的注入器。 - 字符串 root 表示选择的是根注入器,根注入器在整个 Web 应用程序中仅创建服务的一个单例对象,可以把这个单例对象注入任何想要它的类中。
- 字符串 platform 表示选择的是元素注入器,元素注入器使服务可以在整个 Web 应用程序和Angular 自定义元素间共享。关于 Angular 自定义元素,简单地理解就是将 Angular 组件的所有功能打包为原生的 HTML,可以供其他非 Angular 框架使用。因此,platform 作用域大于 root作用域。
- 字符串 any 表示选择的是模块注入器,这意味着同一服务可能有多个实例。它与 root 的区别如下。
- 对非延迟加载的模块,它们共享一个由根注入器提供的实例。
- 对延迟加载的模块,每个模块分别创建一个自己的实例,供模块内单独使用。
- 选择了注入器后,依赖注入还需要一个提供者,因为 Angular 对要用到的任何服务,都要求必须至少注册一个提供者。对服务类来说,提供者就是它自己。因此,上节 (第二章 创建可注入的服务类) 使用命令创建的LogService 服务已经是一个依赖服务了,可以在其他组件中注入并使用它。
四、配置提供者
- 提供者就做了两件事:(1) 告诉注入器如何提供依赖值,(2) 限制服务可使用的范围
- 当使用提供者配置注入器时,注入器就会把提供者和一个 token 关联起来,维护一个内部关系(token-Provider)映射表,当请求一个依赖项时就会引用它。token 就是这个映射表的键。
- 在 Angular 依赖注入系统中,用户可以根据名字在缓存中查找依赖,也可以通过配置过的提供者来创建依赖。我们必须使用提供者来配置注入器,否则注入器就无法知道如何创建此依赖。注入器创建服务实例的最简单方法,就是用这个服务类本身来创建它。但是在现实中,依赖除了服务类外,还可以是函数、对象、接口或值等。因此,Angular 提供了很多类型的提供者,不同的提供者可以针对特定的依赖项提供定制化的创建服务;如对于服务类来说,也可以通过配置提供者来定制化创建服务实例。
[1]. 提供者的类型
-
提供者是一个实现了 Provider 接口的对象,它告诉注入器应该如何获取或创建依赖的服务实例。提供者类型定义如下。
type Provider = TypeProvider | ValueProvider | classProvider | ConstructorProvider | ExistingProvider | FactoryProvider | any[];
[2]. 配置方法
-
在 Angular 中有3个地方可以配置提供者,以便在 Web 应用程序的不同层级使用提供者来配置注入器。
- 在服务本身的 @Injectable()装饰器中。
- 在模块类的 @NgModule()装饰器中。
- 在组件类的 @Component()装饰器中。
-
其中在服务本身的 @Injectable()装饰器中仅有一个元数据的 providedIn 属性,用户可以用它来选择不同的注入器,配置提供者的属性为默认值。其实对服务来说,提供者就是它自己。因此Angular 默认通过调用该服务类的 new 运算符来创建服务实例。
-
在 @NgModule()装饰器和 @Component()装饰器的元数据中都提供了 providers 属性,用户可以通过该属性来配置提供者。
// my-app.module.ts @NgModule({declarations: [ MyAppComponent,], providers: [ MyService ] // <--- 这里// 等同于 // providers: [{ provide: MyComponent, useClass: MyComponent }] }) class MyAppModule {}// my-app.component.ts export class MyAppComponent { constructor(private myService: MyService) { // do something with myService here } }
-
当使用 @NgModule() 装饰器中的 providers 属性配置提供者时,该服务实例对该 NgModule类中的所有组件是可见的,该 NgModule 类中的所有组件都可以注入它。
-
当使用 @Component()装饰器中的 providers 属性配置提供者时,该服务实例只对声明它的组件及其子组件可见,它会为该组件的每一个新实例提供该服务的一个新实例。
1). TypeProvider
-
TypeProvider 称为类型提供者,类型提供者用于告诉注入器使用指定的类来创建服务实例,本质上是通过调用类的 new 运算符来创建服务实例。这也是我们用得最多的一种提供者。具体代码如下。
providers:[LogService ]// LogService是一个由@Injectable()装饰器声明的类
-
在上面的代码中,配置的依赖项是一个LogService类的服务实例,而该类的类型LogService 是该依赖的 token 值。
2). ValueProvider
-
ValueProvider 称为值提供者,ValueProvider接口定义如下。
interface ValueProvider extends ValueSansProvider {provide: anymulti?:boolean //可选参数useValue: any }
-
其中的 provide 属性接收3种类型的 token 值:类、InjectionToken 对象实例及其他任何对象实例。
-
当 provide 属性的 token 值为类和对象实例时,参考下面的代码片段。
const JAVA_BOOK = new Book('Learning Java','Java'); providers:{// 注入的依赖为字符串值,string类作为该依赖的token值{provide:string, useValue:'Hello'}, // 注入的依赖为字符串值,字符串对象实例name作为该依赖的token值{provide:'name', useValue:'He1lo'}, // 注入的依赖为Book对象实例,Book对象实例作为该依赖的token值{provide:Book, useValue:JAVA_BOOK} }
-
InjectionToken 类用来创建 InjectionToken 对象实例,该类的定义如下
class InjectionToken<T>{//接收一个泛型(T)对象constructor(desc:string, //一个描述( desc)参数options?:{ providedIn?: Type<any> | "root" | "platform" | "any"; factory:()=>T;}protected _desc:stringtoString():string }
-
InjectionToken 类接收一个泛型对象和一个描述参数。当 provide 属性为 InjectionToken 对象实例时,useValue 属性接收的类型取决于 InjectionToken 类中的泛型对象类型。
// 创建一个字符串类型的可注入对象 Const HELLO_MESSAGE= new InjectionToken<string>('He1lo!'); providers:[{provide: HELLO_MESSAGE,useValue:'Hello World!'//接收一个字符串,与InjectionToken类的泛型对象string对应 }]
-
Angular 中的接口其实是 TypeScript的功能,而 JavaScript 没有接口,所以当 TypeScript转译成 JavaScript时,接口也就消失了。因此,|njectionToken 类常用于封装接口类型的对象实例,代码如下。
// Config是一个接囗apiEndPoint:string; interface Config {apiEndPoint: string;timeout:number; }//定义一个接口类型实例 const configValue:Config={apiEndPoint: 'def.com',timeout: 5000 }; //定义一个InjectionToken类对象实例,实际是封装config接口 const configToken = new IniectionToken<Config>('demo token'); providers:[{provide:configToken, useValue:configValue //使用configToken作为依赖的token值 }]
3). ClassProvider
-
ClassProvider 称为类提供者,ClassProvider与 ValueProvider类似,它的 provide 属性接收值与 ValueProvider 提供者相同,不同的是 useClass 属性接收一个类,或者该类的子类。
providers:[{provide: LogService,useClass:LogService }]
-
在上面的代码中,依赖项的值是一个LogSenvice 类的实例,而该类的类型 LogService 是该依赖的 token 值。
4). ConstructorProvider
-
ConstructorProvider 可以理解为等同 TypeProvider,它仅有 provide 属性,且接收一个类。
providers:[{provide: LogService}]
-
在上面的代码中,依赖项的值是一个 LogService 类的实例,而该类的类型 LogService 是该依赖的 token 值。
5). ExistingProvider
-
ExistingProvider用于创建别名提供者。假设老的组件依赖于 OldLogger 类。OldLogger 类和 NewLogger 类的接口相同,但是由于某种原因,我们没法修改老的组件来使用 NewLogger类,这时可以使用 useExisting 为 OldLogger 类指定一个别名 NewLogger。
[NewLogger, {provide: OldLogger,useExisting: NewLogger }]
6). FactoryProvider
-
有时候可能需要动态创建依赖值,创建时需要的信息要等运行期间才能获取。这时可以使用FactoryProvider。 FactoryProvider 使用 useFactory 属性来配置该注入器。useFactory 属性接收一个函数。
@NgModule({declarations: [ DiSampleApp ], imports: [ BrowserModule ], bootstrap: [ DiSampleApp ],providers:[{provide: LogService,useFactory: ()=> new LogService()}] })
-
在上面的代码中,依赖项的值是 useFactory属性中的函数返回的对象实例,LogSenvice 类是该依赖的 token 值。
- 创建一个新的项目
ng new demo-serve --routing --css --inimal
- 创建一个 user 组件
ng g c user
- 创建 user 模型
ng g m user
- 创建服务类用于实现本地存储的需求
ng g s service/storage
五、在类中注入服务
- 当完成了 Angular 依赖注入的配置后,注入器通过提供者创建依赖,创建依赖的过程可以这么理解:注入器将查找具体 token 值对应的提供者,然后使用该提供者创建对象实例,作为依赖项存储在注入器中。当完成了依赖的创建后,我们就可以通过依赖注入的方式,在 Angular 中使用该依赖的服务实例的方法和属性了。
- 上面提到过,选择不同的注入器,或在不同的位置配置提供者,服务在 Web 应用程序中的可见性是不同的。如 providedin 属性配置为 root 时,服务是单例的;也就是说,在指定的注入器中最多只有某个服务的一个实例。
- Angular 的依赖注入具有分层注入体系,Web 应用程序有且只有一个根注入器。这意味着下级注入器也可以创建它们自己的服务实例。Angular 会有规律地创建下级注入器。每当 Angular 创建一个在@Component) 装饰器中指定了 providers 属性的组件实例时,它也会为该组件实例创建一个新的子注入器。同样,当在运行期间加载一个新的 NgModule 类时,Angular 也可以为它创建一个拥有自己的提供者的注入器。
- 子模块和组件注入器彼此独立,并且会为所提供的服务分别创建自己的服务实例。当 Angular销毁 NgModule 类或组件实例时,也会销毁这些注入器和注入器中的那些服务实例。
- 子组件注入器是其父组件注入器的子节点,它会继承所有的祖先注入器,其终点则是 Web 应用程序的根注入器。
[1] 注入依赖类实例
-
我们可以通过类的构造函数注入依赖类,即在构造函数中指定参数的类型为注入的依赖类。下面的代码是某个组件类的构造函数,它要求注入 LogService 类。
constructor(private logService: LogService)
-
注入器将会查找 token 值为 LogService 类的提供者,然后将该提供者创建的对象实例作为依赖项,赋值给 logService 变量。当完成了依赖注入后,用户就可以在类中使用该服务实例的方法和属性了。
[2] 注入可选的依赖类实例
-
在实际应用中,有时候某些依赖服务是可有可无的,换句话说,可能存在需要的依赖服务找不到的情况。这时,我们可以通过 @Optional() 装饰器来显式地声明依赖服务,告知 Angular 这是一个可选的依赖服务。同时,我们需要通过代码中的条件来判断依赖服务是否存在,代码如下。
constructor(@Optional() private logService: LogService) [if (this.logService) { // 判断是否存在// 具体业务逻辑} }
[3] 使用 @Inject() 装饰器指定注入实例
-
[1] 注入依赖类实例
小节在介绍注入依赖类实例时,注入的类型是该依赖类的类型,即可以理解为根据token 值来注入依赖类。当我们遇到注入的依赖类是一个值对象、数组或者接口的情况时,需要使用@lnject()
装饰器来显式地指明依赖类的 token 值。如之前我们使用 IniectionToken 类封装了一个接口类型的依赖,然后期望在组件或者服务类中注入该接口类型的依赖时,需要使用 @Injiect() 装饰器来显式地指明依赖的 token 值,代码如下。// 注意这里需要使用@Inject() 装饰器 configToken 是该接口依赖的token值 // Config是注入依赖的类型 constructor(@Inject(configToken) private config: Config) { console.log('new instance is created'); }
-
我们在服务中配置一个值提供者,然后在类中注入该值提供者创建的值
providers: [{provide: 'name'.useValue: '变量name的值' }] // String是注入依赖的类型 constructor(@Inject('name') private config: String){this.title = '值Provide:' + config; }
[4] 注入Injector 类对象实例
-
Iniector 类是 Angular 的注入器对应的 Class 类。既然注入器创建和维护着依赖,那么我们可以直接注入 Injector 类对象实例,然后通过它的方法获取依赖。上面介绍的注入值类型,可以用下面的方式实现。
providers: [{provide: 'name'useValue: '变量name的值' }] // 注入Injector类对象实例 constructor(private injector: Injector) {this.title = '值Provide:' + injector.get('name'); }
-
在上述代码中,我们通过注入Injector类对象实例,然后调用它的 get() 方法来获取对应的依赖。
六、用例
-
创建一个新的项目
ng new demo-indection --routing --minimal
-
创建两个延迟加载模块
这里不再说明,可以查看我的
angular 路由
文章第六章ng g m features/employee --route employee --module app.module
ng g m features/department --route department --module app.module
-
创建配置接口和依赖服务类
# interface 可以简写 i # 新建接口 src/app/shared/config.ts ng g interface shared/config # 新建服务类 src/app/shared/config.service.ts ng g service shared/config
-
编辑接口类
src/app/shared/config.ts
import { InjectionToken } from "@angular/core";export interface Config {apiEndPoint: string;timeout: number; }export const configToken = new InjectionToken<Config>("demo token");
-
编辑依赖服务类
src/app/shared/config.service.ts
import { Injectable, InjectionToken, Inject } from "@angular/core"; import { Config, configToken } from "./config";//通过 @Injectable() 装饰器标记为可以被注入的服务 @Injectable({// 表示当前服务在 Root 注入器中提供,// 简单理解就是这个服务在整个应用所有地方都可以注入,并全局唯一实例。providedIn: "root", }) export class ConfigService {// 注入 Config 接口对象constructor(@Inject(configToken) private config: Config) {console.log("创建一个新的用例");}getValue() {return this.config;} }
-
编辑 employee 组件
src/app/features/employee/employee.component.ts
import { Component, OnInit } from "@angular/core"; import { ConfigService } from "src/app/shared/config.service";@Component({selector: "app-employee",template: ` <p>employee works!</p> `,styles: [], }) export class EmployeeComponent implements OnInit {// 通过构造函数注入服务constructor(private configService: ConfigService) {}ngOnInit(): void {console.log(this.configService.getValue());} }
-
编辑 employee 模块
src/app/features/employee/employee.module.ts
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common";import { EmployeeRoutingModule } from "./employee-routing.module"; import { EmployeeComponent } from "./employee.component"; import { Config, configToken } from "src/app/shared/config";export const configValue: Config = {apiEndPoint: "abc.com",timeout: 5000, };@NgModule({declarations: [EmployeeComponent],imports: [CommonModule, EmployeeRoutingModule],providers: [{provide: configToken,useValue: configValue, // 注册 ValueProvider},], }) export class EmployeeModule {}
-
编辑 department组件
src/app/features/department/department.component.ts
import { Component, OnInit } from "@angular/core"; import { ConfigService } from "src/app/shared/config.service";@Component({selector: "app-department",template: ` <p>department works!</p> `,styles: [], }) export class DepartmentComponent implements OnInit {constructor(private configService: ConfigService) {}ngOnInit(): void {} }
-
编辑 department 模块
src/app/features/department /department .module.ts
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common";import { DepartmentRoutingModule } from "./department-routing.module"; import { DepartmentComponent } from "./department.component"; import { Config, configToken } from "src/app/shared/config";// 自定义配置 export const configValue: Config = {apiEndPoint: "xyz.com",timeout: 4000, };@NgModule({declarations: [DepartmentComponent],imports: [CommonModule, DepartmentRoutingModule],providers: [{provide: configToken,useValue: configValue,},], }) export class DepartmentModule {}
-
编辑根组件
src/app/app.component.ts
import { Component } from "@angular/core";@Component({selector: "app-root",template: `<a routerLink="/employee">雇员</a><br /><a routerLink="department">部门</a><router-outlet></router-outlet>`,styles: [], }) export class AppComponent {title = "demo-indection"; }
-
这时,如果允行项目,点击页面的链接,控制台抛出错误信息,产生空指针错误,详细情况是组件中注入的 ConfgService 类遇到了空指针错误。
- 产生错误信息的原因是,我们在 ConfigService 类中默认使用了
providedln:root'
配置,它表示将 ConfigService 类注入根注入器中,并在 Web 应用程序的启动阶段就实例化好了一个实例对象。然而 ConfigService 类的配置参数是分别在延迟加载模块中注册的,当我们准备单击页面上的链接时,两个延迟加载模块并没有加载,因此 ConfigService 类的配置参数这时不存在,故ConfigService 类遇到了空指针错误。准确地说,它依赖的 configToken 对象为空。 - 解决这个问题的思路是,可以在 AppModule 根模块中初始化 configToken 对象,然后将其注册到根注入器中,这时 ConfigService 类在启动阶段就能读取配置参数了。
- 产生错误信息的原因是,我们在 ConfigService 类中默认使用了
-
编辑根模块
src/app/app.module.ts
- 配置完成后,再次单击页面链接,控制台均输出 AppModule 根模块中配置的内容,说明两个延迟加载模块加载同一个配置参数。但是我们期望的是在不同的延迟加载模块中加载不同的配置参数。这时,我们可以通过配置 providedIn:'any’来达成此目的。
import { BrowserModule } from "@angular/platform-browser"; import { NgModule } from "@angular/core";import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; import { Config, configToken } from "./shared/config";export const configValue: Config = {apiEndPoint: "def.com",timeout: 5000, };@NgModule({declarations: [AppComponent],imports: [BrowserModule, AppRoutingModule],providers: [{// 注册 ValueProviderprovide: configToken,useValue: configValue,},],bootstrap: [AppComponent], }) export class AppModule {}
-
编辑根模块
src/app/shared/config.service.ts
import { Injectable, InjectionToken, Inject } from "@angular/core"; import { Config, configToken } from "./config";@Injectable({providedIn: "any", // 原先的值是 root }) export class ConfigService {// 注入 Config 接口对象constructor(@Inject(configToken) private config: Config) {console.log("创建一个新的用例");}getValue() {return this.config;} }
-
配置完成后,再次单击页面链接,控制台分别输出各自延迟加载模块中配置的内容,说明两个延迟加载模块加载的是不同的配置参数。
- 通过上述示例我们可以看出,合理地使用提供者,可以使 Web 应用程序模块化和参数化。
五、创建依赖
-
上面我们介绍了如何通过注入Iniector 类对象实例获取依赖。同样,我们也可以直接使用Injector 类创建依赖。
constructor(){const injector = Injector.create({ providers: [{ provide: 'name', useValue:'变量name的值'}]});this.title = '值Provide:'+ injector.get('name'); }
-
上述代码通过 Injector 类的 create()方法,创建了一个 Injector 类对象实例,该对象实例中维护着一个 token 值为 name 的值提供者,该值提供者负责创建依赖(一个值对象 )。用户通过调用Injector 类对象实例的 get()方法获得对应值提供者创建的依赖。