Angular 注入上下文(Injection Context):inject() 的可用时机、runInInjectionContext 与 assertInInjectionContext 详解
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
在 Angular 的依赖注入(DI)系统中,inject()函数并不是在任何地方都能调用的——它依赖一个“当前注入器可用”的运行时上下文,即注入上下文(Injection Context)。本文基于 Angular 官方文档 Injection context 展开,系统讲解:哪些代码位置天然处于注入上下文、如何通过runInInjectionContext主动创建上下文、如何用assertInInjectionContext编写可复用的注入辅助函数,并结合@angular/core的源码剖析上下文切换的底层机制与 NG0203 错误的触发路径,帮助你在自定义 API、路由守卫和工具函数中安全地使用函数式注入。
注入上下文是什么:inject()生效的五个时机
Angular 的 DI 系统依赖一个运行时上下文,在其中当前的注入器(injector)是可访问的。这意味着:注入器只在你于该上下文内执行代码时才正常工作。文档明确列出了五个拥有注入上下文的情形:
- 在 DI 系统实例化的类构造期间(通过
constructor),例如@Injectable或@Component的类; - 在这类类的字段初始化器(field initializers)中;
- 在
Provider或@Injectable的useFactory指定的工厂函数中; - 在
InjectionToken的factory函数中; - 在处于注入上下文中的栈帧内执行时(即调用链上游某处已经进入上下文,当前函数帧仍可注入)。
知道当前是否处于注入上下文,决定了你能否使用inject函数来获取依赖。对于在构造函数与字段初始化器中使用inject()的基础示例,可参见 DI 概览指南中“where can inject be used”一节(位于 adev/src/content/guide/di/ 目录下)。
源码视角:inject()如何定位“当前上下文”
从源码结构看,注入上下文的本质是一个“当前注入器”的全局槽位。packages/core/src/di/injector_compatibility.ts 中的公开inject()实现非常简洁:
export function inject<T>(token: ProviderToken<T> | HostAttributeToken, options?: InjectOptions) { return ɵɵinject(token as any, convertToBitFlags(options)); }它把调用转给编译器指令ɵɵinject(同文件 L136-L148),后者优先使用通过 packages/core/src/di/inject_switch.ts 设置的注入实现(渲染引擎内部会切换为带NodeInjector感知的版本),否则回退到injectInjectorOnly。关键的判定逻辑在 injectInjectorOnly 中:
- 若
getCurrentInjector()返回undefined(完全无上下文),抛出 NG0203 错误; - 若返回
null,进入“limp mode”(跛行模式),只能解析providedIn: 'root'的可注入令牌,见 injectRootLimpMode; - 否则通过
currentInjector.retrieve(token, options)完成解析,optional标志未命中时返回null。
这就解释了文档中“注入器只在你执行代码于该上下文内时才工作”这一论断的底层原理:上下文 = 一个被正确设置的getCurrentInjector()返回值。
处于上下文中的栈帧:路由守卫中的典型用法
某些 API 被设计为“在注入上下文中运行”,路由守卫就是典型例子——这使得你可以在守卫函数内部直接使用inject()访问服务,而不必通过守卫函数的参数传递。文档以CanActivateFn为例:
const canActivateTeam: CanActivateFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, ) => { return inject(PermissionsService).canActivate(inject(UserToken), route.params.id); };这里守卫回调的整个栈帧都被 Angular Router 包在了注入上下文中执行,因此inject(PermissionsService)与inject(UserToken)都能正常解析。同一原理同样适用于CanMatchFn、ResolveFn等由框架保证在注入上下文中调用的函数式 API:只要框架执行你的回调前设置了当前注入器,回调内的任意调用链(同一栈帧内同步执行的代码)都可以调用inject()。
主动进入上下文:runInInjectionContext
当你在方法、事件处理器或异步回调里需要注入,但当前并不处于注入上下文时,可以使用runInInjectionContext。它需要拿到一个注入器(如EnvironmentInjector)。文档给出的示例:
// hero.service.ts @Injectable({ providedIn: 'root', }) export class HeroService { private environmentInjector = inject(EnvironmentInjector); someMethod() { runInInjectionContext(this.environmentInjector, () => { inject(SomeService); // Do what you need with the injected service }); } }注意文档强调的返回语义:inject()只有在注入器能够解析请求的令牌时才返回实例;解析失败且非optional时抛出错误。
底层实现:上下文是怎么“切换”又“还原”的
runInInjectionContext的公开 API 定义在 packages/core/src/di/contextual.ts,并通过 packages/core/src/di/index.ts 对外导出。其实现是一个标准的“保存旧值 → 设置新值 → try/finally 恢复”结构:
export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => ReturnT): ReturnT { let internalInjector: PrimitivesInjector; if (injector instanceof R3Injector) { assertNotDestroyed(injector); // 注入器已销毁则报错 internalInjector = injector; } else { internalInjector = new RetrievingInjector(injector); // 包装非 R3Injector 的 Injector } let prevInjectorProfilerContext: InjectorProfilerContext; if (ngDevMode) { prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null}); } const prevInjector = setCurrentInjector(internalInjector); // ① 设置当前注入器 const previousInjectImplementation = setInjectImplementation(undefined); try { return fn(); // ② 在上下文中执行闭包 } finally { setCurrentInjector(prevInjector); // ③ 恢复上一个注入器 ngDevMode && setInjectorProfilerContext(prevInjectorProfilerContext!); setInjectImplementation(previousInjectImplementation); } }有三个从源码可以确认的工程细节值得注意:
- 上下文是同步的:API 文档注释明确说明
inject只能同步使用,不能在异步回调或任何await点之后使用——因为上下文的“进入/退出”依赖调用栈的进入/退出,而不是 Promise 链。 - 状态总是被还原:无论
fn()成功还是抛错,finally都会恢复前一个注入器与注入实现,保证外层上下文不受影响。 - 支持任意
Injector:如果传入的不是R3Injector实例,会被包进 RetrievingInjector 适配器,转调旧版Injector.get()接口,因此任何实现了Injector接口的对象都能作为上下文来源。
另外,isInInjectionContext 提供了无副作用的探测函数,判断依据是“当前存在注入实现或当前注入器非空”,可用于在自定义代码中先探测、再分支处理。
断言上下文:assertInInjectionContext与可复用的注入辅助函数
Angular 提供assertInInjectionContext辅助函数,用于校验当前上下文是否为注入上下文,并在不是时抛出清晰错误。使用时应传入调用函数的引用,让错误信息指向正确的 API 入口,从而得到比默认通用注入错误更清晰、更可操作的消息。文档示例:
import {ElementRef, assertInInjectionContext, inject} from '@angular/core'; export function injectNativeElement<T extends Element>(): T { assertInInjectionContext(injectNativeElement); return inject(ElementRef).nativeElement; }随后,这个辅助函数必须从注入上下文中调用(构造函数、字段初始化器、Provider 工厂,或经runInInjectionContext执行的代码):
import {Component, inject} from '@angular/core'; import {injectNativeElement} from './dom-helpers'; @Component({ /* … */ }) export class PreviewCard { readonly hostEl = injectNativeElement<HTMLElement>(); // 字段初始化器处于注入上下文中,可用 onAction() { const anotherRef = injectNativeElement<HTMLElement>(); // 会失败:运行在注入上下文之外 } }字段初始化器在实例化期间执行,因此合法;而onAction()作为事件处理普通方法运行时,DI 上下文早已结束,调用会抛错。
源码视角:为什么传函数引用而不是字符串
查看 assertInInjectionContext 的实现:
export function assertInInjectionContext(debugFn: Function): void { // Taking a `Function` instead of a string name here prevents the unminified name of the function // from being retained in the bundle regardless of minification. if (!isInInjectionContext()) { throw new RuntimeError( RuntimeErrorCode.MISSING_INJECTION_CONTEXT, ngDevMode && debugFn.name + '() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`', ); } }源码注释解释了设计取舍:接收Function而非字符串名称,是为了避免未压缩的函数名在压缩(minification)后仍被保留在 bundle 中——函数名随压缩器统一重命名/剔除,包体积更友好。同时错误详情仅在ngDevMode下附加,生产环境的RuntimeError只携带错误码MISSING_INJECTION_CONTEXT,这正是文档中 error NG0203 的来源。
在上下文之外使用 DI:NG0203 错误的完整链路
当你在注入上下文之外调用inject()或assertInInjectionContext时,Angular 抛出错误NG0203。从源码可以完整还原这条错误链路:
- 错误码定义在 packages/core/src/errors.ts:
MISSING_INJECTION_CONTEXT = -203(负号是 Angular 运行时错误码的编码约定,展示时转为 NG0203); - 触发点有两个:
assertInInjectionContext的主动断言(contextual.ts L80-L87),以及inject()本身在getCurrentInjector() === undefined时的兜底抛出(injector_compatibility.ts L98-L103),后者附带 devMode 提示:inject()必须从构造函数、工厂函数、字段初始化器或runInInjectionContext包裹的函数中调用; - 官方验收测试在 packages/core/test/acceptance/di_spec.ts 中断言该错误码:
e instanceof RuntimeError && e.code === RuntimeErrorCode.MISSING_INJECTION_CONTEXT,确认了这一行为是契约级的。
由此可以总结出几个高频踩坑点:
- 生命周期钩子里调用
inject()会报错:ngOnInit等钩子在实例构造完成后才执行,此时注入上下文已结束(inject的 API 注释中专门给出了CarComponent.ngOnInit中inject(Engine)的反例); - 事件处理器、
setTimeout回调、订阅回调中调用inject()会报错:这些回调运行在 DI 系统之外的普通调用栈上; - 跨
await使用inject()不可靠:await之后代码已离开原来的同步栈帧,即便原栈帧在上下文中; - 修复模式:在构造函数/字段初始化器中先
inject(EnvironmentInjector)持有引用,需要时再用runInInjectionContext包裹调用;或者把依赖作为构造参数/方法参数显式传递。
小结
| API | 作用 | 源码位置 |
|---|---|---|
inject(token, options?) | 从当前注入上下文解析依赖 | injector_compatibility.ts |
runInInjectionContext(injector, fn) | 以给定注入器为上下文同步执行fn | contextual.ts |
isInInjectionContext() | 探测当前是否处于注入上下文 | contextual.ts |
assertInInjectionContext(fn) | 断言上下文,失败抛 NG0203 | contextual.ts |
掌握注入上下文的边界是正确使用 Angular 函数式注入的前提:在构造函数、字段初始化器、useFactory/factory以及框架保证在上下文内运行的回调(如路由守卫)中直接使用inject();在其余场景用runInInjectionContext显式建立上下文;为封装注入逻辑的公共函数加上assertInInjectionContext,把含糊的运行时错误转化为指向 API 入口的明确报错。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考