辰風依恛
文章38
标签0
分类12
从乾坤到无界:一份可落地的微前端 Demo 笔记

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

从乾坤到无界:一份可落地的微前端 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

主应用

安装依赖

1
npm i qiankun -S

子应用配置

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
registerMicroApps([
{
name: "sub-app",
entry: "//localhost:5174",
container: "#sub-app",
activeRule: "/subApp",
props: {
routerBase: "/subApp",
},
},
]);

start({
sandbox: {
strictStyleIsolation: false,
experimentalStyleIsolation: true,
},
});

子应用

安装依赖

1
npm i vite-plugin-qiankun -D

改造main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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组件库版本尽量也保持一致

    • 主应用开启样式隔离代码:

      1
      2
      3
      4
      5
      6
      start({
      sandbox: {
      strictStyleIsolation: false,
      experimentalStyleIsolation: true,
      },
      });

无界微前端

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

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

主应用

安装依赖

1
npm i wujie-vue3 -S

在主应用中注册无界

1
2
3
import WujieVue from "wujie-vue3";

app.use(WujieVue); // 注册无界组件

在组件中使用 WujieVue

views/SubApp.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<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>

需要在路由中增加一个

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

子应用

子应用需要改造一点内容

改造main.ts

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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();
}

改造路由

需要给路由加上前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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改成*

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

1
2
3
4
5
6
server: {
cors: true,
headers: {
"Access-Control-Allow-Origin": "*",
},
},

注意点

  • 关闭浏览器的Vue.js devtools

    • 如果没有关闭会出现报错,Vue DevTools 在多应用环境下重复定义全局钩子

      1
      2
      3
      4
      Uncaught TypeError: Cannot redefine property: __VUE_DEVTOOLS_GLOBAL_HOOK__
      at Object.defineProperty (<anonymous>)
      at detectIframeApp (<anonymous>:33:10)
      at <anonymous>:69:3
本文作者:辰風依恛
本文链接:https://766187397.github.io/2025/11/30/%E4%BB%8E%E4%B9%BE%E5%9D%A4%E5%88%B0%E6%97%A0%E7%95%8C%EF%BC%9A%E4%B8%80%E4%BB%BD%E5%8F%AF%E8%90%BD%E5%9C%B0%E7%9A%84%E5%BE%AE%E5%89%8D%E7%AB%AF%20Demo%20%E7%AC%94%E8%AE%B0/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×