Angular7入门辅助教程(七)——依赖注入系统之服务

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

Injectable

终于进入了Angular中极为重要的一个知识点:Angular的依赖注入系统(DI系统),接下来的三章的内容都是描述这个DI系统的,因为理解Angular的DI系统对于初学者来说比较困难,但是又极其重要,这一章加上后续两章,其内容是步步深入的,你可能觉得用三章来描述DI系统有点长,那么,如果你去看官方文档,你会发现其关于服务和DI系统的教程长的可怕,而且知识点相对分散,并不是看一遍就能收获很大

心法篇

  • 服务的范围很广,函数、字面量、对象都有可能成为服务
  • 组件不应该向后端获取数据、不应该对数据进行校验,它应该专注于视图,而与视图无关的逻辑应该委托给服务
  • Angular中的服务是通过@Injectable装饰器来标识的(狭义的Angular服务)
  • 我们可以通过将服务传入组件类、服务类的构造函数作为入参来使用服务实例
  • 当然,服务类也可以使用其他服务,使用方式都是一样的

详细教程篇

1、服务的元数据

Angular的服务也有标识,这就是@Injectable装饰器,它接受一个描述服务的元数据对象作为参数,下面我们来解析一下这个元数据对象

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

@Injectable({
  providedIn: 'root',
})
export class MessageService {}
  • providedIn——表示该服务应该注入到的注入器(注入器还没有概念没有关系),这里简单的理解为服务的级别,这也是这篇博客的主要内容,请继续往下看

而紧随@Injectable装饰器的那个类就是服务类,比如,在这个例子中这个服务类就是MessageService

2、服务的创建

我们可以使用Angular CLI来创建一个服务,命令如下

ng generate service 服务名

利用Angular CLI来创建服务同样可以带来规范命名的好处,例如使用如下命令创建一个logger服务,用于打印日志

ng generate service logger

这时,Angular CLI会创建一个名叫logger.service.ts的文件和其相应的名叫logger.service.spec.ts测试文件,而logger.service.ts中会生成最基本的Angular服务原型,内容如下

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

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

  constructor() { }
}

从这里可以看出,这命名是相当规范的! 你也可以手动创建服务类,只需要带上@Injectable()这个装饰器就可以了

3、服务的三种级别

  • 模块级别的服务

顾名思义,模块级别的服务就表示其作用域(这样表述并不准确,在单例服务那一章我会用更准确的方式来表达各种级别的服务的作用域所表示的含义,在这里,就先使用作用域这个词)是在该模块内的,也就是说:模块内的组件、指令、服务都可以使用该服务实例。

我们有两种方式创建这种级别的服务(需要注意的是,这两种方式是有微小的差别的,但在这里不做讨论)

1)、在服务的元数据中指定

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

@Injectable({
  providedIn: 'HeroModule', // 表示该服务属于HeroModule这个NgModule
})
export class MessageService {}

可以看出,我们将服务的元数据对象的providedIn属性的值设为了HeroModule,也就是将其限制在HeroModule内,如果别的模块想使用这个服务,就必须先引入该模块!在后面会有一个具体的例子。

2)、在模块的元数据中指定

不知道你是否还记得,NgModule的元数据对象中有一个providers数组属性,没错,写在这个数组中的服务都是模块级别的,例如下面这样

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoggerService } from '../logger.service';

@NgModule({
    declarations: [],
    imports: [
        CommonModule
    ],
    providers: [LoggerService] // 该服务的作用域限制在该模块内
})
export class HeroModule { }

其实这两种方式的效果是一样的,但是前面说了,还是有区别的,区别就是:在服务元数据中指定范围的服务,在打包生产最终的应用时,如果该服务没有被使用,那么它就会被移除掉,以减少最终生成的包的体积,而在模块元数据中指定的服务不会有这一步操作

  • 组件级别的服务

组件级别的服务就表示该服务可以在该组件以及其子组件中被注入(使用),但和模块级别的服务不一样的是:我们只能在组件的元数据对象中指定这种级别的服务,示例如下

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

@Component({
    selector: 'app-hero',
    templateUrl: './hero.component.html',
    styleUrls: ['./hero.component.css'],
    providers: [LoggerService] // 该组件以及该组件的子组件可以使用该服务实例
})
export class HeroComponent implements OnInit {

    constructor() { }

    ngOnInit() {
    }

}
  • root级别的服务

顾名思义,这种级别的服务是全应用范围的了,其实,这种级别的服务在某种程度上也可以说成是模块级别的服务,只是它是属于根模块(AppModule)级别的服务,也是为了和其他普通模块区分开来,同普通模块一样,我们有两种方式创建这种级别的服务(不过要注意与普通模块的区别)

1)、在服务的元数据中指定

其实,在使用Angular CLI常见的服务,其默认作用域就是root,看下面的例子

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

@Injectable({
  providedIn: 'root' // root级别的服务
})
export class LoggerService {

  constructor() { }
}

2)、在根模块(AppModule)的元数据对中指定

像这样

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { LoggerService } from './logger.service';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule
    ],
    providers: [LoggerService],// 全应用级别的服务
    bootstrap: [AppComponent]
})
export class AppModule { }

其实,这和指定普通模块级别的服务的方式是一样的,但是,官方文档中有这样一句话:一般来说,你不必在providedIn 中指定 AppModule,因为应用中的root注入器就是 AppModule 注入器。 不过,如果你在 AppModule 的 @NgModule 元数据中配置了全应用级的提供商,它就会覆盖通过 @Injectable 配置的那一个!(额。。。。不好意思,这段话的后半部分我也不理解,如果你理解了的话,欢迎给我留言哈)

4、模块级别的服务的小例子

在对Angular服务有了基本的了解后,我们来看一个具体的例子

1)、创建一个名叫test-teaching-provider的Angular项目,然后使用默认选项就好

ng new test-teaching-provider

 并cd进这个文件目录

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

ng generate service logger

并删除其元数据对象,并加上一个getLog方法,得到下面的这个logger.service.ts文件

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

@Injectable()
export class LoggerService {

    constructor() { }

    getLog() {
        return 'test NgModule level service successfully';
    }
}

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

ng generate module hero

并将logger服务添加到其元数据对象的providers属性中,像下面这样

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoggerService } from '../logger.service';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  providers: [LoggerService]
})
export class HeroModule { }

4)、删除app.component.html文件默认内容,并加上下面这段代码

<p>{{ log }}</p>

看到这里,你应该要清楚还需要改某个地方,没错就是app.component.ts这个文件,很明显,上面代码中的log是AppComponent这个类的一个属性 ,现在修改内容如下

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

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

    constructor(private loggerService: LoggerService) {
    }

    ngOnInit() {
        this.log = this.loggerService.getLog();
    }

}

这段代码可能需要解释一下

  • 将目光放到构造函数的参数上面——这就是使用服务的方法,在心法篇我也讲到了
  • 现在看到ngOnInit函数,这是一个生命周期钩子函数(其内容超出了本章节的范围,在这里我们只需要知道通过服务获取数据的代码应该写在ngOnInit函数中,而不应该这在construct中,就像本例这样子,这也是一种最佳实践!

现在,你应该知道这个小例子最后的效果——在界面上面显示“test NgModule level service successfully”这段话

好了,一切准备妥当,好启动应用,见证奇迹

ng serve -o

该命令是启动应用、实时监听应用的修改并打开浏览器,很不幸的是——浏览器中并没有砸门期望的结果,f12打开chrome的调试界面,砸门得到的却是下面这样的错误

上面提示说:NullInjectorError: No provider for LoggerService!(没有提供商提供LoggerService)为什么呢?细心的读者可以发现,我们是在HeroModule中提供LoggerService服务的,而我们却是在AppModule中使用该服务的,不知道你是否还记得——模块级别的服务的作用域只在该模块内(注意,AppModule不同,它上面的服务是全应用级别的,前面也有解释过),当然就不能在别的模块内使用啦!要使用它,除非引入该模块。好,现在我们就来改一下AppModule的代码

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HeroModule } from './hero/hero.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HeroModule // 引入HeroModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

现在,你的浏览器页面应该自动刷新,并出现了期望的结果

而且,控制台也没有报错!这表示,砸门实验成功了!

问题篇

如果我再新建一个有组件(HeroDetailComponent)的模块(HeroDetailModule),并将其包含进AppModule(放在imports数组中),并把HeroDetailComponent组件放在AppComponent的模板中(作为其子组件),在不做其他修改的前提下,我还能在HeroDetailComponent组件中使用LoggerService服务吗?为什么?

更新中。。。

 

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