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

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

大家好,今天我们来聊聊 Vue 3 与微前端架构。微前端是一种将单体前端应用分解为多个小型、自治的前端应用的技术架构。每个微前端应用可以独立开发、测试、部署和更新,从而提高开发效率、降低维护成本,并允许团队采用不同的技术栈。Vue 3 作为一种流行的前端框架,在微前端架构中扮演着重要的角色。

一、微前端架构的核心概念与优势

在深入 Vue 3 与微前端的结合之前,我们先快速回顾一下微前端的核心概念和优势。

核心概念:

  • 独立性: 每个微前端应用都是一个独立的实体,拥有自己的代码仓库、构建流程和部署方式。
  • 技术栈无关性: 不同的微前端应用可以使用不同的技术栈,例如 Vue、React、Angular 等。
  • 自治性: 每个微前端应用可以独立开发、测试和部署,无需依赖其他应用。
  • 组合性: 将多个微前端应用组合成一个完整的用户界面。

优势:

  • 提高开发效率: 将大型应用分解为小型应用,可以并行开发,缩短开发周期。
  • 降低维护成本: 每个微前端应用的代码量较小,易于维护和更新。
  • 技术栈灵活性: 团队可以选择最适合每个功能的框架和技术。
  • 渐进式迁移: 可以逐步将单体应用迁移到微前端架构,降低风险。
  • 团队自治性: 团队可以独立负责自己的微前端应用,提高团队效率。

二、微前端的常见架构模式

微前端的架构模式有很多种,常见的包括:

  • 构建时集成(Build-time Integration): 在构建时将多个微前端应用集成到一个应用中。通常使用 Webpack Module Federation 实现。
  • 运行时集成(Runtime Integration): 在运行时动态加载和渲染微前端应用。常见的实现方式包括:
    • 基于 iframe: 使用 iframe 隔离不同的微前端应用。
    • 基于 Web Components: 将每个微前端应用封装成 Web Components,然后组合成一个完整的应用。
    • 基于路由: 使用路由来控制不同微前端应用的显示和隐藏。
  • Web 服务器组合(Web Server Composition): 通过 Web 服务器将不同的微前端应用组合成一个完整的应用。通常使用反向代理实现。

三、Vue 3 在微前端架构中的应用

Vue 3 由于其轻量级、高性能、易于集成等特点,非常适合在微前端架构中使用。下面我们以运行时集成(基于路由)为例,来演示如何使用 Vue 3 构建微前端应用。

1. 项目结构

假设我们有一个主应用(main-app)和两个微前端应用(app1app2)。项目结构如下:

micro-frontend-demo/
├── main-app/       # 主应用
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/
│   │   ├── router/
│   │   └── main.js
│   ├── package.json
│   └── vue.config.js
├── app1/           # 微前端应用 1
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/
│   │   ├── router/
│   │   └── main.js
│   ├── package.json
│   └── vue.config.js
└── app2/           # 微前端应用 2
    ├── src/
    │   ├── App.vue
    │   ├── components/
    │   ├── router/
    │   └── main.js
    ├── package.json
    └── vue.config.js

2. 主应用(main-app

主应用负责加载和渲染微前端应用。我们需要配置路由,以便根据不同的 URL 显示不同的微前端应用。

  • main-app/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
    {
        path: '/',
        name: 'Home',
        component: {
            template: '<div><h1>Main App - Home</h1><router-link to="/app1">Go to App 1</router-link> | <router-link to="/app2">Go to App 2</router-link></div>',
        },
    },
    {
        path: '/app1',
        name: 'App1',
        component: {
            template: '<div id="app1-container"></div>', // 微前端应用 1 的容器
        },
    },
    {
        path: '/app2',
        name: 'App2',
        component: {
            template: '<div id="app2-container"></div>', // 微前端应用 2 的容器
        },
    },
];

const router = createRouter({
    history: createWebHistory(),
    routes,
});

export default router;
  • main-app/src/App.vue
<template>
  <router-view />
</template>

<script>
import { onMounted } from 'vue';

export default {
  name: 'App',
  setup() {
    const loadMicroFrontend = (appName, containerId, entryPoint) => {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = entryPoint;
        script.onload = () => {
          // 微前端应用加载完成后执行
          window[`render${appName}`](containerId); // 调用微前端应用的渲染函数
          resolve();
        };
        script.onerror = () => {
          reject(new Error(`Failed to load ${appName}`));
        }
        document.head.appendChild(script);
      });
    };

    onMounted(() => {
      // 监听路由变化
      const routePath = window.location.pathname;

      if (routePath === '/app1') {
        loadMicroFrontend('App1', '#app1-container', 'http://localhost:8081/js/app.js').catch(err => console.error(err));
      } else if (routePath === '/app2') {
        loadMicroFrontend('App2', '#app2-container', 'http://localhost:8082/js/app.js').catch(err => console.error(err));
      }
    });

    return {};
  },
};
</script>

这里使用了 onMounted 钩子函数来监听路由变化,并根据当前的 URL 动态加载对应的微前端应用。loadMicroFrontend 函数负责创建 <script> 标签,加载微前端应用的 JavaScript 文件,并在加载完成后调用微前端应用的渲染函数(renderApp1renderApp2)。

3. 微前端应用(app1app2

微前端应用需要暴露一个渲染函数,供主应用调用。

  • app1/src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

let appInstance = null; // 保存应用实例,避免多次渲染

window.renderApp1 = (containerId) => {
  if (appInstance) {
    appInstance.unmount();  //卸载之前的实例
    appInstance = null;
  }

  const app = createApp(App);
  app.use(router);
  appInstance = app; // 保存当前实例
  app.mount(containerId);

  console.log('App1 mounted in', containerId);
};

// 如果是独立运行,则自动挂载
if (!document.getElementById('app1-container')) {
    window.renderApp1('#app'); // 默认挂载点,方便独立运行
}
  • app1/src/App.vue
<template>
  <div>
    <h1>App 1</h1>
    <p>This is App 1 content.</p>
    <router-link to="/app1/page1">Go to Page 1</router-link>
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'App1',
};
</script>
  • app1/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/app1/page1',
    name: 'Page1',
    component: {
      template: '<div><h2>App 1 - Page 1</h2></div>',
    },
  },
];

const router = createRouter({
  history: createWebHistory('/app1'), // 设置 base 路径
  routes,
});

export default router;

app2 的代码结构类似,只是需要修改一些名称和内容。注意每个微前端应用都需要设置不同的端口号,避免冲突。

4. 配置 vue.config.js

为了让主应用能够访问到微前端应用的 JavaScript 文件,我们需要配置 vue.config.js,使其构建后的文件可以通过 HTTP 访问。

  • app1/vue.config.js
module.exports = {
  publicPath: 'http://localhost:8081/', // 设置公共路径
  outputDir: 'dist', // 构建输出目录
  configureWebpack: {
    output: {
      library: 'App1', // 设置库名称,供主应用调用
      libraryTarget: 'window', // 设置库的类型
    },
  },
  devServer: {
    port: 8081, // 设置开发服务器端口号
    headers: {
      'Access-Control-Allow-Origin': '*', // 允许跨域访问
    },
  },
};

app2vue.config.js 文件类似,只需要修改 publicPathdevServer.port 即可。

5. 运行项目

分别启动主应用和两个微前端应用:

# 主应用
cd main-app
npm install
npm run serve

# 微前端应用 1
cd app1
npm install
npm run serve

# 微前端应用 2
cd app2
npm install
npm run serve

现在,您可以通过浏览器访问主应用(例如 http://localhost:8080),然后点击链接跳转到不同的微前端应用。

四、模块加载、状态隔离与路由同步的解决方案

在微前端架构中,模块加载、状态隔离和路由同步是三个重要的挑战。

1. 模块加载

我们已经在上面的例子中演示了如何使用 <script> 标签动态加载微前端应用的 JavaScript 文件。此外,还可以使用 Webpack Module Federation 来实现模块共享和加载。

Webpack Module Federation

Webpack Module Federation 允许不同的 Webpack 构建之间共享模块。每个应用都可以声明自己要暴露的模块,以及要使用的其他应用的模块。

  • main-app/vue.config.js
const { defineConfig } = require('@vue/cli-service')
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: 'http://localhost:8080/',
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'main_app',
        remotes: {
          app1: 'app1@http://localhost:8081/remoteEntry.js', // 引入 app1
          app2: 'app2@http://localhost:8082/remoteEntry.js', // 引入 app2
        },
        shared: ['vue', 'vue-router'], // 共享依赖
      }),
    ],
  },
  devServer: {
    port: 8080,
  },
})
  • app1/vue.config.js
const { defineConfig } = require('@vue/cli-service')
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: 'http://localhost:8081/',
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'app1',
        exposes: {
          './App1Component': './src/components/App1Component.vue', // 暴露组件
        },
        shared: ['vue', 'vue-router'], // 共享依赖
      }),
    ],
  },
  devServer: {
    port: 8081,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
})
  • app2/vue.config.js

app1/vue.config.js 类似,只是需要修改 nameexposesdevServer.port

然后,在主应用中就可以直接使用 app1 暴露的组件:

<template>
  <div>
    <h1>Main App</h1>
    <App1Component />  <!-- 使用 app1 暴露的组件 -->
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue';

export default {
  components: {
    App1Component: defineAsyncComponent(() => import('app1/App1Component')), // 动态引入组件
  },
};
</script>

2. 状态隔离

每个微前端应用都应该拥有自己的状态,避免相互干扰。常见的状态隔离方案包括:

  • 使用不同的 Vuex 实例: 每个微前端应用都使用自己的 Vuex 实例,确保状态隔离。
  • 使用 Shadow DOM: 将每个微前端应用渲染到 Shadow DOM 中,可以有效隔离 CSS 和 JavaScript。
  • 使用自定义事件: 通过自定义事件进行跨应用通信,避免直接访问其他应用的状态。

3. 路由同步

在微前端架构中,需要保证不同应用之间的路由同步。常见的路由同步方案包括:

  • 使用 URL Hash: 将路由信息保存在 URL Hash 中,不同应用可以通过监听 hashchange 事件来同步路由。
  • 使用 History API: 使用 History API 可以修改 URL,而不会刷新页面。不同应用可以通过监听 popstate 事件来同步路由。
  • 使用自定义事件: 通过自定义事件进行跨应用路由同步。

示例:使用 History API 进行路由同步

  • 主应用(main-app
// 监听 popstate 事件
window.addEventListener('popstate', (event) => {
  const routePath = window.location.pathname;
  // 根据路由路径加载不同的微前端应用
  if (routePath === '/app1') {
    loadMicroFrontend('App1', '#app1-container', 'http://localhost:8081/js/app.js').catch(err => console.error(err));
  } else if (routePath === '/app2') {
    loadMicroFrontend('App2', '#app2-container', 'http://localhost:8082/js/app.js').catch(err => console.error(err));
  }
});
  • 微前端应用(app1
// 监听路由变化,并通知主应用
router.afterEach((to, from) => {
  // 构建完整的 URL
  const fullPath = `/app1${to.path}`;

  // 使用 History API 修改 URL
  history.pushState(null, null, fullPath);
});

五、总结

通过今天的讲解,我们了解了 Vue 3 在微前端架构中的应用,并学习了如何实现模块加载、状态隔离和路由同步。微前端架构是一种强大的技术架构,可以帮助我们构建大型、复杂的前端应用。虽然实现起来有一定的复杂性,但是带来的好处也是显而易见的。希望今天的分享能够帮助大家更好地理解和应用微前端技术。

六、简要回顾与建议

这次分享主要涵盖了微前端架构的核心概念,Vue 3 在微前端中的应用,以及模块加载、状态隔离和路由同步的实现方案。建议大家在实际项目中根据具体需求选择合适的微前端架构模式和技术方案。

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

发表回复

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