模块联邦共享依赖在微前端中的实践分享

697 阅读6分钟

背景

在微前端中,微应用加载速度是一个非常影响体验的问题,虽然qiankun等微前端框架提供了preload的方式,但在某些情况下仍然有局限。

那么,有没有什么办法可以从根本上减少微应用(在非独立运行时)的体积呢?

稍加分析我们可以看到,在基座应用与微应用同时依赖一些三方库时,这些库都被重复加载

而webpack5的模块联邦这个新特性可以有效地减少微应用加载js文件大小,共享依赖,加速微应用启动,甚至是打包体积、速度(由配置决定)

原理

每个构建都充当一个容器,也可将其他构建作为容器。通过这种方式,每个构建都能够通过从对应容器中加载模块来访问其他容器暴露出来的模块。

共享模块是指既可重写的又可作为向嵌套容器提供重写的模块。它们通常指向每个构建中的相同模块,例如相同的库。

packageName 选项允许通过设置包名来查找所需的版本。默认情况下,它会自动推断模块请求,当想禁用自动推断时,请将 requiredVersion 设置为 false 。 -- webpack

实践

本文技术栈:

微前端框架使用了qiankun,子应用与基座应用使用了相同的技术栈,如react、antd、ahooks...等,构建工具均采用webpack5+版本

适用场景:微应用与基座应用拥有共同依赖的三方库

基座应用配置

const path = require('path')
const baseConfig = require('./webpack.base.cjs')
const { merge } = require('webpack-merge')
const { ModuleFederationPlugin } = require('webpack').container
const deps = require('../package.json').dependencies

module.exports = merge(baseConfig, {
  plugins: [
    new ModuleFederationPlugin({
      name: 'baseWeb',
      filename: 'remoteEntry.js',
      shared: {
        react: {
          singleton: true,
          requiredVersion: deps.react,
          shareKey: 'react',
          shareScope: 'default',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
          shareKey: 'react-dom',
          shareScope: 'default',
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: deps['react-router-dom'],
          shareKey: 'react-router-dom',
          shareScope: 'default',
        },
        antd: {
          singleton: true,
          requiredVersion: deps.antd,
          shareKey: 'antd',
          shareScope: 'default',
        },
        '@ant-design/icons': {
          singleton: true,
          requiredVersion: deps['@ant-design/icons'],
          shareKey: '@ant-design/icons',
          shareScope: 'default',
        },
        firebase: {
          singleton: true,
          requiredVersion: deps.firebase,
          shareKey: 'firebase',
          shareScope: 'default',
        },
        ahooks: {
          singleton: true,
          requiredVersion: deps.ahooks,
          shareKey: 'ahooks',
          shareScope: 'default',
        },
      },
    }),
  ],
})

singleton是这里的重要配置,代表多个应用使用同一份依赖,关于版本兼容的配置问题大家自行解决

远程应用配置

remotes

const packageName = require('../package.json').name
const path = require('path')
const { ModuleFederationPlugin } = require('webpack').container
const deps = require('../package.json').dependencies

module.exports = {

  plugins: [
    new ModuleFederationPlugin({
      name: 'aiMaterials',
      filename: 'remoteEntry.js',
      remotes: {
        baseWeb: 'baseWeb@https://yourdomain/remoteEntry.js',
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: deps.react,
          shareKey: 'react',
          import: false,
          shareScope: 'default',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
          shareKey: 'react-dom',
          import: false,
          shareScope: 'default',
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: deps['react-router-dom'],
          shareKey: 'react-router-dom',
          import: false,
          shareScope: 'default',
        },
        antd: {
          singleton: true,
          requiredVersion: deps.antd,
          shareKey: 'antd',
          import: false,
          shareScope: 'default',
        },
        '@ant-design/icons': {
          singleton: true,
          requiredVersion: deps['@ant-design/icons'],
          shareKey: '@ant-design/icons',
          import: false,
          shareScope: 'default',
        },
        firebase: {
          singleton: true,
          requiredVersion: deps.firebase,
          shareKey: 'firebase',
          import: false,
          shareScope: 'default',
        },
        ahooks: {
          singleton: true,
          requiredVersion: deps.ahooks,
          shareKey: 'ahooks',
          import: false,
          shareScope: 'default',
        },
      },
    }),
  ],
}

Import:false代表不将共享的依赖打包进dist中,谨慎开启这个选项,如果你想要让微应用独立运行。在提供共享依赖的容器应用中,一定不要打开这个选项。

踩坑

js加载时序问题

现象

配置完模块联邦,打开应用时我们会发现页面白屏并且报错:

Uncaught Error: Shared module is not available for eager consumption: 9645

image.png

分析
  • Eager Consumption(急切加载)意味着代码试图在 Webpack 运行时完全准备好之前直接使用共享模块

  • 主要问题在于初始化顺序:

    • Webpack 运行时需要先初始化
    • 共享模块容器需要被正确注册
    • 模块联邦的远程模块需要被加载
    • 但在急切加载模式下,代码试图在这些步骤完成之前就使用共享模块

解决
  1. 方案一:我们需要将main.tsx的内容转移到bootstrap文件中,并且异步加载bootstrap文件。
// main.tsx 修改为如下内容,并且将main.tsx之前的内容转移到./bootstrap里
import('./bootstrap')

官方文档其实说的很清楚,所以这里也警醒一下自己,将官方文档作为一手资料,其实没有那么多坑

webpack.docschina.org/concepts/mo…

  1. 方案二:开启配置eager:true可以将共享依赖打包进入口文件中,以解决这个问题,但这样可能导致入口文件过大,打开基座应用变慢,大家自行选择

qiankun报错

现象

同理,我们也需要改造远程应用,以解决同样的加载时序问题。

但改造完之后发现微应用仍然白屏,console报错:(如果你没有使用qiankun,忽略这一条)

(或者是,加载的js的path有问题,都是一样的原理)

Uncaught n: application 'ai-materials' died in status LOADING_SOURCE_CODE: [qiankun]: You need to export lifecycle functions in ai-materials entry

image.png 这是因为我们改造了main.tsx入口文件,所以缺少了qiankun所需的配置。可以封装一下,导出qiankun所需的lifecycle

解决:
// 远程应用中的main.tsx
let isQiankun = false

if ((window as any).__POWERED_BY_QIANKUN__) {
// 解决js文件加载path问题,qiankun老生常谈的问题,不做过多解释
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
  isQiankun = true
}

// 创建异步加载函数
const loadApp = async () => {
  // 等待共享作用域初始化
  await __webpack_init_sharing__?.('default')
  // 初始化远程容器
  const container = window.baseWeb
  // 初始化容器
  await container?.init?.(__webpack_share_scopes__.default)
  const { bootstrap, mount, unmount, render } = await import('./bootstrap')
  return { bootstrap, mount, unmount, render }
}

// 导出异步加载的生命周期函数
export const bootstrap = async () => {
  const { bootstrap: bootstrapFn } = await loadApp()
  return bootstrapFn()
}

export const mount = async (props: any) => {
  const { mount: mountFn } = await loadApp()
  return mountFn(props)
}

export const unmount = async (props: any) => {
  const { unmount: unmountFn } = await loadApp()
  return unmountFn(props)
}

// 非 qiankun 环境下独立运行
if (!isQiankun) {
  loadApp().then(({ render }) => {
    render(document.getElementById('ai-materials-root'))
  })
}
如果你发现没有效果

请检查这一段前文提到的这一段逻辑有没有加在远程应用中

webpack.docschina.org/concepts/mo…

--webpack

  // 等待共享作用域初始化
  await __webpack_init_sharing__?.('default')
  // 初始化远程容器
  const container = window.baseWeb
  // 初始化容器
  await container?.init?.(__webpack_share_scopes__.default)

成果

我们可以打开network检查依赖共享的效果:

左边为依赖共享前,右边为依赖共享后,可以看到js文件的大小减少了250KB左右减少到30KB不到(gzip后),当然因为我的远程应用没有写太多业务逻辑,基本上是一个空壳,所以不算百分比了。如果你的项目中微应用与基座应用的共同依赖更多,效果就更好。本文也只是将一些非常常用的三方库做了依赖共享

在压缩体积后,微应用的启动更像是一个页面内的不同路由一样,切换十分丝滑

共享依赖前:

image.png

共享依赖后:

image.png

在开启了import:false(前文远程应用配置所提到)这个配置后,我们可以估算gzip前节省的js体积

在开启import:false前,打包体积大约是1.3 MB,而在开启配置后,打包体积仅有166KB, 节约了构建体积约~1.2MB+

你可以在build中看到消费共享依赖的日志

当然,这个配置与你决定是否允许微应用独立运行相关,如果你希望微应用可以独立运行,不要设置这个配置。

tips:当你不设置import:false时,build体积相比配置模块联邦之前可能会增大,这是因为webpack要支持独立运行 / 共享依赖2种情况,打包的js文件会增多