首页
/ Angular 基于函数 `inject()` 的依赖注入(DI)实战:以 Learn Angular 第 20 步为例

Angular 基于函数 `inject()` 的依赖注入(DI)实战:以 Learn Angular 第 20 步为例

2026-09-07 16:43:30作者:宣聪麟

本节教程对应仓库教程页面 20-inject-based-di 的 README,是 Learn Angular 系列「创建可注入服务 → 把服务注入组件」两步走中的第二步。上一节(19-creating-an-injectable-service)教会你如何用 @Service 声明一个可被 DI 系统托管的服务,本节则回答下一个核心问题:服务建好之后,组件里怎么把它拿过来用? 读完本文,你将掌握 inject() 函数的正确用法、它在 Angular 源码中的实现位置与执行规则(注入上下文),并能独立完成「注入服务 → 用服务初始化组件状态 → 渲染到模板」的完整链路。

本节要解决的核心问题:如何把服务注入组件

创建可注入服务只是 DI 体系的第一半。在 Angular 中,要让组件真正使用某个服务,你需要把服务「注入」进来——也就是让 Angular 的依赖注入系统在组件实例化时,把配置好的实例交给你。Angular 为此提供了非常便捷的函数 inject(),它可以在合适的上下文(injection context)中被调用。

本节以课程示例中的 CarService 为对象,演示两种最基本的注入路径:先用 inject() 把服务装进组件属性,再通过服务方法加工数据、渲染到模板。

一、回顾第 19 步:什么才是「可注入」的服务

在动手注入之前,先确认服务的「可注入」前提。教程第 19 步(README)说明:一个普通类要参与 DI,需要用装饰器标记为服务。在仓库本节的起始代码 car.service.ts 中即是如此:

// adev/src/content/tutorials/learn-angular/steps/20-inject-based-di/src/app/car.service.ts
import {Service} from '@angular/core';

@Service()
export class CarService {
  cars = ['Sunflower GT', 'Flexus Sport', 'Sprout Mach One'];

  getCars(): string[] {
    return this.cars;
  }

  getCar(id: number) {
    return this.cars[id];
  }
}

@Service 装饰器会通知 DI 系统:CarService 默认在整个应用范围内可被访问,无需额外配置。这一点可以从仓库的源码实现得到印证——Service 装饰器makeDecorator 生成,其 ServiceDecorator 元数据中默认 autoProvidedtrue类型定义见 service.ts);只有当你想手动控制作用域(例如限定到某个路由或组件)时,才需要设置 autoProvided: false,并自行把服务写进对应的 providers 列表。更完整的说明可参考 guide/di 的创建与使用服务文档

也就是说:@Service() 让类「可注入」,而 inject() 让类在组件里「被注入」,二者缺一不可。

二、inject() 的基本用法:把 DI 的值初始化给类属性

把由 DI 系统提供的值直接初始化给类的属性,是一种非常常见的写法。教程给出的最小示例:

@Component({...})
class PetCareDashboard {
  petRosterService = inject(PetRosterService);
}

这里有三处关键信息:

  • inject 是从 @angular/core 导入的顶层函数,不需要放在构造函数参数里;
  • inject(PetRosterService) 的返回值类型会被推导为 PetRosterService 实例;
  • 字段初始化(field initializer)在 Angular 的注入上下文内执行,因此可以在这里直接调用 inject()

这种写法和「构造函数注入」相比,代码更少、可读性更强,还允许直接把属性声明为 readonly,是 Angular 官方推荐、并在大量新代码中采用的方式。

三、从源码看 inject() 的实现与重载形态

inject() 并不是魔法,它的真实实现位于 Angular Core 的 DI 兼容层。在仓库中打开 packages/core/src/di/injector_compatibility.ts 可以看到核心逻辑:

export function inject<T>(token: ProviderToken<T> | HostAttributeToken, options?: InjectOptions) {
  return ɵɵinject(token as any, convertToBitFlags(options));
}

也就是说,公开 API 的 inject() 会把调用委托给内部的 ɵɵinject,同时通过 convertToBitFlags(同文件 injector_compatibility.ts)把对象形式的注入选项翻译成位标记。它的重载签名覆盖了多种注入目标(见同文件第 195-225 行附近):

调用形态 用途
inject(token) 注入一个普通 Provider token(类、InjectionTokenFactoryProvider 等),找不到即抛错
inject(token, {optional: true}) 可选注入,找不到时返回 null,不抛错
inject(HostAttributeToken) 注入宿主节点上的静态 HTML 属性
inject(HostAttributeToken, {optional: true}) 属性不存在时返回 null
inject(token, {host/self/skipSelf/...}) 通过选项控制查找范围,等价于装饰器 @Host@Self@SkipSelf@Optional

options.optional 对应 InternalInjectFlags.Optional,以此类推。理解这一点,你在排查「为什么拿不到实例」「为什么返回 null」这类问题时就有了依据。

四、注入上下文:哪些地方能调用 inject(),哪些地方不能

inject() 只能在「注入上下文」(injection context)中调用。这个限制在教程 README 中被标注为「超出本节范围,可参考 DI 指南」,但从实战角度看它是初学者最容易踩的坑,值得提前说清。仓库中的官方说明位于 guide/di/dependency-injection-context.md,要点如下:

允许调用 inject() 的位置:

  • 由 DI 系统实例化的类(如 @Service@Component)的构造函数体中
  • 上述类的字段初始化器里(即 prop = inject(...) 的写法);
  • Provider/@ServiceuseFactory 工厂函数中;
  • InjectionTokenfactory 函数中;
  • 运行在注入上下文中的函数栈帧内(例如 Router 的 CanActivateFn 守卫函数里直接 inject(PermissionsService))。

禁止调用 inject() 的位置: 类实例创建完成之后的方法调用,尤其是生命周期钩子。例如:

@Component({ ... })
export class CarComponent {
  ngOnInit() {
    // ERROR: 实例已创建,太晚了
    const engine = inject(Engine);
  }
}

一旦在非法位置调用,Angular 会抛出运行时错误 NG0203: inject() must be called from an injection context,该错误的完整调试说明见 reference/errors/NG0203.md。修复方法很简单:把 inject() 挪到构造函数或字段初始化器这类合法位置;如果确实需要在注入上下文之外跑一段代码,可以用 runInInjectionContext(需持有 EnvironmentInjector)包裹,测试场景下也可以用 TestBed.runInInjectionContext(() => {...})。此外,Angular 还提供 assertInInjectionContext(fn) 帮助你在自定义辅助函数里提前断言上下文,抛出比默认错误更清晰的提示。

五、动手实操:把 CarService 注入到 App 组件

下面按教程的三个步骤实际操作。本节初始代码位于 src/app/app.ts,初始状态如下:

import {Component, inject} from '@angular/core';
import {CarService} from './car.service';

@Component({
  selector: 'app-root',
  template: ``,
})
export class App {
  display = '';
}

可以看到 CarService 已经导入,display 还是空字符串,模板为空——接下来的三步就是要让它们「动起来」。

Step 1:用 inject()CarService 注入到 carService 属性

App 类中补一行字段初始化:

carService = inject(CarService);

这里需要特别留意教程的提醒:属性 carService 与类 CarService 的区别。JavaScript 对大小写敏感,属性名是你自己起的变量名(习惯上用小写驼峰),而 inject() 的参数必须是真实的类引用(大写开头)。写错大小写要么拿到 undefined,要么直接报找不到 provider 的错误。

Step 2:用注入到的实例初始化 display

inject(CarService) 返回的是一份可用的 CarService 实例,你可以直接调用它公开的方法。用如下实现初始化 display

display = this.carService.getCars().join(' ⭐️ ');

getCars() 会返回 ['Sunflower GT', 'Flexus Sport', 'Sprout Mach One'] 数组,join(' ⭐️ ') 把它们拼成一个带分隔符的字符串。注意:TypeScript 类字段按声明顺序依次初始化,所以 carService 必须声明在 display 之前,display 才能安全地通过 this.carService 访问到已注入的实例——参考答案 answer/src/app/app.ts 正是这样排列的。

Step 3:更新模板渲染数据

把组件的 template 替换为:

template: ` <p>Car Listing: {{ display }}</p> `,

模板插值 {{ display }} 会把第二步计算好的字符串渲染到页面上。三步完成后,完整代码应当是:

import {Component, inject} from '@angular/core';
import {CarService} from './car.service';

@Component({
  selector: 'app-root',
  template: ` <p>Car Listing: {{ display }}</p> `,
})
export class App {
  carService = inject(CarService);

  display = this.carService.getCars().join(' ⭐️ ');
}

六、运行结果与验证思路

应用加载后,页面应显示:

Car Listing: Sunflower GT ⭐️ Flexus Sport ⭐️ Sprout Mach One

验证要点有三个:

  1. 是否真的走 DIcarService 不是手动 new CarService() 出来的,而是 Angular 注入上下文通过当前注入器解析并返回的实例,这意味着未来你可以放心地替换、扩展服务的实现而不改动组件;
  2. 模板数据流display 在字段初始化阶段完成计算,模板只做一次插值,体现了「组件负责取数、模板负责展示」的常见分工;
  3. 服务复用CarService 定义在独立的 car.service.ts 中,任何需要它的组件都可以用同一套 inject(CarService) 语法接入,这正是把逻辑收敛到服务里的价值。

七、常见误区与排查清单

  • 在方法或生命周期钩子中调用 inject():报错 NG0203,把调用移到构造函数或字段初始化器,或用 runInInjectionContext/TestBed.runInInjectionContext 包裹;
  • 属性名与类名混淆inject(CarService) 必须传类,接收侧属性名 carService 只是局部命名;
  • 忘记导入符号inject 需要从 @angular/core 导入,服务类需要从它所在模块文件导入;
  • 字段声明顺序颠倒:被依赖的字段(carService)要写在依赖它的字段(display)之前;
  • 服务没有真正可注入:确认服务类带有 @Service()(且默认 autoProvided,或已按 autoProvided: false 手动登记),详见 DI 总览文档 guide/di/overview.md

小结

这一节让你完成了 DI 的闭环:@Service() 声明可注入能力,inject() 在注入上下文中取出实例,字段初始化与模板插值把数据交给 UI。底层来看,inject() 只是把请求委托给 ɵɵinject 并透传解析选项(injector_compatibility.ts),真正决定能否调用它的是「注入上下文」这一运行时约束。把上下文规则记牢,无论是组件字段注入、工厂函数注入还是路由守卫内的函数式注入,都能举一反三。接下来的教程(例如 Pipes 相关章节)会继续在这一套服务与组件协作的基础上展开。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388