定义
webpack5 新增了模块联邦的能力,项目之间形成共享作用域,可以在一个项目里使用其他项目中的组件,实现跨应用的组件共享。项目A引用了项目B中的组件,A为宿主系统,B为远程系统,通过在B中暴露模块,在A中配置引入的项目名以及引用的组件等,这样在A的业务代码中可以直接通过 import() 来引入组件。
环境
- webpack5.38.1
- vue2.6
注意,这里不能使用vuecli
webpack基本配置
- package.json
{
"name": "vue04",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/plugin-transform-runtime": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@vue/cli-plugin-babel": "^4.5.14",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^5.2.6",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.1",
"less": "^4.1.1",
"less-loader": "^9.0.0",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.7",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"chalk": "^4.1.1",
"vue": "^2.6.14",
"vue-router": "^3.5.1",
"vuex": "^3.6.2"
}
}
- webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
entry: './src/main.js', // 定义入口文件
output: {
// publicPath: '/',
path: path.join(__dirname, '/dist'), // 打包生成文件地址
filename: '[name].bundle.js' // 生成的文件名
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime']
}
},
exclude: /node_modules/ // 不编译node_modules下的文件
},
{
// *.vue
test: /\.vue$/,
loader: 'vue-loader'
},
{
// `*.vue` 文件中的 `<style>` 块以及普通的`*.css`
test: /\.(css|less)$/,
use: ['vue-style-loader', 'css-loader','less-loader']
},
{
// 图片
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10kb
}
}
}
]
},
// 解析路径
resolve: {
// 设置src别名
alias: {
'@': path.resolve(__dirname, 'src'),
},
//后缀名 可以根据需要自由增减
extensions: ['.js', '.vue']
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 生成的文件夹名
template: 'public/index.html', // 模板html
favicon: 'public/favicon.ico', // 图标
}),
new VueLoaderPlugin(),
// 这里我用的是目标配置
new ModuleFederationPlugin({
name: "app2",
remotes: {
app1: 'app1@http://127.0.0.1:8081/remoteEntry.js',
},
})
// 源配置
/*
new ModuleFederationPlugin({
// 提供给其他服务加载的文件
filename: "remoteEntry.js",
// 唯一ID,用于标记当前服务
name: "app1",
// 需要暴露的模块,使用时通过 `${name}/${expose}` 引入
exposes: {
'./HelloWorld': './src/components/HelloWorld.vue',
},
})
*/
]
}
- webpack.dev.js
const webpack = require('webpack')
const path = require('path')
const { merge } = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devServer: {
//contentBase: path.join(__dirname, 'dist'), // 告诉服务器内容的来源。仅在需要提供静态文件时才进行配置
compress: true, // 开启压缩
port: 8081, // 端口
hotOnly: true, // 热模块替换,保存页面状态
hot: true, // 自动刷新
open: false,
historyApiFallback: {
index: '/index.html' //与output的publicPath有关(HTMLplugin生成的html默认为index.html)
},
},
devtool: 'source-map',
plugins: [
new webpack.HotModuleReplacementPlugin(), // 配合 devServer:hot 使用,自动刷新页面
]
})
- webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin()
]
})
配置
app1
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
// 提供给其他服务加载的文件
filename: "remoteEntry.js",
// 唯一ID,用于标记当前服务
name: "app1",
// 需要暴露的模块,使用时通过 `${name}/${expose}` 引入
exposes: {
'./HelloWorld': "./src/components/HelloWorld.vue",
}
})
app2
new ModuleFederationPlugin({
name: "app2",
remotes: {
//文件app1的name filename
app1: "app1@http://localhost:8081/remoteEntry.js",
}
})
Home.vue
动态组件
<template>
<div class="home">
<HelloWorld msg="123123qweqwe" />
</div>
</template>
<script>
// @ is an alias to /src
import img from '@/assets/logo.png'
export default {
name: "Home",
components: {
HelloWorld: () => import('vue03/HelloWorld')
},
};
</script>
<style>
h1{
color: darkred;
}
</style>
### 原理分析
app2引入app1方法:
main.js
```javascript
module.exports = new Promise((resolve, reject) => {
if(typeof app1 !== "undefined") return resolve();
__webpack_require__.l("http://127.0.0.1:8081/remoteEntry.js", (event) => {
if(typeof app1 !== "undefined") return resolve();
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
__webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
__webpack_error__.name = 'ScriptExternalLoadError';
__webpack_error__.type = errorType;
__webpack_error__.request = realSrc;
reject(__webpack_error__);
}, "app1");
}).then(() => (app1));
导出了一个异步函数,去请求了我们配置的app1 的入口文件,获取到remoteEntry.js 返回参数
remoteEntry.js
var app1;
var moduleMap = {
"./HelloWorld": () => {
return Promise.all([__webpack_require__.e("vendors-node_modules_css-loader_dist_runtime_api_js-node_modules_css-loader_dist_runtime_cssW-8dfd4b"), __webpack_require__.e("src_components_HelloWorld_vue")]).then(() => (() => ((__webpack_require__(/*! ./src/components/HelloWorld.vue */ "./src/components/HelloWorld.vue")))));
}
};
var get = (module, getScope) => {
__webpack_require__.R = getScope;
getScope = (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error('Module "' + module + '" does not exist in container.');
})
);
__webpack_require__.R = undefined;
return getScope;
};
var init = (shareScope, initScope) => {
if (!__webpack_require__.S) return;
var oldScope = __webpack_require__.S["default"];
var name = "default"
if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
__webpack_require__.S[name] = shareScope;
return __webpack_require__.I(name, initScope);
};
// This exports getters to disallow modifications
__webpack_require__.d(exports, {
get: () => (get),
init: () => (init)
});
此文件中定义了app1 的全局变量,这边变量导出了两个方法,git, init 当我们请求app1/HelloWorld时调用get 方法异步请求了文件名为src_components_HelloWorld_vue的文件。
src_components_HelloWorld_vue.bundle.js 此为Header 组件的一个webpack chunk 包文件,可运行在浏览器中
var staticRenderFns = [
function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", { staticClass: "hello" }, [
_c("h1", [_vm._v("我是helloworld")])
])
}
]
通过上述我们可以简单总结,跨项目代码共享,要求需要共享的项目代码根据配置文件的导出模块,进行单独打包,生成对应的modules,然后通过一个全局变量建立起两个不同项目之间的连接。
适用场景
适用于新建专门的组件应用服务来管理全部组件和应用,其余业务层只须要根据本身业务所需载入对应的组件和功能模块便可。
效果
优点
- 可以跨项目组件共享,减小项目体积
不足
本地应用和远程应用的技术栈和版本必须兼容,统一用同一套
路由共享(微前端)
app1项目
- 在src目录下新建routes.js,将路由数组导出
import Home from '@/views/Home'
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about1",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"),
},
];
export default routes
- router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import routes from '@/router/routes'
Vue.use(VueRouter);
const router = new VueRouter({
mode: "history",
base: '/app1/',
routes
});
export default router;
- About.vue
<template>
<div class="about">
<h1>This is an about page123123</h1>
</div>
</template>
<style scoped>
h1{
color: orangered;
}
</style>
- webpack.common.js
new ModuleFederationPlugin({
// 提供给其他服务加载的文件
filename: "remoteEntry.js",
// 唯一ID,用于标记当前服务
name: "app1",
// 需要暴露的模块,使用时通过 `${name}/${expose}` 引入
exposes: {
// 路由导出
'./routes': './src/router/routes.js'
},
})
app2项目
由于项目模块只能通过异步加载,所以我这里把它放在了App.vue里面
- App.vue
<script>
const routers1 = () => import("vue03/routes")
export default {
mounted() {
let result = []
new Promise((resolve) => {
resolve(routers1())
}).then(data => {
result = data.default
this.$router.addRoutes(result)
})
}
}
</script>
路由共享效果
优点
- 可以将其他项目路由加载过来,通过路由加载其他项目页面
- 可以将其他项目静态数据加载过来
不足
- 无法跨项目更改其他项目数据
- 所有的方法只能通过异步加载