CHARLIE SAYS

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

Angular 22+ 教程 19:DI 进阶——provide / inject / @Service / injectAsync

上一篇讲清了注入器层级与注入上下文,本文进入 v22 的新能力与高阶用法:@Service() 装饰器、provideApp 聚合、各类 provider 形态、injectAsync 懒加载,以及 providedIn 背后的树摇(tree-shaking)原理。

@Service():新的服务声明方式

v22 中声明服务的推荐方式:

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

@Service()
export class CartService {
  private http = inject(HttpClient);
  items = signal<CartItem[]>([]);

  add(item: CartItem) {
    this.items.update(list => [...list, item]);
  }
}

@Service() 等价于 @Injectable({ providedIn: 'root' }),两条硬规则需要记住:

  • 依赖必须用 inject() 注入@Service() 不支持构造器参数注入——它与 inject 体系对齐,避免对构造器参数元数据的依赖。类字段就是依赖清单,一眼可读。
  • 默认自动提供到 root。类一旦被引用即可注入,无需在任何 providers 数组登记。

autoProvided: false

不想成为全局单例时,关闭自动提供:

@Service({ autoProvided: false })
export class ThemeService {
  mode = signal<'light' | 'dark'>('light');
}

// 显式注册:可以注册在应用、路由或组件层
export const appConfig: ApplicationConfig = {
  providers: [ThemeService],
};

适用场景:需要多实例(每路由/每组件一份)、需要按环境替换实现、或该服务只应被特定区域使用。相比 @Injectable(),语义更明确——“这是个服务,但提供位置由我手动控制”。

provideApp 与 appConfig

v21 之前应用级配置的标准写法:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient()],
};

v21 起提供 provideApp() 作为聚合简写,把各项 provider 直接收拢为应用配置:

bootstrapApplication(
  AppComponent,
  provideApp(provideRouter(routes), provideHttpClient()),
);

细节以官方文档为准;拿不准时保留 ApplicationConfig + providers 数组的写法完全等价、没有任何损失。核心是理解:provideXxx() 系列函数返回的都是 provider 集合,最终都汇入 root 环境注入器。

Provider 形态大全

providers 数组的每一项都是”provide + 如何构造”的声明:

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

export const appConfig: ApplicationConfig = {
  providers: [
    // ClassProvider:token 绑定到另一个类
    { provide: ErrorHandler, useClass: AppErrorHandler },

    // ValueProvider:直接给值(配置、常量)
    { provide: API_BASE_URL, useValue: 'https://api.example.com' },

    // FactoryProvider:工厂函数运行在注入上下文,可直接 inject()
    {
      provide: UrlBuilder,
      useFactory: () => new UrlBuilder(inject(API_BASE_URL), inject(PLATFORM_ID)),
    },

    // ExistingProvider:别名,复用已有 token 的实例
    { provide: Logger, useExisting: ConsoleLogger },

    // multi provider:同一 token 收集多个值,注入得到数组
    { provide: APP_FEATURES, useValue: searchFeature, multi: true },
    { provide: APP_FEATURES, useValue: exportFeature, multi: true },
  ],
};
export class FeatureHostComponent {
  // multi:注入得到的是数组,而不是最后一个
  features = inject(APP_FEATURES); // [searchFeature, exportFeature]
}

几个易混点:

  • useClass新建实例(token 换一个类);useExisting 复用目标 token 已解析的实例(常用于接口别名)。想让两个 token 拿到同一个对象,用 useExisting
  • deps 数组是历史写法;v22 的 factory 在注入上下文中运行,直接 inject() 即可,类型推断也更好。
  • multi 的合并规则:同为 multi: true 的 provider 追加;一个非 multi 的同 token provider 会整体替换。

injectAsync:懒加载依赖

inject() 是同步解析,意味着依赖链上的代码全部进入主 bundle。injectAsync 打破这一点——在注入上下文中声明”这个依赖将来才下载”:

import { Service, inject, injectAsync, onIdle, signal } from '@angular/core';

@Service()
export class ReportService {
  rows = signal<ReportRow[]>([]);

  private loadExporter = injectAsync(
    () => import('./pdf/pdf-exporter').then(m => m.PdfExporter),
    { prefetch: onIdle },
  );

  async export() {
    const Exporter = await this.loadExporter;
    const exporter = new Exporter();
    return exporter.render(this.rows());
  }
}

要点:

  • 第一参数是返回 Promise 的加载函数(通常是动态 import()),返回值本身是 Promise,await 时才拿到模块。
  • prefetch 选项控制预热策略,onIdle 表示浏览器空闲时即开始下载——用户点导出时模块早已就位,体验上仍是同步的。
  • 必须在注入上下文调用(字段初始化器或构造器),与 inject() 一致。
  • @defer 的分工:@defer 面向模板/组件懒加载,injectAsync 面向代码依赖(服务、工具库、WASM 模块)。两者都依赖动态 import() 的自动分包能力。

典型收益场景:PDF 导出器、图表库、公式引擎、富文本编辑器——体积大且仅少数用户触达的能力。

树摇与 providedIn

为什么 providedIn: 'root' 的服务不会拖累包体?

  • provider 信息内联在类定义上(装饰器元数据),编译器能确定”只有当某处注入/引用这个类时,才需要在产物中保留它”——这就是 tree-shakable provider。
  • NgModule providers 数组则是”打包即保留”:数组被引用,里面所有类全部进 bundle,无论是否真的用到。这是 standalone 时代推荐 providedIn / @Service() 的核心原因之一。

providedIn 的取值一览:

取值行为使用频率
'root'@Service() 默认)root 环境注入器单例绝大多数
'platform'平台级单例(多应用共享,如微前端场景)
'any'每个注入它的小环境各一份实例极少,易误用
SomeNgModule注册到指定模块(历史迁移用)存量代码

测试中覆盖 provider 与生产同构:

TestBed.configureTestingModule({
  providers: [{ provide: CartService, useValue: cartStub }],
});

const cart = TestBed.inject(CartService);

常见陷阱

  • 循环依赖:A 构造期注入 B、B 又注入 A,运行时拿到 undefined 或初始化错乱。解法:依赖倒置(抽公共 token)、把交叉调用改为方法级调用、或重拆服务边界。
  • @Service() 类里写构造器参数:不被支持,迁移老服务时统一改成 inject() 字段。
  • 多实例预期错位autoProvided: false 的服务忘了注册,注入时抛 NullInjectorError;报错信息里的 token 名就是排查线索。
  • factory 里做重活:工厂在首次注入时同步执行,网络/大计算应交给 resource() / injectAsync 这类异步原语。
  • multi 误配:某个 provider 忘写 multi: true 会整体覆盖先前的 multi 数组,且不报错。

系列导航

← 算法 019:树:红黑树(R-B Tree) 目录 设计模式 019:迭代器(Iterator) →
← 返回文章列表