一、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
三、使用
子应用
-
公共组件
-
使用公共组件
Hello,{{name}} -
配置
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'], }) ] }
-
使用子应用
Hello,{{name}}
四、效果
子应用
主应用
五、错误处理
-
output 配置问题
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)
-
解决
安装
@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
中进行配置:声明需要引用得远程应用
- 在
-
使用
-
组件中使用
Hello,{{name}}! -
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 }
-
使用
组件中使用
Hello,{{name}}!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
- 动态远程容器