webpack5 Module Federation 模块联邦

一、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

三、使用

子应用

  • 公共组件

    
    
    
  • 使用公共组件

    
    
    
  • 配置 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'],
        })
      ]
    }
    
  • 使用子应用

    
    
    
四、效果

子应用

子应用.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中进行配置:声明需要引用得远程应用
    • 使用

      • 组件中使用

        
        
        
      • 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 }
        
        
      • 使用

        组件中使用

        
        
        

        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)
        })
        

参考资料:

  • 三大应用场景调研,Webpack 新功能 Module Federation 深入解析
  • webpack 5 ModuleFederationPlugin vue 项目初体验
  • 探索 webpack5 新特性 Module federation 引发的javascript共享模块变革
  • Webpack 5 Module Federation: JavaScript 架构的变革者
  • 官方文档
  • ModuleFederationPlugin.json
  • 动态远程容器

你可能感兴趣的:(webpack5 Module Federation 模块联邦)