模块联邦,这个名字也挺奇怪的,我感觉叫动态引用 更合适,有点像 动态库 .so 的这种感觉。
直接依赖远程 js,其映射关系为供给方生成的一个 js,用于查找其库的真实地址。
我们一直也这么用的,比如 vconsole 这种工具,一种方式就是直接加载线上脚本使用。
但这种方式不够工程化,引用、打包、使用时,需要特殊对待,不像本地包那样可以直接 import 'xxx'。
ModuleFederationPlugin 就是提供一种方式,可以将想共享的内容发布到线上,使用方再通过类似本地包的形式去使用它。
供给方 A 的配置:
目标是生成用于共享的库 (font.js, theme.js ...),和用于查找库地址的清单 (entry.js),
            
            
              js
              
              
            
          
          new ModuleFederationPlugin({
  name: 'app_frame',
  // version 号主要是怕升级多台机器过程中,半半拉拉的时候,有 gap,线上一些依赖找不到了,保留老的清单映射
  // filename 就是 库对应清单 js 了
  filename: 'static/js/remote-entry-v2.js',
  // 这是 entry.js 暴露方式,直接在 window 上暴露变量了
  library: { type: 'var', name: 'app_frame' },
  // 需要共享的内容
  exposes: {
    './font.css': path.resolve(paths.appSrc, './css/font.css'),
    './theme.css': path.resolve(paths.appSrc, './css/theme/index.css'),
  },
  // 一些可以共享的库,先按下不讲
  shared: {
    react: {
      // 这个库(比如react)在整个页面上只能有一个实例。如果 B 发现 A 提供的React版本不兼容,它不会加载自己的版本,而是会直接报错
      singleton: true,
      requiredVersion: deps.react,
    },
    'react-dom': {
      singleton: true,
      requiredVersion: deps['react-dom'],
    },
    'react/jsx-runtime': {
      singleton: true,
      requiredVersion: deps.react,
    },
  },
}),需求方 B 的配置:
目的是引用上面生成的 entry.js,然后找到对应共享库
            
            
              js
              
              
            
          
          new ModuleFederationPlugin({
  // 库清单 entry.js 的地址
  remotes: {
    // key 在项目内自行定义, name 为提供容器内 name 字段
    app_frame: `app_frame@https://xxx.com/static/js/remote-entry-v2.js`,
  },
  // shared 这里和上面重复了发现没?其实说的就是:
  // 如果上面清单里有这些库的地址,我就不用我自己生成的了,直接复用清单里的
  shared: {
    react: {
      requiredVersion: reactVersion,
    },
    'react-dom': {
      requiredVersion: reactDomVersion,
    },
    'react/jsx-runtime': {
      requiredVersion: reactVersion,
    },
  },
}),B 在页面中使用的时候,就这么用:
            
            
              js
              
              
            
          
          import 'app_frame/font.css';
import 'app_frame/theme.css';流程
- 
A 先 build 生成 entry.js,font.[hash].js,theme.[hash].js,abc.[hash].js... 放到自己的网站上
- 
B 打开了,先加载 entry.js(因为它是入口,地址也是固定的)- 
找到 font.css/theme.css的地址,加载它们
- 
entry.js中定义的shared的库- 如果满足自己的版本需求,就复用 entry.js里的shared库(直接加载entry.js中的对应地址)
- 如果不满足,就不看它了,加载自己的 .js
 
- 如果满足自己的版本需求,就复用 
 
- 
更进一步
其实上面已经看出端倪了,shared 的都是一些不会经常变的库,这几个要是整合到一起,然后正好也满足 B 的需求,B 就不用加载自己的那份了嘛:
给 A 加上一个提取这几个库的功能:
            
            
              js
              
              
            
          
          optimization: {
  splitChunks: {
    cacheGroups: {
       firmware: {
        test: new RegExp(`node_modules${stp}(react|react-dom)([${stp}]|$)`),
        name: 'firmware',
        chunks: 'all',
        enforce: true,
        priority: 30,
      },
    }
  }
}这样把这三个 react 相关的,放到了 firmware.[hash].js 中,A 当然本身就会加载它,在 B 里我们也做类似的配置:
            
            
              js
              
              
            
          
          optimization: {
  splitChunks: {
    cacheGroups: {
       firmware: {
        test: new RegExp(`node_modules${stp}(react|react-dom)([${stp}]|$)`),
        // 注意这里换了个名字
        name: 'contained-firmware',
        chunks: 'all',
        enforce: true,
        priority: 30,
      },
    }
  }
}这里换了个名字,主要是为了方便验证,到底加载的是哪个 js。
- 如果复用成功,只会加载 firmware.[hash].js,两次(第二次就有浏览器缓存了)
- 如果复用失败,A 加载 firmware.[hash].js,B 加载contained-firmware.[hash].js
如何保证复用成功?
其实很简单,只要保证版本一致就行了,在上面 ModuleFederationPlugin 的配置里,已经写上了。
刷新下,可以观察到,只去加载 firmware.[hash].js 的。