从乾坤到无界:一份可落地的微前端 Demo 笔记

—— 含双框架对比、踩坑记录与源码仓库

微前端入门速览

  1. 为什么需要微前端当巨石应用膨胀到“编译 5 min、上线 3 h、回滚 1 h”时,就把“技术栈升级”“独立部署”“灰度发布”变成了奢望。微前端把单体拆成多个可独立开发、测试、部署的子应用,再在一个外壳(主应用)里动态拼装,兼顾了“体验一体化”与“团队自治”。
  2. 核心诉求技术栈无关 | 独立部署 | 运行时隔离 | 存量平滑迁移 | 性能可接受一句话:让“多团队 + 多技术栈 + 频繁发版”不再互相伤害。
  3. 主流实现路线① 路由分发:nginx 或主应用按路由转发到不同站点(最早、最简单,但体验割裂)。② iframe 彻底隔离:DOM/CSS/JS 天然沙箱,但“弹窗全屏、前进后退、SEO、性能”全是坑。③ JS Entry / 快照:single-spa、乾坤、无界等,把子应用打成 JS Bundle,运行时动态挂载/卸载,兼顾体验与隔离。④ Web Component + Module Federation:更原生、更底层,但浏览器支持度和改造成本需评估。
  4. 乾坤(qiankun)速描
    • 阿里开源,single-spa 的“国内增强版”。
    • 基于 JS Entry + HTML Entry 双模式,支持 webpack、vite、umi。
    • 提供样式隔离(experimentalStyleIsolation)、JS 沙箱(ProxySandbox/LegacySandbox)、全局变量 diff、预加载、资源缓存、全局错误捕获。
    • 社区庞大,中文文档友好,但“样式隔离不彻底”“vite 接入需插件”“IE11 下沙箱性能”仍常被吐槽。
  5. 无界(wujie)速描
    • 腾讯开源,Web Components + iframe 的“混血”方案。
    • 子应用跑在 iframe 里,DOM 通过 Web Component 插回主应用,既利用 iframe 的绝对隔离,又解决“弹窗/全屏/路由同步”顽疾。
    • 支持 vite、webpack、angular、react、vue 几乎零改造;子应用可“热插拔”而不刷新整页。
    • 代价:内存占用略高、初次加载多一次 iframe 创建、IE 直接放弃。
  6. 双框架 30 秒对比隔离强度:无界 > 乾坤接入改造成本:无界 < 乾坤社区/文档:乾坤 >> 无界首屏性能:乾坤略优(无 iframe 开销)多实例共存:无界天然支持,乾坤需手动防冲突浏览器下限:乾坤可降 IE11,无界需 Modern Chrome/Edge
  7. 落地小贴士
    • 先画“子应用拆分图”:按业务域 > 团队边界 > 页面维度逐级拆,别一上来就拆组件。
    • 统一“基座协议”:路由前缀、全局状态、错误码、登录态、灰度 KEY。
    • 把“构建、部署、监控、回滚”做成模板,让业务团队只关心自己目录。
    • 给子应用留“逃生窗口”:万一框架出故障,nginx 转发回独立域名仍可跑。
    • 性能预算:子应用首屏 < 200 KB(gzip),基座 < 100 KB, prefetch 用 IntersectionObserver 懒触发。
    • 监控:子应用白屏、JS Error、资源 404、卸载残留都要上报,否则“互相甩锅”无尽头。

qiankun-demo

使用 qiankun 实现微前端

qiankun 文档:https://qiankun.umijs.org/zh

主应用

安装依赖
npm i qiankun -S
子应用配置

在 main.ts 中加上子应用相关的配置

registerMicroApps([
  {
    name: "sub-app",
    entry: "//localhost:5174",
    container: "#sub-app",
    activeRule: "/subApp",
    props: {
      routerBase: "/subApp",
    },
  },
]);
​
start({
  sandbox: {
    strictStyleIsolation: false,
    experimentalStyleIsolation: true,
  },
});

子应用

安装依赖
npm i vite-plugin-qiankun -D
改造 main.ts
import "./assets/main.css";
​
import { createApp, type App as VueApp } from "vue";
import App from "./App.vue";
import { handleCreateRouter } from "./router";
import { renderWithQiankun, qiankunWindow } from "vite-plugin-qiankun/dist/helper";
​
let app: VueApp | null = null;
​
// 渲染函数
const render = (props: any = {}) => {
  const { container, routerBase } = props;
  app = createApp(App);
  app.use(handleCreateRouter(routerBase));
  app.mount(container ? container.querySelector("#app") : "#app");
};
​
renderWithQiankun({
  bootstrap() {
    console.log("Vue3 微应用 bootstrap");
  },
  mount(props) {
    console.log("props", props);
    render(props);
  },
  update() {
    console.log("Vue3 微应用 update");
  },
  unmount() {
    console.log("Vue3 微应用 unmount");
    app?.unmount();
    app = null;
  },
});
// 独立运行时
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render();
}
改造 vite.config.ts
import { fileURLToPath, URL } from "node:url";
​
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
import qiankun from "vite-plugin-qiankun";
const pkg = require("./package.json");
const appName = pkg.name;
​
// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // vueDevTools(),
    qiankun(appName, {
      useDevMode: true, // 开发环境强制启用
    }),
  ],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  server: {
    cors: true,
    allowedHosts: true,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
});

注意点

  • 子应用的样式会影响主应用
    • 我的主应用开启了样式隔离,但是没啥效果,不确定是就是这种设定,或者我写的有问题,又或是 vue3+vite 版本不支持
    • 所有全局样式尽量保持一致,使用的 UI 组件库版本尽量也保持一致
    • 主应用开启样式隔离代码:
      start({
        sandbox: {
          strictStyleIsolation: false,
          experimentalStyleIsolation: true,
        },
      });
      

无界微前端

无界微前端示例代码 lord-app(主应用)、sub-app(子应用)

官网文档:https://wujie-micro.github.io/doc/

主应用

安装依赖
npm i wujie-vue3 -S
在主应用中注册无界
import WujieVue from "wujie-vue3";
​
app.use(WujieVue); // 注册无界组件
在组件中使用 WujieVue

views/SubApp.vue

<template>
  <WujieVue
    width="100%"
    height="100%"
    name="sub-app"
    :url="subAppUrl"
    :sync="true"
    :props="subProps"
    @before-load="beforeLoad"
    @after-mount="afterMount"></WujieVue>
</template>

<script setup lang="ts">
import { ref } from "vue";

// 主应用地址
const subAppUrl = ref("http://localhost:5174");
// 传给子应用的数据
const subProps = ref({
  baseUrl: "/subApp",
  token: "Bearer 1234567890",
});
// 子应用加载前
const beforeLoad = () => {
  console.log("子应用加载前");
};
// 子应用加载完成
const afterMount = () => {
  console.log("子应用挂载完成");
};
</script>

<style scoped></style>

需要在路由中增加一个

{
	path: "/subApp",
  name: "subApp",
  component: () => import("@/views/SubApp.vue"),
},

子应用

子应用需要改造一点内容

改造 main.ts

需要根据 POWERED_BY_WUJIE 判断环境,抽离出了渲染执行的函数

import "./assets/main.css";

import { createApp } from "vue";
import App from "./App.vue";
import { createRouterInstance } from "./router";

let app: ReturnType<typeof createApp>;
const render = (props: any = {}) => {
  let { container, baseUrl } = props;
  app = createApp(App);
  app.use(createRouterInstance(baseUrl || "/"));
  app.mount(container ? container.querySelector("#app") : "#app");
};

// 判断是否运行在无界环境中
if ((window as any).__POWERED_BY_WUJIE__) {
  // 声明 mount 函数,供无界在适当时机调用
  (window as any).__WUJIE_MOUNT = () => {
    const props = (window as any).$wujie?.props;
    render(props);
  };
  // 声明 unmount 函数,供无界在适当时机调用
  (window as any).__WUJIE_UNMOUNT = () => {
    app?.unmount();
  };
} else {
  // 正常启动
  render();
}
改造路由

需要给路由加上前缀

import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";

let routerInstance: ReturnType<typeof createRouter> | null = null;
// 获取路由示例
export function getRouter() {
  if (!routerInstance) {
    throw new Error("[Router] 路由实例未初始化!请在 main.ts 中先调用 createRouterInstance()");
  }
  return routerInstance;
}

// 工厂函数
export function createRouterInstance(basePath: string = "/") {
  // 单例模式:防止重复创建
  if (routerInstance) {
    console.warn("[Router] 路由实例已存在,返回现有实例");
    return routerInstance;
  }

  const router = createRouter({
    history: createWebHistory(basePath),
    routes: [
      {
        path: "/",
        name: "home",
        component: HomeView,
      },
      {
        path: "/about",
        name: "about",
        // route level code-splitting
        // this generates a separate chunk (About.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import("../views/AboutView.vue"),
      },
    ],
  });

  // 注册守卫

  // 缓存实例
  routerInstance = router;
  return router;
}
改造 vite.config.ts

本地调试需要开启 cors 或者 headers 请求头 Access-Control-Allow-Origin 改成*

后续发布到正式服务器还是得设置这种

server: {
  cors: true,
  headers: {
  	"Access-Control-Allow-Origin": "*",
  },
},

注意点

  • 关闭浏览器的 Vue.js devtools
    • 如果没有关闭会出现报错,Vue DevTools 在多应用环境下重复定义全局钩子
      Uncaught TypeError: Cannot redefine property: __VUE_DEVTOOLS_GLOBAL_HOOK__
          at Object.defineProperty (<anonymous>)
          at detectIframeApp (<anonymous>:33:10)
          at <anonymous>:69:3
      
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐