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

Vue 3 与微前端:构建可伸缩的前端应用

大家好!今天我们来探讨一个日益重要的前端架构模式:微前端,以及如何利用 Vue 3 在微前端架构中实现模块加载、状态隔离与路由同步。

什么是微前端?

微前端,顾名思义,就是把一个大型的前端应用拆分成多个小的、自治的、可独立部署和维护的应用,这些小的应用可以在同一个浏览器窗口中运行,共同构成一个完整的用户体验。

传统单体应用面临着代码库庞大、团队协作困难、技术栈锁定、部署缓慢等问题。微前端架构则试图解决这些问题,它具有以下优势:

  • 独立开发和部署: 每个微应用可以由独立的团队开发和部署,互不影响。
  • 技术栈无关性: 每个微应用可以选择最适合自己的技术栈,不必受限于整个应用的统一技术选型。
  • 增量升级: 可以逐步将旧应用迁移到微前端架构,而不需要一次性重构整个应用。
  • 更好的可伸缩性: 可以根据业务需求独立扩展每个微应用。
  • 更强的容错性: 一个微应用的故障不会影响到其他微应用。

微前端架构模式

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

  • 构建时集成: 将所有微应用的构建产物集成到一个主应用中。
  • 运行时集成: 在运行时动态加载微应用。
    • iframe: 使用 iframe 标签来隔离微应用。
    • Web Components: 使用 Web Components 技术将微应用封装成自定义元素。
    • JavaScript Modules: 使用 JavaScript 模块来加载微应用。
    • Single-SPA: 一个流行的微前端框架,提供了一套完整的微前端解决方案。

我们今天的重点是使用 JavaScript Modules 和 Vue 3 实现运行时集成,因为它具有轻量、灵活的特点,并且与 Vue 3 的组件化思想非常契合。

示例:一个简单的微前端应用

假设我们要构建一个电商平台,包含以下微应用:

  • 商品列表 (product-list): 显示商品列表。
  • 购物车 (cart): 显示购物车内容。
  • 用户中心 (user-profile): 显示用户个人信息。

我们可以将这些微应用独立开发,然后通过主应用将它们集成在一起。

1. 主应用 (main-app)

主应用负责加载和管理微应用。它需要一个路由机制来控制显示哪个微应用。

// main-app/src/App.vue
<template>
  <div id="app">
    <nav>
      <router-link to="/product-list">商品列表</router-link> |
      <router-link to="/cart">购物车</router-link> |
      <router-link to="/user-profile">用户中心</router-link>
    </nav>
    <div id="micro-app-container">
      <component :is="currentComponent" />
    </div>
  </div>
</template>

<script>
import { defineComponent, ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';

export default defineComponent({
  name: 'App',
  setup() {
    const currentComponent = ref(null);
    const route = useRoute();

    const loadMicroApp = async (routeName) => {
      try {
        let module;
        switch (routeName) {
          case 'product-list':
            module = await import('http://localhost:8081/product-list.js'); // 微应用部署在 8081 端口
            break;
          case 'cart':
            module = await import('http://localhost:8082/cart.js'); // 微应用部署在 8082 端口
            break;
          case 'user-profile':
            module = await import('http://localhost:8083/user-profile.js'); // 微应用部署在 8083 端口
            break;
          default:
            currentComponent.value = null;
            return;
        }
        currentComponent.value = module.default;
      } catch (error) {
        console.error(`Failed to load micro app ${routeName}:`, error);
        currentComponent.value = null; // 显示一个错误组件,或者其他处理
      }
    };

    onMounted(() => {
      loadMicroApp(route.name);
    });

    watch(
      () => route.name,
      (newRouteName) => {
        loadMicroApp(newRouteName);
      }
    );

    return {
      currentComponent,
    };
  },
});
</script>
// main-app/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/product-list',
    name: 'product-list',
  },
  {
    path: '/cart',
    name: 'cart',
  },
  {
    path: '/user-profile',
    name: 'user-profile',
  },
];

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

export default router;

关键点:

  • 使用 import() 动态导入微应用的 JavaScript 模块。
  • currentComponent 响应式变量用于控制显示哪个微应用组件。
  • onMountedwatch 确保在路由变化时加载相应的微应用。
  • try...catch 块处理加载微应用失败的情况。

2. 商品列表微应用 (product-list)

// product-list/src/ProductList.vue
<template>
  <div>
    <h2>商品列表</h2>
    <ul>
      <li v-for="product in products" :key="product.id">{{ product.name }} - {{ product.price }}</li>
    </ul>
  </div>
</template>

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'ProductList',
  setup() {
    const products = ref([
      { id: 1, name: '商品 A', price: 10 },
      { id: 2, name: '商品 B', price: 20 },
      { id: 3, name: '商品 C', price: 30 },
    ]);

    return {
      products,
    };
  },
});
</script>
// product-list/src/main.js
import { createApp } from 'vue';
import ProductList from './ProductList.vue';

let app;

const mount = (el) => {
    app = createApp(ProductList);
    app.mount(el);
};

if (process.env.NODE_ENV === 'development') {
    // 在开发环境中,直接挂载到页面上,方便调试
    const devRoot = document.createElement('div');
    devRoot.id = 'product-list-dev-root';
    document.body.appendChild(devRoot);
    mount('#product-list-dev-root');
}

export default ProductList;

vue.config.js

module.exports = {
  publicPath: 'http://localhost:8081', // 设置公共路径
  configureWebpack: {
    output: {
      library: 'productList', // 设置库名称
      libraryTarget: 'umd', // 设置库类型为umd
    },
  },
  devServer: {
    port: 8081,
    headers: {
      'Access-Control-Allow-Origin': '*', // 允许跨域访问
    },
  },
};

关键点:

  • 微应用导出一个 Vue 组件。
  • librarylibraryTarget 配置确保微应用可以作为 UMD 模块被加载。
  • publicPath 设置为微应用的部署路径。
  • devServer.headers 允许跨域访问,方便开发调试。
  • mount 函数用于手动挂载组件,以便在主应用中动态加载。

3. 其他微应用 (cart, user-profile)

购物车和用户中心微应用的结构与商品列表类似,只是组件内容不同,端口号分别为 8082 和 8083。这里不再赘述。

4. 运行

  1. 分别启动三个微应用 (product-list, cart, user-profile) 的开发服务器。
  2. 启动主应用的开发服务器。
  3. 在浏览器中访问主应用,通过导航链接切换不同的微应用。

状态隔离

微前端架构中,状态隔离是一个重要的问题。不同的微应用应该拥有独立的状态,避免互相干扰。

1. 避免全局变量

最简单的方法是避免使用全局变量。每个微应用应该将自己的状态存储在自己的组件内部,或者使用 Vuex 等状态管理工具来管理状态。

2. 使用命名空间

如果需要使用全局对象,可以使用命名空间来避免冲突。

例如,可以定义一个全局对象 MyApp,然后在每个微应用中使用不同的属性来存储状态。

// product-list/src/main.js
window.MyApp = window.MyApp || {};
window.MyApp.productList = {
  state: {
    products: [
      { id: 1, name: '商品 A', price: 10 },
      { id: 2, name: '商品 B', price: 20 },
      { id: 3, name: '商品 C', price: 30 },
    ],
  },
};

3. 使用 Vuex Modules

Vuex 的模块化功能可以帮助我们将不同微应用的状态隔离到不同的模块中。

每个微应用可以拥有自己的 Vuex 模块,然后将这些模块注册到主应用的 Vuex store 中。

// product-list/src/store/index.js
import { createStore } from 'vuex';

export default createStore({
  state: {
    products: [
      { id: 1, name: '商品 A', price: 10 },
      { id: 2, name: '商品 B', price: 20 },
      { id: 3, name: '商品 C', price: 30 },
    ],
  },
  mutations: {
    addProduct(state, product) {
      state.products.push(product);
    },
  },
  actions: {},
  getters: {},
});

在主应用中,可以将每个微应用的 store 模块动态注册到主 store 中。

// main-app/src/App.vue

import { useStore } from 'vuex';
import { onMounted } from 'vue';

export default {
  setup() {
    const store = useStore();

    onMounted(async () => {
      // 动态注册 product-list 的 store 模块
      const productListStore = await import('http://localhost:8081/store.js');
      store.registerModule('productList', productListStore.default);
    });

    return {};
  },
};

这种方式可以确保每个微应用的状态是独立的,并且可以通过 Vuex 提供的 API 进行访问和修改。

路由同步

在微前端架构中,路由同步是一个挑战。当用户在不同的微应用之间切换时,需要保持路由状态的同步。

1. 基于 Hash 的路由

最简单的路由同步方式是使用基于 Hash 的路由。主应用和微应用都使用 Hash 路由,然后通过监听 window.location.hash 的变化来实现路由同步。

2. 基于 History API 的路由

可以使用 History API (pushState, replaceState) 来实现更友好的路由体验。但是,需要确保主应用和微应用都使用相同的 History API 配置。

3. 使用路由事件

可以通过自定义事件来实现路由同步。当一个微应用的路由发生变化时,触发一个自定义事件,然后主应用监听该事件并更新自己的路由。

// product-list/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory('/product-list'), // 微应用的 base 路径
  routes: [
    {
      path: '/',
      component: () => import('../components/ProductList.vue'),
    },
  ],
});

router.afterEach((to, from) => {
  // 触发自定义事件
  window.dispatchEvent(new CustomEvent('micro-app-route-change', {
    detail: {
      path: to.fullPath,
    },
  }));
});

export default router;

在主应用中,监听 micro-app-route-change 事件并更新自己的路由。

// main-app/src/App.vue
import { useRouter } from 'vue-router';
import { onMounted } from 'vue';

export default {
  setup() {
    const router = useRouter();

    onMounted(() => {
      window.addEventListener('micro-app-route-change', (event) => {
        const path = event.detail.path;
        router.push(path);
      });
    });

    return {};
  },
};

4. 使用 Single-SPA

Single-SPA 提供了一套完整的微前端路由解决方案。它可以帮助我们管理多个微应用的路由,并且可以实现更高级的路由功能,例如路由拦截、路由重定向等。

共享依赖

在微前端架构中,共享依赖也是一个需要考虑的问题。如果每个微应用都加载自己的依赖,会导致资源浪费和性能问题。

1. 使用 CDN

可以将一些常用的依赖,例如 Vue, Vue Router, Vuex 等,放在 CDN 上,然后让所有微应用共享这些依赖。

2. 使用 Webpack Externals

Webpack 的 externals 配置可以帮助我们将一些依赖排除在构建产物之外,然后让微应用在运行时从全局环境中获取这些依赖。

// webpack.config.js
module.exports = {
  externals: {
    vue: 'Vue',
    'vue-router': 'VueRouter',
    vuex: 'Vuex',
  },
};

需要在主应用中引入这些依赖的 CDN 链接。

<!DOCTYPE html>
<html>
  <head>
    <title>Micro Frontend Demo</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://unpkg.com/vue-router@4"></script>
    <script src="https://unpkg.com/vuex@4"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

总结

方面 解决方案
模块加载 使用 import() 动态导入微应用的 JavaScript 模块。
状态隔离 避免全局变量,使用命名空间,使用 Vuex Modules。
路由同步 基于 Hash 的路由,基于 History API 的路由,使用路由事件,使用 Single-SPA。
共享依赖 使用 CDN,使用 Webpack Externals。
开发调试 在开发模式下,可以采取一些策略,例如在每个微应用的 main.js 中,如果检测到 process.env.NODE_ENV === 'development',则将该微应用直接挂载到页面上,以便进行独立调试,而在生产环境中,则只导出组件或模块,供主应用动态加载。

微前端架构的持续演进

微前端架构是一个不断发展的领域,它在解决大型前端应用的可维护性和可伸缩性方面具有巨大潜力。今天我们探讨了使用 Vue 3 和 JavaScript Modules 实现微前端的一种方法,着重讲解模块加载、状态隔离和路由同步的关键技术。随着前端技术的不断发展,我们可以期待更多创新的微前端解决方案出现,帮助我们构建更加健壮和灵活的前端应用。

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

发表回复

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