webpack5 Module Federation 模塊聯(lián)邦

一、Module Federation 介紹

多個(gè)獨(dú)立的構(gòu)建可以組成一個(gè)應(yīng)用程序,這些獨(dú)立的構(gòu)建之間不應(yīng)該存在依賴(lài)關(guān)系,因此可以單獨(dú)開(kāi)發(fā)和部署它們奋早。

這通常被稱(chēng)作微前端,但并不僅限于此赠橙。

官方文檔

二耽装、配置

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期揪,作為輸出的模塊名掉奄,使用的時(shí)通過(guò)${name}/${expose} 的方式使用
  • library:其中這里的 name 為作為 umd 的 name。備注:具體使用沒(méi)查找到資料
  • remotes:聲明需要引用的遠(yuǎn)程應(yīng)用
  • exposes:遠(yuǎn)程應(yīng)用暴露出的模塊名
  • shared:共享依賴(lài)包

參數(shù)參考資料:ModuleFederationPlugin.json

三凤薛、使用

子應(yīng)用

  • 公共組件

    <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: '子應(yīng)用'
        }
      }
    }
    </script>
    
  • 配置 webpack.config.js 暴露子應(yīng)用

    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'), // 相對(duì)于根目錄
          filename: './index.html', // 相對(duì)于 output 的路徑
          inject: 'false',
          minify: {
            removeComments: true // 刪除注釋
          }
        }),
        new ModuleFederationPlugin({
          name: 'app1', // 應(yīng)用名 全局唯一 不可沖突
          library: { type: 'var', name: 'app1'}, // UMD 標(biāo)準(zhǔn)導(dǎo)出 和 name 保持一致即可
          filename: 'remoteEntry.js', // 遠(yuǎn)程應(yīng)用被其他應(yīng)用引入的js文件名稱(chēng)
          exposes: { // 遠(yuǎn)程應(yīng)用暴露出的模塊名
            './Button': './src/components/Button.vue',
          },
          // shared: ['vue'], // 依賴(lài)包
        })
      ]
    }
    

主應(yīng)用

  • 配置 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'), // 相對(duì)于根目錄
          filename: './index.html', // 相對(duì)于 output 的路徑
          inject: 'false',
          minify: {
            removeComments: true // 刪除注釋
          }
        }),
        new ModuleFederationPlugin({
          name: 'app2',
          // library: { type: 'var', name: 'app1' },
          remotes: { // 聲明需要引用的遠(yuǎn)程應(yīng)用姓建。如上圖app1配置了需要的遠(yuǎn)程應(yīng)用app2.
            app1: 'app1@http://localhost:3000/remoteEntry.js'
          },
          // shared: ['vue'],
        })
      ]
    }
    
  • 使用子應(yīng)用

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

子應(yīng)用

子應(yīng)用.jpg

主應(yīng)用

主應(yīng)用.jpg
五缤苫、錯(cuò)誤處理
  • output 配置問(wèn)題

    webpack5 模塊聯(lián)邦 錯(cuò)誤.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/'
  },
  • 主應(yīng)用中使用 let store = import('app1/store') 進(jìn)行引入 store為異步promise,可以使用 await 進(jìn)行處理活玲,處理如下

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

    此時(shí)會(huì)報(bào)錯(cuò)涣狗,報(bào)錯(cuò)信息如下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 中進(jìn)行配置

        module.exports = {
          presets: [
            '@babel/preset-env',
          ],
          plugins: [
            '@babel/plugin-syntax-top-level-await', // 此處為新增配置
            '@babel/plugin-transform-runtime',
          ]
        }
        
      • webpack.config.js 當(dāng)中配置 experiments topLevelAwait

        module.exports = {
          entry: '',
          output: {},
          mode: ,
          module: {...},
          plugins: [...],
          experiments: {
            topLevelAwait: true, // 此處為新增配置
          }
        }
        
六、動(dòng)態(tài)遠(yuǎn)程容器
  • 靜態(tài)遠(yuǎn)程容器

    • 在webpack中進(jìn)行配置舒憾,配置如下

      new ModuleFederationPlugin({
         name: 'app2',
         // library: { type: 'var', name: 'app1' },
         remotes: { // 聲明需要引用的遠(yuǎn)程應(yīng)用镀钓。如上圖app1配置了需要的遠(yuǎn)程應(yīng)用app2.
           app1: 'app1@http://localhost:3000/remoteEntry.js',
         },
         // shared: ['vue'],
      })
      
      • remotes中進(jìn)行配置:聲明需要引用得遠(yuǎn)程應(yīng)用
    • 使用

      • 組件中使用

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

        import Vue from 'vue'
        import App from './app.vue'
        import router from './router/index'
        
        // import store from 'app1/store' // 該用法引入會(huì)報(bào)錯(cuò) Uncaught TypeError: Cannot read property 'call' of undefined
        // let store = import('app1/store') // 該引入方式 store 為一個(gè) promise
        let store = await import('app1/store') // 應(yīng)使用該方式引入
        
        console.log(store, 'store')
        
        new Vue({
          el: "#app",
          // store,
          router,
          render: h => h(App)
        })
        
        
    • 動(dòng)態(tài)遠(yuǎn)程容器

      • webpack 中不用配置remotes

      • 增加 asyncLoadModules.js 文件

        /**
         * 加載模塊
         * @param {*} scope 服務(wù)名
         * @param {*} module 子應(yīng)用導(dǎo)出模塊路徑
         */
        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)
          })
        }
        
      • 創(chuàng)建 remoteRef.js 文件珍剑,引用指定模塊

        import { useDynamicScript, loadComponent  } from "./asyncLoadModules";
        
        await useDynamicScript('http://localhost:3000/remoteEntry.js') // 遠(yuǎn)程模塊地址
        
        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: '主應(yīng)用'
            }
          }
        }
        </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)
        })
        

參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末招拙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子措译,更是在濱河造成了極大的恐慌别凤,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件领虹,死亡現(xiàn)場(chǎng)離奇詭異规哪,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)塌衰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)诉稍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蝠嘉,“玉大人,你說(shuō)我怎么就攤上這事杯巨≡楦妫” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵服爷,是天一觀的道長(zhǎng)杜恰。 經(jīng)常有香客問(wèn)我,道長(zhǎng)仍源,這世上最難降的妖魔是什么心褐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮笼踩,結(jié)果婚禮上逗爹,老公的妹妹穿的比我還像新娘。我一直安慰自己戳表,他們只是感情好桶至,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著匾旭,像睡著了一般镣屹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上价涝,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天女蜈,我揣著相機(jī)與錄音,去河邊找鬼色瘩。 笑死伪窖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的居兆。 我是一名探鬼主播覆山,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼泥栖!你這毒婦竟也來(lái)了簇宽?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吧享,失蹤者是張志新(化名)和其女友劉穎魏割,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體钢颂,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钞它,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遭垛。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡尼桶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耻卡,到底是詐尸還是另有隱情疯汁,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布卵酪,位于F島的核電站幌蚊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏溃卡。R本人自食惡果不足惜溢豆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瘸羡。 院中可真熱鬧漩仙,春花似錦、人聲如沸犹赖。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)峻村。三九已至麸折,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粘昨,已是汗流浹背垢啼。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留张肾,地道東北人芭析。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吞瞪,于是被迫代替她去往敵國(guó)和親馁启。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容