Angular 应用实现 Lazy Load(懒加载)的项目实战经验分享

发布时间 2023-11-18 10:26:16作者: JerryWang_汪子熙

笔者之前两篇掘金社区文章,分别介绍了企业级 Angular 应用开启 PWA 特性和服务器端渲染,从而提升用户体验的两种设计思路:

除了这两种 Angular 开发的最佳实践之外,将 Angular 应用进行庖丁解牛似的拆分,按照应用的业务逻辑,将代码块分离成若干个 Module,每个 Module 通过 Lazy Load 的方式按需加载,也是企业级 Angular 应用经常采取的一种优化手段。

从以上描述不难看出,Lazy Load 和代码拆分(Code Splitting)是相辅相成的。换句话说,代码拆分是 Lazy Load 机制工作的前提条件。代码拆分的结果是,当用户在浏览器地址栏里输入应用 url 时,不必一次性加载整个 Angular 应用程序,而是根据需要,动态加载特定部分的代码。Angular 代码拆分技术,有助于减少应用程序初始加载时间,和减小应用程序的整体大小。

这二者结合起来使用,无疑也是提升用户体验的另一种手段,因为用户可以更快速地开始使用应用,并且避免长时间的等待。

笔者所在的 Angular 开发团队,在开发 Spartacus 这个电商 Storefront 时,从语义化版本(Semantic Version) 的 Major 版本进行迭代时,也曾在项目代码重构过程中,对应用代码进行拆分和引入 Lazy Load 的支持。本文将笔者项目开发中的一些经验分享出来。

下面我们先以 Spartacus 6.0 为例,看看 Lazy Load 的直观效果。

Angular Lazy Load 的表现行为

我们首先在浏览器里打开 Spartacus 首页,在 Chrome 开发者工具 Network 面板观察首页渲染时,需要加载的 JavaScript 资源文件:

图1:Spartacus 首页渲染时加载的资源文件

可以看到包含了 storefinder.js, product.js, product-configurator.js 和 organization.js 等等。这些资源文件包含的都是 Spartacus 核心(Core)功能的实现。

我们首先点击 Clear 图标清除已经捕捉的 Network 请求,然后点击购物车图标,进入购物车页面:

图2:Spartacus 首页渲染所依赖的 core library 的加载行为

此时从 Network 面板能观察到一系列以 feature-libs 前缀开头的资源文件。这些资源文件就是以 Lazy Load 的方式被加载,解析和执行的。

图3:和购物车相关的 Modules 实现了 Lazy Load

显然,用户在访问首页时,只是浏览首页陈列的商品,这个业务行为同购物车的显示,逻辑上没有关联关系,因此将购物车的 UI 和服务,拆分成另一个单独的 Module,并对之施加以 Lazy Load,这是很自然并且合理的设计思路。

如何判断一个 Angular module 已经启用了 Lazy Load

最直接的办法,就是执行命令行 yarn start,在开发模式下查看 module 构建的情况。

如下图所示,Initial Chunk Files 列下的清单,就是使用 Eager Load 加载策略的 Angular module, 这也是 Angular module 缺省的加载方式。我们图 2 的 Network 面板里看到的 JavaScript 文件,都能在这个 Initial Chunk Files 下面找到。

而 Lazy Chunk Files 下给出的则是启用了 Lazy Load 的 module 清单。图3 罗列的点击了 Cart 图标之后出现在 Network 面板里的 JavaScript 文件,则会出现在这个列之下。

Angular module 启用 Lazy Load 的具体实现步骤

了解了 Angular module 启用了 Lazy Load 后的表现行为,以及判断标准,接下来我们就要学习如何启用 Lazy Load 了。

我们从图3 Network 面板里随便挑选一个被 Lazy Load 加载的 JavaScript 文件,比如 feature-libs_cart_quick-order_public_api_ts.js

然后在 Angular 项目里根据这个 public_api 文件,找到对应的 module 实现源代码,如下图所示:

图6:QuickOrderFeatureModule 实现 Lazy Load 的关键代码

@NgModule({
  imports: [QuickOrderRootModule],
  providers: [
    provideConfig({
      featureModules: {
        [CART_QUICK_ORDER_FEATURE]: {
          module: () =>
            import('@spartacus/cart/quick-order').then(
              (m) => m.QuickOrderModule
            ),
        },
      },
      i18n: {
        resources: quickOrderTranslations,
        chunks: quickOrderTranslationChunksConfig,
        fallbackLang: 'en',
      },
    }),
  ],
})
export class QuickOrderFeatureModule {}

其中的核心逻辑和奥妙,就在于第 25 行代码的 import('@spartacus/cart/quick-order').

代码第 22 行 featureModules 是一个映射,它将特性名称(在这里是 CART_QUICK_ORDER_FEATURE)映射到一个特性模块的加载函数。这个加载函数是一个返回 Promise 的函数,当这个 Promise 解析时,它将返回这个特性模块。在上面的源代码中,当用户点击了 Cart 图标需要加载 QuickOrderFeatureModule 功能模块时,Angular 将调用这个函数,动态地导入 QuickOrderModule。这就是 Angular 模块懒加载的关键部分:模块不会在应用启动时加载,而是在真正需要它们的时候才会动态加载

关于 import 关键字的更多详细介绍,可以查看 developer.mozilla.org 的官方文档:

图7:JavaScript 里实现 module 动态加载的 import 关键字

Lazy Load Angular module 的定制化支持

我们团队负责开发的 Spartacus 不仅包含了一个精简版的开箱即用的 Storefront,而且提供了一组 SDK,客户基于这组 SDK,能够快速开发出能够满足自己实际电商业务需求的 Storefront. 那么 Spartacus 标准的 Module,如果开启了 Lazy Load 机制,并且客户又希望对其进行定制,应该如何操作呢?

首先创建一个自定义 Feature Module,在这个 Feature Module 里,使用静态导入的方式,引入我们想要 Lazy Load 的 Spartacus 的标准 Module, 然后在 @NgModule 注解修饰的代码块里,在 providers 区域导入所有需要的 customizations.

下面是一个例子,在代码关键位置我加上了详尽的中文注释。

// 新建一个文件,取名为 custom-rulebased-configurator.module.ts,然后将下面源代码复制进去
import { RulebasedConfiguratorModule } from '@spartacus/product-configurator/rulebased';  // 使用静态导入,引入 Spartacus 标准的 Feature Module

@NgModule({
  imports: [RulebasedConfiguratorModule], // 将标准 Feature Module 导入到自定义 Module 的 imports 区域
  providers: [
    // 此处添加自定义 Module 实际的 Customization 开发
    { provide: ConfiguratorCartService, useClass: CustomConfiguratorCartService }
  ]
})
export class CustomRulebasedConfiguratorModule {}

最后,在 Storefront 应用的 app.module.ts 里,使用 import 关键字提供的动态导入功能,将上述代码里创建的自定义 Module,即 CustomRulebasedConfiguratorModule, 进行 Lazy Load 即可:

provideConfig({
  featureModules: {
    [RULEBASED_PRODUCT_CONFIGURATOR_FEATURE]: {
      module: () =>
        import('../custom-rulebased-configurator.module').then(
          (m) => m.CustomRulebasedConfiguratorModule
        ),
    },
  },
},

以上步骤完成之后,在构建阶段,Webpack 会自动将上述步骤创建的自定义 Module,构建成一个单独的 JavaScript Chunk 文件块, 这也再次印证了 Code Splitting 是发生在构建阶段,而非运行阶段

总结

本文首先介绍了 Angular Module Lazy Load 的基本概念和在企业级 Angular 应用中能给用户带来的价值,从最终用户视角给出了 Module 启用 Lazy Load 之后的表现行为,以及判断一个 Module 是否成功启用 Lazy Load 的标准。作为实战教程,本文也详细介绍了给定一个 Angular Module,如何针对其启用 Lazy Load,以及如何对一个已经启用了 Lazy Load 的 Module 进行定制化开发。