本文由达观数据研究院根据《Total Guide To Angular 6+ Dependency Injection — providedIn vs providers》编译,如有不当,还请指正。
Angular 6为我们提供了更好的语法——provideIn,用于将服务注册到Angular依赖注入机制中。
然而,新语法带来了非常多使用上的困惑,在GitHub评论,Slack和Stack Overflow上看到一些开发者经常混淆。所以,现在,让我们把这一切都说清楚。
依赖注入回顾(可选)
使用旧语法进行依赖注入—— providers: []
使用新语法进行依赖注入—— providedIn: 'root' | SomeModule
providedIn
的使用场景
在项目中如何使用新语法的最佳实践
总结
让我们快速回顾一下依赖注入是什么,如果感觉简单,你可以跳过这一小节。
依赖注入(DI)是一种创建依赖其他对象的方法。在创建一个新的对象实例时,依赖注入系统将会提供依赖对象(称为依赖关系) - Angular Docs
我们的组件和服务都是类,每个类都有一个名为constructor的特殊函数,当我们想要在我们的应用程序中创建该类的对象(实例)时调用它。
在我们的服务中,我们都看到过类似于 constructor(private http: HttpClient)
这样的代码。假如没有Angular DI机制,我们必须手动提供HttpClient来创建我们自己的服务。
我们的代码会像这样:const myService = new MyService(httpClient)
;但是,我们还需要获得httpClient
对象。
于是,我需要再实例一个HttpClient:const httpClient = new HttpClient(httpHandler)
;但httpHandler
又从哪来?如果这样创建下去,到底什么时候是个头。而且,这个过程相当繁琐,而且很容易出错。
幸好,Angular 的DI机制自动地帮我们完成了上述的所有操作,我们所要做的只是在组件的构造函数中指定依赖项,组件将会很轻松地就能用到这些依赖。可天下没有免费的午餐...
为了让工程实践做的更好,Angular必须了解我们想要注入到组件和服务中的每一个实体。
在Angular 6 发布以前, 唯一的方法是在 providers: []
中指定服务,如下:
根据具体使用场景, providers: []
将有三种不同的用法:
1、在预加载的模块的@NgModule
装饰器中指定 providers: []
2、在懒加载的模块的@NgModule
装饰器中指定 providers: []
3、在@Component
和@Directive
装饰器中指定 providers: []
在预加载模块中使用providers: []
在这种情况下,服务将是全局单例的。即使它被多个模块的providers: []
重复申明,它也不会重新创建实例。注入器只会创建一个实例,这是因为它们最终都会注册到根级注入器。
在懒加载模块中使用providers: []
在应用程序运行初始化后一段时间,懒加载模块中提供的服务实例才会在子注入器(懒加载模块)上创建。如果在预加载模块中注入这些服务,将会报 No provider for MyService!
错误。
在@Component
和@Directive
中使用providers: []
服务是按组件实例化的,并且可以在组件及其子树中的所有子组件中访问。在这种情况下,服务不是单例的,每次我们在另一个组件的模板中使用组件时,我们都会获得所提供服务的新实例。 这也意味着服务实例将与组件一起销毁......
上面图中,RandomService
在 RandomComponent
中被注册,因此,每当我们在模板中使用
组件时,我们将得到不同的随机数。
如果在模块级别提供 RandomService
并且将被作为单例提供,则不会出现这种情况。 在这种情况下,
组件的每次使用都会显示相同的随机数,因为该数字是在服务实例化期间生成的。
随着Angular 6的出现,我们可以使用全新的语法在我们的应用程序中建立依赖项, 官方名称是“Tree-shakable providers”,我们通过使用 @Injectable
装饰器的新增的 provideIn
属性来使用它。
我们可以将provideIn视为以反向方式指定依赖关系。 现在不是模块申明需要哪些服务,而是服务本身宣布它应该提供给哪些模块使用
申明的模块可以是 root
或其他任何可用模块。另外,root
实际上是 AppModule
的别名,这是一个很好的语法糖,我们因此不需要额外导入 AppModule
。
新语法非常简单,现在让我们实践一下,来探索在应用程序开发过程中可能遇到的一些有趣场景......
使用 providedIn: 'root'
在大多数情况下,这是对我们有用的最常见的解决方案。此解决方案的主要好处是,只有真正“使用”这些服务时才会打包服务代码。 “使用”代表注入某些组件或其他服务。
另一方面,providedIn: 'root'
在代码可复用方面为开发人员带来了巨大的积极影响。
在 `providedIn` 出现之前,需要在主模块的 `providers: []` 中注入所有公共服务。然后,组件需要导入该模块,这将导致所有(可能的大量)的服务导入进该组件,即使我们只想使用其中一个服务。
现在,providedIn: 'root'
解决了这个问题,我们不需要在模块中导入这些服务,我们要做的仅仅是使用它们。
懒加载 providedIn: 'root'
解决方案
如果我们在懒加载中使用 providedIn: 'root'
来实现服务会发生什么?
从技术上讲,'root'
代表 AppModule
,但Angular足够聪明,如果该服务只是在惰性组件/服务中注入,那么它只会绑定在延迟加载的bundle中。
如果我们又额外将服务注入到其他正常加载的模块中,那么该服务会自动绑定到 mian
的bundle中。
简单来讲:
1、如果服务仅被注入到懒加载模块,它将捆绑在懒加载包中
2、如果服务又被注入到正常模块中,它将捆绑在主包中
这种行为的问题在于,在拥有大量模块和数百项服务的大型应用程序中,它可能变得非常不可预测。
幸运的是,有一种方法可以防止这种情况的发生,我们将在下面的章节中探讨如何加强模块的边界。
使用 providedIn: EagerlyImportedModule
这个解决方案通常没有意义,我们应该坚持使用 provideIn:'root'
。
它可用于防止应用程序的其余部分注入服务而无需导入相应的模块,但这其实并不是必需的。
附注 - 延迟加载模块的多重好处
Angular最大的优点之一是我们可以非常容易的将应用程序分成完全独立的逻辑块,这有以下好处…
1、更小的初始化代码,这意味着更快的加载和启动时间
2、懒惰加载的模块是真正隔离的。主机应用程序应该引用它们的唯一一点是某些路由的 loadChildren
属性。
这意味着,如果使用正确,可以将整个模块删除或外部化为独立的应用程序/库。可能有数百个组件和服务的模块可以在不影响应用程序其余部分的情况下随意移动,这是非常令人惊奇的!
这种隔离的另一个巨大好处是,对懒惰模块的逻辑进行更改永远不会导致应用程序的其他部分出错。
使用 providedIn: LazyLoadedModule
这个解决方案非常棒,因为它可以帮助我们防止在所需模块之外使用我们的服务。在开发大型应用程序时,保持依赖关系图是非常有必要的,因为无约束的无处不在的注入可能会导致无法解决的巨大混乱!
不幸的是,有一个小问题……循环依赖
幸运的是,我们可以通过创建一个 LazyServiceModule
来避免这个问题,它将是 LazyModule
的一个子模块,并将被用作我们想要提供的所有懒加载服务的“锚”。如下图所示:
虽然有点不方便,但我们只需增加一个模块,这种方法结合了两者的优点:
1. 它防止我们将懒加载的服务注入应用程序的正常加载模块
2. 只有当服务被真正注入其他惰性组件时,它才会打包到服务中
新语法能在 @Component
和 @Directive
中使用吗?
不,它们并不能。
我们仍然需要在 @Component
或 @Directive
中使用 provider:[]
来创建多个服务实例(每个组件)。 目前还没有办法解决这个问题......
库
当处理开发库、实用程序或任何其他形式的可重用 Angular 逻辑时,providedIn: 'root'
是非常好的解决方案。
当消费者应用程序只需要可用库功能的一个子集时,它也处理的非常好。只有真正使用的东西才会打包进我们的应用程序中,我们都希望打包出来的文件越小越好。
懒加载模块
使用 providedIn: LazyServicesModule
,然后由 LazyModule
导入,再由 Angular 路由器惰性加载,以实施严格的模块边界和可维护的架构!
这种方法可以防止我们将懒加载的服务注入应用程序的正常加载模块
使用providedIn: 'root'
, 'root'
将会正常工作,服务也会被正确捆绑,但是使用 providedIn: LazyServiceModule
为我们提供了早期的“missing provider”错误,这是一个很好的早期信号,这有助于我们重新思考我们的架构。
什么时候使用老的 providers:[]
语法?
我们需要将配置传递给我们的服务吗?
或者换句话说,我们是否有一个使用 SomeModule.forRoot(someConfig)
解决的场景?
在这种情况下,我们仍然需要使用 providers: []
,因为新的语法无助于我们定制服务。
另一方面,如果我们曾经使用 SomeModule.forRoot()
来阻止延迟加载模块创建服务的其他实例,我们可以简单地使用 providedIn: 'root'
来实现这一点。
将 providedIn: 'root'
用于在整个应用程序中作为单例可用的服务;
永远不要使用 providedIn:EagerLiymportedmodule
,您不需要它,如果有一些非常特殊的用例,那么请使用 providers: []
来代替;
使用 providedIn: LazyServiceModule
来防止我们将懒加载的服务注入应用程序的正常加载模块;
如果我们想使用 LazyServiceModule
,那么我们必须将其导入 LazyModule
,以防止循环依赖警告。然后,LazyModule
将以标准方式使用 Angular Router 为某些路由进行懒加载。
使用 @Component
或 @Directive
内部的 providers: []
,为特定的组件子树提供服务,这也将导致创建多个服务实例(每个组件使用一个服务实例)
始终尝试保守地确定您的服务范围,以防止依赖蔓延和由此产生的巨大混乱!
关于译者
王玉略:达观数据前端开发工程师,负责达观数据前端开发,喜欢探索新技术,致力于将代码与日常生活相结合,提高生活效率。