MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular服务与依赖注入:解耦与复用

2021-12-074.6k 阅读

Angular 服务

什么是 Angular 服务

在 Angular 应用程序中,服务是一个广义的概念,它本质上是一个类,提供特定的功能。服务旨在将应用程序的逻辑从组件中分离出来,实现代码的模块化、可维护性和可复用性。例如,数据获取逻辑、日志记录、状态管理等功能都可以封装在服务中。

服务的作用

  1. 分离关注点:将特定功能的代码封装在服务中,使组件只专注于展示和用户交互。例如,在一个电商应用中,商品数据的获取和处理逻辑可以放在一个服务中,而商品列表组件只负责接收服务提供的数据并展示。这样,当数据获取逻辑发生变化时,只需要修改服务代码,而不影响组件的展示逻辑。
  2. 提高代码复用性:一个服务可以被多个组件复用。比如,一个日志记录服务可以在不同的组件中使用,记录组件的操作日志。如果没有服务,每个组件都需要重复编写日志记录代码,这不仅增加了代码量,而且后期维护也变得困难。
  3. 便于测试:由于服务独立于组件,对服务进行单元测试相对容易。例如,我们可以对数据获取服务进行测试,验证其是否能正确获取数据,而不需要依赖组件的上下文。

创建服务

在 Angular 中,使用 Angular CLI 可以方便地创建服务。执行以下命令:

ng generate service my - service

这将在 src/app 目录下生成一个名为 my - service.ts 的服务文件,其基本结构如下:

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

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

@Injectable() 装饰器标记这个类为一个可注入的服务。providedIn: 'root' 表示该服务在应用程序的根模块中提供,这是 Angular 6 及以上版本推荐的方式,它会自动将服务注册到根注入器中。

服务示例 - 简单的数据存储服务

假设我们需要一个服务来存储和获取用户的名称。以下是实现代码:

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

@Injectable({
  providedIn: 'root'
})
export class UserNameService {
  private userName: string = '';

  setUserName(name: string) {
    this.userName = name;
  }

  getUserName(): string {
    return this.userName;
  }
}

在这个服务中,我们定义了一个私有属性 userName 来存储用户名,并提供了 setUserNamegetUserName 方法来设置和获取用户名。

依赖注入

什么是依赖注入

依赖注入(Dependency Injection,简称 DI)是一种设计模式,它允许我们将一个对象(或服务)所依赖的其他对象(或服务)传递给它,而不是在对象内部创建这些依赖。在 Angular 中,依赖注入是一个核心特性,它使组件能够轻松地使用服务。

依赖注入的原理

Angular 使用注入器(Injector)来管理服务的创建和依赖关系。注入器是一个容器,它知道如何创建和提供各种服务。当一个组件需要某个服务时,它向注入器请求该服务,注入器会检查是否已经创建了该服务实例。如果已经创建,则直接返回该实例;如果没有创建,则创建一个新的实例并返回。

依赖注入的优势

  1. 解耦组件与服务:组件不需要知道服务的具体创建细节,只需要声明它依赖于某个服务。例如,一个组件依赖于数据获取服务来展示数据,组件只需要声明依赖该数据获取服务,而不需要关心服务是如何获取数据的,这使得组件和服务之间的耦合度降低。
  2. 便于测试:在测试组件时,可以使用模拟的服务来替换真实的服务。例如,在测试一个依赖数据获取服务的组件时,可以创建一个模拟的数据获取服务,返回预定义的数据,这样可以独立测试组件的逻辑,而不受真实数据获取过程的影响。

在组件中使用依赖注入

假设我们有一个 UserComponent,它依赖于前面创建的 UserNameService。以下是 UserComponent 的代码:

import { Component } from '@angular/core';
import { UserNameService } from './user - name.service';

@Component({
  selector: 'app - user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent {
  constructor(private userNameService: UserNameService) {}

  setName() {
    this.userNameService.setUserName('John Doe');
  }

  getName() {
    return this.userNameService.getUserName();
  }
}

UserComponent 的构造函数中,我们声明了对 UserNameService 的依赖。Angular 注入器会自动创建 UserNameService 的实例并传递给 UserComponent 的构造函数。

依赖注入的作用域

  1. 根作用域:当我们在服务的 @Injectable 装饰器中使用 providedIn: 'root' 时,服务在根注入器中提供,整个应用程序共享一个该服务的实例。例如,上述的 UserNameService 在根注入器中提供,所有依赖它的组件都使用同一个实例。
  2. 模块作用域:我们也可以在模块中提供服务。在模块的 providers 数组中添加服务,这样服务的作用域就是该模块。例如:
import { NgModule } from '@angular/core';
import { MyService } from './my - service';

@NgModule({
  providers: [MyService]
})
export class MyModule {}

MyModule 及其子模块中的组件依赖 MyService 时,使用的是该模块注入器创建的 MyService 实例。不同模块注入器创建的 MyService 实例是不同的。 3. 组件作用域:在组件的 providers 数组中提供服务,服务的作用域就是该组件及其子组件。例如:

import { Component } from '@angular/core';
import { MyService } from './my - service';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  providers: [MyService]
})
export class MyComponent {}

MyComponent 及其子组件中依赖 MyService 时,使用的是 MyComponent 注入器创建的 MyService 实例。这意味着,不同的 MyComponent 实例使用的 MyService 实例是不同的。

深入理解依赖注入的机制

注入器的层次结构

Angular 的注入器形成了一个层次结构。根注入器位于层次结构的顶部,应用程序的每个模块都有自己的注入器,每个组件也可以有自己的注入器。当一个组件请求一个服务时,它首先在自己的注入器中查找。如果找不到,则向上查找父组件的注入器,然后是模块注入器,最后是根注入器。

例如,假设有一个 AppModule,其中包含一个 ParentComponentParentComponent 又包含一个 ChildComponent。如果 ChildComponent 请求一个服务,它会先在自己的注入器(如果有)中查找。如果没有找到,它会查找 ParentComponent 的注入器,然后是 AppModule 的注入器,最后是根注入器。

服务的单例性

在根注入器或模块注入器中提供的服务通常是单例的。这意味着在整个应用程序或模块范围内,只有一个该服务的实例。例如,当我们在根注入器中提供 UserNameService 时,所有依赖它的组件都共享同一个 UserNameService 实例。这确保了数据的一致性和状态的统一管理。

但是,当在组件作用域中提供服务时,每个组件实例都会有自己独立的服务实例。这在某些情况下是有用的,比如每个组件需要维护自己独立的状态。

依赖注入的生命周期

  1. 服务的创建:当一个服务首次被请求时,注入器会创建该服务的实例。例如,当 UserComponent 首次被创建并请求 UserNameService 时,根注入器(因为 UserNameService 在根注入器中提供)会创建 UserNameService 的实例。
  2. 服务的销毁:在 Angular 应用程序中,服务的销毁由注入器管理。当注入器被销毁时(例如,模块被卸载或组件被销毁,如果服务是在组件作用域中提供的),它所管理的服务实例也会被销毁。不过,在大多数情况下,我们不需要手动管理服务的销毁,Angular 会自动处理这些情况。

依赖注入与模块加载

在 Angular 中,模块的加载顺序会影响依赖注入。当一个模块被加载时,它的注入器会被创建,并初始化该模块提供的所有服务。如果一个模块依赖于另一个模块提供的服务,那么被依赖的模块必须先被加载。

例如,假设有 ModuleAModuleBModuleB 依赖于 ModuleA 提供的服务。在应用程序启动时,ModuleA 必须先被加载,其注入器创建并初始化服务,然后 ModuleB 才能正常加载并使用 ModuleA 提供的服务。

解耦与复用的实践

解耦组件与服务

  1. 通过接口实现解耦:为服务定义接口,组件依赖于接口而不是具体的服务类。例如,假设我们有一个数据获取服务,我们可以先定义一个接口:
export interface DataFetcher {
  fetchData(): Promise<any>;
}

然后创建具体的服务类实现这个接口:

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

@Injectable({
  providedIn: 'root'
})
export class HttpDataFetcher implements DataFetcher {
  async fetchData(): Promise<any> {
    // 实际的 HTTP 请求逻辑
    return Promise.resolve({ data: 'Some fetched data' });
  }
}

在组件中,依赖于接口:

import { Component } from '@angular/core';
import { DataFetcher } from './data - fetcher.interface';

@Component({
  selector: 'app - data - component',
  templateUrl: './data - component.html'
})
export class DataComponent {
  constructor(private dataFetcher: DataFetcher) {}

  async loadData() {
    const data = await this.dataFetcher.fetchData();
    console.log(data);
  }
}

这样,当数据获取的实现方式发生变化(例如,从 HTTP 请求改为从本地存储获取数据),只需要创建一个新的实现 DataFetcher 接口的服务类,而组件代码不需要修改。 2. 避免组件与服务的紧密耦合:组件应该只调用服务的公共接口,而不依赖于服务的内部实现细节。例如,UserNameService 中,UserComponent 只调用 setUserNamegetUserName 方法,而不关心 userName 属性是如何存储的。

复用服务

  1. 跨组件复用:一个服务可以被多个不同的组件复用。比如,前面提到的 UserNameService 可以被 UserComponentProfileComponent 等多个组件使用,这些组件都可以通过依赖注入获取 UserNameService 的实例并使用其功能。
  2. 跨模块复用:如果一个服务在根注入器中提供,它可以被应用程序的任何模块中的组件复用。例如,一个日志记录服务在根注入器中提供,不同模块中的组件都可以依赖注入该日志记录服务来记录日志。

示例 - 复用数据获取服务

假设我们有一个通用的数据获取服务 DataService,用于从后端获取数据。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(private http: HttpClient) {}

  fetchData(url: string) {
    return this.http.get(url);
  }
}

现在有两个组件 ProductListComponentOrderListComponent,它们都需要从后端获取数据。

// ProductListComponent
import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app - product - list',
  templateUrl: './product - list.html'
})
export class ProductListComponent {
  products: any[] = [];

  constructor(private dataService: DataService) {}

  ngOnInit() {
    this.dataService.fetchData('/api/products').subscribe((data: any[]) => {
      this.products = data;
    });
  }
}
// OrderListComponent
import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app - order - list',
  templateUrl: './order - list.html'
})
export class OrderListComponent {
  orders: any[] = [];

  constructor(private dataService: DataService) {}

  ngOnInit() {
    this.dataService.fetchData('/api/orders').subscribe((data: any[]) => {
      this.orders = data;
    });
  }
}

在这个例子中,DataServiceProductListComponentOrderListComponent 复用,实现了代码的复用,减少了重复代码。

依赖注入的高级特性

多重注入

在某些情况下,我们可能需要为一个组件提供多个相同类型的服务实例。Angular 支持多重注入来满足这种需求。例如,假设我们有一个 LoggerService,不同的组件可能需要不同配置的 LoggerService 实例。 首先,在服务的 @Injectable 装饰器中设置 multi: true

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

@Injectable({
  providedIn: 'root',
  multi: true
})
export class LoggerService {
  constructor(private name: string) {}

  log(message: string) {
    console.log(`${this.name}: ${message}`);
  }
}

然后在组件的 providers 数组中提供多个 LoggerService 实例:

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

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  providers: [
    { provide: LoggerService, useValue: new LoggerService('Component1Logger') },
    { provide: LoggerService, useValue: new LoggerService('Component2Logger') }
  ]
})
export class MyComponent {
  constructor(private loggers: LoggerService[]) {}

  ngOnInit() {
    this.loggers.forEach(logger => logger.log('Some log message'));
  }
}

MyComponent 的构造函数中,我们注入了一个 LoggerService 数组,这样就可以使用多个不同配置的 LoggerService 实例。

条件注入

有时,我们需要根据不同的条件提供不同的服务实例。Angular 可以通过 useFactory 来实现条件注入。例如,假设我们有两个数据获取服务 HttpDataFetcherMockDataFetcher,在开发环境中我们希望使用 MockDataFetcher,在生产环境中使用 HttpDataFetcher。 首先定义两个服务:

// HttpDataFetcher
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class HttpDataFetcher {
  constructor(private http: HttpClient) {}

  fetchData() {
    return this.http.get('/api/data');
  }
}
// MockDataFetcher
import { Injectable } from '@angular/core';

@Injectable()
export class MockDataFetcher {
  fetchData() {
    return Promise.resolve({ mockData: 'Mocked data' });
  }
}

然后在模块的 providers 数组中使用 useFactory 来根据条件提供服务:

import { NgModule } from '@angular/core';
import { HttpDataFetcher } from './http - data - fetcher';
import { MockDataFetcher } from './mock - data - fetcher';

export function dataFetcherFactory(isProduction: boolean) {
  return isProduction? new HttpDataFetcher() : new MockDataFetcher();
}

@NgModule({
  providers: [
    {
      provide: 'DataFetcher',
      useFactory: dataFetcherFactory,
      deps: ['IS_PRODUCTION']
    },
    { provide: 'IS_PRODUCTION', useValue: true }
  ]
})
export class AppModule {}

在这个例子中,dataFetcherFactory 函数根据 isProduction 的值决定返回 HttpDataFetcher 还是 MockDataFetcher 的实例。deps 数组指定了 dataFetcherFactory 函数依赖的其他服务或值。

注入令牌

注入令牌(Injection Token)是一种用于在依赖注入中标识服务的方式。当我们需要注入一个没有类定义的对象(例如一个简单的值或函数),或者需要区分多个相同类型的服务时,注入令牌就非常有用。 例如,假设我们需要注入一个 API 地址:

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

export const API_URL = new InjectionToken<string>('API_URL');

然后在模块的 providers 数组中提供这个值:

import { NgModule } from '@angular/core';
import { API_URL } from './api - url.token';

@NgModule({
  providers: [
    { provide: API_URL, useValue: 'https://example.com/api' }
  ]
})
export class AppModule {}

在组件中使用注入令牌来注入这个值:

import { Component } from '@angular/core';
import { API_URL } from './api - url.token';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html'
})
export class MyComponent {
  constructor(@Inject(API_URL) private apiUrl: string) {}

  ngOnInit() {
    console.log('API URL:', this.apiUrl);
  }
}

通过注入令牌,我们可以方便地管理和注入一些配置值或其他非类的对象。

总结

Angular 的服务与依赖注入机制是构建可维护、可复用 Angular 应用程序的关键。通过将功能封装在服务中,并利用依赖注入将服务注入到组件中,我们实现了组件与服务的解耦,提高了代码的复用性。深入理解依赖注入的机制,如注入器的层次结构、服务的单例性、依赖注入的生命周期等,以及掌握解耦与复用的实践方法,能够帮助我们编写高质量的 Angular 应用程序。同时,依赖注入的高级特性,如多重注入、条件注入和注入令牌,为我们解决复杂的依赖管理问题提供了有力的工具。在实际开发中,合理运用这些知识,能够提升开发效率,降低维护成本,打造出更加健壮和灵活的 Angular 应用。