毫无疑问,依赖注入(DI)是 Angular 框架中很重要的一部分。简单来说,注入器(Injector)会维护一个关于如何创建依赖的列表,我们可以从注入器获取依赖关系。这些功能始终是借助类的构造函数来实现的。自从 Angular 14 开始,有一个强大的新方案可以替代基于构造函数的 DI,也就是本篇文章的主角 - Inject Function。
什么是 Inject Function
在讲 Inject Function 之前,先来回顾一下传统的基于构造函数的 DI 是怎样使用的。
在传统的用法中,Angular 中的组件、指令或者管道如果需要使用某个服务,必须通过构造函数参数注入才可以使用。
typescript
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor() { }
}
@Component({
...
})
export class AppComponent {
constructor(private dataService: DataService) {}
}
如果这个服务的 Token
不是类而是 InjectionToken
,则需要通过 @Inject()
参数装饰器使用:
typescript
@Component({
...
})
export class AppComponent {
constructor(@Inject(MY_TOKEN) myToken: string) {
console.log(myToken)
}
}
接下来看一下 Inject Function 的写法:
ts
@Component({
...
})
export class AppComponent {
private dataService = inject(DataService);
private myToken = inject(MY_TOKEN);
}
通过代码我们可以看到,如果要在组件中获取依赖,只需要调用 inject
方法,写法大大的简化了,也不需要仅仅为了依赖注入而显式定义构造函数。
看到这里大家可能觉得只是写法上简单了一些,别急,让我们来看下一个例子:
在项目开发中,某个 service 依赖另一个 service 是很常见的情况,Angular 给我们提供了一种写法 - factory provider
。
ts
export const MY_TOKEN = new InjectionToken<string>("my.token");
export function provideMyToken(): FactoryProvider {
return {
provide: MY_TOKEN,
useFactory: ({ token }: UserService) => token,
deps: [UserService],
};
}
当 deps
数组中有多项时,这种写法的缺点就体现出来了,因为 deps
的顺序对应必须对应工厂函数参数的列表,我们需要手动确保顺序。
而使用 inject
函数写法如下:
ts
export function provideMyToken(): FactoryProvider {
return {
provide: MY_TOKEN,
useFactory: () => inject(UserService).token,
};
}
这样看起来是不是简化的更多了呢?而且更不易出错。
Inject vs 构造函数
现在我们已经了解了 inject
函数的基本语法,接下来我们列举几个场景,来感受一下基于 inject
写法的优点。
继承
Angular 组件是基于 class 的,所以继承在项目开发中很常见。假设我们有一个基类 AnimalComponent
:
ts
@Injectable()
export class AnimalComponent implements OnInit {
constructor(public data: DataService, public router: Router) {}
}
我们的基类依赖于 DataService
服务和 Router
。现在有一个子类 CatComponent
需要继承 AnimalComponent
,如果子类也有自己的构造函数,则必须把基类的所有依赖通过 super
调用传递过去:
ts
export class CatComponent extends AnimalComponent {
constructor(data: DataService, router: Router) {
super(data, router);
}
}
当我们大量使用继承或者基类中依赖过多时,会在子类中产生大量无用的模板代码,这是一个很大的缺点。
现在让我们来看一下使用 inject
函数的解决方案:
ts
@Injectable()
export class AnimalComponent {
public readonly router = inject(Router);
public readonly data = inject(DataService);
}
这样子类的构造函数就可以省略掉只为了传递给基类的那些依赖,不需要传递任何参数给 super
调用,也不用关心基类使用了哪些服务。
ts
export class CatComponent extends AnimalComponent {
constructor() {
super();
}
}
PS: 如果子类不需要任何依赖,构造函数可以直接省略。
路由守卫
路由守卫是 Angular 中一个很重要的功能,可以让用户在访问某些路由之前检查是否满足条件。我们以最常用的 canActivate
为例。
在之前,我们首先需要创建一个类,并且实现 CanActivate
接口,然后把这个类放到对应的路由配置中。
ts
// auth guard
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.authService.isAdmin() ? true : this.router.navigate(['/login']);
}
}
// routes
{
path: 'dashboard',
canActivate: [AuthGuard],
loadComponent: () => import('./pages/dashboard.component')
},
这会产生很多的样板文件代码,实际上核心的逻辑就那么一行。甚至哪怕你的守卫没有任何依赖项,也必须编写一个 Injectable 类。作为一名追求效率的开发人员,肯定希望减少这种无用的代码。
那接下来看一下使用函数式守卫 + inject
函数的解决方案:
ts
{
path: 'dashboard',
canActivate: [
() => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.isAdmin() ? true : router.navigate(['/login']);
},
],
loadComponent: () => import('./pages/dashboard.component'),
}
我们只需要写一个函数,并且函数中可以使用 inject
获取依赖,省去了被开发者诟病的样板代码。
DI functions
普通的 JavaScript 函数在使用 inject
函数之后,我们可以称之为 DI functions。这些函数让 Angular 变得更加灵活,既可以用面向对象的模式组织业务代码,又可以通过函数编写可复用的逻辑。
ts
export function logToken() {
const { token } = inject(UserService);
console.log(token);
}
@Component({
...
})
export class AppComponent {
constructor() {
logToken();
}
}
上面代码写了一个简单的可复用的 logToken()
函数,在过去,我们需要把代码放到一个 service 中,之后在组件的构造函数中注入服务,才能实现同样的功能。
当然也可以稍微变一下,根据依赖来为组件或服务提供数据:
ts
export function useToken(): string {
const { token } = inject(UserService);
return token;
}
@Component({
...
})
export class AppComponent implements OnInit {
public readonly token = useToken();
ngOnInit(): void {
console.log(this.token);
}
}
甚至我们可以结合 Observable,来创造一个定制的 Observable:
ts
export function useCompletedTodos$(): Observable<Todo[]> {
return inject(DataService).data$.pipe(map((todos) => todos.filter((todo) => todo.completed)));
}
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule],
template: `
<div>
@for (todo of completedTodos$ | async; track $index) {
<div>{{ todo.title }}</div>
}
</div>
`,
styleUrls: ["./app.component.scss"],
})
export class AppComponent {
public readonly completedTodos$ = useCompletedTodos$();
}
总结
与传统的基于构造函数的 DI 相比,inject
函数是一种更简单的从注入器获取依赖的方法。它不仅简化了常见的场景,而且还可以借助简单的 JavaScript 函数共享可重用逻辑并访问依赖注入系统。
需要注意的是,inject
函数只能用于构造器阶段,这意味着其只能在构造器函数作用域(constructor function scope)和字段初始化器(field initializers)中使用。