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

探索Angular依赖注入的奥秘

2021-07-267.0k 阅读

理解依赖注入的基本概念

在深入探讨Angular的依赖注入之前,我们先来理解一下依赖注入(Dependency Injection,简称DI)的基本概念。依赖注入是一种设计模式,它的核心思想是将对象所依赖的其他对象通过外部传递进来,而不是在对象内部自己创建。

想象一下,我们有一个 Car 类,它依赖于 Engine 类。在传统的编程方式中,Car 类可能会在其内部创建 Engine 实例:

class Engine {
  start() {
    console.log('Engine started');
  }
}

class Car {
  private engine: Engine;
  constructor() {
    this.engine = new Engine();
  }
  drive() {
    this.engine.start();
    console.log('Car is driving');
  }
}

这里 Car 类紧密耦合到 Engine 类,这意味着如果我们想要更换 Engine 的实现(例如,从燃油发动机换成电动发动机),我们需要修改 Car 类的代码。这违反了软件设计中的开闭原则(Open - Closed Principle),即软件实体应该对扩展开放,对修改关闭。

而依赖注入的方式是这样的:

class Engine {
  start() {
    console.log('Engine started');
  }
}

class Car {
  private engine: Engine;
  constructor(engine: Engine) {
    this.engine = engine;
  }
  drive() {
    this.engine.start();
    console.log('Car is driving');
  }
}

// 使用依赖注入
const engine = new Engine();
const car = new Car(engine);
car.drive();

在这个例子中,Car 类不再自己创建 Engine 实例,而是通过构造函数接收一个已经创建好的 Engine 实例。这样,如果我们想要更换 Engine 的实现,只需要在创建 Car 实例时传入不同的 Engine 实现,而不需要修改 Car 类的代码。

Angular 中的依赖注入系统

Angular 拥有一套强大且灵活的依赖注入系统。它基于模块(Module)和注入器(Injector)的概念工作。

模块(Module)

在Angular中,模块是一个组织代码的单元。我们可以把相关的组件、服务、指令等组合在一个模块中。一个典型的Angular应用至少有一个根模块(通常命名为 AppModule),也可以有多个特性模块。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { AppComponent } from './app.component';
import { MyService } from './my.service';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [MyService],
  bootstrap: [AppComponent]
})
export class AppModule {}

在上面的 AppModule 中,providers 数组用于注册服务。当我们在 providers 中注册了 MyService,Angular 的依赖注入系统就知道如何创建 MyService 的实例。

注入器(Injector)

注入器是Angular依赖注入系统的核心。它负责创建和管理对象的实例,并提供依赖对象。每个Angular应用都有一个根注入器,它在应用启动时创建。此外,每个组件也可以有自己的注入器,形成一个注入器树。

当一个组件请求一个依赖时,注入器首先在自己的作用域内查找该依赖的提供者(provider)。如果找不到,它会向上一级注入器查找,直到根注入器。

服务(Service)与依赖注入

服务是Angular应用中一个非常重要的概念。它是一个可注入的类,通常用于封装一些与业务逻辑相关的功能,比如数据获取、数据处理等。

创建服务

我们可以使用Angular CLI来创建一个服务:

ng generate service my - service

这会生成一个 my - service.ts 文件,内容如下:

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

@Injectable({
  providedIn: 'root'
})
export class MyService {
  constructor() {}
  getData() {
    return 'Some data';
  }
}

@Injectable() 装饰器用于标记这个类是可注入的。providedIn: 'root' 表示这个服务会在根注入器中提供,意味着整个应用中只会有一个该服务的实例。

在组件中使用服务

我们可以在组件中注入并使用这个服务:

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

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent {
  data: string;
  constructor(private myService: MyService) {}
  ngOnInit() {
    this.data = this.myService.getData();
  }
}

在组件的构造函数中,我们通过类型声明 private myService: MyService 注入了 MyService。Angular的依赖注入系统会自动创建 MyService 的实例并注入到组件中。

依赖注入的提供者(Provider)

在Angular中,提供者(provider)告诉注入器如何创建一个依赖。除了在模块的 providers 数组中注册提供者,我们还可以在组件、指令等的 providers 数组中注册。

不同级别的提供者

  1. 根级别提供者:如前面 MyService 的例子,当我们使用 providedIn: 'root' 时,服务在根注入器中提供。这意味着整个应用共享一个该服务的实例。
  2. 组件级别提供者:我们可以在组件的 providers 数组中注册服务:
@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css'],
  providers: [MyService]
})
export class MyComponent {
  constructor(private myService: MyService) {}
}

这样,每个 MyComponent 实例都会有自己独立的 MyService 实例。这在一些场景下非常有用,比如每个组件需要有自己独立的数据状态。

提供者的类型

  1. 类提供者:最常见的提供者类型,我们前面注册 MyService 就是类提供者。
providers: [MyService]
  1. 值提供者:有时候我们可能想要提供一个简单的值,而不是一个类的实例。例如:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { AppComponent } from './app.component';

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

然后我们可以在组件中注入这个值:

import { Component, Inject } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent {
  constructor(@Inject('API_URL') private apiUrl: string) {}
}
  1. 工厂提供者:当我们需要更复杂的逻辑来创建一个依赖时,可以使用工厂提供者。例如,假设我们有一个 Logger 服务,它的实例化需要根据环境变量来决定日志级别:
import { NgModule, Inject, InjectionToken } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { AppComponent } from './app.component';

export const LOG_LEVEL = new InjectionToken<string>('log - level');

function loggerFactory(logLevel: string) {
  return {
    log(message: string) {
      if (logLevel === 'debug') {
        console.log(`DEBUG: ${message}`);
      } else {
        console.log(message);
      }
    }
  };
}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [
    {
      provide: 'Logger',
      useFactory: loggerFactory,
      deps: [LOG_LEVEL]
    },
    {
      provide: LOG_LEVEL,
      useValue: 'debug'
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

在组件中使用:

import { Component, Inject } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent {
  constructor(@Inject('Logger') private logger: any) {
    this.logger.log('Component initialized');
  }
}

这里 loggerFactory 是一个工厂函数,它接受 LOG_LEVEL 作为依赖,并返回一个 Logger 实例。

依赖注入的生命周期

理解依赖注入的生命周期对于编写高效、可维护的Angular应用非常重要。

服务的创建与销毁

  1. 单例服务:当一个服务在根注入器中提供(例如使用 providedIn: 'root'),它在整个应用中是单例的。它会在应用启动时被创建,并且在应用销毁时被销毁。
  2. 组件级服务:对于在组件 providers 数组中注册的服务,它的生命周期与组件相关。当组件被创建时,服务实例被创建;当组件被销毁时,服务实例也被销毁。

依赖注入链的构建

当一个组件请求一个依赖时,注入器会按照注入器树的结构来查找提供者并构建依赖注入链。例如,假设我们有一个组件 A,它依赖于服务 S1,而 S1 又依赖于服务 S2。当 A 被创建时,注入器首先会创建 S2 的实例(如果 S2 还没有被创建),然后使用 S2 的实例来创建 S1 的实例,最后将 S1 的实例注入到 A 中。

高级依赖注入技巧

多提供者(Multi - providers)

有时候我们可能需要为一个依赖提供多个实例。例如,我们有一个 Logger 服务,我们希望在不同的模块或组件中有不同的日志记录策略。我们可以使用多提供者来实现:

import { NgModule, Provider } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { AppComponent } from './app.component';
import { Logger } from './logger.service';

const debugLoggerProvider: Provider = {
  provide: Logger,
  useValue: {
    log(message: string) {
      console.log(`DEBUG: ${message}`);
    }
  },
  multi: true
};

const infoLoggerProvider: Provider = {
  provide: Logger,
  useValue: {
    log(message: string) {
      console.log(`INFO: ${message}`);
    }
  },
  multi: true
};

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [Logger, debugLoggerProvider, infoLoggerProvider],
  bootstrap: [AppComponent]
})
export class AppModule {}

在组件中,我们可以注入一个 Logger 数组:

import { Component, Inject } from '@angular/core';
import { Logger } from './logger.service';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent {
  constructor(@Inject(Logger) private loggers: Logger[]) {
    this.loggers.forEach(logger => logger.log('Component initialized'));
  }
}

这里 multi: true 表示这是一个多提供者,允许为同一个依赖提供多个实例。

依赖注入与测试

依赖注入使得单元测试变得更加容易。我们可以通过提供模拟的依赖来测试组件或服务,而不需要依赖真实的实现。

例如,假设我们有一个 UserService 用于获取用户数据,它依赖于一个 HttpClient 来进行HTTP请求。在测试 UserService 时,我们可以提供一个模拟的 HttpClient

import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
import { HttpClient } from '@angular/common/http';

describe('UserService', () => {
  let service: UserService;
  let httpClientSpy: jasmine.SpyObj<HttpClient>;

  beforeEach(() => {
    httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
    TestBed.configureTestingModule({
      providers: [
        UserService,
        { provide: HttpClient, useValue: httpClientSpy }
      ]
    });
    service = TestBed.inject(UserService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

这里我们使用 TestBed 来配置测试模块,并通过 { provide: HttpClient, useValue: httpClientSpy } 提供了一个模拟的 HttpClient。这样我们就可以在不进行真实HTTP请求的情况下测试 UserService

依赖注入的最佳实践

  1. 保持服务单一职责:每个服务应该只负责一个特定的功能。例如,一个 DataService 应该只负责数据的获取和存储,而不应该包含与业务逻辑无关的UI相关代码。
  2. 合理使用注入器级别:根据服务的性质和需求,选择合适的注入器级别。如果一个服务是全局共享的,应该在根注入器中提供;如果每个组件需要自己独立的实例,应该在组件级别提供。
  3. 避免循环依赖:循环依赖是指两个或多个类相互依赖。在Angular中,循环依赖会导致应用启动失败。要避免循环依赖,需要合理设计服务之间的依赖关系,确保依赖关系是单向的。
  4. 使用注入令牌(Injection Token):当我们需要注入一个非类的依赖(如字符串、对象等),或者需要为同一个类提供不同的实例时,使用注入令牌可以使代码更加清晰和可维护。

通过深入理解和合理使用Angular的依赖注入系统,我们可以构建更加模块化、可测试和可维护的前端应用。无论是小型项目还是大型企业级应用,依赖注入都是一个强大的工具,可以帮助我们管理组件和服务之间的依赖关系,提高代码的质量和可扩展性。在实际开发中,不断实践和总结经验,将依赖注入的优势发挥到最大。同时,随着Angular的不断发展,依赖注入系统也可能会有新的特性和改进,我们需要持续关注官方文档和社区动态,以保持技术的先进性。

在处理复杂的业务逻辑和大型项目时,依赖注入的设计和实现变得尤为关键。例如,在一个电商应用中,可能有多个服务如 ProductServiceCartServiceUserService 等,它们之间存在着复杂的依赖关系。通过合理地使用依赖注入,我们可以确保这些服务之间的交互清晰、可维护,并且易于进行单元测试和集成测试。同时,在应用的性能优化方面,依赖注入也能起到一定的作用,比如通过合理设置服务的作用域,避免不必要的实例创建和销毁,提高应用的运行效率。

在团队开发中,统一的依赖注入规范和设计模式有助于新成员快速理解项目结构和代码逻辑。例如,规定所有的服务都在模块的 providers 中注册,并且使用有意义的命名约定来命名注入令牌和服务,这样可以使代码更加清晰易读。另外,对于一些通用的服务,如 LoggerServiceErrorHandlerService 等,可以在根模块中提供,以便整个应用共享使用。

总之,掌握Angular依赖注入的奥秘,对于构建高质量、可扩展的前端应用至关重要。通过不断的实践和学习,我们可以更好地利用这一强大的功能,为用户带来更好的应用体验。同时,在遇到复杂的依赖关系和设计问题时,要善于运用各种依赖注入技巧和最佳实践,确保项目的顺利进行和长期维护。