Angular 学习之路 16 – 服务

前面我们已经介绍过组件。我们说过,组件是 Angular 的核心概念之一。Angular 的一切都是围绕着组件构建的。Angular 将组件定义为一种展示手段:用户看到的就是组件。但是,对于一个完整的应用,仅有展示显然是不够的:我们还有一系列业务逻辑和与之相匹配的数据。Angular 建议,组件应该只是对数据的展示,不应该持有数据。数据和业务逻辑应该交给服务

所谓服务 service,是一个典型的 TypeScript 类,存储有应用程序所需要的数据、函数以及各种其它特性。与其它的各种类一样,服务也应该有明确的职责和良好的定义。

Angular 将服务与组件区别开来,主要是为了增强模块的独立性以及可复用性。通过分离组件与展示无关的功能,可以将组件类变得更精简和高效。理想状态下,组件的唯一职责就是实现用户体验。组件应该通过数据绑定展示数据,从而成为视图(通常是模板)和业务逻辑(通常是数据模型)之间的媒介。

组件将具体的任务委托给服务,例如从服务器获取数据、验证用户输入等。为达到这一目的,就需要让组件获得对应的服务实例。前面说过,服务就是一个普通的 TypeScript 类,那么,我们就可以通过new运算符创建一个服务的实例。但是,这意味着每个组件都是不同的服务实例。如果服务保存有数据,那么,这些数据就会不一致——除非所有数据都是类的属性,而不是实例的属性。通常,这与服务的目的有些违背。服务作为一种基础支撑,应当是单例的,唯一的(Angular 提供了多种层次的“唯一”,有应用程序级别的,有模块级别的,也有组件级别的)。因此,Angular 提供了依赖注入机制。组件并不需要自己通过new创建服务实例,而是将服务类注入到自己的构造函数中。

依赖注入 Dependency Injection (DI) 通过 Java Spring 框架发扬光大。这是一种编程思路。DI 往往与 IoC 相关联。所谓 IoC,即 Inversion of Control,控制反转。通常我们将自己控制类的创建,而控制反转的含义就是,我们自己不创建类,而是交给容器去创建。一般,容器将类创建好,保存在容器内部,当我们需要使用的时候就告诉容器说,我这个类依赖于那个类的实例,你把那个类的实例给我,容器就将这个依赖放到我们的类中,好像打针一样,即注入。

值得注意的是,Angular 只是提出这样的原则,并不强制;尽管Angular 的确提供了很大的便利来帮助你遵循这些原则。

下面我们来看一个服务的示例。

我们有一个Logger类,用于记录日志:

export class Logger {
  log(msg: any)   { console.log(msg); }
  error(msg: any) { console.error(msg); }
  warn(msg: any)  { console.warn(msg); }
}

我们想在一个组件,比如AppComponent中使用这个服务。该怎么做呢?

前面我们说过,服务一般使用依赖注入,将其注入到所需要的地方。为了让一个普通类成为服务,能够支持依赖注入,需要使用@Injectable()装饰器。@Injectable()装饰器提供了用于依赖注入的元数据信息,允许 Angular 将其作为一个依赖,注入到组件、其它服务等位置。类似的, @Injectable()装饰器还暗示了这个组件或者其它的类,比如服务等,有另外的依赖。也就是说,不管你的类是不是要注入到别的类,只要使用依赖注入(有依赖或者被依赖),都需要使用这个修饰器。

注意,可以注入的依赖不一定是类,还可以是函数,甚至某些具体的数值。

简单来说,Angular 维护一系列注入器 injector。这些注入器有的是应用程序作用域,也有模块或者组件作用域,还可以自己创建自己的注入器。注入器创建依赖的实例,作为这些实例的提供容器,并且会尽可能复用这些实例。

注入器用于创建依赖的实例并注入,而提供这些依赖的,被称为提供者 provider。对于任何一个依赖,必须注册至少一个提供者。提供者可以作为服务的元数据,告诉 Angular,这个服务是全局的,可以在任何地方使用;也可以将服务注册到特定的模块或组件,这样,就只能在这个模块或组件中才能使用。

通常,服务都会作为全局依赖。那么,我们就可以将上面的Logger服务增加@Injectable()装饰器,从而成为一个真正的服务:

@Injectable({
  providedIn: 'root',
})
export class Logger {
  ...
}

@Injectable()装饰器告诉 Angular,这是一个服务,可以作为依赖注入到任何可以注入的地方。 @Injectable()装饰器的参数providedIn: 'root'则意味着,这个服务被注册到root,即根注入器,也就是全局。那么,这个服务就可以在整个应用的范围内使用。

这样,我们就可以使用这个服务了。例如,我们要在AppComponent类中使用:

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

  constructor(
    private readonly logger: Logger
  ) { }

  sayHello(): void {
    this.logger.log('I\'m saying hello!');
  }

}

注意AppComponent的构造函数参数是private readonly logger: Logger。前面的访问修饰符private表示logger会作为类的属性,等价于

export class AppComponent {

  private readonly logger: Logger;

  constructor(
    logger: Logger
  ) {
    this.logger = logger;
  }

}

这是 TypeScript 的语法,并不是 Angular 的特性。

Angular 所做的,就是把服务Logger通过构造函数注入到AppComponent,作为一个私有的只读属性logger。之后,我们就可以在AppComponent类中,通过this.logger访问服务的所有暴露出来的方法了。

这里只是 Angular 服务和依赖注入的最基本用法。在后面的章节,我们会详细介绍依赖注入的各种方式(注入类、注入函数、注入数值等)以及各种作用域(全局、模块或者组件)。

Leave a Reply