Angular 基于函数 `inject()` 的依赖注入(DI)实战:以 Learn Angular 第 20 步为例
本节教程对应仓库教程页面 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 元数据中默认 autoProvided 为 true(类型定义见 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(类、InjectionToken、FactoryProvider 等),找不到即抛错 |
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/@Service的useFactory工厂函数中;InjectionToken的factory函数中;- 运行在注入上下文中的函数栈帧内(例如 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
验证要点有三个:
- 是否真的走 DI:
carService不是手动new CarService()出来的,而是 Angular 注入上下文通过当前注入器解析并返回的实例,这意味着未来你可以放心地替换、扩展服务的实现而不改动组件; - 模板数据流:
display在字段初始化阶段完成计算,模板只做一次插值,体现了「组件负责取数、模板负责展示」的常见分工; - 服务复用:
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 相关章节)会继续在这一套服务与组件协作的基础上展开。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00