一、Module Federation 介紹
多個(gè)獨(dú)立的構(gòu)建可以組成一個(gè)應(yīng)用程序,這些獨(dú)立的構(gòu)建之間不應(yīng)該存在依賴(lài)關(guān)系,因此可以單獨(dú)開(kāi)發(fā)和部署它們奋早。
這通常被稱(chēng)作微前端,但并不僅限于此赠橙。
二耽装、配置
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)用
五缤苫、錯(cuò)誤處理
-
output 配置問(wèn)題
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)
-
解決
安裝
@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) })
-
參考資料: