webpack5联邦模块

本文介绍了webpack5的模块联邦特性,允许项目之间共享作用域,实现跨应用组件共享。通过在项目A引用项目B的组件,如在Vue2项目中,配置webpack使A能直接import()引入B的组件。详细阐述了配置过程,包括webpack的基本配置、模块联邦配置以及在项目中的实际应用。同时分析了原理,展示了路由共享的效果和优缺点。
摘要由CSDN通过智能技术生成

定义

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>

路由共享效果

在这里插入图片描述

优点

  • 可以将其他项目路由加载过来,通过路由加载其他项目页面
  • 可以将其他项目静态数据加载过来

不足

  • 无法跨项目更改其他项目数据
  • 所有的方法只能通过异步加载
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值