上一篇讲清了注入器层级与注入上下文,本文进入 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 数组,且不报错。