延迟加载,也称为代码拆分,可让您将 JavaScript 代码分成多个块。 结果是当用户访问第一页时,您不必加载完整应用程序的所有 JavaScript。 相反,只加载给定页面所需的块。 在浏览店面时,会在真正需要使用到某些功能时,再加载这些功能块的实现模块。
这种方法可以显着改善“交互时间”,尤其是在低端移动设备访问复杂 Web 应用程序的情况下。
Spartacus Approach to Lazy Loading
代码拆分是一种必须在应用程序构建时完成的技术。 Angular 提供的代码拆分通常是基于路由的,这意味着着陆页有一个块,产品页面有另一个块,依此类推。 由于 Spartacus 主要是 CMS 驱动的,因此无法在构建时决定每个路由的实际应用逻辑。 业务用户最终将通过引入或删除组件来改变页面结构。 这就是为什么需要另一种延迟加载方法的原因,Spartacus 支持以下两种不同粒度的延迟加载技术:
- CMS 组件的延迟加载
- CMS 驱动的功能模块延迟加载
Defining Dynamic Imports Only in the Main Application
动态导入是一种用于促进延迟加载并允许代码拆分的技术,只能在类型为 Application 的 Angular 应用程序中使用。无法在预构建库(类型为 Library )中定义动态导入。
这是一个不幸的限制,导致必须由客户添加一些应用程序代码。 尽管自定义代码的数量被限制在最低限度,但我们将在未来版本的原理图库中添加一项功能,以自动添加延迟加载模块。
Avoiding Static Imports for Lazy-Loaded Code
为了使代码拆分成为可能,您的静态 JavaScript 代码(主应用程序包)不应该对您想要延迟加载的代码进行任何静态导入。一个例子就是 QuickOrderService. 构建器会注意到代码已经包含在内,因此不会为其生成单独的块。 这在从库中导入 symbol 的情况下尤其重要。
至少在 Angular 9 和 Angular 10 里,将静态导入与动态导入混合用于相同的库入口点,即使对于不同的 symbol,也会破坏该库入口点的延迟加载和 tree shaking 优化。 如果您要这样做,它将在构建中静态地包含整个入口点。 因此,强烈建议您为必须静态加载的代码创建特定的入口点,并为可以延迟加载的代码创建单独的入口点。
Configuration in Lazy-Loaded Modules
如果在延迟加载模块内部提供了额外的配置,Spartacus 会将其合并到全局应用程序配置中,以支持现有组件和服务的延迟加载场景。 在大多数情况下,尤其是当延迟加载的模块主要提供默认配置时,这可以可靠地工作。 但是,如果过度使用它会导致问题,尤其是当两个模块为配置的同一部分提供不同的配置时。 可以通过在主应用程序中提供必要的覆盖来修复诸如此类的场景。
这种合并功能是通过默认启用的兼容性机制实现的,但您可以使用 disableConfigUpdates 功能标志禁用它。 如果您正在开发必须从延迟加载的模块中挂钩到配置的新模块,则应改用 ConfigurationService.unifiedConfig$。
Providers in Lazy-Loaded Modules
延迟加载模块中提供的注入令牌对根应用程序中提供的服务不可见。
这尤其适用于 multi-provided 的令牌,例如 HttpInterceptors、各种处理程序等。
为了减轻这个缺点,一些 Spartacus 功能,例如 PageMetaService(使用 PageMetaResolver 令牌)或 ConverterService(主要使用适配器序列化器和规范化器),在我们的实现里使用 unified 注入器。 只有这样做,根应用程序才可以访问延迟加载的令牌,并可以利用它们来实现全局功能。
对于不依赖于统一注入器的机制(例如,来自大多数非 Spartacus 库的功能,例如核心 Angular 库),建议您始终使用 eager 模式加载包含了这些令牌的模块。
Unified Injector
统一注入器提供了一种注入令牌或多提供令牌的方法,这种方法同时考虑到根注入器和来自延迟加载功能的注入器。 注入器公开一个可观察的对象,每次统一注入器的状态发生变化时,该观察对象都会为指定的令牌发出一组新的可注入对象。
Avoiding Importing the HttpClientModule in Your Lazy-Loaded Modules
一般来说,HttpClientModule 应该在根应用程序中导入,而不是在库中。 例如,如果您将它导入到延迟加载的库中,则根库中的所有注入器对于源自延迟加载模块的 HTTP 调用都是不可见的。
虽然技术上可以在库中导入 HttpClientModule ,但在大多数情况下这不是预期的,并且可能会导致难以解释的错误,因此请记住这一点。
Lazy Loading of CMS Components
CMS 代码的延迟加载是通过在 CMS 映射配置中指定动态导入代替静态引用的组件类来实现的。 下面是一个例子:
{
cmsComponents: {
SimpleResponsiveBannerComponent: {
component: () => import('./lazy/lazy.component').then(m => m.LazyComponent)
}
}
}
Lazy Loading of Modules
CMS 驱动的功能模块延迟加载允许以下内容:
- 懒加载不仅是组件代码,还有核心部分(包括NgRx状态)
- 在第一次需要时只加载一次功能提供共享的、延迟加载的依赖模块
- 当实现被相关功能配置覆盖时,CMS 请求组件会触发功能模块的延迟加载。
Exposing Smart Proxy Facades From lazy loaded Features
代理外观提供了一种灵活的方式来从延迟加载的功能模块中公开核心功能,这样使用这些外观的组件就不必知道它是否延迟加载以及是否需要初始化。
对代理外观的方法或属性的任何访问都会触发所有相关功能的延迟加载和初始化。
Combined Injector
任何延迟加载的模块都可以从根应用程序注入器和依赖模块注入器注入(即可以访问)服务和令牌。 这是可能的,因为每次实例化具有依赖项的功能模块时都会创建 CombinedInjector。
当一个被延迟加载模块覆盖的 CMS 组件被实例化时,它可以注入(即访问)以下服务:
- ModuleInjector 层次结构,从功能模块注入器开始,包括依赖模块和根注入器
- ElementInjector 层次结构,它是在每个 DOM 元素上隐式创建的
Preparing Libraries to Work with Lazy Loading
Providing Fine-Grained Entry Points in Your Library
从相同的入口点混合静态和动态导入会破坏延迟加载并影响摇树,因此任何将直接用于动态导入的库都应该公开细粒度的辅助入口点以优化代码拆分。
作为惯例,Spartacus 公开功能的根入口点(root entry points),例如 @spartacus/orgainzation/administration/root。 这种类型的入口点包含所有不应或不能延迟加载的代码,比如 Proxy Facade 层。来自根入口点的模块应该在根应用程序中静态导入,这意味着它们将被预先加载并在主应用程序块中可用。
有关对 Angular 库中辅助入口点的支持的更多信息,请参阅 GitHub 上 ng-packagr 文档中的辅助入口点。
Separating Static Code from Lazy-Loaded Code
当您使用 Angular Dependency Injection 时,注入器中的提供程序列表不应在注入器初始化后更改。这种范式特别适用于任何多提供的令牌、处理程序,尤其适用于任何 Angular 原生多提供的令牌,例如 HTTP_INTERCEPTOR、APP_INITIALIZER 等。
结果是延迟加载模块中的任何多提供令牌对于根或其他延迟加载块中提供的模块和服务将不可见,但使用注入的多提供令牌除外统一喷油器。
一些 Spartacus 功能,例如 PageMetaService 或 ConverterService,使用 UnifiedInjector 来了解可以延迟加载的令牌,以便全局逻辑(例如 SEO 功能)即使逻辑延迟加载该功能也能可靠地工作。例如,商店定位器页面元解析器可以使用商店定位器功能延迟加载。
Spartacus 配置也是通过提供配置块来定义的,由于兼容机制将配置从延迟加载功能贡献到全局配置,因此处理方式略有不同。这种机制可以通过功能标志禁用,将来会默认关闭,以支持统一配置功能。
如果根服务无法看到延迟加载提供程序的问题,则始终可以将此类代码包含在预先可用的静态链接模块中。建议在您的库中创建一个单独的入口点(按照惯例,命名为 root,例如 my-library/root),其中包含最少的代码,将包含在主包中,并且从一开始就可用。
Simple Strategies for Optimized and Balanced Code Splitting
每个业务功能都略有不同,但总的来说,出于折中考虑,可以按照以下几点进行优化:
大多数功能都提供了一系列 UI 组件,如果您访问一个组件,很可能很快也会需要其他组件。为了最大限度地减少网络流量和不必要的粒度开销,您可以考虑在同一个 chunk 中捆绑多个组件。
功能的大多数 UI 组件通常需要核心逻辑。何谓核心?例如 Facade 的实现类即 Service, 以及适配器。在这种情况下,您可以考虑使用 lazy loading 机制来加载 core 模块。
一个特性的一些核心服务也可能被其他 feature 频繁使用。在这种情况下,您可以考虑将核心代码与 UI 组件分开。
功能的某些部分使用非常频繁(例如每个页面都使用的购物车图标组件),而有些仅在某些特定场景中使用(例如购物车摘要)。为了获得最佳体验,建议根据使用情况将功能拆分为逻辑部分,并为最少、最需要的部分创建单独的入口点(和功能),并为不太常用的代码创建另一个入口点。