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)中使用。

参考资料

相关推荐
剑亦未配妥7 分钟前
移动端触摸事件与鼠标事件的触发机制详解
前端·javascript
人工智能训练师6 小时前
Ubuntu22.04如何安装新版本的Node.js和npm
linux·运维·前端·人工智能·ubuntu·npm·node.js
Seveny076 小时前
pnpm相对于npm,yarn的优势
前端·npm·node.js
yddddddy7 小时前
css的基本知识
前端·css
昔人'7 小时前
css `lh`单位
前端·css
Nan_Shu_6149 小时前
Web前端面试题(2)
前端
知识分享小能手9 小时前
React学习教程,从入门到精通,React 组件核心语法知识点详解(类组件体系)(19)
前端·javascript·vue.js·学习·react.js·react·anti-design-vue
蚂蚁RichLab前端团队10 小时前
🚀🚀🚀 RichLab - 花呗前端团队招贤纳士 - 【转岗/内推/社招】
前端·javascript·人工智能
孩子 你要相信光10 小时前
css之一个元素可以同时应用多个动画效果
前端·css
huangql52010 小时前
npm 发布流程——从创建组件到发布到 npm 仓库
前端·npm·node.js