Angular7入门辅助教程(八)——服务提供商

如果有任何的非技术障碍,比如如何新建Angular项目,请先到我的"Angular7入门辅助教程"专栏参考这篇博客:Angular7入门辅助教程——前篇

Provider

这一章我们来了解一下Angular依赖注入系统中一个很重要的概念——服务提供商,有人使用服务,那么就应该有人提供服务吧!这样理解有没有简单些,实际上,就是这样的!(等阅读完这一节你就会明白)

心法篇

  • 服务提供商会告诉Angular怎样来找到对应的服务(通过DI令牌)、以及如何创建服务实例(这句话是这章节的核心,这章节的所有内容都是围绕这句话来的,现在不懂没有关系,在详细教程篇第一点就会详细讲到)
  • 有四中类别的服务提供商:useClass、useExising、useValue、useFactory,后面会一一详细讲到
  • 组件类、模块类、服务类等的构造函数中只能是带有某种装饰器的类或者由InjectionToken创建的DI令牌(不理解请看useFactory服务提供商这一小节的内容)

详细教程篇

1、服务提供商的原型

服务提供商是一个对象,它会告诉Angular怎样来找到对应的服务(通过DI令牌)、以及如何创建服务实例,不知道你是否还记得——不管是组件元数据对象的providers属性还是模块元数据对象的providers属性,对providers的描述都是:providers表示服务提供商列表!!!这时候,你可能会反驳了,怎么前面一章“依赖注入系统之服务”其中的元数据对象的providers数组中就只有一个类名,难道这就是服务提供商?没错,就是服务提供商,只不过是useClass这种服务提供商的简写形式,现在我们来解开服务提供商的神秘面纱

服务提供商的原型

{provide: DI令牌, useClass/useExisting/useValue/useFactory: 服务类名}
  • provide——DI令牌,它告诉Angular应该在哪里提供该服务,(如何使用DI令牌,后面会详细讲解)
  • useClass/useExisting/useValue/useFactory——服务提供商提供服务的方式,常见的就是使用new(useClass)(这个过程由Angular来完成)也有可能直接提供一个服务实例(useFactory),后面会一一详细讲解

吊炸天是吧!而下面的两种写法是等价的

providers:[LoggerService] 
<=> 
providers:[{provider: LoggerService, useClass: LoggerService}]

现在是不是对前一章的那种写法有清晰的认识了呢!现在。我们只需要记住这个原型即可,在记住这个原型后,请继续往下看

2、DI令牌

前面多次讲到,DI令牌是用来查找服务的,那么,我们怎样使用呢?或许,你应该看到这样的写法

constructor(private loggerService: LoggerService) {
    }

告诉你一个小秘密,DI令牌就藏在其中,而providers数组中的内容是这样的

  providers: [LoggerService]

或许你已经猜到!没错这里的DI令牌就是LoggerService,所以下面这段话很重要——写在构造函数中的那个你以为是服务类的类名其实是DI令牌!这个例子其实并没有将这段话的意思表达的淋漓尽致,请继续往下看(InjectionToken这个类用来创建DI令牌),在这里我们只需要有这么一个印象就好了

3、useClass服务提供商

这或许是最近经常使用的,也最简单的服务提供商了,在前面你也接触过,在这里就不多讲了,但需要注意的是

providers:[LoggerService] 

 Angular会帮我们new出一个LoggerService的实例

4、useExisting服务提供商

或者你也可以叫它别名提供商,为什么这么叫呢,从字面上来说是“使用存在"的意思,其实没毛病,就是使用存在的服务实例!官方有一个很好的例子,我在这里引用一下:别名提供商

想象有这样的一个场景:原本应用中有一个名叫OldLoggerService的服务类,现在我们需要使用一个新的名叫NewLoggerService的服务。而这两个服务类的接口相同,出于某种原因,我们没法修改老的组件来使用NewLoggerService,而你的想法是:当来组件使用OldLoggerService来记录信息时,你希望它使用的是NewLoggerService的实例而不是OldLoggerService的实例,为了达到这个目的,我们就可以使用useExisting这种类别的提供商,代码如下

providers: [ NewLoggerService, 
            { provide: OldLoggerService, useExisting: NewLoggerService}]

使用第二个服务提供商

constructor(private oldLoggerService: OldLoggerService)

代码解析

  • 这里使用了两个服务提供商
  • 第一个服务提供商结合前面的知识可以知道是useClass类别的提供商
  • 第二个服务提供商是useExisting服务提供商
  • 注意:第二个服务提供商你不能用useClass,因为你的本意是使用同一个服务实例(单例服务,在下一章节讲),而useClass会导致创建一个新的服务实例
  • 结合前面讲到的DI令牌的知识,在使用第二个服务提供商的时候,OldLoggerService是DI令牌,但是Angular提供的实例却是NewLoggerService的实例(oldLoggerService指向这个实例)(因为服务提供商对象的第二个属性是用来指明提供服务的方式的)

5、InjectionToken

在讲解下一种服务提供商之前,我们现在了解一下InjectionToken这个类(因为下一种服务提供商useValue需要用到它),它是用来手动创建DI令牌的!下面通过例子来详细了解一下

1)、新建一个Angular项目,取名test-teaching-provider

ng new test-teaching-provider

并cd进这个文件目录

2)、新建一个名叫assist.ts的文件,内容如下

import { InjectionToken } from "@angular/core";

export const MY_DI_TOKEN = new InjectionToken<string>('test useVaule');
  • 通过InjectionToken的参数可以知道,这个DI令牌我是用来测试useValue这种服务提供商的,其实,这个参数的作用也是如此——更清楚的描述这个令牌的作用,并没有其他啥作用
  • 通过InjectionToken的泛型参数我们可以知道,这个DI令牌使用来表示string类型的服务

现在我们就有了一个名叫MY_DI_TOKEN的DI令牌,那么,这个令牌的使用我将和下一小节(useValue)一起讲解

6、useValue服务提供商

你也可以叫它值提供商,在上一章的心法篇,我也说过,服务有很多种类,现在,我们利用就useValue来提供一个string类型的服务(连接上一小节InjectionToken)

在上一小节中我们创建了一个名叫MY_DI_TOKEN的DI令牌,现在来使用它,代码如下

1)、新建一个名叫test-teaching-privider的Angular项目

ng new test-teaching-privider

并cd进该文件目录

2)、启动应用

ng serve -o

现在你的应用应该是这个样子的

现在我们来使用useValue来更改标题(红色框中的内容)

3)、修改AppComponent的内容如下

import { Component, Inject } from '@angular/core';
import { MY_DI_TOKEN } from './assist';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
    providers: [{ provide: MY_DI_TOKEN, useValue: 'test useValue' }] // 注册值提供商
})
export class AppComponent {
    title = 'test-teaching-provider';

    constructor(@Inject(MY_DI_TOKEN) myTitle: string) {// 使用值提供商
        this.title = myTitle;
    }

}

现在你的界面应该变成了这样

是不是和代码中的这个字符串一样啊

代码解析

  • 目光聚焦于构造函数,可以发现多了一个名叫@Inject()的东西,而里面的参数就是砸门新建的DI令牌,它是参数装饰器,当我们使用InjectionToken来创建DI令牌的时候,我们就可以以这种方式来使用它!

7、useFactory服务提供商

现在有这样一个场景:你新建了一个名叫hero-detail.service.ts的服务文件,而它的内容如下

import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';

@Injectable({
    providedIn: 'root'
})
export class HeroDetailService {

    constructor(
        private loggerService: LoggerService,
        private auth: boolean
    ) { }

    getStr() {
        return 'test useFactory successfully and ' + this.loggerService.getLog();
    }
}

注意,其中LoggerService是一个服务类(也就是在服务类中使用其他服务,我前面也说过,和在组件中使用服务的方式是一样的),现在的问题是:这个服务的构造函数中多了另外一个boolean类型的参数,那么,如果是你,你会怎样正确使用该服务,(学到这里,我想大多数人会按照如下的步骤进行),为了清晰起见,我们从头开始

1)、新建项目名叫test-teaching-provider,并cd进该项目

ng new test-teaching-provider

2)、新建一个名叫logger的服务

ng generate service logger

其内容如下

import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class LoggerService {

    constructor() { }

    getLog() {
        return 'use LoggerService in HeroDetailService';
    }
}

 可以看出,这个服务类的作用是为了说明如果在服务中使用另外一个服务

3)、再新建一个名叫hero-detail的服务

ng generate service hero-detail

修改其内容如下

import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';

@Injectable({
    providedIn: 'root'
})
export class HeroDetailService {

    constructor(
        private loggerService: LoggerService,
        private auth: boolean
    ) { }

    getStr() {
        return 'test useFactory successfully and ' + this.loggerService.getLog();
    }
}

可以发现,这个服务类我是为了测试useFactory,如果测试成功,那么期望的结果是界面显示:test useFactory successfully and use LoggerService in HeroDetailService

4)、删除app.component.html默认内容,修改为一下内容

<p>{{ str }}</p>

5)、修改app.component.ts文件内容为

import { Component, Inject, OnInit } from '@angular/core';
import { HeroDetailService } from './hero-detail.service';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
    providers: [HeroDetailService] // 注册组件级别的服务提供商
})
export class AppComponent implements OnInit {
    title = 'test-teaching-provider';
    str: string

    constructor(private heroDetailService: HeroDetailService) {// 注入HeroDetailService
    }

    ngOnInit() {
        this.str = this.heroDetailService.getStr();
    }

}

现在,你应该可以启动应用,如果不出意外的话,应该是期望的结果,可是,意外就此发生,浏览器的调试界面出现了下面的错误

(No provider for Boolean),可能你也想到了,问题就出在砸门新增的HeroDetailService上面,没错,不知道你是否还记得(我在这一章心法篇讲过的一句话)

现在你应该恍然大悟,看到HeroDetailService的构造函数的两个参数

  • 第一个LoggerService是带有@Injectable装饰器的类
  • 第二个参数既不是带有Angular某种装饰器,也没有使用InjectionToken创建的DI令牌,所以问题就出在第二个参数上面

这时,你应该想到一种更正的方法,参见useValue服务提供商这一小节,好,现在我们就来改一改

修改步骤如下

1)、新建assist.ts文件,内容如下

import { InjectionToken } from "@angular/core";

export const MY_DI_TOKEN = new InjectionToken<boolean>('');

2)、修改HeroDetailService的内容如下

import { Injectable, Inject } from '@angular/core';
import { LoggerService } from './logger.service';
import { MY_DI_TOKEN } from './assist';

@Injectable({
    providedIn: 'root'
})
export class HeroDetailService {

    constructor(
        private loggerService: LoggerService,
        @Inject(MY_DI_TOKEN) private auth: boolean
    ) { }

    getStr() {
        return 'test useFactory successfully and ' + this.loggerService.getLog();
    }
}

3)、还有一步,别忘了为MY_DI_TOKEN注册服务提供商(我们在app.component.ts中为其注册)

providers: [HeroDetailService, { provide: MY_DI_TOKEN, useValue: true }] // 注册组件级别的服务提供商

现在,你的界面应该出现了期望的结果

如果你能自己做到这一步,说明前面的知识你已经掌握的足够好了!!

那么,你可能会问了,讲了这么多,额。。。。好像还没有涉及到useFactory。。。是的,接下来我们用一种更好的方式解决上面问题——useFactory,工厂函数,前面我应该也说过,我们可以直接提供一个服务实例,而不需要Angular帮我们new,回到该项目修改以前(也就是报错的时候,我们不使用useValue来解决此问题)使用useFactory的步骤如下

1)、新建一个assist.ts文件,内容如下

import { LoggerService } from "./logger.service";
import { HeroDetailService } from "./hero-detail.service";


export const factoryFun = (loggerService: LoggerService)=>{
    return new HeroDetailService(loggerService, true);
}

可以发现函数factoryFun(注意,这里是箭头函数,不能使普通的js函数,具体原因我也不太清楚。。。不好意思,原来是电脑的问题,电脑抽风了)返回一个HeroDetailService的实例,

2)、修改app.component.ts文件,使用该工厂函数

    providers: [HeroDetailService, {provide: HeroDetailService, 
        useFactory: factoryFun, deps:[LoggerService]}] // 注册组件级别的服务提供商

注意如下几点 

  • 该服务提供商的令牌还是HeroDetailService,
  • useFactory的值是factoryFun,是一个函数
  • deps数组:DI令牌数组,Angular会将这些令牌对应的服务注入到工厂函数的参数列表中

现在,期望的结果又出现了

其实,在实际应用中,不会这样使用useFactory提供商,在这里我只是为了说明useFactory的使用方法,更好的例子见官方教程:useFactory

8、类接口

还有一个很重要的知识点——类接口,在Angular中,类接口指的是:用作DI令牌的抽象类,因为本片教程已经很长了,就不在详细讲解了,请直接看官方教程:类-接口

问题篇

不知道你现在有没有清楚下面三者的关系

[provide: a, _: b] // 其中_表示我并不关心它的值(他可能是useClass/useValue/useExisting/useFactory中的某一个)

constructor(private c: a){ }

也就是a、b、c三者的关系,如果你觉得自己清楚了,那么请告诉我,下面代码期望的结果是什么?

1)、LoggerService内容如下

import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class LoggerService {

    constructor() { }

    getLog() {
        return 'logger';
    }
}

2)、HeroDetailService内容如下

import { Injectable, Inject } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class HeroDetailService {

    constructor() { }

    getLog() {
        return 'heroDetail';
    }
}

3)、app.component.ts的内容如下

import { Component, OnInit } from '@angular/core';
import { HeroDetailService } from './hero-detail.service';
import { LoggerService } from './logger.service';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
    providers: [{provide: HeroDetailService, useClass: LoggerService}] // 注册组件级别的服务提供商
})
export class AppComponent implements OnInit {
    title = 'test-teaching-provider';
    str: string

    constructor(private heroDetailService: HeroDetailService) {// 注入HeroDetailService
    }

    ngOnInit() {
        this.str = this.heroDetailService.getLog();
    }

}

现在你来告诉我,str的值是多少?是heroDetail,还是logger(如果不知道就先试一试)

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页