Skip to content
小沐沐吖

微前端之 Vite + qiankun + Vue3

1176 字约 4 分钟

ElementPlus微前端qiankunVue.js

2024-11-08

qiankun可能是你见过最完善的微前端解决方案 🧐

  • 简单: 任意 js 框架均可使用。微应用接入像使用接入一个 iframe 系统一样简单,但实际不是 iframe。
  • 完备: 几乎包含所有构建微前端系统时所需要的基本能力,如 样式隔离、js 沙箱、预加载等。
  • 生产可用: 已在蚂蚁内外经受过足够大量的线上系统的考验及打磨,健壮性值得信赖。

1、主应用改造

技术栈:Vite + Vue3

1.1、安装 qiankun

npm i qiankun

1.2、子应用管理配置

项目src目录下新建文件夹qiankun进行集中管理,新建config.js

// /src/qiankun/config,js

const subApps = [
  {
    name: "OneSon", // 子应用名称
    entry: "//localhost:3001", // 子应用入口
    container: "#subapp-viewport", // 子应用挂载的dom
    activeRule: "/one-son", // 子应用触发的路由规则
    props: {}, // 子应用传递的参数
  },
  {
    name: "TwoSon",
    entry: "//localhost:3002",
    container: "#subapp-viewport",
    activeRule: "/two-son",
    props: {}, // 子应用传递的参数
  },
];

/**
 * 创建子应用
 * @param {Object} props 父应用传递给子应用的参数
 * @returns
 */
export const createSubApps = (props) => {
  return subApps.map((item) => {
    return {
      ...item,
      props: {
        ...item.props,
        ...props,
      },
    };
  });
};

1.3、子应用注册

// src/qiankun/qiankunHooks.js

const useQianKunHooks = () => {
  const registerApps = (props) => {
    try {
      registerMicroApps(createSubApps(props), {
        beforeLoad: [
          async (app) => {
            console.log("before load", app.name);
          },
        ],
        beforeMount: [
          async (app) => {
            console.log("before mount", app.name);
          },
        ],
        afterMount: [
          async (app) => {
            console.log("after mount", app.name);
          },
        ],
      });
    } catch (e) {
      console.error(e);
    }
  };
  const initQianKun = (props) => {
    if (window.qiankunStarted) return;
    registerApps(props);
    start({
      prefetch: "all",
      sandbox: {
        experimentalStyleIsolation: true, // 样式隔离(非严格模式)
      },
      // 过滤子应用资源加载
      excludeAssetFilter: (assetUrl) => {
        const witeList = ["map.qq.com"];
        return !witeList.some((url) => assetUrl.includes(url));
      },
    });
  };

  return {
    initQianKun,
  };
};

export default useQianKunHooks;

1.4、Layout 改造

src/components目录下新建组件SubAppComponent.vue,作为子应用compontent组件,里面只需要<template></template>标签

<!-- /src/components/SubAppComponent.vue -->

<template></template>

修改src/layout/main.vue文件

  • template结构

router-view下新增<div id="subapp-viewport"></div>作为子应用的挂载容器;

<el-container class="h-screen">
  <el-aside width="200px" class="bg-[#001529]">
    <el-scrollbar>
      <el-menu
        active-text-color="#fff"
        background-color="#001529"
        text-color="#B2B6B9"
        :default-active="route.path"
        router
      >
        <el-menu-item index="/">
          <el-icon><Setting /></el-icon>
          <span>Dashboard</span>
        </el-menu-item>
      </el-menu>
    </el-scrollbar>
  </el-aside>
  <el-main class="bg-gray-100">
    <router-view />
    <div id="subapp-viewport"></div>
  </el-main>
</el-container>
  • script setup逻辑
import useQianKunHooks from "@/qiankun/qiankunHooks";
import { onBeforeMount } from "vue";
const { initQianKun } = useQianKunHooks();

onBeforeMount(() => {
  // 初始化乾坤,传入数据;如token
  const props = {
    token: "adminToken",
  };
  initQianKun(props);
});

1.5、添加子应用路由

// src/router/index.js

import {
  createRouter,
  createWebHistory,
  createWebHashHistory,
} from "vue-router";
import Layout from "@/layout/main.vue";

const routes = [
  {
    path: "/",
    component: Layout,
    redirect: "",
    children: [
      {
        path: "",
        name: "dashboard",
        component: () => import("@/views/dashboard/index.vue"),
      },
      // 子应用one-son
      {
        path: "/one-son",
        name: "one-son",
        children: [
          {
            path: "/one-son/user",
            name: "one-son-user",
            component: () => import("@/components/SubAppComponent.vue"),
          },
        ],
      },
      // 子应用two-son
      {
        path: "/two-son",
        name: "two-son",
        children: [
          {
            path: "/two-son/dept",
            name: "two-son-dept",
            component: () => import("@/components/SubAppComponent.vue"),
          },
        ],
      },
    ],
  },
];

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

/**
 * 全局前置守卫
 */
router.beforeEach((to, from) => {});

export default router;

layout中添加对应菜单

<el-menu
  active-text-color="#fff"
  background-color="#001529"
  text-color="#B2B6B9"
  :default-active="route.path"
  router
>
  <el-menu-item index="/">
    <el-icon><Setting /></el-icon>
    <span>Dashboard</span>
  </el-menu-item>
  <el-menu-item index="/one-son/user">
    <el-icon><Setting /></el-icon>
    <span>用户管理</span>
  </el-menu-item>
  <el-menu-item index="/two-son/dept">
    <el-icon><Setting /></el-icon>
    <span>部门管理</span>
  </el-menu-item>
</el-menu>

注意

  • 若子应用在开发环境中使用代理,则需要把子应用的代理也加入到主应用里面

2、子应用改造

2.1、修改 vite.config 配置

  • 安装vite-plugin-qiankun插件
  • 引用qiankun插件,第一个参数为子应用名称,需要和主应用中注册的子应用名称一致
// vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import WindiCSS from "vite-plugin-windicss";
import qiankun from "vite-plugin-qiankun";

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    WindiCSS(),
    qiankun("OneSon", {
      useDevMode: true,
    }),
  ],
  resolve: {
    alias: {
      "@": "/src",
    },
  },
});

2.2、改造入口 main

  • 添加registerMicroApps相关配置
// src/main.js

import { createApp } from "vue";
import "virtual:windi.css";
import App from "./App.vue";
import router from "./router";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import {
  renderWithQiankun,
  qiankunWindow,
} from "vite-plugin-qiankun/dist/helper";

let app;

const bootstrap = (container) => {
  app = createApp(App);
  app.use(ElementPlus);
  app.use(router);
  app.mount(container ? container.querySelector("#app") : "#app");
};

const initQiankun = () => {
  renderWithQiankun({
    mount(props) {
      const { container, token } = props;
      console.log("主应用传递的token:" + token);
      bootstrap(container);
    },
    bootstrap() {},
    unmount(props) {
      app.unmount();
    },
  });
};

qiankunWindow.__POWERED_BY_QIANKUN__ ? initQiankun() : bootstrap();

2.3、改造路由(History)

// src/router/index.js

import {
  createRouter,
  createWebHistory,
  createWebHashHistory,
} from "vue-router";
import Layout from "@/layout/main.vue";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";

const routes = [
  {
    path: "/",
    component: Layout,
    redirect: "/user",
    children: [
      {
        path: "user",
        name: "user",
        component: () => import("@/views/user/index.vue"),
      },
    ],
  },
];

const router = createRouter({
  routes,
  // 判断是否是qiankun环境
  history: createWebHistory(
    qiankunWindow.__POWERED_BY_QIANKUN__ ? "/one-son" : ""
  ),
});

/**
 * 全局前置守卫
 */
router.beforeEach((to, from) => {});

export default router;

扩展

路由使用Hash模式

  1. 将主应用路由模式改为createWebHashHistory
  2. 子应用修改路由模式为
// src/router/index.js

const router = createRouter({
  routes,
  history: createWebHashHistory(
    qiankunWindow.__POWERED_BY_QIANKUN__ ? "/#/two-son" : ""
  ),
});

强烈不推荐,控制台有警告 ⚠

3、演示

演示

4、FAQ

  1. WindiCSS主子应用相互影响样式,暂未解决