设计一个 Vue 微前端(Micro-Frontends)架构,并处理子应用之间的通信、路由隔离和状态共享。

观众朋友们,晚上好!欢迎来到“Vue 微前端那些事儿”系列讲座。我是你们的老朋友,今天咱们就来聊聊 Vue 微前端,争取用最接地气的方式,把这个听起来高大上的东西给它扒个精光。

开场白:微前端,你到底是啥玩意儿?

要说这微前端,其实也没啥神秘的,就是把一个庞大的前端应用拆成若干个小的、自治的应用,每个小应用都可以独立开发、独立部署、独立升级。 这样一来,就算某个小应用挂了,也不会影响整个大盘子。就好像一艘航空母舰,上面搭载了各种功能的舰载机,每架飞机都可以独立完成任务,就算一架坠毁了,也不会影响航母的整体作战能力。

第一部分:架构设计——搭个台子唱大戏

要做微前端,首先得有个架构。咱们这里就以一个简单的例子来说明,假设我们要构建一个包含“首页”、“商品列表”、“用户中心”三个模块的电商平台。

  1. 技术选型:

    • 主应用 (Main App): Vue + Vue Router + Vuex (或者Pinia)
    • 子应用 (Micro Apps): Vue + Vue Router + Vuex (或者Pinia),当然,子应用的技术栈不一定非得是Vue,也可以是 React, Angular,甚至老旧的 jQuery 项目(想想就刺激)。
    • 通信方式: Custom Events, Shared Global State, Message Queue
    • 构建工具: Webpack, Parcel, Vite (随便你喜欢哪个)
    • 部署方式: Nginx, Docker
  2. 目录结构 (示例):

    micro-frontend-demo/
    ├── main-app/            # 主应用
    │   ├── src/
    │   │   ├── App.vue
    │   │   ├── router/
    │   │   │   └── index.js
    │   │   ├── store/
    │   │   │   └── index.js
    │   │   └── main.js
    │   ├── public/
    │   └── package.json
    ├── micro-app-home/     # 子应用:首页
    │   ├── src/
    │   │   ├── App.vue
    │   │   ├── router/
    │   │   │   └── index.js
    │   │   └── main.js
    │   ├── public/
    │   └── package.json
    ├── micro-app-products/ # 子应用:商品列表
    │   ├── src/
    │   │   ├── App.vue
    │   │   ├── router/
    │   │   │   └── index.js
    │   │   └── main.js
    │   ├── public/
    │   └── package.json
    ├── micro-app-user/     # 子应用:用户中心
    │   ├── src/
    │   │   ├── App.vue
    │   │   ├── router/
    │   │   │   └── index.js
    │   │   └── main.js
    │   ├── public/
    │   └── package.json
    ├── nginx/              # Nginx 配置 (可选)
    └── README.md
  3. 主应用的核心代码:

    // main-app/src/App.vue
    <template>
      <div id="app">
        <h1>主应用</h1>
        <router-link to="/home">首页</router-link> |
        <router-link to="/products">商品列表</router-link> |
        <router-link to="/user">用户中心</router-link>
        <router-view />
      </div>
    </template>
    
    <script>
    export default {
      name: 'App'
    }
    </script>
    
    // main-app/src/router/index.js
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    
    const routes = [
      {
        path: '/home',
        name: 'Home',
        component: () => import(/* webpackChunkName: "home" */ '../components/MicroAppWrapper.vue').then(m => {
           return {template:`<micro-app name="home" url="http://localhost:8081/"></micro-app>`};
        })
      },
      {
        path: '/products',
        name: 'Products',
        component:  () => import(/* webpackChunkName: "products" */ '../components/MicroAppWrapper.vue').then(m => {
           return {template:`<micro-app name="products" url="http://localhost:8082/"></micro-app>`};
        })
      },
      {
        path: '/user',
        name: 'User',
        component: () => import(/* webpackChunkName: "user" */ '../components/MicroAppWrapper.vue').then(m => {
           return {template:`<micro-app name="user" url="http://localhost:8083/"></micro-app>`};
        })
      }
    ]
    
    const router = new VueRouter({
      mode: 'history',
      base: process.env.BASE_URL,
      routes
    })
    
    export default router
    
    //main-app/src/components/MicroAppWrapper.vue
    <template>
        <div ref="microAppContainer"></div>
    </template>
    
    <script>
    import microApp from '@micro-zoe/micro-app'
    
    export default {
      mounted () {
        // 组件挂载后手动渲染微前端组件。
        // 也可以在路由配置里面直接配置
        microApp.start()
      }
    }
    </script>
    
    // main-app/src/main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    import microApp from '@micro-zoe/micro-app'
    
    Vue.config.productionTip = false
    Vue.use(microApp)
    
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
  4. 子应用的核心代码(以首页为例):

    // micro-app-home/src/App.vue
    <template>
      <div id="app">
        <h1>首页</h1>
        <p>欢迎来到首页,这里是各种促销活动!</p>
      </div>
    </template>
    
    <script>
    export default {
      name: 'App'
    }
    </script>
    
    // micro-app-home/src/router/index.js
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: {
          template: '<div>首页内容</div>'
        }
      }
    ]
    
    const router = new VueRouter({
      mode: 'history', // 注意:子应用也要使用history模式
      base: window.__MICRO_APP_BASE_ROUTE__ || '/', // 非常重要!
      routes
    })
    
    export default router
    
    // micro-app-home/src/main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    
    Vue.config.productionTip = false
    
    let app = null;
    
    function render(props = {}) {
      const { container } = props;
      app = new Vue({
        router,
        render: h => h(App)
      }).$mount(container ? container.querySelector('#app') : '#app')
    }
    
    // 独立运行时
    if (!window.__MICRO_APP_ENVIRONMENT__) {
      render();
    }
    
    // 导出生命周期 - bootstrap
    export async function bootstrap() {
      console.log('vue3 app bootstraped');
    }
    
    // 导出生命周期 - mount
    export async function mount(props) {
      console.log('vue3 app mount', props);
      render(props);
    }
    
    // 导出生命周期 - unmount
    export async function unmount(props) {
      console.log('vue3 app unmount', props);
      app.$destroy();
      app.$el.innerHTML = '';
      app = null;
    }

    注意事项:

    • 子应用的 router 必须使用 history 模式。
    • 子应用的 routerbase 属性要设置为 window.__MICRO_APP_BASE_ROUTE__ || '/'__MICRO_APP_BASE_ROUTE__ 是主应用传递给子应用的,用于区分不同子应用的路由前缀。
    • 子应用需要导出 bootstrap, mount, unmount 三个生命周期函数。

第二部分:子应用通信——鸡犬之声相闻

微前端架构下,子应用之间难免需要通信,就像邻居之间借个酱油啥的。 常用的通信方式有以下几种:

  1. Custom Events (自定义事件):

    这是最简单粗暴的方式,就是主应用监听子应用抛出的事件,或者子应用监听主应用抛出的事件。

    • 主应用监听子应用事件:

      // main-app/src/App.vue
      mounted() {
        window.addEventListener('subAppEvent', (event) => {
          console.log('主应用收到子应用事件:', event.detail);
        });
      }
    • 子应用触发事件:

      // micro-app-home/src/App.vue
      methods: {
        sendEventToMainApp() {
          window.dispatchEvent(new CustomEvent('subAppEvent', { detail: { message: 'Hello from Home!' } }));
        }
      }
  2. Shared Global State (共享全局状态):

    这种方式就是创建一个全局的共享状态,主应用和子应用都可以访问和修改这个状态。 可以用 Vuex 或者 Pinia 来实现。

    • 创建共享状态:

      // main-app/src/store/index.js
      import Vue from 'vue'
      import Vuex from 'vuex'
      
      Vue.use(Vuex)
      
      const store = new Vuex.Store({
        state: {
          sharedData: 'Initial shared data'
        },
        mutations: {
          updateSharedData(state, payload) {
            state.sharedData = payload;
          }
        },
        actions: {
          updateSharedData({ commit }, payload) {
            commit('updateSharedData', payload);
          }
        }
      })
      
      export default store
    • 主应用和子应用访问共享状态:

      // main-app/src/App.vue
      computed: {
        sharedData() {
          return this.$store.state.sharedData;
        }
      },
      methods: {
        updateSharedData() {
          this.$store.dispatch('updateSharedData', 'New data from Main App');
        }
      }
      
      // micro-app-home/src/App.vue
      computed: {
        sharedData() {
          return window.microApp.getData().sharedData; // 通过micro-app提供的API获取
        }
      },
      methods: {
        updateSharedData() {
           window.microApp.dispatch({sharedData: 'New data from Home App'})// 通过micro-app提供的API修改
        }
      }

    注意: 子应用需要通过主应用暴露的API来访问和修改共享状态,不能直接访问主应用的 Vuex 实例。

  3. Message Queue (消息队列):

    这种方式就是使用一个消息队列(比如 RabbitMQ, Kafka)来作为主应用和子应用之间的通信桥梁。 主应用和子应用都向消息队列发送消息,然后消息队列负责将消息传递给目标应用。

    这种方式比较复杂,但是可以实现更灵活、更可靠的通信。 这里就不展开讲了,有兴趣的同学可以自己研究一下。

  4. props传递

    主应用可以将数据当成 props 传递给子应用,实现单向数据流。

  5. 使用window对象

    挂载到window对象上的变量,在主应用和子应用都可以访问到,可以用来进行通信。

各种通信方式的优缺点:

通信方式 优点 缺点 适用场景
Custom Events 简单易用 需要手动管理事件监听和触发,容易出错 简单的、一次性的通信
Shared Global State 方便共享状态,易于管理 需要考虑状态冲突的问题,状态管理不当容易导致性能问题 需要共享大量状态的场景
Message Queue 灵活可靠,可以实现异步通信 比较复杂,需要引入额外的消息队列服务 需要高可靠、高并发的通信场景
props传递 数据流向清晰 只支持单向数据流,无法实现子应用主动向主应用通信 主应用需要向子应用传递数据的场景
window对象 简单直接 全局污染,容易引起命名冲突,不推荐大量使用 传递少量数据,或者作为兜底方案

第三部分:路由隔离——各走各的路

微前端架构下,每个子应用都有自己的路由,如何保证每个子应用的路由互不干扰呢?

  1. 路由前缀:

    这是最常用的方式,就是给每个子应用分配一个唯一的路由前缀。 比如,首页的路由前缀是 /home,商品列表的路由前缀是 /products,用户中心的路由前缀是 /user

    主应用根据 URL 的前缀来判断应该加载哪个子应用。

    // main-app/src/router/index.js
    const routes = [
      {
        path: '/home/*',  // 匹配所有以 /home 开头的路由
        name: 'Home',
        component: Home
      },
      {
        path: '/products/*', // 匹配所有以 /products 开头的路由
        name: 'Products',
        component: Products
      },
      {
        path: '/user/*', // 匹配所有以 /user 开头的路由
        name: 'User',
        component: User
      }
    ]
  2. Hash 路由:

    如果不想使用路由前缀,可以使用 Hash 路由。 每个子应用都使用自己的 Hash 路由,互不干扰。

    // micro-app-home/src/router/index.js
    const router = new VueRouter({
      mode: 'hash', // 使用 Hash 路由
      base: '/',
      routes
    })
  3. Web Components:

    Web Components 是一种浏览器原生组件技术,可以将子应用封装成一个独立的 Web Component,然后直接在主应用中使用。

    这种方式可以实现更彻底的路由隔离,但是需要对 Web Components 有一定的了解。

第四部分:状态共享——你中有我,我中有你

微前端架构下,有些状态需要在主应用和子应用之间共享,比如用户的登录信息、全局配置等。

  1. Shared Global State (共享全局状态):

    前面已经讲过了,可以使用 Vuex 或者 Pinia 来实现共享全局状态。

  2. Cookies:

    可以使用 Cookies 来存储一些全局的状态,比如用户的登录信息。 但是要注意 Cookies 的安全问题,不要存储敏感信息。

  3. LocalStorage:

    可以使用 LocalStorage 来存储一些全局的状态,比如用户的偏好设置。 但是要注意 LocalStorage 的容量限制,不要存储太大的数据。

  4. 主应用传递 props:

    主应用可以将一些状态作为 props 传递给子应用。

第五部分:部署——上线见真章

微前端应用部署起来稍微麻烦一点,需要考虑各个子应用的部署方式。

  1. 独立部署:

    每个子应用都独立部署到自己的服务器上,然后主应用通过 URL 来加载子应用。 这种方式比较灵活,但是需要管理多个服务器。

  2. 统一部署:

    将所有子应用都打包到一起,然后部署到同一个服务器上。 这种方式比较简单,但是不够灵活。

  3. Docker:

    可以使用 Docker 来构建每个子应用的镜像,然后使用 Docker Compose 来编排所有子应用。 这种方式比较方便,可以实现快速部署和扩展。

Nginx 配置示例:

server {
    listen 80;
    server_name your-domain.com;

    location / {
        root /path/to/main-app/dist;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    location /home/ {
        proxy_pass http://localhost:8081/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /products/ {
        proxy_pass http://localhost:8082/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /user/ {
        proxy_pass http://localhost:8083/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

第六部分:总结——微前端,路漫漫其修远兮

微前端是一种非常有用的架构模式,可以解决大型前端应用的复杂性问题。 但是,微前端也不是银弹,它会带来一些额外的复杂性,比如通信、路由、状态管理等。

在选择微前端架构之前,要仔细评估项目的需求和团队的能力,不要盲目跟风。 只有在真正需要的时候,才能发挥微前端的优势。

好了,今天的讲座就到这里。 希望大家对 Vue 微前端有了更深入的了解。 谢谢大家!

Q&A 环节(模拟):

观众: 老师,微前端的兼容性怎么样?老旧的浏览器支持吗?

老师: 好问题! 微前端本身不涉及浏览器兼容性问题,兼容性取决于子应用的技术栈。 如果子应用使用了 ES6+ 的语法,就需要进行 Babel 转译,以兼容老旧的浏览器。 另外,如果子应用使用了 Web Components,需要使用 Polyfill 来兼容老旧的浏览器。

观众: 老师,微前端的性能怎么样?会不会影响用户体验?

老师: 性能是微前端需要重点关注的问题。 子应用的加载速度、渲染速度、通信速度都会影响用户体验。 可以通过以下方式来优化性能:

  • 代码分割: 将子应用的代码分割成多个小的 chunk,按需加载。
  • 懒加载: 只在需要的时候才加载子应用。
  • 缓存: 对子应用的代码和数据进行缓存。
  • CDN: 使用 CDN 来加速子应用的加载速度。
  • 优化通信: 减少主应用和子应用之间的通信次数。

观众: 老师,微前端的调试难度大吗?

老师: 调试微前端应用确实比调试单体应用要麻烦一些。 可以使用以下工具来辅助调试:

  • 浏览器开发者工具: 可以使用浏览器开发者工具来调试主应用和子应用的代码。
  • Vue Devtools: 可以使用 Vue Devtools 来调试 Vue 子应用。
  • 日志: 在主应用和子应用中添加日志,方便排查问题。
  • Mock 数据: 使用 Mock 数据来模拟子应用的接口,方便调试。

总的来说,微前端的调试需要一定的经验和技巧,但是只要掌握了方法,也可以高效地进行调试。

(完)

发表回复

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