Angular7入门辅助教程(九)——单例服务

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

通过前两章关于服务的基础知识的学习,我想你应该对Angular中的服务有一个大致的了解,但是,如果我猜的没错的话:在实际应用中,你应该还不会使用服务(如果不信,看看你自己是否能不在我的帮助下面自己敲出本章的所有实例!),因为前面两章都只在说服务服务,却没有谈到服务实例,而在实际应用中,我们使用的都是服务的实例(你必须清楚自己到底使用的是哪个服务的哪个实例!),也就是说,前两章是Angular服务的理论层面,是基础,而这一章是应用层面,是前两章的总结,是升华,所以,本章节包含大量的实例!而且会引入一个核心知识点——注入器,本章的所有的东西都是围绕这个概念进行

心法篇

  • 在你使用服务之前,你必须先将该服务提供给Angular的依赖注入系统,我们需要一个注入器对象来注册提供商,注入器负责在需要时选取和注入提供商,而提供商负责提供和交付需要的服务实例
  • 单例服务,顾名思义,就是只有一个服务实例,无论你在什么地方注入该服务,使用的都是同一个服务的实例
  • Angular中有一个注入器的概念,前面提到,服务提供商是用来告诉Angular如何查找服务以及如何提供服务实例,而注入器是用来保存服务提供商以及服务实例,你不用自己建立注入器,Angular会帮你完成(将注入器想象成一个注射器,里面有服务实例,这个注射器会在用到该服务的地方将服务实例注入,这样是不是好理解一些)
  • 是否记得前面提到过三种级别的服务,对应三种级别的服务作用域,我说过,使用作用域不太准确,在这一节,替换成注入器,因此,三种级别的服务分部对应应用级注入器、普通模块级注入器(为什么加上普通两个字呢?是为了和AppModule区分开来)、组件级注入器,
  • 服务在这里不再指带有@Injectable装饰器的服务了,而是广义的服务,所有你想用到的东西都可以当做服务来使用,结合这三章,你就可以随心所欲的使用服务了
  • Angular趋向于使用已经存在的服务实例,而不是新建
  • 在Angular中,存在一个与组件树平行的注入器树!!!!
  • Angular会优先使用自己本级别的注入器中的服务实例,如果在自己的注入器中没有找到相应的服务实例,就会去父级注入器(包括父组件注入器,模块级注入器)中找,直到超出根注入器——这叫注入器冒泡,如果不做处理的话,Angular就会报错

心法篇中有两点极其重要,我单独拉出来在说一遍

  • 在Angular中,存在一个与组件树平行的注入器树!!!!
  • 如果在自己的注入器中没有找到相应的服务实例,就会去父级注入器(包括父组件注入器,模块级注入器)中找,直到超出根注入器——这叫注入器冒泡,如果不做处理的话,Angular就会报错

这些内容如果是首次接触,有点难懂,没关系,所有的这些知识,在本章我都会通过实例的方式来讲解

详细教程篇

1、单例服务

说到底,本章的重点是如何建一个在某范围使用的单例服务,以及如何正确使用它,所以我将本章的标题命名为单例服务,那么,在Angular中,单例服务是如何体现的呢?

  • 创建应用级别的单例服务

很简单,你应该都能猜到,应用级别的服务,也就是在AppModule中提供的服务,

即有两种方式:1、设置服务元数据对象providedIn:'root',以及2、在AppModule元数据对象providers中指定相应的提供商,

但是,注意重点来了:这个服务提供商你不能再次写在其他的惰性加载模块(这个概念不作要求)的元数据对象的providers或者组件的元数据的providers,如果你这样做了,Angular会新建一个服务实例,并注入到本级别的注入器中,这样,这个服务就不再是单例的了!(请继续往下看)

  • 创建模块级别的单例服务

该服务的一个实例可以在本模块的任何地方被使用!,创建方式也有两种(同创建模块级别的服务),但是,注意重点来了,同样的,你也不能在别的地方再配置一遍该服务提供商,例如:该模块中的组件元数据对象的providers中,否则,该服务又不再是单例

  • 创建组件级别的单例服务

该服务的一个实例能在该组件以及其子组件中使用,创建方式只有一种(同创建组件级别的服务),就是在组件的元数据中配置服务提供商

2、单例服务出顾茅庐

下面我们来看一个同属于一个模块下面的两个不同的组件使用同一个应用级别的服务

(这些基础的命令在后面的例子中就不再写了)

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

ng new test-teaching-a

2)、新建一个组件,名叫parent的组件

ng generate component parent

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

ng generate service logger

4)、将app.component.html内容替换成下面这个

<h1>{{ title }}</h1>
<ul>
  <li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
</ul>
<button (click)="addLog()">AddLog</button>
<hr />
<app-parent></app-parent>

5)、将app.component.ts内容替换成下面这个

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'test-teaching-a';

  constructor(private loggerService: LoggerService) {}

  addLog() {
    this.loggerService.addLog('add log from appComponent successfully');
  }
}

6)、修改logger文件内容如下

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

@Injectable({
  providedIn: 'root'
})
export class LoggerService {
  private logges: string[] = [];

  constructor() {}

  addLog(log: string) {
    this.logges.push(log);
  }
  getLogges() {
    return this.logges;
  }
}

7)、修改parent.component.ts文件

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

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit {
  constructor(private loggerService: LoggerService) {}

  ngOnInit() {}

  addLog() {
    this.loggerService.addLog('add log from parent component');
  }
}

8)、修改parent.component.html文件

<ul>
    <li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
  </ul>
  <button (click)="addLog()">AddLog</button>

9)、启动应用

界面如下

现在无论点击哪个按钮,都会同步显示信息,例如

因此,AppComonent和ParentComponent用的是用同一个服务实例

3、非单例服务出顾茅庐

在上一个小例子中,我们做如下修改:我们只需要在parent.component.ts中添加下面一句代码,就变成了非单例服务,

providers: [LoggerService]

这时候,你点击按钮,会类似出现下面的界面


注意与前一个实例的对比,在这里,横线上下方的log日志不再同步!!因此它们使用的是不同的服务实例

讲到这里,你是不是觉得很有意思(这才是出顾茅庐),继续往下看,后面会更有意思——会有颠覆性的变化,会让你感觉自己赚了一个亿

4、注入器冒泡

注入器冒泡的意思是:Angular查找服务的方式是优先在属于自己的注入器中查找服务,如果找不到,就向父组件、父模块的注入器中查找,直到找到根注入器,如果没有做特殊处理,此时,Angular就会报错!而Angular一旦找到需要的服务,就会立刻停止冒泡!下面我们来看一下具体的例子,在这个例子中,我们新建两个组件parent、child,加上appComponent,它们形成祖先、父、子的关系,并产生期望的输入

1)、在第3小节(非单例服务出顾茅庐)的基础上新增一个名叫child的组件

2)、修改parent.component.html的代码如下

<p>parent works!</p>
<ul>
  <li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
</ul>
<button (click)="addLog()">AddLog</button>
<hr />
<app-child></app-child>

3)、修改child.component.ts的代码如下

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

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
  constructor(private loggerService: LoggerService) {}

  ngOnInit() {}

  addLog() {
    this.loggerService.addLog('add log from child component');
  }
}

4)、修改child.component.html的代码如下

<p>child works!</p>
<ul>
  <li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
</ul>
<button (click)="addLog()">AddLog</button>

 5)、启动应用

你可以自己点击界面上面的三个按钮,观察一下现象,这是我的界面

可以发现,红框中的内容总是同步的,而与绿框中的内容不相干,因此可以推断,parent和child组件使用的同一个服务实例,而appComponent使用的是另外一个服务实例,下面我们来分析代码,看看为什么是这样子的!

  • 在这个例子中,三个组件形成的关系是appComponent<parent<child
  • 其中,我在根注入器以及parent中放入了loggerService提供商
  • 在运行应用时,Angular发现child中需要一个loggerService实例,然后去child对应的注入器中找,结果没找到(因为你没有在child中配置提供商),然后就去父组件(parent)中找,因为我们在parent中配置了LoggerService提供商,所以找到了对应的实例(一旦找到,就立即停止冒泡),因此,child使用的是parent注入器中的LoggerService实例
  • appComponent和parent查找服务的方式类似,所以就产生了上面的运行结果

注入器冒泡是Angular默认的查找服务的方式,但是我们也可以改变这种方式,下面来看具体可以怎么做!

5、干预注入器冒泡

下面介绍几种干预注入器冒泡的过程——使用参数装饰器

  • @Self():只允许在自己的注入器中查找服务

在上面例子的基础上,我们对child.component.ts文件做如下的修改

// 添加@Self()装饰器
  constructor(@Self() private loggerService: LoggerService) {}

现在,如果不出意外,浏览器的控制台应该报错了

这错误你应该很熟悉了吧!没有找到服务提供商,这时期望的结果,应为我们使用@Self,就告诉Angular:我只是用我自己的服务实例,但是,child组件的元数据对象我们并没有添加LoggerService提供商,因此后台报错!

  • @Optional():使服务可选,也就是找到就用,没找到也不报错(没有该装饰器的情况下,没有找到是会报错的,就像上面的例子那样)

在上一个例子的基础上,我们还是修改child.component.ts文件

import { Component, OnInit, Self, Optional } from '@angular/core';
import { LoggerService } from '../logger.service';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
  // 添加@Self()装饰器
  // 再添加@Optional()装饰器
  constructor(@Optional() @Self() private loggerService: LoggerService) {}

  ngOnInit() {}

  // 加上一层判断
  addLog() {
    if (this.loggerService) {
      this.loggerService.addLog('add log from child component');
    }
  }
}

继续修改child.component.html文件如下

<p>child works!</p>
<div *ngIf="loggerService">
    <ul>
        <li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
      </ul>
</div>
<div *ngIf="!loggerService">
    loggerService is not found!!
</div>
<button (click)="addLog()">AddLog</button>

 现在,你可以试一下,点点界面上面的按钮,看看会出现什么变化,这是我的界面

 可以发现child中并没有找到LoggerService实例,而且点击最后一个按钮也没有反应,后台也不报错!!这就是期望的结果

  • @SkipSelf():跳过自己的注入器,向父级注入器中查找

在上一个例子的基础上,我们对parent.component.ts文件如下修改

    // 添加@SkipSelf()装饰器
  constructor(@SkipSelf() private loggerService: LoggerService) {}

为了更清楚的演示该装饰器的作用,我们删除child构造函数中的两个装饰器,使其变成这样

constructor(private loggerService: LoggerService) {}

现在,你可以去界面上面点击按钮,看看会有什么不同的变化(注意对比第4小节的结果)

这种结果产生的原因,自己应该可以分析出来了吧!因为我们在parent中添加了@SkipSelf装饰器,它就不会在自己的注入器中查找服务实例,向其父组件中查找(这里注意的是:它的父组件是appComponent,也没有找到,继续向上找,下一级是根注入器,这时才找到LoggerService实例),而child在parent中就可以找到服务实例!因此——child使用的是parent注入器中的服务实例,而appcomponent、parent使用的是根注入器中的服务实例!!!!

  • @Host():在其宿主组件注入器中查找服务实例

这里使用官方的例子会更好:用 @Optional 来让依赖是可选的,以及使用 @Host 来限定搜索方式

6、父子组件之间的通信

学到这里,不知道你看到这个标题,你有没有什么想法,细心的读者可能已经发现,这一章前面所讲过的所有例子,都是父子组件通信的例子,因为,父子组件可以使用同一个服务实例,这个服务实例就成了它们之间通信的媒介!因此,使用服务是一种很好的实现不同组件之间交互的一种很好的方式!在这一小节,我们来看看另外一种父子组件相互通信的方式——子组件拿到父组件的引用,从而可以调用父组件的属性和方法!

在开始举例之前,有一件事我必须先讲清楚!——这个方法本质上还是利用服务来实现通信,因为Angular在创建组件实例的时候,会将该实例注入到组件自身的注入器中!而我们拿到该引用的方式和使用服务一样,也是注入器注入父组件实例到子组件的构造函数中。

所以,本小节的真正的目的是为了说明:任何东西都可以成为服务,而不仅仅是带有@Injectable装饰器的Angular服务

现在,有这样一个场景:存在一颗这样的组件树,appComponent<parent<child,处于某种原因,parent、child组件都要访问自己的直接父组件中的getStr()方法!并显示在屏幕上,这时,该怎么做

1)、新建test-teaching-a项目,并cd进该文件目录

2)、新建两个组件:parent、child

3)、分别修改appComponent、parent、child组件的模板文件如下

<h1>{{ title }}</h1>
<hr />
<app-parent></app-parent>
<p>parent works!</p>
<h2>{{ str }}</h2>
<hr />
<app-child></app-child>
<p>child works!</p>
<h2>{{ str }}</h2>
<hr />

4)、分别在appComponent、parent组件的组件类中添加一个getStr方法,代码如下

getStr() {
    return 'app component';
  }
getStr() {
    return 'parent component';
  }

5)、不知道你有没有学习上一章(服务提供商)的类接口,现在我们来实用一下,创建一个名叫Parent的抽象类(注意并不用实现它,如果不明白,请看官方的类接口教程:类-接口),内容如下

export abstract class Parent {
  getStr: () => string;
}

至于为什么要这么一个抽象类:都说了,它是砸门的类接口,主要的目的是作为DI令牌,而里面有一个的getStr方法是因为在这个例子中,我们需要调用parent、child组件的直接父组件的getStr方法,这也是类接口的另外一个目的——可以自由开放可供外部调用的API!比如,在这里我就只允许parent、child组件调用直接父组件中的getStr方法,而不允许其他动作,比如访问属性!

6)、分别修改appComponent、parent、child组件的组件类的内容如下

app.component.ts

import { Component, forwardRef } from '@angular/core';
import { Parent } from './Parent';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  // 添加这一句代码
  providers: [{ provide: Parent, useExisting: forwardRef(() => AppComponent) }]
})
export class AppComponent {
  title = 'test-teaching-a';

  getStr() {
    return 'app component';
  }
}

parent.component.ts 

import {
  Component,
  OnInit,
  forwardRef,
  SkipSelf,
  Optional
} from '@angular/core';
import { Parent } from '../Parent';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css'],
  // 添加这句代码
  providers: [
    { provide: Parent, useExisting: forwardRef(() => ParentComponent) }
  ]
})
export class ParentComponent implements OnInit {
  str: string;
  // 注入appComponent实例
  constructor(@SkipSelf() @Optional() private appComponent: Parent) {
    this.str = appComponent.getStr();
  }

  ngOnInit() {}

  getStr() {
    return 'parent component';
  }
}
import { Component, OnInit, Optional } from '@angular/core';
import { Parent } from '../Parent';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
  str: string;
  // 注入parent 组件实例
  constructor(@Optional() private parentComponent: Parent) {
    this.str = parentComponent.getStr();
  }

  ngOnInit() {}
}

代码解析

  • 这一小节的内容需要用到很多以前学到的知识点,比如@SkipSelf等参数装饰器、useExisting服务提供商等等
  • 注意:appComponent、parent组件的元数据对象的providers属性中注册的都是useExisting提供商!这是为什么?因为,Angular会在创建组件实例的时候,会将该组件实例注入到组件自己的注入器中,因此,使用useExisting就可以直接使用已经存在的这个组件实例!如果使用useClass会新建实例,你自己可以试一下
  • 在注意到appComponent、parent组件的元数据对象的providers属性,发现useExisting的内容都使用了forwardRef()函数,这是用来向前引用(间接引用),用来打破循环依赖(因为@Component比组件类先执行,这时还没有组件的实例,你就不能直接引用它,否则,会报循环依赖的错误,如果还不理解,见官方文档:使用前向引用forwardRef打破循环依赖,我怎么去掉了这个函数还是执行成功了,怎么和我以前测试的不一样,你么可以自己试一下,原谅我的知识水平有限)
  • 注意parent.component.ts构造函数的代码,发现有一个@SkipSelf参数装饰器,这个不能少,否则。又是循环依赖,你可以试一下

7)、现在启动应用

如果没有错误的话,应该会出现下面的界面

 红框中的内容就是期望的结果:parent调用appComponent的getStr方发,child调用parent的getStr方法

7、模块级别的服务

不知道你有没有发现,这一章至此,项目中都只有一个模块——AppModule,如果我们在其他模块中注册一个模块级别的服务提供商,那么,这个服务实例在何种级别的注入器中?(其实“依赖注入系统之服务”这一章结尾的问题篇例子就是这种情况)现在我们用一个新的例子来测试

1)、新建一个名叫test-teaching-b的项目,并cd进项目文件目录

2)、新建一个名叫hero的模块

ng generate module hero

3)、在hero目录下新建一个名叫hero的组件

ng generate component hero/hero

 这样,这个组件就属于hero模块,而不是AppModule

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

ng generate service logger

5)、将logger配置到hero.module的元数据中

providers:[LoggerService]

这样就表示这个loggerService是属于hero模块的

6)、在hero模块中导出hero组件,并在AppModule中引入hero模块,因为,我们需要使用hero模块中的hero组件

hero.module.ts

exports: [HeroComponent]

app.module.ts

imports: [BrowserModule, HeroModule],

7)、使用hero组件,修改appComponent组件的模板如下

<h1>{{ title }}</h1>
<ul>
  <li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
</ul>
<button (click)="addLog()">addLog</button>
<hr />
<app-hero></app-hero>

这样,appComponent,heroComponent就形成了父子关系

8)、修改hero.component.html文件如下

<p>hero works!</p>
<ul>
  <li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
</ul>
<button (click)="addLog()">addLog</button>
<hr />

9)、分别修改appComponent、heroComponent的组件类文件如下

constructor(private loggerService: LoggerService) {}

  addLog() {
    this.loggerService.addLog('app component');
  }
constructor(private loggerService: LoggerService) {}

  addLog() {
    this.loggerService.addLog('hero component');
  }

10)、现在启动应用,点击按钮,看看会发生什么

可以发现,红框内的内容总是同步的,因此可以得出,他们在使用同一个service实例,如果还不放心,我们在appComponent的构造函数中添加@SkipSelf(使的appComponent到根注入器中找服务实例),测试,发现,红框内的内容还是同步的,这...可以的出一个什么结论呢?——LoggerService这个服务变成了全应用级的服务了!这也是我举这个例子想说明的知识点,(如果还不放心,你可以再建一个模块,并使用LoggerService。看看是不是所有内容都是同步的)

注意:这个结论只适用于普通的模块包含,也就是通过imports数组包含其他模块,不适用惰性加载的模块,因为,惰性加载的模块,Angular会为其新建一个子注入器,里面的东西都是新的(当然包括服务)

哎!美中不足的是:这里讲解的注入器的知识并不包含惰性加载模块,最后简单了解一下一个关于惰性加载模块注入器的知识——Angular会为每一个惰性加载模块重新生成一个注入器(模块级别),然后,在这个新的子注入器中,是不是又可以使用这章节的所有知识点了(将这个子注入器类比AppModule)

问题篇

指令那一章我也说过,组件是特殊的指令,而且指令中也可以配置服务提供商!那么,指令中的服务提供商会被注册到何种级别的注入器中呢?或者说,指令的注入器和组件的注入器有什么关系?(看来这题的答案你就知道:其实这样描述是不准确的!)

更新中。。。

 

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