先搞懂:依赖注入到底帮我们解决了什么问题
你有没有写过这样的代码?在组件里直接new一个服务实例:
// 不用DI的情况:组件紧耦合服务
export class HeroComponent {
private heroService = new HeroService(); // 直接创建实例
heroes = this.heroService.getHeroes();
}
看起来没问题,但如果HeroService
需要依赖HttpService
呢?你得改成new HeroService(new HttpService())
——如果依赖链更长,组件里的new会像套娃一样越来越复杂。更麻烦的是,当你想在测试里替换HeroService
为模拟服务时,得修改组件代码,完全没灵活性。

而用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) |
踩坑提醒:如果同一个令牌在多个注入器里都有提供商,注入器会优先用层级最接近的。比如你在UserComponent
的providers
里配置了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
的工厂函数必须返回一个Promise
或Observable
,否则应用不会等它完成。
实战场景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个常见错误及解决办法
最后跟你唠唠我踩过的坑,帮你少走弯路:
- 注入失败提示“No provider for XXX”:检查有没有给
XXX
配置提供商(比如providedIn
或providers
数组); - 实例重复创建:检查
providedIn
是不是选对了(比如想全局共享就用root
,不要写在组件providers
里); - 非类依赖注入报错:是不是忘了用
@Inject(令牌)
(比如@Inject(API_URL)
); - 异步配置没加载完就用:是不是没加
APP_INITIALIZER
,或者工厂函数没返回Promise; - 测试中模拟服务不生效:检查提供商的令牌是不是和真实服务一致(比如
provide: HeroService
有没有写错)。
看到这,你应该对Angular的DI系统有清晰的认识了吧?其实DI不难,关键是明确“要什么”“怎么来”“范围在哪”这三个问题。下次写组件时,不妨想想:这个依赖该用哪个注入器?用什么令牌?要不要异步加载?
如果还有疑问,欢迎在评论区留言——毕竟DI的水有点深,但踩过几次坑就会了~
原创文章,作者:zhiji,如若转载,请注明出处:https://zube.cn/archives/202