Angular Inject Function:你还在使用构造函数注入依赖吗

毫无疑问,依赖注入(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)中使用。

参考资料

相关推荐
Fan_web10 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常12 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记4 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java4 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele4 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范