从零到一构建Vue微前端应用:qiankun整合全流程

141 阅读5分钟

前言:为什么需要微前端?

场景痛点
当企业级应用逐渐演变为“巨石应用”时,你可能面临:

  • 多个团队维护同一仓库,协作困难,有可能改到同一文件,造成冲突,解决冲突增加心智负担。
  • 技术栈升级成本高(如Vue 2迁移到Vue 3)
  • 独立部署困难,一个小改动需要全量发布

微前端核心思想

  • 将单体应用拆分为多个独立开发、独立部署 的子应用
  • 主应用作为容器,动态加载子应用
  • 技术栈无关性(Vue、React、Angular共存)

一、qiankun框架核心原理

1. 关键技术实现

  • JS沙箱:通过 Proxy 代理 window对象,隔离子应用全局变量

  • 样式隔离:动态加载/卸载子应用样式表(Shadow DOM可选)

  • 资源加载:劫持 fetch/XMLHttpRequest,匹配子应用资源路径

    (这些会在另外的文章详细解释,这里先不多赘述)

2. 生命周期管理

// 子应用必须导出以下生命周期钩子
export async function bootstrap() { /* 初始化 */ }
export async function mount(props) { /* 渲染 */ }
export async function unmount() { /* 卸载 */ }

二、创建主应用

  1. 创建主应用项目
    在终端中运行以下命令:

    vue create main-app                                                                                              
    

    选择 Vue 2 模板,其他配置按默认即可。

  2. 安装 qiankun 和 router

    npm install qiankun -S  
    npm install vue-router@3                                                                                  
    
  3. 修改主应用入口文件 src/main.js
    替换为以下代码:

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import { registerMicroApps, start } from 'qiankun'                                                                
    
    Vue.config.productionTip = false
    
    // 注册微应用
    registerMicroApps([
      {
        name: 'vue-sub-app', // 子应用名称
        entry: '//localhost:8081', // 子应用入口(开发环境)
        container: '#subapp-container', // 挂载容器
        activeRule: '/sub-app' // 激活路径
      }
    ])
    
    // 启动 qiankun
    start()
    
    new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
    
  4. 修改主应用布局 src/App.vue
    替换为以下代码:

    <template>
      <div id="app">
        <nav>
          <router-link to="/">主应用首页</router-link>                                                          
          <router-link to="/sub-app">子应用</router-link>
        </nav>
        <router-view></router-view>
        <div id="subapp-container"></div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'App'
    }
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    nav {
      margin-bottom: 20px;
    }
    nav a {
      margin: 0 10px;
    }
    </style>
    
  5. 修改主应用路由 src/router/index.js
    替换为以下代码:

    import Vue from "vue";
    import VueRouter from "vue-router";
    import MainHome from "../views/MainHome.vue";                                                                    
    
    Vue.use(VueRouter);
    
    const routes = [
      {
        path: "/",
        name: "MainHome",
        component: MainHome,
      },
    ];
    
    const router = new VueRouter({
      mode: "history",
      routes,
    });
    
    export default router;
    
  6. 主应用 MainHome 文件

    <template>
      <div>MainHome</div>                                                                                           
    </template>
    
    <script>
    export default {
      name: "MainHome",
    };
    </script>
       
    
  7. 启动主应用
    运行以下命令启动主应用:

    npm run serve                                                                                                    
    

    主应用将运行在 http://localhost:8080


三、创建子应用

  1. 创建子应用项目
    在终端中运行以下命令:

    vue create sub-app                                                                                                
    

    同样选择 Vue 2 模板。

  2. 修改子应用入口文件src/main.js
    替换为以下代码:

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'                                                                                
    
    Vue.config.productionTip = false
    
    let instance = null
    
    function render(props = {}) {
      const { container } = props
      instance = new Vue({
        router,
        render: h => h(App)
      }).$mount(container ? container.querySelector('#app') : '#app')
    }
    
    // 独立运行时
    if (!window.__POWERED_BY_QIANKUN__) {
      render()
    }
    
    // qiankun 生命周期钩子
    export async function bootstrap() {
      console.log('[vue] vue app bootstraped')
    }
    
    export async function mount(props) {
      console.log('[vue] props from main framework', props)
      render(props)
    }
    
    export async function unmount() {
      instance.$destroy()
      instance.$el.innerHTML = ''
      instance = null
    }
    
  3. 修改子应用路由src/router/index.js
    替换为以下代码:

    import Vue from "vue";
    import VueRouter from "vue-router";
    import SubHome from "../views/SubHome.vue";                                                                      
    
    Vue.use(VueRouter);
    
    const routes = [
      {
        path: "/",
        name: "SubHome",
        component: SubHome,
      },
    ];
    
    const router = new VueRouter({
      mode: "history",
      base: window.__POWERED_BY_QIANKUN__ ? "/sub-app/" : "/", // 主应用设置了激活路径
      routes,
    });
    
    export default router;
    
  4. 修改子应用配置vue.config.js
    在子应用根目录下创建或修改 vue.config.js

    const { defineConfig } = require('@vue/cli-service')
    module.exports = defineConfig({
      transpileDependencies: true,
      devServer: {                                                                                                  
        port: 8081, // 确保端口唯一
        headers: {
          'Access-Control-Allow-Origin': '*' // 允许跨域
        }
      },
      configureWebpack: {
        output: {
          library: `vue-sub-app`, // 定义子应用的全局变量名 qiankun会通过该变量名访问钩子函数
          libraryTarget: 'umd', // 指定打包输出的模块格式为 UMD
          chunkLoadingGlobal: `webpackJsonp_vue-sub-app` //Webpack 会使用 window.webpackJsonp_vueSubApp 来管理子应用的异步 chunk 加载。
    
        }
      }
    })
    

    UMD 是一种通用的模块定义格式,兼容多种环境(如 CommonJS、AMD 和浏览器全局变量)。

  5. 子应用App文件

    <template>
      <div id="app">
        <img alt="Vue logo" src="./assets/logo.png" />                                                                
        <router-view></router-view>
      </div>
    </template>
    
    <script>
    export default {
      name: "App",
    };
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
        
    
  6. 子应用SubHome文件
    运行以下命令启动子应用:

    <template>
      <div id="app">SubHome</div>                                                                                   
    </template>
    
    <script>
    export default {
      name: "SubHome",
    };
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
        
    
  7. 启动子应用
    运行以下命令启动子应用:

    npm run serve                                                                                              
    

    子应用将运行在 http://localhost:8081


四、运行微前端项目1. 启动主应用和子应用

  • 主应用:http://localhost:8080
  • 子应用:http://localhost:8081
  1. 访问主应用
    打开浏览器,访问 http://localhost:8080,点击导航栏中的“子应用”链接,子应用的内容将加载到主应用的 #subapp-container 容器中。

五、项目结构

main-app/              # 主应用
│   ├── src/
│   │   ├── main.js    # 主应用入口
│   │   ├── App.vue    # 主应用布局
│   │   └── router/    # 主应用路由
│   └── vue.config.js
sub-app/               # 子应用
│   ├── src/
│   │   ├── main.js    # 子应用入口
│   │   ├── App.vue    # 子应用布局
│   │   └── router/    # 子应用路由
│   └── vue.config.js

这样就成功搭建了一个基于 Vue 2 和 qiankun 的微前端项目。主应用和子应用可以独立开发和运行,同时通过 qiankun 实现微前端集成。

接下来说一些优化。


六、沙箱配置优化

// 启动qiankun,配置沙箱
start({
  sandbox: {
    strictStyleIsolation: true,    // // 子应用将被包裹在Shadow DOM中
    experimentalStyleIsolation: false, // 样式前缀隔离
  },
});
// 需要严格隔离样式 开启 strictStyleIsolation 
// 需要兼容旧浏览器且允许样式继承 开启 experimentalStyleIsolation

特性strictStyleIsolation: true (Shadow DOM)experimentalStyleIsolation: true (样式前缀)
样式隔离强度完全隔离部分隔离
全局样式继承❌ 无法继承✅ 可以继承
浏览器兼容性❌ 不支持IE✅ 支持所有浏览器
第三方库样式处理✅ 自动隔离❌ 需手动处理
DOM操作限制❌ 子应用无法直接访问主应用DOM✅ 无限制

七、子应用间通信

1.主应用初始化全局状态

// main-app/src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import { registerMicroApps, start, initGlobalState } from "qiankun";

Vue.config.productionTip = false;

// 初始化全局状态
const actions = initGlobalState({
  user: null,
  token: "",
});

// 监听全局状态变化
actions.onGlobalStateChange((state, prevState) => {
  console.log("主应用监听到状态变化:", prevState, "→", state);
});

// 注册微应用
registerMicroApps([
  {
    name: "vue-sub-app", // 子应用名称
    entry: "//localhost:8081", // 子应用入口(开发环境)
    container: "#subapp-container", // 挂载容器
    activeRule: "/sub-app", // 激活路径,
    props: {
      // 传递通信方法给子应用
      onGlobalStateChange: actions.onGlobalStateChange,
      setGlobalState: actions.setGlobalState,
    },
  },
]);

// 启动 qiankun
start();

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

2.子应用 main.js 钩子函数修改

// sub-app/src/App.vue
export async function mount(props) {
  console.log("[vue] props from main framework", props);
  window.mainAppProps = props;
  render(props);
}

3.子应用使用全局状态

// sub-app/src/App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  mounted() {
    const { onGlobalStateChange, setGlobalState } = window.mainAppProps;

    // 监听全局状态变化
    onGlobalStateChange((state) => {
      console.log("子应用监听到状态变化:", state);
    });

    // 更新全局状态
    setGlobalState({ user: { name: "张三" } });
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

还有许多的优化方案和实用场景,后续持续更新。。