阅读(3252) (0)

Angular 为库准备的轻量级注入令牌

2022-07-12 17:21:07 更新

使用轻量级注入令牌优化客户应用的大小

本页面会提供一个概念性的概述,它介绍了一种建议库开发者使用的依赖注入技术。使用轻量级注入令牌设计你的库,这有助于优化那些用到你库的客户应用的发布包体积。

你可以使用可摇树优化的提供者来管理组件和可注入服务之间的依赖结构,以优化发布包体积。这通常会确保如果提供的组件或服务从未被应用实际使用过,那么编译器就可以从发布包中删除它的代码。

但是,由于 Angular 存储注入令牌的方式,可能会导致未用到的组件或服务最终进入发布包中。本页描述了依赖注入的一种设计模式,它通过使用轻量级注入令牌来支持正确的摇树优化。

这种轻量级注入令牌设计模式对于库开发者来说尤其重要。它可以确保当应用只用到了你库中的某些功能时,可以从客户应用的发布包中删除未使用过的代码。

当某应用用到了你的库时,你的库中可能会提供一些客户应用未用到的服务。在这种情况下,应用开发人员会期望该服务是可摇树优化的,不让这部分代码增加应用的编译后大小。由于应用开发人员既无法了解也无法解决库的摇树优化问题,因此这是库开发人员的责任。为了防止未使用的组件被保留下来,你的库应该使用轻量级注入令牌这种设计模式。

什么时候令牌会被保留

为了更好地解释令牌被保留的条件,我们考虑一个提供卡片组件的库,它包含一个卡片体,还可以包含一个可选的卡片头。

<lib-card>
  <lib-header>…</lib-header>
</lib-card>

在一个可能的实现中,​<lib-card>​ 组件使用 ​@ContentChild()​ 或者 ​@ContentChildren()​ 来获取 ​<lib-header>​ 和 ​<lib-body>​,如下所示。

@Component({
  selector: 'lib-header',
  …,
})
class LibHeaderComponent {}

@Component({
  selector: 'lib-card',
  …,
})
class LibCardComponent {
  @ContentChild(LibHeaderComponent)
  header: LibHeaderComponent|null = null;
}

因为 ​<lib-header>​ 是可选的,所以元素可以用最小化的形式 ​<lib-card></lib-card>​ 出现在模板中。在这个例子中,​<lib-header>​ 没有用过,你可能期望它会被摇树优化掉,但事实并非如此。这是因为 ​LibCardComponent ​实际上包含两个对 ​LibHeaderComponent ​引用。

@ContentChild(LibHeaderComponent) header: LibHeaderComponent;

  • 其中一个引用位于类型位置上 - 即,它把 ​LibHeaderComponent ​用作了类型:​header: LibHeaderComponent​;。
  • 另一个引用位于值的位置 - 即,LibHeaderComponent 是 ​@ContentChild()​ 参数装饰器的值:​@ContentChild(LibHeaderComponent)​。

编译器对这些位置的令牌引用的处理方式也不同。

  • 编译器在从 TypeScript 转换完后会删除这些类型位置上的引用,所以它们对于摇树优化没什么影响。
  • 编译器必须在运行时保留值位置上的引用,这就会阻止该组件被摇树优化掉。

在这个例子中,编译器保留了 ​LibHeaderComponent ​令牌,它出现在了值位置上,这就会防止所引用的组件被摇树优化掉,即使应用开发者实际上没有在任何地方用过 ​<lib-header>​。如果 ​LibHeaderComponent ​很大(代码、模板和样式),把它包含进来就会不必要地大大增加客户应用的大小。

什么时候使用轻量级注入令牌模式

当一个组件被用作注入令牌时,就会出现摇树优化的问题。有两种情况可能会发生。

  • 令牌用在内容查询中值的位置上。
  • 该令牌用作构造函数注入的类型说明符。

在下面的例子中,两处对 ​OtherComponent ​令牌的使用导致 ​OtherComponent ​被保留下来(也就是说,防止它在未用到时被摇树优化掉)。

class MyComponent {
  constructor(@Optional() other: OtherComponent) {}

  @ContentChild(OtherComponent)
  other: OtherComponent|null;
}

虽然转换为 JavaScript 时只会删除那些只用作类型说明符的令牌,但在运行时依赖注入需要所有这些令牌。这些工作把 ​constructor(@Optional() other: OtherComponent)​ 改成了 ​constructor(@Optional() @Inject(OtherComponent) other)​。该令牌现在处于值的位置,并使该摇树优化器保留该引用。

对于所有服务,库都应该使用可摇树优化的提供者,在根级而不是组件构造函数中提供依赖。

使用轻量级注入令牌

轻量级注入令牌设计模式包括:使用一个小的抽象类作为注入令牌,并在稍后为它提供实际实现。该抽象类固然会被留下(不会被摇树优化掉),但它很小,对应用程序的大小没有任何重大影响。

下例举例说明了这个 ​LibHeaderComponent ​的工作原理。

abstract class LibHeaderToken {}

@Component({
  selector: 'lib-header',
  providers: [
    {provide: LibHeaderToken, useExisting: LibHeaderComponent}
  ]
  …,
})
class LibHeaderComponent extends LibHeaderToken {}

@Component({
  selector: 'lib-card',
  …,
})
class LibCardComponent {
  @ContentChild(LibHeaderToken) header: LibHeaderToken|null = null;
}

在这个例子中,​LibCardComponent ​的实现里,​LibHeaderComponent ​既不会出现在类型的位置也不会出现在值的位置。这样就可以让 ​LibHeaderComponent ​完全被摇树优化掉。​LibHeaderToken ​被留下了,但它只是一个类声明,没有具体的实现。它很小,并且在编译后保留时对应用程序的大小没有实质影响。

不过,​LibHeaderComponent ​本身实现了抽象类 ​LibHeaderToken​。你可以放心使用这个令牌作为组件定义中的提供者,让 Angular 能够正确地注入具体类型。

总结一下,轻量级注入令牌模式由以下几部分组成。

  1. 一个轻量级的注入令牌,它表现为一个抽象类。
  2. 一个实现该抽象类的组件定义。
  3. 注入这种轻量级模式时使用 ​@ContentChild()​ 或者 ​@ContentChildren()​。
  4. 实现轻量级注入令牌的提供者,它将轻量级注入令牌和它的实现关联起来。

使用轻量级注入令牌进行 API 定义

那些注入了轻量级注入令牌的组件可能要调用注入的类中的方法。因为令牌现在是一个抽象类,并且可注入组件实现了那个抽象类,所以你还必须在作为轻量级注入令牌的抽象类中声明一个抽象方法。该方法的实现代码(及其所有相关代码)都会留在可注入组件中,但这个组件本身仍可被摇树优化。这样就能让父组件以类型安全的方式与子组件(如果存在)进行通信。

比如,​LibCardComponent ​现在要查询 ​LibHeaderToken ​而不是 ​LibHeaderComponent​。这个例子展示了该模式如何让 ​LibCardComponent ​与 ​LibHeaderComponent ​通信,却不用实际引用 ​LibHeaderComponent​。

abstract class LibHeaderToken {
  abstract doSomething(): void;
}

@Component({
  selector: 'lib-header',
  providers: [
    {provide: LibHeaderToken, useExisting: LibHeaderComponent}
  ]
  …,
})
class LibHeaderComponent extends LibHeaderToken {
  doSomething(): void {
    // Concrete implementation of `doSomething`
  }
}

@Component({
  selector: 'lib-card',
  …,
})
class LibCardComponent implement AfterContentInit {
  @ContentChild(LibHeaderToken)
  header: LibHeaderToken|null = null;

  ngAfterContentInit(): void {
    this.header && this.header.doSomething();
  }
}

在这个例子中,父组件会查询令牌以获取子组件,并持有结果组件的引用(如果存在)。在调用子组件中的方法之前,父组件会检查子组件是否存在。如果子组件已经被摇树优化掉,那运行期间就没有对它的引用,当然也没有调用它的方法。

为你的轻量级注入令牌命名

轻量级注入令牌只对组件有用。Angular 风格指南中建议你使用“Component”后缀命名组件。比如“LibHeaderComponent”就遵循这个约定。

为了维护组件及其令牌之间的对应关系,同时又要区分它们,推荐的写法是使用组件基本名加上后缀“​Token​”来命名你的轻量级注入令牌:“​LibHeaderToken​”。