Angular:了解模块和服务

…并使用它们更好地组织应用程序!

这个给你! 您开始对这个框架感到满意,突然之间……

我应该把服务放在哪里? 我是否以正确的方式导入它们? 我的模块是否组织合理并且可以扩展? 这个组件在正确的位置吗?
如何命名该文件夹?

我发现大多数开发人员都在为这个问题而苦恼,因为他们对Angular模块的工作方式以及它们对依赖注入机制的重要性没有清晰的认识。

在本文中,我们将为您接下来的所有项目打下基础! 😃

功能模块

在Angular中,每个不是AppModule的模块从技术上来讲都是功能模块,它具有以下警告:

—必须声明所需的所有组件,指令和管道

我们将在短期内讨论模块作用域,但是现在很重要的一点是要了解,在AppModule的声明数组中一次声明一个组件还不够:为了在模块中使用组件,必须在该特定模块中声明它。模块 。 指令和管道也是如此。

—必须导入CommonModule而不是BrowserModule

虽然必须将BrowserModule导入AppModule中(这是在浏览器中运行该应用程序所必需的),但不得将该模块导入其他地方:相反,我们必须导入CommonModule ,其中包含Angular的通用指令,例如ngIfngForngClass ,等等…BrowserModule也重新导出CommonModule,以便您也可以在AppModule中使用此指令。

-它不会引导任何东西

显然,负责引导组件的唯一模块是AppModule!

我们将使用功能模块来定义所有视图 ,因为每个视图 (或场景 )将拥有自己的模块! 既然我们在谈论场景,我们也在谈论路由 。 每个“ View Module ”(以这种方式称呼) 都可以由路由器延迟加载 ,我已经在文章中提到过。 😉

延迟加载不仅可以节省字节和内存,还可以使您以正确的方式考虑模块:例如, 每个模块都应该有自己的路由模块 ! 这是一个小技巧……如果您以此方式组织模块,您仍然可以使用loadChildren关键字,而无需实际延迟加载模块,以证明您的模块是独立且结构良好的:

这样,父模块就无需导入子模块的组件以将它们放入路由配置中:从localhost / contacts /开始,ContactsModule将负责其路由。 那不是很棒吗?

使用惰性方法的另一个好处是,我们还可以延迟加载所有模块,并在应用程序启动后立即预加载它们:

更好的是,我们可以定义用于预加载模块的自定义策略! 我们只需要在要预加载的路由上设置一个属性,并编写一个新类用作我们的preloadingStrategy :此类将检查路由的属性,如果找到它们,路由器将预加载这些路由!

核心模块

问题“ 我应将所有全球​​服务放在哪里? ”为:AppModule。 这是因为服务是基于应用程序范围的 ,这意味着可以从每个模块访问它们。

这意味着,如果我们在子模块中提供服务,则该服务在父模块中仍然可用! 那怎么可能呢? 那是因为他们共享同一个喷油器! 因此,您可能会认为,在这种情况下,您可以在任何需要的地方提供服务…… 错!

我刚才说的是正确的,但是如果那些子模块是延迟加载的,则不是这样:每个延迟模块都有自己的注入器! 这意味着惰性模块中提供的服务只能在该模块中访问。 但是它仍然可以访问以前由非延迟模块(例如AppModule)提供的服务!

如此……您应该将服务(例如AuthServiceUserService等)放在哪里? 从技术上讲,在AppModule中 ,因为每个人都可以使用它们。 但是,我们真的不希望AppModule变得一团糟。Angular建议将所有全局服务放在一个单独的模块CoreModule中 ,然后将其导入AppModule中。 这种方式与直接在AppModule中提供服务相同!

等等…如果其他人导入了CoreModule怎么办? 我们可以预防吗? 我们可以! 我们可以使用一些技巧来做到这一点:在我们的CoreModule内部,我们可以… 注入CoreModule ! 如果Angular正确注入它,则意味着已经创建了CoreModule,我们可能会抛出错误:

如果您所在的团队中有一些经验不足的开发人员,并且您想确保没有奇怪的事情发生,这将特别有用。😃您可以使用相同的方法来确保您的单例服务是单例,只需将它们注入自身并注入它们,抛出一个错误!

注意:我们正在使用Optional装饰器,因为显然不需要此依赖关系(在正确的情况下它将为null ),并且我们正在使用SkipSelf,因为我们正在CoreModule中要求CoreModule依赖关系! 此时,Angular会使用WTF,但是由于有了这个装饰器,在实例化CoreModule之后开始注入。

下一个问题的时间到了:我将可重复使用的组件放在哪里? 当然,在共享模块中!

共享模块

共享模块是声明组件以使其可重用的理想场所:这样,您将不会在每个模块中重新导入相同的组件,而只需导入共享模块。

棒极了! 但是有一个问题。 😅不用共享模块本身,就可以了! 惰性模块有问题!

惰性模块的行为很奇怪:由于它们具有自己的注入器,因此如果导入提供某些服务的模块,它们将创建这些服务的自己的实例。 喘气! 这是否意味着由于惰性模块的行为, 我们无法在共享模块中提供服务?

好吧,您可能以为我们已经在CoreModule中声明了我们的全局服务,这是事实,但是如果共享模块中声明的组件需要某些服务怎么办? 如果导入共享模块的模块(也许是惰性模块)需要从中获得服务怎么办? 我们仍然可以解决它 。 👍

Angular为我们提供了一个特殊的接口,可用于将服务附加到模块 ,称为ModuleWithProviders ,这里是:

有趣的是,我们可以导入具有此特定形状的对象而不是普通模块! 我们的任务如下:

  • 在我们的AppModule中,我们将导入共享模块及其附带的提供程序
  • 在所有其他模块中,我们将在没有任何提供程序的情况下导入该模块,因为它们已在AppModule中提供

那么,我们该如何进行呢? 很简单:我们不在SharedModule元数据中提供服务; 相反,我们在模块内部定义了一个静态方法 ,该方法返回SharedModule istelf 提供程序的数组!

太棒了! 现在,要做的是:在AppModule中,您可以导入SharedModule.forRoot() ,而在所有其他模块中,您可以导入SharedModule 。 这样,服务将仅在AppModule级别上提供(并且可以在任何地方访问),而其他模块仍然可以访问组件。

Psst…

您已经看到过类似的东西吗? 是的, 这就是RouterModule的工作方式 😄您可以在AppModule中使用RouterModule.forRoot(),并在所有其他模块中使用RouterModule.forChild()。 仅因为您需要提供一组路由,才为Child提供了第二种方法,但是如果您不需要将任何东西传递给共享模块,则拥有第二种方法只是导入模块是没有用的。
实际上,这个 forRoot东西是许多Angular库采用的约定,但是您当然可以随意命名该静态方法。

进口服务

因此,我们的服务可以在4个不同的地方提供:

  • CoreModule中 ,它将保存我们的全局和单例服务
  • SharedModule中 ,它将保存自身所需的服务
  • 在“ 查看模块 ”本身中,如果仅该模块需要服务
  • 组件中 ,如果只需要它们

尽管对于后两种方案没有问题,但是从Core / SharedModule导入单个服务可能很麻烦。 想象一下,您的一个View Modules位于文件夹结构的深处。 为了导入和使用全局服务,您必须编写如下代码:

从“ ../../../../core/services/auth.service”导入{AuthService};

那是糟糕的! 如果文件夹结构发生变化怎么办?

我们可以轻松地通过以下方式向tsconfig.json文件添加几行:

另外,我们想从“ @ app / core ”导入我们的服务,而不是从“ @ app / core / services / auth.service导入 ,这很繁琐。 为此,在我们的CoreModule文件夹中,use可以创建一个index.ts文件并重新导出我们的所有服务:现在我们可以直接从’ @ app / core ‘导入!

最终情况

这或多或少是我们应该如何构造模块和文件夹的方式:

从那里,您显然可以使用不同的共享模块(也许是UIKitModule ,为什么不使用)来扩展您的应用程序,或者可以将SharedModule扩展到其他模块。 不要害怕创建模块,它们不会咬人!

接下来我们该怎么办

我们讨论了很多模块,但是除此之外,为了拥有一个结构良好的应用程序,我们还可以做更多的事情。 我们可以:

  • 编写无状态的微组件,而不是肿的有状态 组件
  • 在我们的无状态组件上使用ChangeDetection.OnPush可以避免常见的性能陷阱(但要小心)
  • 使用路由器防护来保护我们的路线
  • 使用路由解析器从我们的视图模块中删除依赖项
  • 使用拦截器来处理验证逻辑和其他内容
  • 使用ReduxMobX之类的库来处理整个应用程序的业务逻辑

… 等等! 但是会有其他文章的时间😉如有任何问题或要求,请不要害羞并发表评论!