嘿,各位观众老爷们,早上好/下午好/晚上好!我是你们的老朋友,今天咱们来聊聊一个能让前端项目“合体”的黑科技——Vue 项目中的 Webpack 联邦模块(Module Federation)。别害怕,听起来高大上,其实就是让不同的 Vue 项目像搭积木一样,共享代码,实现微前端。
今天咱们就来一场“联邦模块一日游”,从入门到精通,手把手教你把这个“共享经济”在你的 Vue 项目里玩转起来!
一、 啥是联邦模块?为啥要用它?
首先,咱得明白啥是联邦模块。简单来说,它就是 Webpack 5 提供的模块共享方案。想象一下,你有一个“大哥”Vue 项目,里面有一些常用的组件、函数,你想让其他“小弟”Vue 项目也能用,以前你可能得复制粘贴,或者发个 npm 包。现在有了联邦模块,直接让“小弟”项目远程引用“大哥”项目的模块,是不是很方便?
为什么要用联邦模块?
- 代码共享: 减少重复代码,提高开发效率。
- 独立部署: 各个微前端应用可以独立部署、更新,互不影响。
- 技术栈无关: 理论上,只要是 Webpack 项目,都可以使用联邦模块,不局限于 Vue。
- 灵活组合: 可以根据业务需求,灵活组合不同的微前端应用。
- 版本隔离: 不同的微前端应用可以使用不同版本的依赖,避免冲突。
二、 实战演练:搭建一个简单的联邦模块项目
咱们用两个简单的 Vue 项目来演示,一个叫 host
(大哥),一个叫 remote
(小弟)。host
项目提供一个 HelloWorld
组件,remote
项目引用这个组件。
1. 初始化项目
首先,创建两个 Vue 项目:
vue create host
vue create remote
一路回车,默认选项即可。
2. 配置 host
项目 (大哥)
-
安装 Webpack: (如果项目已经安装了 webpack,则忽略此步骤)
cd host npm install webpack webpack-cli --save-dev
-
创建
webpack.config.js
文件:在
host
项目的根目录下,创建一个webpack.config.js
文件,内容如下:const { ModuleFederationPlugin } = require('webpack').container; const HtmlWebpackPlugin = require('html-webpack-plugin'); const { VueLoaderPlugin } = require('vue-loader'); const path = require('path'); module.exports = { mode: 'development', devtool: 'source-map', entry: './src/main.js', output: { publicPath: 'http://localhost:8080/', // 重要!这里必须是 host 项目的访问地址 }, devServer: { port: 8080, hot: true, headers: { 'Access-Control-Allow-Origin': '*', // 允许跨域 }, }, module: { rules: [ { test: /.vue$/, use: 'vue-loader', }, { test: /.css$/, use: ['vue-style-loader', 'css-loader'], }, ], }, resolve: { extensions: ['.vue', '.js'], }, plugins: [ new ModuleFederationPlugin({ name: 'host', // 必须唯一,作为模块的标识 filename: 'remoteEntry.js', // 暴露的入口文件,供 remote 项目引用 exposes: { './HelloWorld': './src/components/HelloWorld.vue', // 暴露的模块,key 是模块名,value 是模块路径 }, shared: ['vue'], // 共享的依赖,避免重复加载 }), new HtmlWebpackPlugin({ template: './public/index.html', }), new VueLoaderPlugin(), ], };
代码解释:
ModuleFederationPlugin
:联邦模块的核心插件。name
:模块的名称,必须唯一。filename
:暴露的入口文件,remote
项目会通过这个文件来访问host
项目的模块。exposes
:暴露的模块,key
是模块名,value
是模块路径。shared
:共享的依赖,host
和remote
项目都用到的依赖,可以避免重复加载,提高性能。
HtmlWebpackPlugin
:生成 HTML 文件,用于开发环境。VueLoaderPlugin
:处理 Vue 组件。publicPath
:非常重要! 必须是 host 项目的访问地址,否则remote项目会加载不到资源。
-
修改
package.json
:修改
package.json
文件,添加webpack
和webpack-dev-server
命令:"scripts": { "serve": "webpack serve --config webpack.config.js", "build": "webpack --mode production --config webpack.config.js", "lint": "vue-cli-service lint" },
-
创建
HelloWorld.vue
组件:在
src/components
目录下创建一个HelloWorld.vue
组件,内容随意,例如:<template> <div class="hello"> <h1>Hello from Host!</h1> </div> </template> <script> export default { name: 'HelloWorld', }; </script> <style scoped> .hello { color: green; } </style>
-
启动
host
项目:npm run serve
现在,
host
项目应该在http://localhost:8080
运行。
3. 配置 remote
项目 (小弟)
-
安装 Webpack: (如果项目已经安装了 webpack,则忽略此步骤)
cd remote npm install webpack webpack-cli --save-dev
-
创建
webpack.config.js
文件:在
remote
项目的根目录下,创建一个webpack.config.js
文件,内容如下:const { ModuleFederationPlugin } = require('webpack').container; const HtmlWebpackPlugin = require('html-webpack-plugin'); const { VueLoaderPlugin } = require('vue-loader'); const path = require('path'); module.exports = { mode: 'development', devtool: 'source-map', entry: './src/main.js', output: { publicPath: 'http://localhost:8081/', // 重要!这里必须是 remote 项目的访问地址 }, devServer: { port: 8081, hot: true, }, module: { rules: [ { test: /.vue$/, use: 'vue-loader', }, { test: /.css$/, use: ['vue-style-loader', 'css-loader'], }, ], }, resolve: { extensions: ['.vue', '.js'], }, plugins: [ new ModuleFederationPlugin({ name: 'remote', // 必须唯一,作为模块的标识 remotes: { host: 'host@http://localhost:8080/remoteEntry.js', // 引用 host 项目的模块,key 是模块名,value 是模块地址 }, shared: ['vue'], // 共享的依赖,避免重复加载 }), new HtmlWebpackPlugin({ template: './public/index.html', }), new VueLoaderPlugin(), ], };
代码解释:
ModuleFederationPlugin
:联邦模块的核心插件。name
:模块的名称,必须唯一。remotes
:引用远程模块,key
是模块名,value
是模块地址,格式为模块名@远程入口文件地址
。shared
:共享的依赖,host
和remote
项目都用到的依赖,可以避免重复加载,提高性能。
HtmlWebpackPlugin
:生成 HTML 文件,用于开发环境。VueLoaderPlugin
:处理 Vue 组件。publicPath
:非常重要! 必须是 remote 项目的访问地址。
-
修改
package.json
:修改
package.json
文件,添加webpack
和webpack-dev-server
命令:"scripts": { "serve": "webpack serve --config webpack.config.js", "build": "webpack --mode production --config webpack.config.js", "lint": "vue-cli-service lint" },
-
在
remote
项目中使用HelloWorld
组件:修改
remote
项目的src/App.vue
文件,引入HelloWorld
组件:<template> <div id="app"> <HelloWorld /> </div> </template> <script> import { defineAsyncComponent } from 'vue' export default { name: 'App', components: { HelloWorld: defineAsyncComponent(() => import('host/HelloWorld')) } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
代码解释:
import('host/HelloWorld')
:从host
项目引入HelloWorld
组件。这里的host
就是在remote
项目的webpack.config.js
文件中配置的remotes
里的key
。defineAsyncComponent
:使用异步组件引入,因为远程模块需要异步加载。
-
启动
remote
项目:npm run serve
现在,
remote
项目应该在http://localhost:8081
运行,并且显示host
项目的HelloWorld
组件!
三、 进阶技巧:共享更多东西
上面的例子只是共享了一个简单的组件,实际上,你可以共享任何 JavaScript 模块,例如:
- 工具函数: 封装一些常用的工具函数,例如日期格式化、字符串处理等。
- Vuex Store: 共享 Vuex Store,实现状态共享。
- UI 组件库: 共享 UI 组件库,统一风格。
1. 共享工具函数
-
在
host
项目中创建工具函数:在
host
项目的src
目录下创建一个utils.js
文件,内容如下:export function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }
-
在
host
项目的webpack.config.js
文件中暴露工具函数:exposes: { './HelloWorld': './src/components/HelloWorld.vue', './utils': './src/utils.js', // 暴露工具函数 },
-
在
remote
项目中使用工具函数:修改
remote
项目的src/App.vue
文件,引入工具函数:<template> <div id="app"> <HelloWorld /> <p>Today is: {{ formattedDate }}</p> </div> </template> <script> import { defineAsyncComponent } from 'vue' import { formatDate } from 'host/utils'; // 引入工具函数 export default { name: 'App', components: { HelloWorld: defineAsyncComponent(() => import('host/HelloWorld')) }, data() { return { formattedDate: formatDate(new Date()), // 使用工具函数 }; }, } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
现在,
remote
项目应该显示今天的日期,并且使用了host
项目的formatDate
函数。
2. 共享 Vuex Store
共享 Vuex Store 稍微复杂一些,需要一些额外的配置。
-
在
host
项目中创建 Vuex Store:在
host
项目的src
目录下创建一个store
目录,并在其中创建一个index.js
文件,内容如下:import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: { count: 0, }, mutations: { increment(state) { state.count++; }, }, actions: { increment(context) { context.commit('increment'); }, }, getters: { getCount(state) { return state.count; }, }, });
-
在
host
项目的webpack.config.js
文件中暴露 Vuex Store:exposes: { './HelloWorld': './src/components/HelloWorld.vue', './utils': './src/utils.js', './store': './src/store/index.js', // 暴露 Vuex Store }, shared: ['vue', 'vuex'], // 共享 vuex
-
在
remote
项目中使用 Vuex Store:-
安装 Vuex:
cd remote npm install vuex --save
-
在
remote
项目的src/main.js
文件中引入 Vuex Store:import Vue from 'vue' import App from './App.vue' import store from 'host/store'; // 引入 Vuex Store Vue.config.productionTip = false new Vue({ store, // 注入 Vuex Store render: h => h(App), }).$mount('#app')
-
在
remote
项目的src/App.vue
文件中使用 Vuex Store:<template> <div id="app"> <HelloWorld /> <p>Today is: {{ formattedDate }}</p> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </div> </template> <script> import { defineAsyncComponent } from 'vue' import { formatDate } from 'host/utils'; export default { name: 'App', components: { HelloWorld: defineAsyncComponent(() => import('host/HelloWorld')) }, data() { return { formattedDate: formatDate(new Date()), }; }, computed: { count() { return this.$store.getters.getCount; // 获取 count }, }, methods: { increment() { this.$store.dispatch('increment'); // 触发 increment action }, }, } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
现在,
remote
项目可以使用host
项目的 Vuex Store,并且可以修改host
项目的状态。 -
四、 注意事项和常见问题
publicPath
的配置:host
和remote
项目的webpack.config.js
文件中的output.publicPath
必须正确配置,否则会导致资源加载失败。host
的publicPath
必须指向 host 项目的访问地址,remote的publicPath
必须指向 remote 项目的访问地址。shared
的配置:shared
选项用于共享依赖,避免重复加载。需要注意的是,共享的依赖必须是版本兼容的,否则可能会导致运行时错误。 建议共享vue vue-router vuex 这些基础库。- 跨域问题: 如果
host
和remote
项目运行在不同的域名下,需要配置跨域,可以在host
项目的webpack-dev-server
中配置headers
选项,允许跨域访问。 - 版本冲突: 不同的微前端应用可能使用不同版本的依赖,可能会导致冲突。可以使用
shared
选项的version
属性来指定共享依赖的版本。 - 异步加载: 远程模块需要异步加载,可以使用
import()
语法或defineAsyncComponent
函数来实现。 - 类型提示: 如果共享的是 TypeScript 模块,需要配置类型提示,可以使用
d.ts
文件来声明模块的类型。
五、 总结
联邦模块是一个强大的工具,可以帮助我们构建灵活、可扩展的微前端应用。掌握联邦模块,可以提高开发效率,减少重复代码,实现代码共享。
联邦模块配置参数表
参数名 | 类型 | 描述 |
---|---|---|
name |
string |
模块的名称,必须唯一。 |
filename |
string |
暴露的入口文件名。 |
exposes |
object |
暴露的模块,key 是模块名,value 是模块路径。 |
remotes |
object |
引用远程模块,key 是模块名,value 是模块地址,格式为 模块名@远程入口文件地址 。 |
shared |
array |
共享的依赖,避免重复加载。 |
shared[].import |
string |
(可选)如果设置为 false,则不会从远程位置获取共享模块。 |
shared[].requiredVersion |
string |
(可选)指定共享模块的所需版本。如果远程提供的模块版本与此不匹配,则会抛出错误。 |
shared[].version |
string |
(可选) 指定当前模块的版本。 如果其他模块需要共享该模块,并且指定了requiredVersion,那么会校验版本是否匹配。 |
shared[].singleton |
boolean |
(可选)设置为 true,表示只允许存在一个共享模块实例。 |
shared[].eager |
boolean |
(可选) 设置为 true,会立即加载共享模块。默认情况下,共享模块是按需加载的。 |
联邦模块常见问题表
问题 | 可能原因 | 解决方案 |
---|---|---|
远程模块加载失败 | publicPath 配置错误、跨域问题、网络问题。 |
检查 publicPath 是否正确配置,配置跨域,检查网络连接。 |
共享依赖版本冲突 | 不同的微前端应用使用了不同版本的依赖。 | 使用 shared 选项的 version 属性来指定共享依赖的版本,尽量保持版本一致。 |
远程模块类型提示缺失 | 共享的是 TypeScript 模块,但是没有配置类型提示。 | 使用 d.ts 文件来声明模块的类型。 |
远程模块代码修改后,本地项目没有更新 | 浏览器缓存、Webpack 缓存。 | 清除浏览器缓存、Webpack 缓存,重新启动项目。 |
多个模块都引用同一个共享模块,导致重复加载 | shared 配置不正确。 |
确保 shared 配置正确,并且共享模块的版本一致。 |
异步加载远程模块报错 | 异步加载方式不正确。 | 使用 import() 语法或 defineAsyncComponent 函数来实现异步加载。 |
加载远程模块时出现 CORS 错误 | host 项目没有允许 remote 项目的跨域请求。 | 在 host 项目的 webpack-dev-server 配置中,添加 headers: { 'Access-Control-Allow-Origin': '*' } ,或者根据实际情况配置允许的域名。 |
今天的“联邦模块一日游”就到这里,希望大家有所收获。记住,实践是检验真理的唯一标准,赶紧动手试试吧! 如果还有其他问题,欢迎在评论区留言,我会尽力解答。 祝大家编程愉快!