公共模块之模块联邦

发布时间 2023-08-19 20:37:04作者: 空山与新雨

目录

前言

工作中公共模块通过子仓库在多个项目中使用,其中公共头部,登录,反馈、举报等模块业务与技术栈都和项目耦合很深,在每个项目都会将这些公共模块打包进去,为了减少流量成本,考虑将这些模块打包后放到cdn,对比了webpack中external、 dll、模块联邦方案,最终选择模块联邦。

external 方案 :external的场景是将三方模块当做外链引入, 依赖在公共模块和项目中无法共享;当然如果将依赖都通过external加载,可以解决共享问题,多版本同时存在的问题又无法解决。并且无法做到按需加载。

dll 方案:dll 使用场景是公共模块的处理,问题和external方案差不多

模块联邦概念

Container 容器
ModuleFederationPlugin 处理后打包出来的模块被称为 Container,可以加载其他的 Container,可以被其他的 Container 加载;

Host 宿主容器

消费其它容器的容器可以称为Host,一般动态去加载remote容器。

Remote 远程容器

被其他容器消费的容器可以称为Remote; 主要是导出模块供其他模块消费

因为Host和Remote都是Container,所以两者是相对的,一个Container既可以是Host,也可以是Remote;

Shared 共享

本地和远程可以共享的依赖

使用配置

Remote和Host使用同一个插件,在配置上有些许不同;

Remote例子:

new ModuleFederationPlugin({
  name: 'vue2App',
  filename: 'remoteEntry.js',
  library: { type: 'var', name: 'vue2App' },
  exposes: {
    './vue2': './node_modules/vue/dist/vue',
    './Button': './src/components/Button',
  },
  shared: {
    lodash: {
      strictVersion: true,
      requiredVersion: '^3',
    }
  }
}),

name字段是用来配置模块容器的名字,当用作被消费的容器的时候(remote) name字段是必需的。

filename 打包后产物的文件名,在host中配置的remotes字段中的文件链接就是这个文件。

exposes 指定需要导出的模块

library 指定打包产物的模块类型,在浏览器中使用一般配置为 { type: 'var', name: '自定义的全局变量' }

shared 指定需要共享的依赖模块,如果两个应用都使用了相同的依赖,则可以使用 shared 来共享依赖,减少资源加载量;支持使用semver规范配置兼容。

shared: [
  "lodash/",
  {
    react: {
      import: false,
      singleton: true
    }
  }
]

其中的requiredVersion使用Remote容器中配置的。 Host端可以配置version表示版本。

remotes 指定使用远程模块的别名和地址,地址的格式是固定的,@ 前面的名字(比如:vue2App) 需要和模块的library.name保持一致。Host端使用

new ModuleFederationPlugin({
    name: "main_app",
    remotes: {
        vue2App: 'vue2App@http://localhost:3001/remoteEntry.js'
    },
    shared: ['vue']
})

模块联邦优点

  1. 配置简单灵活

  2. 支持独立部署,上线

  3. 依赖共享机制;

  4. 容器可以嵌套使用,容器可以使用来自其他容器的模块。容器之间也可以循环依赖。

  5. 可以同步和异步方式都可以使用

    虽然vue2App/Button来自于remote,但是可以用同步的方式使用。

    import Vue2Button from 'vue2App/Button';
    console.log(Vue2Button)
    
    

    同样可以异步使用

    import('vue2App/Button').then(()=>{})
    

模块联邦缺点

  1. 模块联邦对runtime 运行时做了大量改造,会对我们页面的运行时性能造成一定的负面影响
  2. 需要额外处理远程模块的版本管理

动态远程模块

联邦模块的remote地址是一个固定的地址,实际开发是需要区分开发、测试、灰度、生产环境的,甚至需要区分版本,这时候怎么能让这个地址成为动态呢?

  1. 方案一, 通过插件将remotes地址将指定格式的表示式解析为环境依赖的变量,比如下面的[window.xinyu_version_prod]在页面执行的时候会取当前页面的window.xinyu_version_prod变量,所以如果能在页面脚本执行前把变量注入,就能实现动态加载远程模块。

    官方的demo中一个动态remote地址的例子,有人整理成了一个webpack插件

    const ExternalTemplateRemotesPlugin = require('external-remotes-plugin');
    
    const DynamicAddress = process.env.NODE_ENV === 'develop' 
      ? 'https://xxxxx/[window.xinyu_version_test]' : 'https://xxxxx/[window.xinyu_version_prod]';
    
    plugins:[
      new ModuleFederationPlugin({
        name: 'detail2022',
        remotes: {
          xinyu: `xinyu@${DynamicAddress}/remoteEntry.js`,
        },
      }),
      new ExternalTemplateRemotesPlugin(),
    
    ]
    
    
  2. 方案二 不通过remotes配置远程模块,而是手动获取远程模块,官方文档链接github例子链接

    通过手动加载remoteEntry.js之后,再通过webpack提供的方法获取模块,这样就可以在代码中动态加载。下面是一个例子

    function loadComponent(scope, module) {
      return async () => {
        // Initializes the share scope. This fills it with known provided modules from this build and all remotes
        await __webpack_init_sharing__('default');
        const container = window[scope]; // or get the container somewhere else
        // Initialize the container, it may provide shared modules
        await container.init(__webpack_share_scopes__.default);
        const factory = await window[scope].get(module);
        const Module = factory();
        return Module;
      };
    }
    
    function loadScript(url){
    
      return new Promise((resolve,reject)=>{
        const element = document.createElement('script');
    
        element.src = url;
        element.type = 'text/javascript';
        element.async = true;
    
        element.onload = () => {
          resolve()
        };
        document.body.appendChild(element);
      })
    
    }
    
    components: {
      Vue2Button: defineAsyncComponent(() => {
        return new Promise(async(resolve) => {
          await loadScript('http://localhost:3001/remoteEntry.js'); // 根据环境使用不同的地址,以做到动态获取模块
          const Button = await loadComponent('xinyu', './Button')();
          const vue2 = await loadComponent('xinyu', './vue2')();
          window.Vue2 = vue2;
          resolve(vue2ToVue3(Button.default, 'vue2Button'))
        })
      })
    },
    

    缺点:不能利用webpack对异步的同步处理,代码繁琐。

    import add from 'xinyu/add'; // 需要自己load代码,无法这样同步使用了
    
    add()
    
  3. 基于promise的动态远程模块,文档

    remote容器暴露的全局变量上只有这两个方法,通过代理的方式修改这两个方法,实现动态remote.

    该方案和external-remotes-plugin的使用效果差不多,都可以根据环境来动态切换远程模块。