Vue 3与微前端架构:实现模块加载、状态隔离与路由同步
大家好!今天我们来深入探讨Vue 3与微前端架构的结合,重点关注模块加载、状态隔离和路由同步这三个核心问题。微前端是一种将前端应用分解为更小、更易于管理和独立部署的架构风格。 Vue 3的特性,如Composition API、Teleport和自定义元素,为构建高效的微前端应用提供了强大的支持。
一、微前端架构概述
首先,我们需要理解微前端架构的基本概念。微前端的核心思想是将一个大型前端应用拆分成多个小型、自治的模块,这些模块可以由不同的团队独立开发、测试和部署。 最终,这些独立的模块组合成一个完整的用户界面。
微前端的常见实现方式:
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Web Components | 技术栈无关,组件级别复用,隔离性好 | 学习曲线陡峭,需要polyfills支持旧浏览器 | 组件库共享,需要高度隔离的场景 |
| iFrame | 完全隔离,技术栈无关 | 性能损耗大,路由同步、状态共享复杂,用户体验差 | 需要完全隔离的遗留系统集成 |
| Module Federation (Webpack 5) | 代码共享,运行时集成,按需加载 | 需要Webpack 5支持,配置复杂 | 基于Webpack的应用,需要代码共享和运行时集成的场景 |
| Single-SPA | 框架无关,灵活,可渐进式迁移 | 需要维护一个基座应用,配置较为复杂 | 需要框架无关、灵活的集成方案,以及遗留系统渐进式迁移的场景 |
| Qiankun | 基于Single-SPA,提供了更便捷的微前端解决方案,支持HTML Entry | 对子应用有一定的侵入性,需要修改子应用的构建配置 | 快速集成多个现有应用,对技术栈兼容性要求高的场景 |
今天,我们将主要关注基于Single-SPA和Module Federation的Vue 3微前端实现。
二、基于Single-SPA的Vue 3微前端
Single-SPA是一个用于组合多个JavaScript应用的框架。它允许你将多个SPA应用组合成一个更大的应用,每个SPA应用都可以独立开发、测试和部署。
2.1 基座应用(Root Config)
首先,我们需要创建一个基座应用,它负责加载和管理各个微前端应用。
// index.js (基座应用的入口)
import { registerApplication, start } from 'single-spa';
import * as Vue from 'vue';
import App from './App.vue';
// 注册微前端应用
registerApplication(
'vue-app-one', // 应用名称
() => import('vue-app-one'), // 加载微前端应用的函数
(location) => location.pathname.startsWith('/app1') // 激活函数,判断当前URL是否激活该应用
);
registerApplication(
'vue-app-two',
() => import('vue-app-two'),
(location) => location.pathname.startsWith('/app2')
);
// 启动 Single-SPA
start();
// 创建 Vue 根实例 (可选,用于共享状态或全局组件)
const app = Vue.createApp(App);
app.mount('#app');
App.vue (基座应用)
<template>
<div>
<h1>基座应用</h1>
<nav>
<router-link to="/app1">应用一</router-link> |
<router-link to="/app2">应用二</router-link>
</nav>
<div id="single-spa-container"></div>
</div>
</template>
<script>
import { RouterLink } from 'vue-router';
export default {
components: {
RouterLink
}
}
</script>
在这个基座应用中,我们使用registerApplication函数注册了两个微前端应用:vue-app-one和vue-app-two。 import('vue-app-one') 和 import('vue-app-two') 是动态导入,会在需要时才加载对应的微前端应用代码。 激活函数 (location) => location.pathname.startsWith('/app1') 决定了哪个微前端应用应该在哪个路由下被激活。 start() 函数启动 Single-SPA。single-spa-container 是微前端应用挂载的容器。
2.2 微前端应用
接下来,我们创建两个微前端应用:vue-app-one和vue-app-two。 每个微前端应用都需要导出 Single-SPA 生命周期钩子函数:bootstrap、mount和unmount。
vue-app-one/src/main.js (微前端应用一的入口)
import { createApp, h } from 'vue';
import App from './App.vue';
import router from './router';
let appInstance = null;
export async function bootstrap(props) {
console.log('vue-app-one bootstrap', props);
}
export async function mount(props) {
console.log('vue-app-one mount', props);
appInstance = createApp({
render: () => h(App, props),
});
appInstance.use(router);
appInstance.mount('#vue-app-one'); //挂载到独立容器
}
export async function unmount(props) {
console.log('vue-app-one unmount', props);
if (appInstance) {
appInstance.unmount();
appInstance = null;
}
}
// 如果是独立运行,则直接挂载
if (!document.getElementById('single-spa-container')) {
bootstrap({}).then(mount({}));
}
vue-app-one/src/App.vue (微前端应用一的组件)
<template>
<div>
<h1>应用一</h1>
<router-view></router-view>
</div>
</template>
vue-app-one/src/router/index.js (微前端应用一的路由)
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/app1',
name: 'App1Home',
component: {
template: '<div>应用一首页</div>',
},
},
{
path: '/app1/about',
name: 'App1About',
component: {
template: '<div>应用一关于页面</div>',
},
},
];
const router = createRouter({
history: createWebHistory('/app1'), // 注意:这里需要设置base,与激活函数对应
routes,
});
export default router;
vue-app-two/src/main.js (微前端应用二的入口)
import { createApp, h } from 'vue';
import App from './App.vue';
import router from './router';
let appInstance = null;
export async function bootstrap(props) {
console.log('vue-app-two bootstrap', props);
}
export async function mount(props) {
console.log('vue-app-two mount', props);
appInstance = createApp({
render: () => h(App, props),
});
appInstance.use(router);
appInstance.mount('#vue-app-two'); //挂载到独立容器
}
export async function unmount(props) {
console.log('vue-app-two unmount', props);
if (appInstance) {
appInstance.unmount();
appInstance = null;
}
}
// 如果是独立运行,则直接挂载
if (!document.getElementById('single-spa-container')) {
bootstrap({}).then(mount({}));
}
vue-app-two/src/App.vue (微前端应用二的组件)
<template>
<div>
<h1>应用二</h1>
<router-view></router-view>
</div>
</template>
vue-app-two/src/router/index.js (微前端应用二的路由)
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/app2',
name: 'App2Home',
component: {
template: '<div>应用二首页</div>',
},
},
{
path: '/app2/contact',
name: 'App2Contact',
component: {
template: '<div>应用二联系页面</div>',
},
},
];
const router = createRouter({
history: createWebHistory('/app2'), // 注意:这里需要设置base,与激活函数对应
routes,
});
export default router;
重要事项:
- 生命周期函数: 每个微前端应用都需要导出
bootstrap、mount和unmount这三个生命周期函数。bootstrap用于应用的初始化,mount用于应用的挂载,unmount用于应用的卸载。 - 路由配置: 每个微前端应用的路由都需要设置
base属性,以避免路由冲突。base属性需要与基座应用中registerApplication函数的激活函数对应。 - 容器: 每个微前端应用需要挂载到不同的 DOM 元素上,本例中分别是
#vue-app-one和#vue-app-two。需要在基座应用的index.html中添加对应的容器:<div id="vue-app-one"></div>和<div id="vue-app-two"></div>。 -
构建配置: 微前端应用通常需要构建成 UMD 模块,以便 Single-SPA 可以动态加载它们。 确保你的构建配置正确生成了 UMD 模块。 通常可以在
vue.config.js中进行配置:module.exports = { configureWebpack: { output: { library: 'vue-app-one', // 应用名称 libraryTarget: 'umd', }, }, devServer: { port: 8081, // 确保每个微前端应用使用不同的端口 headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", "Access-Control-Allow-Headers": "*" } } };
2.3 状态隔离
Single-SPA本身不提供状态隔离机制。 为了实现状态隔离,你需要使用一些额外的技术,比如:
- Vuex Namespaces: 为每个微前端应用创建独立的 Vuex 模块,并使用命名空间来隔离状态。
- Custom Events: 使用自定义事件在微前端应用之间进行通信。
2.4 路由同步
Single-SPA会自动处理路由同步。 当用户在不同的微前端应用之间导航时,Single-SPA 会自动激活和卸载相应的应用。 但是,你需要确保每个微前端应用的路由配置正确,并且与基座应用的路由配置一致。
三、基于Module Federation的Vue 3微前端
Module Federation 是 Webpack 5 提供的一种代码共享机制。 它允许你将一个 Webpack 构建的应用拆分成多个独立的模块,这些模块可以在运行时被其他应用加载和使用。
3.1 基座应用(Host)
首先,我们需要创建一个基座应用,它负责加载和使用各个微前端模块。
webpack.config.js (基座应用的Webpack配置)
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
devtool: 'inline-source-map',
devServer: {
static: './dist',
port: 3000,
},
output: {
publicPath: 'http://localhost:3000/', // 必须设置,否则加载不到远程模块
},
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader',
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'Host', // 模块名称,全局唯一
remotes: {
'vue-app-one': 'vue_app_one@http://localhost:3001/remoteEntry.js', // 远程模块的名称和地址
'vue-app-two': 'vue_app_two@http://localhost:3002/remoteEntry.js',
},
shared: { vue: { singleton: true, requiredVersion: '^3.0.0' } }, // 共享依赖
}),
new HtmlWebpackPlugin({
template: './index.html',
}),
new VueLoaderPlugin(),
],
resolve: {
extensions: ['.vue', '.js'],
},
};
src/index.js (基座应用的入口)
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');
src/App.vue (基座应用)
<template>
<div>
<h1>基座应用</h1>
<nav>
<router-link to="/app1">应用一</router-link> |
<router-link to="/app2">应用二</router-link>
</nav>
<div id="single-spa-container">
<router-view></router-view>
</div>
</div>
</template>
<script>
import { RouterLink } from 'vue-router';
export default {
components: {
RouterLink
}
}
</script>
src/router/index.js (基座应用的路由)
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/app1',
name: 'App1',
component: () => import('vue-app-one/App'), // 动态导入远程模块
},
{
path: '/app2',
name: 'App2',
component: () => import('vue-app-two/App'), // 动态导入远程模块
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
在这个基座应用中,我们使用 ModuleFederationPlugin 插件配置了远程模块:vue-app-one 和 vue-app-two。 remotes 属性指定了远程模块的名称和 remoteEntry.js 的地址。 shared 属性指定了共享的依赖,例如 vue。 在路由配置中,我们使用动态导入 import('vue-app-one/App') 和 import('vue-app-two/App') 加载远程模块。
3.2 微前端应用(Remote)
接下来,我们创建两个微前端应用:vue-app-one 和 vue-app-two。
vue-app-one/webpack.config.js (微前端应用一的Webpack配置)
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
devtool: 'inline-source-map',
devServer: {
static: './dist',
port: 3001,
},
output: {
publicPath: 'http://localhost:3001/',
},
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader',
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'vue_app_one', // 模块名称,全局唯一
filename: 'remoteEntry.js', // 导出模块的名称
exposes: {
'./App': './src/App.vue', // 导出模块
},
shared: { vue: { singleton: true, requiredVersion: '^3.0.0' } }, // 共享依赖
}),
new HtmlWebpackPlugin({
template: './index.html',
}),
new VueLoaderPlugin(),
],
resolve: {
extensions: ['.vue', '.js'],
},
};
vue-app-one/src/index.js (微前端应用一的入口)
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
vue-app-one/src/App.vue (微前端应用一的组件)
<template>
<div>
<h2>应用一</h2>
<p>这是应用一的内容。</p>
</div>
</template>
vue-app-two/webpack.config.js (微前端应用二的Webpack配置)
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
devtool: 'inline-source-map',
devServer: {
static: './dist',
port: 3002,
},
output: {
publicPath: 'http://localhost:3002/',
},
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader',
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'vue_app_two', // 模块名称,全局唯一
filename: 'remoteEntry.js', // 导出模块的名称
exposes: {
'./App': './src/App.vue', // 导出模块
},
shared: { vue: { singleton: true, requiredVersion: '^3.0.0' } }, // 共享依赖
}),
new HtmlWebpackPlugin({
template: './index.html',
}),
new VueLoaderPlugin(),
],
resolve: {
extensions: ['.vue', '.js'],
},
};
vue-app-two/src/index.js (微前端应用二的入口)
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
vue-app-two/src/App.vue (微前端应用二的组件)
<template>
<div>
<h2>应用二</h2>
<p>这是应用二的内容。</p>
</div>
</template>
重要事项:
ModuleFederationPlugin配置: 每个微前端应用都需要使用ModuleFederationPlugin插件进行配置。name属性指定了模块的名称,filename属性指定了导出模块的名称,exposes属性指定了要导出的模块。publicPath:output.publicPath必须设置正确,否则加载不到远程模块。shared: 使用shared属性共享依赖,避免重复加载。singleton: true表示只加载一个版本的依赖。- 端口: 确保每个微前端应用使用不同的端口。
3.3 状态隔离
Module Federation 本身不提供状态隔离机制。你需要使用一些额外的技术,比如:
- Vuex Namespaces: 为每个微前端应用创建独立的 Vuex 模块,并使用命名空间来隔离状态。
- 自定义事件: 使用自定义事件在微前端应用之间进行通信。
3.4 路由同步
路由同步需要在基座应用中进行处理。 你可以使用 Vue Router 的 history 模式来管理路由,并使用动态导入加载远程模块。 确保每个微前端应用的路由配置正确,并且与基座应用的路由配置一致。
四、状态共享和通信
微前端架构中,状态共享和通信是一个重要的挑战。 以下是一些常用的解决方案:
- Shared Global State: 使用一个共享的全局状态管理工具,例如 Vuex 或 Redux。 你需要确保每个微前端应用都可以访问和修改这个全局状态。 但是,这种方式可能会导致状态冲突和耦合。
- Custom Events: 使用自定义事件在微前端应用之间进行通信。 这种方式更加灵活,但是需要手动管理事件的注册和触发。
- Message Passing: 使用
postMessageAPI 在不同的微前端应用之间传递消息。 这种方式可以跨域通信,但是需要处理消息的序列化和反序列化。 - Shared Library: 创建一个共享的库,其中包含一些通用的组件和函数。 每个微前端应用都可以使用这个共享库。 这种方式可以提高代码的复用性,但是需要维护一个独立的共享库。
五、模块加载策略
模块加载策略直接影响微前端应用的性能和用户体验。
- 延迟加载 (Lazy Loading): 只在需要时才加载模块。 这可以减少初始加载时间,提高应用的响应速度。
- 预加载 (Preloading): 在后台加载模块,以便在用户需要时可以立即使用。 这可以提高用户体验,但是可能会增加带宽消耗。
- 按需加载 (On-Demand Loading): 根据用户的行为和需求,动态加载模块。 这可以最大限度地减少带宽消耗,并提高应用的性能。
六、总结与展望
今天我们深入探讨了Vue 3与微前端架构的结合,介绍了基于Single-SPA和Module Federation两种实现方式,并讨论了模块加载、状态隔离和路由同步等关键问题。 Vue 3的Composition API、Teleport 和自定义元素等特性,为构建高效的微前端应用提供了强大的支持。
微前端架构可以提高前端应用的灵活性、可维护性和可扩展性。 但是,它也带来了一些挑战,例如状态管理、路由同步和模块加载。 选择合适的微前端架构和技术方案,可以帮助你构建出更加健壮和可维护的前端应用。展望未来,微前端架构将继续发展,并涌现出更多优秀的解决方案,为前端开发带来更大的便利。
更多IT精英技术系列讲座,到智猿学院