[微前端][vue3 + vite + qiankun] 使用详解

2,470 阅读2分钟

微前端[vue3 + vite + qiankun] 使用详解

本文用于记录qiankun结合vite搭建微前端项目完整过程

主要步骤如下:
1.主、子需要安装哪些依赖?
2.如何配置主、子应用?
3.如何应用间传参?
4.如何部署各个应用?

项目结构

└── micro-app
      ├── main-app              # 主应用
      ├── app1                  # 子应用1
      ├── app2                  # 子应用2

1.主、子应用配置及需要的依赖

1.1 依赖

主应用安装 qiankun,子应用安装 vite-plugin-qiankun

1.2 配置

1.如何使用qiankun
2.如何配置vite
3.如何配置路由
1.2.1 主应用
  • 新建 src/micro/index.ts 文件,注册子应用

    // main > micro > index.ts
    import { registerMicroApps, start } from "qiankun";
    import type { ObjectType, RegistrableApp } from "qiankun";
    
    const appConfig: RegistrableApp<ObjectType>[] = [
      {
        name: "app1",
        entry: "http://localhost:5555/",
        container: "#app",
        activeRule: "/app1",
      },
      {
        name: "app2",
        entry: "http://localhost:5556/",
        container: "#app",
        activeRule: "/app2",
      },
    ];
    
    registerMicroApps(appConfig);
    
    start({
      sandbox: {
        experimentalStyleIsolation: true,
      },
    });
    
  • 初始化

    // main > src > main.ts
    // 在main.ts中导入
    import "../micro";
    
  • 路由配置

    // main > router > index.ts
    import { createRouter, createWebHistory } from "vue-router";
    const importViews = () => import(`../layout/index.vue`); // 使用动态路由方式会报错
    
    import AppContainer from "../layout/index.vue";
    
    const router = createRouter({
      history: createWebHistory(),
      routes: [
        {
          name: "home",
          path: "/",
          redirect: "/app1/home",
        },
        {
          name: "app1Home",
          path: "/app1/home",
          component: AppContainer,
        },
        {
          name: "app1about",
          path: "/app1/about",
          component: AppContainer,
        },
        {
          name: "app2Home",
          path: "/app2/home",
          component: AppContainer,
        },
        {
          name: "app2about",
          path: "/app2/about",
          component: AppContainer,
        },
      ],
    });
    
    export default router;
    
1.2.2 子应用
  • vite 配置

    // app1 > vite.config.ts > index.ts
    import { fileURLToPath, URL } from "node:url";
    
    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";
    import vueJsx from "@vitejs/plugin-vue-jsx";
    import qiankun from "vite-plugin-qiankun";
    
    // https://vite.dev/config/
    export default defineConfig({
      plugins: [
        vue(),
        vueJsx(),
        qiankun("app1", {
          useDevMode: true,
        }),
      ],
      server: {
        port: 5555,
        cors: true,
      },
      resolve: {
        alias: {
          "@": fileURLToPath(new URL("./src", import.meta.url)),
        },
      },
    });
    
  • 接入 qiankun 导出生命周期函数

    // app1 > src > main.ts
    import {
      renderWithQiankun,
      qiankunWindow,
      type QiankunProps,
    } from "vite-plugin-qiankun/dist/helper";
    
    function render(props: QiankunProps = {}) {
      const { container } = props;
      const app = createApp(App);
      app.use(createPinia());
      app.use(router);
      container
        ? app.mount(container.querySelector("#app") as HTMLElement)
        : app.mount("#app");
    }
    
    function initApp() {
      if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
        console.log("%c 独立渲染", "color: red; font-size: 20px;");
        render();
        return;
      }
      renderWithQiankun({
        mount(props) {
          console.log("%c qiankun 渲染", "color: red; font-size: 20px;");
          console.log(props);
          render(props);
        },
        bootstrap() {
          console.log("bootstrap");
        },
        unmount(props) {
          console.log("unmount", props);
        },
        update(props) {
          console.log("update", props);
        },
      });
    }
    
    initApp();
    
  • 路由配置

    各页面路径需跟主应用路由路径保持一直

    // app1 > src > router > index.ts
    import { createRouter, createWebHistory } from "vue-router";
    import HomeView from "../views/HomeView.vue";
    
    const router = createRouter({
      history: createWebHistory(),
      routes: [
        {
          name: "home",
          path: "/",
          redirect: "/app1/home",
        },
        {
          path: "/app1/home",
          name: "app1Home",
          component: HomeView,
        },
        {
          path: "/app1/about",
          name: "app1about",
          component: () => import("../views/AboutView.vue"),
        },
      ],
    });
    
    export default router;
    

2.应用传参

应用传参有一下几种情况
1.主 ->2.子 ->3.子 ->

2.1 主 -> 子

  • 主应用定义全局共享数据

    // main > src > stores > global.ts
    import { defineStore } from "pinia";
    
    export const useGlobalState = defineStore("globalState", {
      state: () => {
        const userInfo = ref({
          name: "张三",
          age: 18,
        });
    
        return {
          userInfo,
        };
      },
      actions: {
        changeUserInfo(userInfo: any) {
          this.userInfo = userInfo;
        },
      },
    });
    
  • 主应用通过 props 传参

    // main > src > micro > store > index.ts
    import { useGlobalState } from "@/stores/globalState";
    import { storeToRefs } from "pinia";
    import { watch } from "vue";
    
    export const initMicroAppStoreProps: Record<string, any> = {
      app1: {
        handleProps(app: any) {
          /**
           *  主 < -- > 子 应用通信
           *
           *  实现:
           *       1. watch
           *       2. pinia
           */
          const _globalStore = useGlobalState();
    
          app["props"]["globalData"] = _globalStore;
          app["props"]["onChangeMainStore"] = (
            callbackFn: (globalStore: any) => void
          ) => {
            watch(
              () => _globalStore.$state,
              () => {
                callbackFn(_globalStore);
              },
              {
                deep: true,
              }
            );
          };
        },
      },
    };
    
  • 子应用接受数据

    // app1 > src > main.ts
    import "./assets/main.css";
    
    import { createApp, ref } from "vue";
    import { createPinia } from "pinia";
    
    import App from "./App.vue";
    import router from "./router";
    
    import {
      renderWithQiankun,
      qiankunWindow,
      type QiankunProps,
    } from "vite-plugin-qiankun/dist/helper";
    
    let app: null | any = null;
    
    const globalStore = ref(null);
    function setGlobalStore(store: any) {
      globalStore.value = { ...store };
    }
    
    function render(props: QiankunProps = {}) {
      const { container } = props;
      app = createApp(App);
      app.use(createPinia());
      app.use(router);
      container
        ? app.mount(container.querySelector("#app") as HTMLElement)
        : app.mount("#app");
    }
    
    function initApp() {
      if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
        console.log("%c app1 独立渲染", "color: red; font-size: 20px;");
        render();
        return;
      }
      renderWithQiankun({
        bootstrap() {},
        async mount(props) {
          await render(props);
          setGlobalStore(props.globalData);
    
          props.onChangeMainStore((val: any) => {
            setGlobalStore(val);
          });
    
          app.provide("globalStore", globalStore);
        },
        unmount(props) {
          app.unmount();
          app._container = "";
          app = null;
          globalStore.value = null;
        },
        update(props) {},
      });
    }
    
    initApp();
    
  • 子应用页面中使用数据

    <!-- app1 > src > views > home.vue  -->
    <template>
      <main>
        <h1>app1 home</h1>
        <p>name: {{ globalStore.userInfo.name }}</p>
        <p>age: {{ globalStore.userInfo.age }}</p>
    
        <button @click="changeUserInfo">changeUserInfo</button>
      </main>
    </template>
    
    <script setup lang="ts">
      import { inject, watch } from "vue";
    
      const globalStore = inject("globalStore") as any;
    
      const changeUserInfo = () => {
        globalStore.value.changeUserInfo({
          name: "李四",
          age: globalStore.value.userInfo.age + 20,
        });
      };
    </script>
    

2.1 子 -> 主

也是一样的道理
在主应用先定义数据,然后子应用调用主应用的方法改变数据,实现传递到主应用

传参流程:

主应用 --> 子应用1 --> 主应用

2.1 子 -> 子

也是一样的道理
在主应用先定义数据,然后子应用调用主应用的方法改变数据,实现传递到主应用,
然后再传给其他子应用

传参流程:

主应用 --> 子应用1 --> 主应用 --> 子应用2

3.部署

使用所有应用部署在同一个 nginx 的方式部署,方便更新升级管理各个应用

  • nginx 配置文件

      worker_processes  1;
    
      events {
          worker_connections  1024;
      }
    
      http {
          include       mime.types;
          default_type  application/octet-stream;
          sendfile        on;
          keepalive_timeout  65;
    
          server {
              listen       97;
              server_name  localhost;
    
              location / {
                  root   html;
                  index  index.html index.htm;
                  try_files $uri $uri/ /index.html;
              }
    
              # 子应用 app1 的静态资源路径
              location ^~ /app1/ {
                  alias  html/app1/;
                  try_files $uri $uri/ /app1/index.html;
    
                  if (!-e $request_filename) {
                      rewrite ^/app1/(.*)$ /index.html last;
                  }
              }
    
              # 子应用 app2 的静态资源路径
              location ^~ /app2/ {
                  alias  html/app2/;
                  try_files $uri $uri/ /app2/index.html;
    
                  if (!-e $request_filename) {
                      rewrite ^/app2/(.*)$ /index.html last;
                  }
              }
              error_page   500 502 503 504  /50x.html;
              location = /50x.html {
                  root   html;
              }
          }
      }