美文网首页
webpack5 Module Federation 模块联邦

webpack5 Module Federation 模块联邦

作者: 带刀打天下 | 来源:发表于2021-06-28 18:10 被阅读0次
    一、Module Federation 介绍

    多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。

    这通常被称作微前端,但并不仅限于此。

    官方文档

    二、配置

    ModuleFederationPlugin

    new ModuleFederationPlugin({
     name: "app1",
     library: { type: "var", name: "app1" },
     filename: "remoteEntry.js",
     remotes: {
        app2: 'app2',
        app3: 'app3',  
    },
      exposes: {
        antd: './src/antd',
        button: './src/button',  
    },
      shared: ['vue', 'vue-router'],
    })
    
    • name:必须,唯一 ID,作为输出的模块名,使用的时通过${name}/${expose} 的方式使用
    • library:其中这里的 name 为作为 umd 的 name。备注:具体使用没查找到资料
    • remotes:声明需要引用的远程应用
    • exposes:远程应用暴露出的模块名
    • shared:共享依赖包

    参数参考资料:ModuleFederationPlugin.json

    三、使用

    子应用

    • 公共组件

      <template>
        <div>
          <button>hahaha</button>
        </div>
      </template>
      <style scoped>
        button {
          font-size: 18px;
          color: red;
        }
      </style>
      
    • 使用公共组件

      <template>
        <div>
          Hello,{{name}}
          <Button/>
        </div>
      </template>
      <script>
      export default {
        components: {
          Button: () => import('../components/Button.vue')
        },
        data () {
          return {
            name: '子应用'
          }
        }
      }
      </script>
      
    • 配置 webpack.config.js 暴露子应用

      const path = require('path')
      const { VueLoaderPlugin } = require('vue-loader')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
      
      module.exports = {
        entry: './src/index.js',
        output: {
          filename: 'bundle.js',
          path: path.join(process.cwd(), '/dist'),
          // publicPath: 'http://localhost:3000/'
        },
        mode: 'development',
        devServer: {
          port: 3000,
          host: '127.0.0.1',
          contentBase: path.join(process.cwd(), "/dist"),
          publicPath: '/',
          open: true,
          hot: true,
          overlay: { errors: true }
        },
        module: {
          rules: [
            {
              test: /\.vue$/,
              loader: 'vue-loader',
              include: [
                path.resolve(process.cwd(), 'src'),
              ]
            },
            {
              test: /\.js$/,
              loader: 'babel-loader',
              exclude: /node_modules/
            },
            {
              test: /\.css$/,
              use: ['style-loader', 'css-loader']
            }
          ]
        },
        plugins: [
          new VueLoaderPlugin(),
          new HtmlWebpackPlugin({
            template: path.resolve(process.cwd(), './index.html'), // 相对于根目录
            filename: './index.html', // 相对于 output 的路径
            inject: 'false',
            minify: {
              removeComments: true // 删除注释
            }
          }),
          new ModuleFederationPlugin({
            name: 'app1', // 应用名 全局唯一 不可冲突
            library: { type: 'var', name: 'app1'}, // UMD 标准导出 和 name 保持一致即可
            filename: 'remoteEntry.js', // 远程应用被其他应用引入的js文件名称
            exposes: { // 远程应用暴露出的模块名
              './Button': './src/components/Button.vue',
            },
            // shared: ['vue'], // 依赖包
          })
        ]
      }
      

    主应用

    • 配置 webpack.config.js

      const path = require('path')
      const { VueLoaderPlugin } = require('vue-loader')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
      
      module.exports = {
        entry: './src/index.js',
        output: {
          filename: 'bundle.js',
          path: path.join(process.cwd(), '/dist'),
          // publicPath: 'http://localhost:3001/'
        },
        mode: 'development',
        devServer: {
          port: 3001,
          host: '127.0.0.1',
          contentBase: path.join(process.cwd(), "/dist"),
          publicPath: '/',
          open: true,
          hot: true,
          overlay: { errors: true }
        },
        module: {
          rules: [
            {
              test: /\.vue$/,
              loader: 'vue-loader',
              include: [
                path.resolve(process.cwd(), 'src'),
              ]
            },
            {
              test: /\.js$/,
              loader: 'babel-loader',
              exclude: /node_modules/
            },
            {
              test: /\.css$/,
              use: ['style-loader', 'css-loader']
            }
          ]
        },
        plugins: [
          new VueLoaderPlugin(),
          new HtmlWebpackPlugin({
            template: path.resolve(process.cwd(), './index.html'), // 相对于根目录
            filename: './index.html', // 相对于 output 的路径
            inject: 'false',
            minify: {
              removeComments: true // 删除注释
            }
          }),
          new ModuleFederationPlugin({
            name: 'app2',
            // library: { type: 'var', name: 'app1' },
            remotes: { // 声明需要引用的远程应用。如上图app1配置了需要的远程应用app2.
              app1: 'app1@http://localhost:3000/remoteEntry.js'
            },
            // shared: ['vue'],
          })
        ]
      }
      
    • 使用子应用

      <template>
        <div>
          Hello,{{name}}
          <Button/>
        </div>
      </template>
      <script>
      export default {
        components: {
          Button: () => import('app1/Button')
        },
        data () {
          return {
            name: '主应用'
          }
        }
      }
      </script>
      
    四、效果

    子应用

    子应用.jpg

    主应用

    主应用.jpg
    五、错误处理
    • output 配置问题

      webpack5 模块联邦 错误.png
    vue.runtime.esm.js:623 [Vue warn]: Failed to resolve async component: function Button() {
          return __webpack_require__.e(/*! import() */ "webpack_container_remote_app1_Button").then(__webpack_require__.t.bind(__webpack_require__, /*! app1/Button */ "webpack/container/remote/app1/Button", 23));
        }
    Reason: ChunkLoadError: Loading chunk vendors-node_modules_vue-hot-reload-api_dist_index_js-node_modules_vue-loader_lib_runtime_com-3bffdf failed.
    

    解决:删除 publicPath 配置,如果需要配置如下

    output: {
        filename: 'bundle.js',
        path: path.join(process.cwd(), '/dist'),
        publicPath: 'http://localhost:3000/'
      },
    
    • 主应用中使用 let store = import('app1/store') 进行引入 store为异步promise,可以使用 await 进行处理,处理如下

      let store = await import('app1/store')
      

      此时会报错,报错信息如下Module parse failed: The top-level-await experiment is not enabled (set experiments.topLevelAwait: true to enabled it)

      webpack5 await.png
      • 解决

        • 安装 @babel/plugin-syntax-top-level-await : npm i @babel/plugin-syntax-top-level-await -D

        • babel.config.js 中进行配置

          module.exports = {
            presets: [
              '@babel/preset-env',
            ],
            plugins: [
              '@babel/plugin-syntax-top-level-await', // 此处为新增配置
              '@babel/plugin-transform-runtime',
            ]
          }
          
        • webpack.config.js 当中配置 experiments topLevelAwait

          module.exports = {
            entry: '',
            output: {},
            mode: ,
            module: {...},
            plugins: [...],
            experiments: {
              topLevelAwait: true, // 此处为新增配置
            }
          }
          
    六、动态远程容器
    • 静态远程容器

      • 在webpack中进行配置,配置如下

        new ModuleFederationPlugin({
           name: 'app2',
           // library: { type: 'var', name: 'app1' },
           remotes: { // 声明需要引用的远程应用。如上图app1配置了需要的远程应用app2.
             app1: 'app1@http://localhost:3000/remoteEntry.js',
           },
           // shared: ['vue'],
        })
        
        • remotes中进行配置:声明需要引用得远程应用
      • 使用

        • 组件中使用

          <template>
            <div class="aaa">
              Hello,{{name}}!
              <Button/>
            </div>
          </template>
          <script>
          export default {
            components: {
              Button: () => import('app1/Button')
            },
            computed: {},
            data () {
              return {
                name: '主应用'
              }
            }
          }
          </script>
          
        • js 中使用

          import Vue from 'vue'
          import App from './app.vue'
          import router from './router/index'
          
          // import store from 'app1/store' // 该用法引入会报错 Uncaught TypeError: Cannot read property 'call' of undefined
          // let store = import('app1/store') // 该引入方式 store 为一个 promise
          let store = await import('app1/store') // 应使用该方式引入
          
          console.log(store, 'store')
          
          new Vue({
            el: "#app",
            // store,
            router,
            render: h => h(App)
          })
          
          
      • 动态远程容器

        • webpack 中不用配置remotes

        • 增加 asyncLoadModules.js 文件

          /**
           * 加载模块
           * @param {*} scope 服务名
           * @param {*} module 子应用导出模块路径
           */
          export const loadComponent = (scope, module) => {
            return async () => {
              console.log(__webpack_init_sharing__, '__webpack_init_sharing__')
              // 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
              console.log(container, 'container')
              console.log(__webpack_share_scopes__.default, '__webpack_share_scopes__.default')
              // 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;
            };
          }
          // 加载 打包好后得 js 文件
          export const useDynamicScript = (url) => {
            return new Promise((resolve, reject) => {
              const element = document.createElement("script")
              element.src = url
              element.type = "text/javascript"
              element.async = true
              element.onload = (e) => {
                resolve(true)
              }
              element.onerror = () => {
                reject(false)
              }
              document.head.appendChild(element)
            })
          }
          
        • 创建 remoteRef.js 文件,引用指定模块

          import { useDynamicScript, loadComponent  } from "./asyncLoadModules";
          
          await useDynamicScript('http://localhost:3000/remoteEntry.js') // 远程模块地址
          
          const { default: store } = await loadComponent('app1', './store')()
          const { default: buttonFromVue2 } = await loadComponent('app1', './Button')()
          
          export { store, buttonFromVue2 }
          
          
        • 使用

          组件中使用

          <template>
            <div class="aaa">
              Hello,{{name}}!
              <buttonFromVue2/>
            </div>
          </template>
          <script>
          import { buttonFromVue2 } from '../remoteRef.js'
          export default {
            components: {
              buttonFromVue2
            },
            computed: {},
            data () {
              return {
                name: '主应用'
              }
            }
          }
          </script>
          

          js 中使用

          import Vue from 'vue'
          import App from './app.vue'
          import router from './router/index'
          import { store } from './remoteRef.js'
          
          console.log(store, 'store')
          
          new Vue({
            el: "#app",
            // store,
            router,
            render: h => h(App)
          })
          

    参考资料:

    相关文章

      网友评论

          本文标题:webpack5 Module Federation 模块联邦

          本文链接:https://www.haomeiwen.com/subject/drfcultx.html