Angular服务与依赖注入:解耦与复用
Angular 服务
什么是 Angular 服务
在 Angular 应用程序中,服务是一个广义的概念,它本质上是一个类,提供特定的功能。服务旨在将应用程序的逻辑从组件中分离出来,实现代码的模块化、可维护性和可复用性。例如,数据获取逻辑、日志记录、状态管理等功能都可以封装在服务中。
服务的作用
- 分离关注点:将特定功能的代码封装在服务中,使组件只专注于展示和用户交互。例如,在一个电商应用中,商品数据的获取和处理逻辑可以放在一个服务中,而商品列表组件只负责接收服务提供的数据并展示。这样,当数据获取逻辑发生变化时,只需要修改服务代码,而不影响组件的展示逻辑。
- 提高代码复用性:一个服务可以被多个组件复用。比如,一个日志记录服务可以在不同的组件中使用,记录组件的操作日志。如果没有服务,每个组件都需要重复编写日志记录代码,这不仅增加了代码量,而且后期维护也变得困难。
- 便于测试:由于服务独立于组件,对服务进行单元测试相对容易。例如,我们可以对数据获取服务进行测试,验证其是否能正确获取数据,而不需要依赖组件的上下文。
创建服务
在 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
来存储用户名,并提供了 setUserName
和 getUserName
方法来设置和获取用户名。
依赖注入
什么是依赖注入
依赖注入(Dependency Injection,简称 DI)是一种设计模式,它允许我们将一个对象(或服务)所依赖的其他对象(或服务)传递给它,而不是在对象内部创建这些依赖。在 Angular 中,依赖注入是一个核心特性,它使组件能够轻松地使用服务。
依赖注入的原理
Angular 使用注入器(Injector)来管理服务的创建和依赖关系。注入器是一个容器,它知道如何创建和提供各种服务。当一个组件需要某个服务时,它向注入器请求该服务,注入器会检查是否已经创建了该服务实例。如果已经创建,则直接返回该实例;如果没有创建,则创建一个新的实例并返回。
依赖注入的优势
- 解耦组件与服务:组件不需要知道服务的具体创建细节,只需要声明它依赖于某个服务。例如,一个组件依赖于数据获取服务来展示数据,组件只需要声明依赖该数据获取服务,而不需要关心服务是如何获取数据的,这使得组件和服务之间的耦合度降低。
- 便于测试:在测试组件时,可以使用模拟的服务来替换真实的服务。例如,在测试一个依赖数据获取服务的组件时,可以创建一个模拟的数据获取服务,返回预定义的数据,这样可以独立测试组件的逻辑,而不受真实数据获取过程的影响。
在组件中使用依赖注入
假设我们有一个 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
的构造函数。
依赖注入的作用域
- 根作用域:当我们在服务的
@Injectable
装饰器中使用providedIn: 'root'
时,服务在根注入器中提供,整个应用程序共享一个该服务的实例。例如,上述的UserNameService
在根注入器中提供,所有依赖它的组件都使用同一个实例。 - 模块作用域:我们也可以在模块中提供服务。在模块的
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
,其中包含一个 ParentComponent
,ParentComponent
又包含一个 ChildComponent
。如果 ChildComponent
请求一个服务,它会先在自己的注入器(如果有)中查找。如果没有找到,它会查找 ParentComponent
的注入器,然后是 AppModule
的注入器,最后是根注入器。
服务的单例性
在根注入器或模块注入器中提供的服务通常是单例的。这意味着在整个应用程序或模块范围内,只有一个该服务的实例。例如,当我们在根注入器中提供 UserNameService
时,所有依赖它的组件都共享同一个 UserNameService
实例。这确保了数据的一致性和状态的统一管理。
但是,当在组件作用域中提供服务时,每个组件实例都会有自己独立的服务实例。这在某些情况下是有用的,比如每个组件需要维护自己独立的状态。
依赖注入的生命周期
- 服务的创建:当一个服务首次被请求时,注入器会创建该服务的实例。例如,当
UserComponent
首次被创建并请求UserNameService
时,根注入器(因为UserNameService
在根注入器中提供)会创建UserNameService
的实例。 - 服务的销毁:在 Angular 应用程序中,服务的销毁由注入器管理。当注入器被销毁时(例如,模块被卸载或组件被销毁,如果服务是在组件作用域中提供的),它所管理的服务实例也会被销毁。不过,在大多数情况下,我们不需要手动管理服务的销毁,Angular 会自动处理这些情况。
依赖注入与模块加载
在 Angular 中,模块的加载顺序会影响依赖注入。当一个模块被加载时,它的注入器会被创建,并初始化该模块提供的所有服务。如果一个模块依赖于另一个模块提供的服务,那么被依赖的模块必须先被加载。
例如,假设有 ModuleA
和 ModuleB
,ModuleB
依赖于 ModuleA
提供的服务。在应用程序启动时,ModuleA
必须先被加载,其注入器创建并初始化服务,然后 ModuleB
才能正常加载并使用 ModuleA
提供的服务。
解耦与复用的实践
解耦组件与服务
- 通过接口实现解耦:为服务定义接口,组件依赖于接口而不是具体的服务类。例如,假设我们有一个数据获取服务,我们可以先定义一个接口:
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
只调用 setUserName
和 getUserName
方法,而不关心 userName
属性是如何存储的。
复用服务
- 跨组件复用:一个服务可以被多个不同的组件复用。比如,前面提到的
UserNameService
可以被UserComponent
、ProfileComponent
等多个组件使用,这些组件都可以通过依赖注入获取UserNameService
的实例并使用其功能。 - 跨模块复用:如果一个服务在根注入器中提供,它可以被应用程序的任何模块中的组件复用。例如,一个日志记录服务在根注入器中提供,不同模块中的组件都可以依赖注入该日志记录服务来记录日志。
示例 - 复用数据获取服务
假设我们有一个通用的数据获取服务 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);
}
}
现在有两个组件 ProductListComponent
和 OrderListComponent
,它们都需要从后端获取数据。
// 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;
});
}
}
在这个例子中,DataService
被 ProductListComponent
和 OrderListComponent
复用,实现了代码的复用,减少了重复代码。
依赖注入的高级特性
多重注入
在某些情况下,我们可能需要为一个组件提供多个相同类型的服务实例。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
来实现条件注入。例如,假设我们有两个数据获取服务 HttpDataFetcher
和 MockDataFetcher
,在开发环境中我们希望使用 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 应用。