CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-183 · Angular 高级教程

Angular 22+ 教程 18:依赖注入——注入器层级与注入上下文

依赖注入(DI)是 Angular 的骨架:组件、指令、管道、路由、HttpClient 全部构建在注入器之上。本文讲清楚两件事:注入器是一棵什么样的树,以及 inject() 为什么必须在”注入上下文”里调用。下一篇讲 @Service()injectAsync() 等进阶话题。

DI 解决什么问题

不依赖注入时,一个订单服务需要 HTTP 客户端就得自己 new,测试时无法替换、单例无法共享、层级作用域无从谈起。DI 把”我需要什么”(声明)与”谁来提供”(注册)分离:类只声明依赖,注入器负责构造与装配,测试时整棵依赖树可以按 token 替换。

注入器是一棵树

Angular 的注入器不是单例容器,而是两族交织的树:

graph TD
    P["Platform Injector(平台级,浏览器实例一个)"] --> R["Root EnvironmentInjector<br/>providedIn: 'root' / app.config providers"]
    R --> RA["Route EnvironmentInjector<br/>(懒加载路由 providers,按路由隔离)"]
    R --> RB["Root Component NodeInjector"]
    RB --> C1["PageComponent NodeInjector<br/>providers: [SelectionService]"]
    C1 --> C2["ChildComponent NodeInjector"]
    RA --> RP["LazyPage NodeInjector"]
  • EnvironmentInjector(环境注入器)platformroot → 各路由(懒加载路由拥有独立环境注入器)。provideRouter()provideHttpClient()providedIn: 'root' 的服务都注册在环境注入器层。
  • NodeInjector(节点注入器):每个组件实例对应一个节点注入器,承载组件 providers 里声明的依赖。

查找规则:从当前组件的 NodeInjector 沿组件树向上,途中每层先查本层再向上;到顶后转入 EnvironmentInjector 链继续向上,直到 platform;仍找不到则抛 NullInjectorError(NG0201)。

历史上还有 NgModule 注入器(module injector),随 standalone 全面普及已被环境注入器体系取代,读旧资料时把它近似理解为 root 环境注入器即可。

注册服务的几种方式

// 1. root 单例(v22 推荐 @Service(),等价 @Injectable({ providedIn: 'root' }))
@Service()
export class CartService {
  items = signal<CartItem[]>([]);
}
// 2. 组件级:每个组件实例一份
@Component({
  selector: 'app-data-table',
  providers: [SelectionService],
})
export class DataTableComponent {}
// 3. 路由级:路由片段内单例(懒加载路由天然隔离)
export const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component'),
    providers: [AdminGuardService],
  },
];
// 4. 应用级:app.config.ts 的 providers 数组
export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient()],
};
注册位置作用域生命周期典型用途
@Service() / providedIn: 'root'全应用单例应用级无状态服务、共享状态
组件 providers该组件子树随组件销毁每实例状态(选中集、草稿)
路由 providers路由子树内路由配置级路由私有服务
app.config providers全应用应用级框架能力配置

组件上还有 viewProviders:与 providers 的区别是它对投影内容不可见——投影进来的子组件注入时跳过这一层。默认用 providers,想对投影内容隐藏实现时用 viewProviders

inject() vs 构造器注入

同一个依赖的两种写法:

// 构造器注入(历史写法,完全可用)
export class OrderComponent {
  constructor(
    private cart: CartService,
    @Inject(API_BASE_URL) private baseUrl: string,
    @Optional() private logger?: Logger,
  ) {}
}
// inject() 写法(v22 推荐)
export class OrderComponent {
  private cart = inject(CartService);
  private baseUrl = inject(API_BASE_URL);
  private logger = inject(Logger, { optional: true });
}
维度inject()构造器注入
书写位置字段初始化器、构造器、provider 工厂仅构造器参数
修饰符选项对象 { optional, self, skipSelf, host }@Optional / @Self / @SkipSelf / @Host 装饰器
InjectionToken泛型直接推断类型需要 @Inject 装饰器
TS 依赖不依赖 emitDecoratorMetadata依赖装饰器元数据
可读性类字段即依赖清单参数集中但装饰器易堆积
限制必须在注入上下文调用无注入上下文限制

v22 的新 API(@Service() 等)在设计上只与 inject() 配合,新代码统一用 inject() 即可。

注入上下文(Injection Context)

inject() 能工作的位置称为注入上下文:

  • 组件/指令/管道/服务的构造函数
  • 上述类的字段初始化器
  • InjectionTokenfactory
  • provider 的 useFactory 工厂函数;
  • runInInjectionContext(injector, fn) 的回调。

以下位置调用会抛 NG0203(inject() must be called from an injection context):

export class SaveComponent {
  save() {
    setTimeout(() => {
      const toast = inject(ToastService); // NG0203:回调不在注入上下文
      toast.show('已保存');
    }, 0);
  }
}

修复方式两种:把依赖提升为字段;或在字段保存 Injector 后显式切换上下文:

export class SaveComponent {
  private injector = inject(Injector);

  save() {
    setTimeout(() => {
      const toast = runInInjectionContext(this.injector, () => inject(ToastService));
      toast.show('已保存');
    }, 0);
  }
}

顺带一提:effect() 的回调运行在注入上下文中,可以直接 inject()——这是信号体系带来的便利。

InjectionToken:注入非类值

服务是类,可以直接当 token;配置常量、接口实现需要 InjectionToken

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

export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL', {
  providedIn: 'root',
  factory: () => 'https://api.example.com',
});
  • 泛型参数让 inject(API_BASE_URL) 直接得到 string 类型。
  • factory 提供默认值,注入方可在任意层覆盖:
providers: [{ provide: API_BASE_URL, useValue: 'https://api.staging.example.com' }]
  • token 让实现与消费解耦:注入方依赖抽象 token,不必 import 具体实现类,这也让测试替身与多实现替换成为可能。

修饰符一览

修饰符语义inject() 等价
@Optional()找不到不报错,注入 nullinject(X, { optional: true })
@Self()只查当前注入器,不向上inject(X, { self: true })
@SkipSelf()跳过当前注入器,从父级开始查inject(X, { skipSelf: true })
@Host()向上查找不超过宿主组件边界(内容投影场景)inject(X, { host: true })

skipSelf 最常见的用途是”子组件覆盖父级服务并叠加原实现”:

@Injectable()
export class AuditLogger extends BaseLogger {
  private parent = inject(BaseLogger, { skipSelf: true });

  log(msg: string) {
    this.parent.log(msg);
    this.auditTrail.push(msg);
  }
}

注意这里的继承只复用类型,实例链路靠 skipSelf 拿到父级注入的同 token 实例——比 super() 更符合 DI 的组合思路。

实战:组件级服务的价值

@Injectable()
export class SelectionService {
  selected = signal<Set<number>>(new Set());

  toggle(id: number) {
    this.selected.update(set => {
      const next = new Set(set);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  }
}

@Component({
  selector: 'app-data-table',
  providers: [SelectionService], // 每个表格实例私有
  template: `...`,
})
export class DataTableComponent {
  selection = inject(SelectionService); // 本组件子树内同一实例
}

@Component({ selector: 'app-table-toolbar' })
export class TableToolbarComponent {
  // 子组件注入的是宿主表格的那份实例,而非全局单例
  selection = inject(SelectionService);
}

两个表格实例互不干扰——这就是层级注入器提供的”作用域”能力,是全局单例方案(手动传 props 或 context)给不了的。

系列导航

← 算法 018:树:平衡二叉树(AVL) 目录 设计模式 018:解释器(Interpreter) →
← 返回文章列表