Vue 3与微前端(Micro-Frontends)架构:实现模块加载、状态隔离与路由同步

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-onevue-app-twoimport('vue-app-one')import('vue-app-two') 是动态导入,会在需要时才加载对应的微前端应用代码。 激活函数 (location) => location.pathname.startsWith('/app1') 决定了哪个微前端应用应该在哪个路由下被激活。 start() 函数启动 Single-SPA。single-spa-container 是微前端应用挂载的容器。

2.2 微前端应用

接下来,我们创建两个微前端应用:vue-app-onevue-app-two。 每个微前端应用都需要导出 Single-SPA 生命周期钩子函数:bootstrapmountunmount

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;

重要事项:

  • 生命周期函数: 每个微前端应用都需要导出 bootstrapmountunmount 这三个生命周期函数。 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-onevue-app-tworemotes 属性指定了远程模块的名称和 remoteEntry.js 的地址。 shared 属性指定了共享的依赖,例如 vue。 在路由配置中,我们使用动态导入 import('vue-app-one/App')import('vue-app-two/App') 加载远程模块。

3.2 微前端应用(Remote)

接下来,我们创建两个微前端应用:vue-app-onevue-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: 使用 postMessage API 在不同的微前端应用之间传递消息。 这种方式可以跨域通信,但是需要处理消息的序列化和反序列化。
  • Shared Library: 创建一个共享的库,其中包含一些通用的组件和函数。 每个微前端应用都可以使用这个共享库。 这种方式可以提高代码的复用性,但是需要维护一个独立的共享库。

五、模块加载策略

模块加载策略直接影响微前端应用的性能和用户体验。

  • 延迟加载 (Lazy Loading): 只在需要时才加载模块。 这可以减少初始加载时间,提高应用的响应速度。
  • 预加载 (Preloading): 在后台加载模块,以便在用户需要时可以立即使用。 这可以提高用户体验,但是可能会增加带宽消耗。
  • 按需加载 (On-Demand Loading): 根据用户的行为和需求,动态加载模块。 这可以最大限度地减少带宽消耗,并提高应用的性能。

六、总结与展望

今天我们深入探讨了Vue 3与微前端架构的结合,介绍了基于Single-SPA和Module Federation两种实现方式,并讨论了模块加载、状态隔离和路由同步等关键问题。 Vue 3的Composition API、Teleport 和自定义元素等特性,为构建高效的微前端应用提供了强大的支持。

微前端架构可以提高前端应用的灵活性、可维护性和可扩展性。 但是,它也带来了一些挑战,例如状态管理、路由同步和模块加载。 选择合适的微前端架构和技术方案,可以帮助你构建出更加健壮和可维护的前端应用。展望未来,微前端架构将继续发展,并涌现出更多优秀的解决方案,为前端开发带来更大的便利。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注