分享

SAP Spartacus 的延迟加载 Lazy load 设计原理

 汪子熙 2021-07-10

官方链接

延迟加载,也称为代码拆分,可让您将 JavaScript 代码分成多个块。 结果是当用户访问第一页时,您不必加载完整应用程序的所有 JavaScript。 相反,只加载给定页面所需的块。 在导航到其他页面时,会在需要时加载其他块。

这种方法可以显着改善“交互时间”,尤其是在低端移动设备访问复杂 Web 应用程序的情况下。

Spartacus Approach to Lazy Loading

代码拆分是一种必须在应用程序构建时完成的技术。 Angular 提供的代码拆分通常是基于路由的,这意味着着陆页有一个块,产品页面有另一个块,依此类推。

由于 Spartacus 主要是 CMS 驱动的,因此无法在构建时决定每个路由的实际应用逻辑。业务用户最终将通过引入或删除组件来改变页面结构。 这就是为什么需要另一种延迟加载方法的原因,Spartacus 通过以下方式提供:

  • CMS 组件的延迟加载

  • CMS 驱动的功能模块延迟加载 - CMS-driven lazy loading of feature modules

Defining Dynamic Imports Only in the Main Application

动态导入是一种用于促进延迟加载并允许代码拆分的技术,只能在主应用程序 - main application 中使用。 无法在预构建库中定义动态导入。

这是一个不幸的限制,导致必须由客户添加一些应用程序代码。 尽管自定义代码的数量被限制在最低限度,但我们将在未来版本的 schematics library 中添加一项功能,以自动添加延迟加载模块。

Avoiding Static Imports for Lazy-Loaded Code

为了使代码拆分成为可能,您的静态 JavaScript 代码(主应用程序包)不应该对您想要延迟加载的代码进行任何静态导入。 如果你真的这么做了,构建器会注意到代码已经包含在内,因此不会为其生成单独的块。 这在从库中导入符号的情况下尤其重要。

在撰写本文时(Angular 9 和 Angular 10),将静态导入与动态导入混合用于相同的库入口点,即使对于不同的符号,也会破坏该库入口点的延迟加载和 tree shaking. 如果您要这样做,它将在构建中静态地包含整个入口点。 因此,强烈建议您为必须静态加载的代码创建特定的入口点,并为可以延迟加载的代码创建单独的入口点。

Configuration in Lazy-Loaded Modules

如果延迟加载模块内部提供了额外的配置,Spartacus 会将其合并到全局应用程序配置中,以支持现有组件和服务的延迟加载场景。 在大多数情况下,尤其是当延迟加载的模块主要提供默认配置时,这可以可靠地工作。 但是,如果过度使用它会导致问题,尤其是当两个模块为配置的同一部分提供不同的配置时。 可以通过在主应用程序中提供必要的覆盖来修复诸如此类的场景。

这种合并功能是通过默认启用的兼容性机制实现的,但您可以使用 disableConfigUpdates 功能标志禁用它。 如果您正在开发必须从延迟加载的模块中挂钩到配置的新模块,则应改用 ConfigurationService.unifiedConfig$。 此功能在下一节中描述。

Unified Configuration

统一配置提供了一种获取全局配置的方法,该配置包括根配置和来自已加载延迟加载模块的配置。

ConfigurationService.unifiedConfig$ 将统一配置公开为每次更改时发出新配置的 observable。 例如,每次加载和实例化具有提供的配置的延迟加载模块时都会发生这种情况。

所有配置部分都按照严格的顺序合并,实际配置始终覆盖默认配置,并且根模块(即app shell)中定义的配置具有优先权。

以下示例显示了根应用程序和两个延迟加载模块中提供的不同配置的合并顺序,其中列表中的每个后续项都可以覆盖前一项:

  • 默认根配置

  • 延迟加载模块 1 的默认配置

  • 延迟加载模块 2 的默认配置

  • 延迟加载模块 1 的配置

  • 延迟加载模块 2 的配置

  • 根配置(始终优先)

Providers in Lazy-Loaded Modules

延迟加载模块中提供的注入令牌对根应用程序中提供的服务不可见。 这尤其适用于多提供的令牌,例如 HttpInterceptors、各种处理程序等。

为了减轻这个缺点,一些 Spartacus 功能,例如 PageMetaService(使用 PageMetaResolver 令牌)或 ConverterService(主要使用适配器序列化器和规范化器),后台使用统一注入器。 通过这样做,他们可以访问延迟加载的令牌,并可以利用它们来实现全局功能。

对于不依赖于统一注入器的机制(例如,来自大多数非 Spartacus 库的功能,例如核心 Angular 库),建议您始终使用这些类型的令牌预先加载模块。

unified injector

统一注入器提供了一种注入令牌或多提供令牌的方法,同时考虑到根注入器和来自延迟加载功能的注入器。 注入器公开一个可观察的对象,每次统一注入器的状态发生变化时,该观察对象都会为指定的令牌发出一组新的可注入对象。

Avoiding Importing the HttpClientModule in Your Lazy-Loaded Modules

一般来说,HttpClientModule 应该在根应用程序中导入,而不是在库中。 例如,如果您将它导入到延迟加载的库中,则根库中的所有注入器对于源自延迟加载模块的 HTTP 调用都是不可见的。

虽然技术上可以在库中导入 HttpClientModule ,但在大多数情况下这不是预期的,并且可能导致难以解释的错误,因此请记住这一点。

Lazy Loading of CMS Components

Configuration of Lazy Loading CMS Components

CMS 代码的延迟加载是通过在 CMS 映射配置中指定动态导入代替静态引用的组件类来实现的。 下面是一个例子:

{
  cmsComponents: {
    SimpleResponsiveBannerComponent: {
      component: () => import('./lazy/lazy.component').then(m => m.LazyComponent)
    }
  }
}

Technical Details

CMS 组件映射中对动态导入的支持是使用可定制的组件处理程序(特别是 LazyComponentHandler)实现的。

可以扩展此处理程序以自定义其行为、添加特殊钩子或不同的触发器,或者实现可以选择性地重用现有处理程序的全新处理程序。

Lazy Loading of Modules

  • 懒加载不仅是组件代码,还有核心部分(包括NgRx状态)

  • 在第一次需要时只加载一次功能

  • 提供共享的、延迟加载的依赖模块

  • 当实现被相关功能配置覆盖时,CMS 请求组件会触发功能模块的延迟加载。

Configuration of Lazy-Loaded Modules

  • 功能模块的动态导入(必须在主应用程序中定义)

  • 有关特定功能涵盖哪些 CMS 组件的信息(可以成为库的一部分并静态导入)。 此信息以 cmsComponents 键下的字符串数组的形式提供。 下面是一个例子:

{
  featureModules: {
    organization: {
      module: () =>
        import('@spartacus/my-account/organization').then(
          (m) => m.OrganizationModule
        ),
      cmsComponents: [
        'OrderApprovalListComponent',
        'ManageBudgetsListComponent',
        'ManageCostCentersListComponent',
        'ManagePermissionsListComponent',
        'ManageUnitsListComponent',
        'ManageUserGroupsListComponent',
        'ManageUsersListComponent',
      ],
    },
  },
}

例子:

[图片上传中...(image.png-c043ed-1625895062179-0)]

Component Mapping Configuration in Lazy-Loaded Modules

延迟加载模块中的默认 CMS 映射配置应该以与静态导入模块完全相同的方式定义。

Spartacus 能够从延迟加载功能中提取 CMS 组件映射配置,并使用它来解析该功能所涵盖的组件类和工厂。 这就是为什么可以并推荐使用在延迟加载模块中提供默认 CMS 映射配置的标准方式的原因。 因此,完全相同的模块和库入口点可以根据需要动态或静态导入,并且仍然可以通过在应用程序中提供配置覆盖来从应用程序级别覆盖延迟加载的 CMS 配置。

Defining Shared-Dependency Modules

通过在功能配置的依赖项属性中提供动态导入数组,可以将一些逻辑提取到共享的延迟加载模块中,该模块可以定义为功能模块的延迟加载依赖项。 下面是一个例子:

{
  featureModules: {
    organization: {
      module: () =>
        import('@spartacus/my-account/organization').then(
          (m) => m.OrganizationModule
        ),
      dependencies: [
        () =>
          import('@spartacus/storefinder/core').then(
            (m) => m.OrganizationModule
          ),
        // ,,
      ],
    },
  },
}

当延迟加载依赖它的第一个特性时,这种未命名的依赖模块只会被实例化一次。 它的提供者为传递给特征模块的组合注入器做出贡献,因此,所有特征服务和组件都可以访问依赖模块提供的服务。

Combined Injector

任何延迟加载的模块都可以从根应用程序注入器和依赖模块注入器注入(即可以访问)服务和令牌。 这是可能的,因为每次实例化具有依赖项的功能模块时都会创建 CombinedInjector。

当一个被延迟加载模块覆盖的 CMS 组件被实例化时,它可以注入(即访问)以下服务:

  • ModuleInjector 层次结构,从功能模块注入器开始,包括依赖模块和根注入器

  • ElementInjector 层次结构,在每个 DOM 元素上隐式创建

Initializing Lazy Loaded Modules

Spartacus 提供了一个 MODULE_INITIALIZER 来代替 Angular APP_INITIALIZER 来初始化延迟加载的模块。

APP_INITIALIZER 机制在任何延迟加载发生之前完成应用程序的初始化,因此在加载时可能需要运行初始化逻辑的延迟加载功能无法这样做。

MODULE_INITIALIZER 注入令牌可用于在旨在延迟加载的模块中提供初始化函数。 MODULE_INITIALIZER 由 Spartacus 延迟加载机制支持,因此,使用 MODULE_INITIALIZER 提供的初始化函数将在它们定义的模块被延迟加载之前运行。

您可以像配置 APP_INITIALIZER 一样配置 MODULE_INITIALIZER。 下面是一个例子:

...

import { MODULE_INITIALIZER } from '@spartacus/core';

...

export function myFactoryFunction(
  dependencyOne: DependencyOne
) {
  const result = () => {
      // add initialization logic here
  };
  return result;
}

@NgModule({
  providers: [
    {
      provide: MODULE_INITIALIZER,
      useFactory: myFactoryFunction,
      deps: [DependencyOne],
      multi: true,
    },
  ],
})
export class MyLazyLoadedModule {}

Preparing Libraries to Work with Lazy Loading

Providing Fine-Grained Entry Points in Your Library

在您的库中提供细粒度的入口点。

从相同的入口点混合静态和动态导入会破坏延迟加载并影响 tree-shaking,因此任何将直接用于动态导入的库都应该公开细粒度的辅助入口点以优化代码拆分。

作为惯例,Spartacus 暴露功能的根入口点,例如@spartacus/orgainzation/administration/root。 这种类型的入口点包含所有不应或不能延迟加载的代码。 来自根入口点的模块应该在根应用程序中静态导入,这意味着它们将被预先加载并在主应用程序块中可用。

Separating Static Code from Lazy-Loaded Code

当您使用 Angular Dependency Injection 时,注入器中的提供程序列表在注入器初始化后不应更改。这种范式特别适用于任何多提供的令牌、处理程序,尤其适用于任何 Angular 原生多提供的令牌,例如 HTTP_INTERCEPTOR、APP_INITIALIZER 等。

结果是延迟加载模块中的任何多提供令牌对于根或其他延迟加载块中提供的模块和服务将不可见,但使用 unified injector 注入的多提供令牌除外。

一些 Spartacus 功能,例如 PageMetaService 或 ConverterService,使用 UnifiedInjector 来了解可以延迟加载的令牌,以便全局逻辑(例如 SEO 功能)即使逻辑延迟加载该功能也能可靠地工作。例如,商店定位器页面元解析器可以在使用商店定位器功能的前提下,被延迟加载。

Spartacus 配置也是通过提供配置块来定义的,由于兼容机制将配置从延迟加载功能贡献到全局配置,因此处理方式略有不同。这种机制可以通过特性标志禁用,将来会默认关闭,以支持统一配置特性。

如果根服务无法看到延迟加载提供程序的问题,则始终可以将此类代码包含在预先可用的静态链接模块中。建议在您的库中创建一个单独的入口点(按照惯例,命名为 root,例如 my-library/root),其中包含最少的代码,将包含在主包中,并且从一开始就可用。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章