使用 @Service 装饰器创建可注入服务:Angular 依赖注入入门实战
依赖注入(Dependency Injection,DI)是 Angular 框架中最强大的特性之一,它让框架能在应用运行时自动"提供"你所需要的一切资源(service 或其它对象),组件无需关心对象如何创建与组装。本篇文章对应 Angular 仓库中 Learn Angular 交互式教程的第 19 步 19-creating-an-injectable-service,围绕其中的 CarService 实例展开,带你掌握用 @Service 装饰器把一个普通 TypeScript 类变为可注入服务的方法,并理解其在依赖注入系统底层是如何编译生效的,为下一步将服务真正注入组件铺平道路。
本教程所有源码与练习都位于仓库内 19-creating-an-injectable-service 目录下,其中 src/app/car.service.ts 是练习初始文件,answer/src/app/car.service.ts 是完成后的参考答案,可随时对照查阅。
什么是服务(Service)?为什么要把它做成可注入
服务是用来与数据、API 交互的常用载体。要设计出可复用的服务,核心思路是:把业务逻辑封装在服务内部,当应用任何地方需要它时,再共享给对应组件。这样组件保持轻量、专注视图,数据获取与业务规则只维护一份。
教程中的 car.service.ts 就是一个典型例子,它维护了一份车辆清单并对外提供查询方法:
export class CarService {
cars = ['Sunflower GT', 'Flexus Sport', 'Sprout Mach One'];
getCars(): string[] {
return this.cars;
}
getCar(id: number) {
return this.cars[id];
}
}
这个类此刻还只是普通类——你可以手动 new CarService(),但它尚未接入 Angular 的依赖注入体系,无法被组件通过注入的方式使用。让它"可注入"的关键一步,就是添加 @Service 装饰器。
添加 @Service 装饰器,让类可被 DI 注入
为了让一个类能被 DI 系统注入,请在类上方使用 @Service 装饰器:
import {Service} from '@angular/core';
@Service()
class UserService {
// methods to retrieve and return data
}
@Service 装饰器做了两件事:
- 把类标记为 service;
- 通知 DI 系统:
UserService可以在应用的任何地方被访问。
默认情况下 Angular 会在整个应用范围内提供该服务,因此无需编写任何额外配置即可全局注入使用。
本步练习:为 CarService 添加装饰器
在编辑器中打开 car.service.ts,把 @Service() 装饰器添加到 CarService 类上即可。参考 answer/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];
}
}
现在 CarService 已经是可注入(injectable)状态,可以正式参与到 DI 流程中——教程的下一步将把它注入到组件里使用。
@Service 是 @Injectable 的现代化替代
从 Angular 的发展脉络看,@Service 装饰器是传统 @Injectable({providedIn: 'root'}) 写法的现代、更符合直觉的简化形式(shorthand)。它默认就把类提供在 root injector(根注入器)上,省去了每次手写 providedIn: 'root' 的样板。两者的取舍可参考 creating-and-using-services 指南中的对照表:
| 特性 / 需求 | @Service |
@Injectable |
|---|---|---|
inject() 函数支持 |
是 | 是 |
| 基于构造函数的 DI | 否 | 是 |
| 隐式的根级单例提供 | 是 | 否(需显式 {providedIn: 'root'}) |
高级 provider 键(useClass 等) |
否 | 是 |
| 自定义初始化工厂(factory) | 是 | 是 |
非根作用域(如 platform) |
否 | 是 |
何时选哪个?如果你的新服务是一个使用 inject() 获取依赖的单例类,优先 @Service;如果仍需要以下能力,则继续使用 @Injectable:
- 构造函数式依赖注入——
@Service只配合inject()函数使用; - 高级 provider 配置(
useClass、useValue、useExisting、useFactory),@Service只暴露单一的factory选项; - 非根作用域,例如
providedIn: 'platform'。
autoProvided 与 factory 选项
@Service 默认将类提供在根注入器(root injector)。如果需要手动提供(例如要把服务作用域限定到某个路由或组件上),可以设置 autoProvided: false:
import {Service} from '@angular/core';
@Service({autoProvided: false})
export class AnalyticsLogger {
trackEvent(name: string) {
console.log('event:', name);
}
}
设置之后,你就要像使用普通 @Injectable() 那样,负责把服务手动加入某处的 providers 数组。此外,@Service 还支持通过 factory 选项自定义单例的创建方式(factory 运行在注入上下文中,内部可以使用 inject() 读取其它依赖),可参考指南中的 Analytics 示例。
源码级验证:@Service 在 Angular 内部如何编译
@Service 并不只是一层语法糖,其底层实现可追溯到核心源码 packages/core/src/di/service.ts。
在 packages/core/src/di/service.ts 中,Service 通过 makeDecorator 创建,其元数据类型定义了两个选项:
export interface Service {
/**
* Determines whether the service should be provided automatically or by the user.
* Defaults to `true`.
*/
autoProvided?: boolean;
/**
* A function to invoke to create a value for this service.
*/
factory?: () => unknown;
}
也就是说:
autoProvided默认为true(对应教程中"默认全局提供"的行为);factory用于自定义创建逻辑;- 当传入
{autoProvided: false}时,该服务不再被自动暴露给 DI 系统,交由用户在providers中登记。
同时,makeDecorator 还注册了一个 compileService 编译钩子(见 service.ts)。当类声明真正被编译时,JIT 编译器会执行 packages/core/src/di/jit/service.ts 中的 compileService:
- 如果类上还没有
NG_PROV_DEF定义,就通过编译器编译出 provider 定义ɵprov,并把autoProvided与factory元数据透传给编译器(见getServiceMetadata); - 如果类上还没有
NG_FACTORY_DEF定义,则通过reflectDependencies反射类的构造函数依赖,编译出工厂函数ɵfac。
换言之,@Service() 装饰后,Angular 会为类生成 ɵprov(provider 定义)与 ɵfac(工厂函数),这正是 DI 系统能够在运行时创建并注入实例的基础。这种"编译期打补丁"的实现也解释了为何服务一旦被 @Service 标记,整个应用的注入器都能感知它。
后续衔接:把服务注入组件(inject())
@Service 让服务变得可注入只是依赖注入的第一步。下一步教程 20-inject-based-di 演示了在组件中通过 inject() 函数获取服务实例的完整形态——在教程练习完成后,App 组件会这样消费 CarService:
import {Component, inject} from '@angular/core';
import {CarService} from './car.service';
@Component({
selector: 'app-root',
template: '<p> {{ carService.getCars() }} </p>',
})
export class App {
carService = inject(CarService);
}
从本步到下一步的衔接关系清晰可见:
- 本步:
CarService加@Service()→ 类已注册进 DI 体系; - 下步:组件里
inject(CarService)→ 拿到同一个实例并使用其方法。
之所以两个文件(src/app/car.service.ts 与 src/app/app.ts)会被同时打开编辑,也是为了让学习者直观体会到"先创建服务、后注入使用"的完整闭环(练习配置见 config.json)。
关键要点回顾
- 服务适合封装与数据/API 交互的逻辑,逻辑放在服务里、按需共享给应用各处,即可实现可复用;
- 给类加上
@Service()装饰器,即可让 DI 系统在整个应用范围内提供它,无需额外配置; @Service是@Injectable({providedIn: 'root'})的现代化简写,但只支持inject(),不支持构造函数注入与高级 provider 键;- 需要限定作用域时使用
@Service({autoProvided: false})并自行加入providers;需要自定义创建方式时使用factory选项; - 从源码(service.ts、di/jit/service.ts)可以看到,
@Service会在编译期为类生成ɵprov与ɵfac,这正是"可注入"能力的底层保证; - 想深入了解 DI 概念本身,可继续阅读官方指南 dependency-injection essentials 对应章节,以及 di/creating-and-using-services。
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