Angular依赖注入系统实战指南:从原理到落地的5个关键场景

先搞懂:依赖注入到底帮我们解决了什么问题
你有没有写过这样的代码?在组件里直接new一个服务实例:

// 不用DI的情况:组件紧耦合服务
export class HeroComponent {
  private heroService = new HeroService(); // 直接创建实例
  heroes = this.heroService.getHeroes();
}

看起来没问题,但如果HeroService需要依赖HttpService呢?你得改成new HeroService(new HttpService())——如果依赖链更长,组件里的new会像套娃一样越来越复杂。更麻烦的是,当你想在测试里替换HeroService为模拟服务时,得修改组件代码,完全没灵活性。

Angular依赖注入系统实战指南:从原理到落地的5个关键场景

而用DI的写法是这样的:

// 用DI的情况:组件只声明依赖,实例由注入器创建
export class HeroComponent {
  constructor(private heroService: HeroService) {} // 注入器自动传入实例
  heroes = this.heroService.getHeroes();
}

看出区别了吗?DI把“创建实例”的责任从组件转嫁到了注入器,组件只需要“声明要什么”,不用关心“怎么来的”。这就是DI的核心价值:解耦依赖创建与使用,让代码更可维护、可测试

核心概念拆解:令牌、提供商、注入器的三角关系
要玩转DI,得先理清三个核心概念的关系——它们就像“钥匙、说明书、工具箱”,缺一个都不行。我整理了一张表,帮你快速对应:

概念 作用说明 实战示例
令牌(Token) 依赖的“身份证”,注入器通过它找到对应的依赖 HeroService类 / new InjectionToken('API_URL')
提供商(Provider) 告诉注入器“如何做这个依赖”的规则(比如用哪个类、给什么值) { provide: HeroService, useClass: HeroService }
注入器(Injector) 执行“找令牌→查提供商→造实例→送过去”的“快递员” 根注入器(全局)/ 模块注入器(局部)/ 组件注入器(更局部)

举个例子:当你在组件里写constructor(private heroService: HeroService)时,Angular会做三件事:
1. 用HeroService类作为令牌,向注入器“要东西”;
2. 注入器找到HeroService对应的提供商(比如{ provide: HeroService, useClass: HeroService });
3. 按照提供商的规则创建HeroService实例,注入到组件里。

实战场景1:组件与服务的基础注入——避免重复创建实例
最常见的场景就是“服务注入到组件”,但新手常踩的坑是重复创建实例。比如你写了个LoggerService用于打印日志,想让所有组件共享同一个实例,该怎么配置?

正确的服务写法(用providedIn: 'root'):

// logger.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root' // 告诉Angular:这个服务由根注入器提供,全局共享一个实例
})
export class LoggerService {
  log(message: string) {
    console.log(`[Logger] ${message}`);
  }
}

组件中注入(直接声明依赖即可):

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

@Component({
  selector: 'app-hero',
  template: `<h1>Hero List</h1>`
})
export class HeroComponent {
  constructor(private logger: LoggerService) {
    this.logger.log('HeroComponent初始化'); // 打印[Logger] HeroComponent初始化
  }
}

关键说明providedIn: 'root'是Angular 6+推荐的写法,它会自动把服务注册到根注入器,整个应用只有一个实例。如果你把providedIn改成UserModule,那这个服务只会在UserModule的组件里共享;如果写在组件的providers数组里(比如@Component({ providers: [LoggerService] })),那每个组件实例都会新创建一个LoggerService——这通常是你不想看到的(除非你特意要隔离)。

实战场景2:用InjectionToken解决非类依赖——配置项的灵活注入
如果依赖不是类(比如API地址、配置对象),该怎么注入?这时候需要用InjectionToken来创建非类令牌

比如你有个DataService需要用到API地址,直接写死在代码里肯定不好维护,应该用DI注入:

步骤1:定义令牌(用InjectionToken):

// app.tokens.ts
import { InjectionToken } from '@angular/core';

// 创建令牌,泛型指定依赖类型(这里是string)
export const API_URL = new InjectionToken<string>('API_URL');

步骤2:配置提供商(告诉注入器“API_URL的值是多少”):
AppModule里配置全局的API地址:

// app.module.ts
import { API_URL } from './app.tokens';

@NgModule({
  providers: [
    { provide: API_URL, useValue: 'https://api.myapp.com' } // 用useValue给固定值
  ]
})
export class AppModule {}

步骤3:注入使用(在DataService里用@Inject(令牌)获取值):

// data.service.ts
import { Injectable, Inject } from '@angular/core';
import { API_URL } from './app.tokens';

@Injectable({ providedIn: 'root' })
export class DataService {
  constructor(@Inject(API_URL) private apiUrl: string) {} // 用@Inject注入非类令牌

  fetchData() {
    return fetch(`${this.apiUrl}/data`); // 使用注入的API地址
  }
}

为什么要用InjectionToken 因为字符串令牌容易冲突(比如你和同事都用'API_URL'当令牌),而InjectionToken是唯一的——就像用“身份证号”代替“名字”找⼈,不会认错。

实战场景3:控制依赖的作用域——根注入器vs模块/组件注入器
有时候你需要限制依赖的范围,比如“用户模块里的服务只能在用户模块用”,或者“某个组件的子组件才能用某个服务”。这时候要选对注入器的层级:

我用UserService举个例子,不同配置的效果:

配置方式 注入器层级 实例范围 适用场景
providedIn: 'root' 根注入器 整个应用共享1个实例 全局服务(比如Logger、Auth)
providedIn: UserModule 模块注入器 UserModule内的组件共享1个实例 模块内通用服务(比如UserListService)
组件providers数组 组件注入器 每个组件实例都有新实例 组件私有服务(比如ModalService)

踩坑提醒:如果同一个令牌在多个注入器里都有提供商,注入器会优先用层级最接近的。比如你在UserComponentproviders里配置了UserService,那么UserComponent及其子组件会用这个组件注入器的实例,而不是根注入器的。

实战场景4:异步依赖的注入——处理需要API请求的配置
有时候依赖需要异步加载(比如从服务器拿配置),这时候要用到APP_INITIALIZER令牌——它能让应用在初始化前完成异步操作,再继续加载。

比如你有个ConfigService需要从/assets/config.json加载配置,然后其他服务要用这个配置:

步骤1:写配置服务(包含异步加载方法):

// config.service.ts
@Injectable()
export class ConfigService {
  config!: { apiUrl: string; timeout: number }; // 加载后的数据

  loadConfig(): Promise<void> {
    return fetch('/assets/config.json')
      .then(res => res.json())
      .then(data => this.config = data);
  }
}

步骤2:配置APP_INITIALIZER(让应用等配置加载完再启动):
AppModule里注册初始化器:

// app.module.ts
import { APP_INITIALIZER } from '@angular/core';
import { ConfigService } from './config.service';

@NgModule({
  providers: [
    ConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: (configSvc: ConfigService) => () => configSvc.loadConfig(), // 工厂函数,返回加载Promise
      deps: [ConfigService], // 工厂函数依赖的服务
      multi: true // 允许多个初始化器(比如同时加载用户信息、配置)
    }
  ]
})
export class AppModule {}

步骤3:使用加载后的配置(其他服务直接注入ConfigService即可):

// data.service.ts
@Injectable({ providedIn: 'root' })
export class DataService {
  constructor(private configSvc: ConfigService) {}

  fetchData() {
    return fetch(`${this.configSvc.config.apiUrl}/data`, {
      timeout: this.configSvc.config.timeout // 用加载后的timeout配置
    });
  }
}

注意APP_INITIALIZER的工厂函数必须返回一个PromiseObservable,否则应用不会等它完成。

实战场景5:测试中的依赖模拟——用useClass/useValue替换真实服务
写测试时,你肯定不想用真实的HeroService(比如它会发真实API请求),这时候DI的提供商替换功能就派上用场了。

比如你要测试HeroComponent,想让它用MockHeroService代替真实服务:

步骤1:写模拟服务(实现和真实服务一样的方法,但返回假数据):

// mock-hero.service.ts
export class MockHeroService {
  getHeroes() {
    return [{ id: 1, name: 'Mock Batman' }, { id: 2, name: 'Mock Superman' }]; // 假数据
  }
}

步骤2:在测试中替换提供商(用useClass指定模拟服务):

// hero.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroComponent } from './hero.component';
import { HeroService } from './hero.service';
import { MockHeroService } from './mock-hero.service';

describe('HeroComponent', () => {
  let component: HeroComponent;
  let fixture: ComponentFixture<HeroComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [HeroComponent],
      providers: [
        // 用MockHeroService替换真实的HeroService
        { provide: HeroService, useClass: MockHeroService }
      ]
    }).compileComponents();
  });

  it('应该显示模拟的英雄列表', () => {
    fixture = TestBed.createComponent(HeroComponent);
    component = fixture.componentInstance;
    fixture.detectChanges(); // 触发变更检测
    const heroNames = fixture.nativeElement.querySelectorAll('.hero-name');
    expect(heroNames.length).toBe(2); // 模拟服务返回2个英雄
    expect(heroNames[0].textContent).toContain('Mock Batman'); // 检查第一个英雄名字
  });
});

如果是简单值,用useValue更方便:比如要替换API_URL令牌的值,直接写{ provide: API_URL, useValue: 'https://mock.api.com' }就行。

那些年踩过的坑:5个常见错误及解决办法
最后跟你唠唠我踩过的坑,帮你少走弯路:

  1. 注入失败提示“No provider for XXX”:检查有没有给XXX配置提供商(比如providedInproviders数组);
  2. 实例重复创建:检查providedIn是不是选对了(比如想全局共享就用root,不要写在组件providers里);
  3. 非类依赖注入报错:是不是忘了用@Inject(令牌)(比如@Inject(API_URL));
  4. 异步配置没加载完就用:是不是没加APP_INITIALIZER,或者工厂函数没返回Promise;
  5. 测试中模拟服务不生效:检查提供商的令牌是不是和真实服务一致(比如provide: HeroService有没有写错)。

看到这,你应该对Angular的DI系统有清晰的认识了吧?其实DI不难,关键是明确“要什么”“怎么来”“范围在哪”这三个问题。下次写组件时,不妨想想:这个依赖该用哪个注入器?用什么令牌?要不要异步加载?

如果还有疑问,欢迎在评论区留言——毕竟DI的水有点深,但踩过几次坑就会了~

原创文章,作者:zhiji,如若转载,请注明出处:https://zube.cn/archives/202

(0)

相关推荐