💥不说废话,带你上手使用 qiankun 微前端并深入理解原理!

一. 前置

本文假设你已经:

  1. 熟悉至少一种现代前端框架(Vue 或 React)。
  2. 理解单页应用 的基本概念和开发模式。
  3. Webpack 的基本概念和基本原理。

前三四五章帮助快速理解微前端并且学会如何接入

后面章节为深入原理解析

二. 本文能给你带来什么

读完本文,你将获得:

  1. 理解微前端架构的价值
  2. 亲手搭建一个包含 Vue 主应用和 React/Vue 子应用的完整 qiankun 项目。
  3. 深入 qiankunJS 沙箱样式隔离底层原理 并能够解决实际简单问题。
  4. 掌握 应用 之间 数据传递状态同步 的各种方式和优缺点。

三. 简单说微前端要干什么

它主要解决了三大难题:

  1. 技术栈异构

整个项目被锁定在 单一技术栈 上, 微前端使得每个微应用都可以使用任何技术栈,由主应用分别进行加载。

  1. 降低维护成本,拆分巨石应用

大型应用所有代码耦合在一起,编译部署慢,新人上手成本高。微前端可以按业务拆分应用。

  1. 减少项目团队协作问题,团队独立开发与部署

多团队在 同个代码库协作代码冲突依赖问题 频发,发布流程又慢又危险。 微前端使得每个团队拥有自己微应用的代码库,可以独立决策、开发、部署。

四. qiankun 怎么运行的

一个简易流程图:

graph TD A[用户访问主应用] --> B[主应用启动] B --> C[注册子应用信息表] C --> D{监听 URL 变化} D --> E[用户点击导航/URL改变] E --> F[匹配子应用规则] F --> G{找到匹配的子应用?} G -->|否| D G -->|是| H[HTTP请求子应用入口地址] H --> I[获取子应用HTML文本] I --> J[解析HTML提取资源清单] J --> K[提取JS文件列表] J --> L[提取CSS文件列表] K --> M[创建JS沙箱环境] M --> N[在沙箱中执行JS代码] L --> O[动态插入CSS到head标签] O --> P[CSS在全局生效但会被管理] N --> Q[调用子应用mount函数] P --> Q Q --> R[子应用渲染到指定容器] R --> S[子应用运行中] S --> T{URL再次变化?} T -->|是| U[调用当前子应用unmount] U --> V[从head中移除子应用CSS] V --> W[清理JS沙箱] W --> F T -->|否| S style A fill:#e1f5fe style R fill:#c8e6c9 style M fill:#fff3e0 style O fill:#fce4ec style V fill:#ffebee

qiankun 的工作流程可以分解为以下几个核心步骤:

1. 主应用注册子应用信息

  • 在主应用启动时,你需要告诉 qiankun 你有哪些子应用,并提供几个关键信息:
  1. name: 给子应用起一个唯一的英文名,作为它的身份标识。
  2. entry: 子应用独立运行时的访问地址,比如 http://localhost:8081qiankun 会通过这个地址去获取子应用的资源。
  3. container: 主应用里一个空的 <div> 元素的 ID,告诉 qiankun 将来把这个子应用渲染到哪里。
  4. activeRule: 一个路径规则,比如 /app1。当浏览器 URL 匹配到这个规则时,qiankun 就知道该加载这个子应用了。

2. 监听路由变化动态加载子应用

  • 主应用启动 qiankun 后,它会开始监听浏览器地址栏的 URL 变化,并拿新的 URL 去和第一步注册的所有子应用的 activeRule 进行匹配。一旦匹配成功,就触发下一步。

3. 从 entry 获取子应用内容

  • qiankun 匹配到要加载的子应用后,它会向该子应用的 entry 地址发送一个 HTTP 请求并拿到返回的 index.html 文件。
  • 之后,qiankun 调用解析器一样,分析这个文本,从中找出所有 <script> 标签的 src 地址和 <link rel="stylesheet"><style> 标签里的 CSS 内容。
  • 至此,qiankun 就拿到了一份关于这个子应用需要加载的所有 JS 和 CSS 资源的清单。

4. 执行子应用JS

  1. qiankun 为子应用创建一个"代理"的 window 对象。

  2. 当子应用的 JS 代码尝试写入 全局变量时(如 window.myVar = 1),Proxy 会拦截这个操作,并将这个变量存储在一个只属于当前子应用的内部对象里,而不会 污染真实的 window 对象。

  3. 当子应用的 JS 代码尝试读取 全局变量时(如 console.log(window.myVar)),Proxy 会先从那个内部对象里查找。如果找不到,它会安全地"穿透"到真实的 window 对象上去读取(比如读取 window.location)。

5. 插入子应用 CSS

  1. qiankun 默认以 <style> 标签的形式,将 css 直接插入 到主文档的 <head> 之中
  2. 样式都是全局的,子应用之间、子应用与主应用之间会相互污染,怎么解决可以看下面精讲

6. 子应用生命周期

  • qiankun 要求每个子应用都必须导出几个特定的函数(bootstrap, mount, unmount)。
  • 在完成上述所有准备工作后,qiankun 会调用子应用的 mount 函数,并把前面指定的 container 容器作为参数传给它。子应用在 mount 函数里执行自己的渲染逻辑(比如 ReactDOM.render()createApp().mount()),把自己渲染到那个容器里。
  • 当 URL 变化,需要切换到另一个子应用时,qiankun 会先调用当前子应用的 unmount 函数,让它执行清理工作(比如销毁实例、清除定时器),然后再去加载新的子应用。

五. Vue 3 + React + qiankun 搭建流程

这一章带你运用上面的原理从头搭建一个超简单 qiankun 工程,并梳理运行流程

看效果:

前置

准备一个 webpack 作为打包器的主应用以及两个子应用(qiankunwebpack 支持更好更方便),并且要求都是单页应用。

第一步:主应用 Vue3 (main-app) 配置

  1. 安装 qiankun:
csharp 复制代码
pnpm add qiankun
  1. 创建 qiankun 配置文件(对应注册子应用) : main-app/src/qiankun-setup.js
js 复制代码
import { registerMicroApps, start } from 'qiankun';

const apps = [
  {
    name: 'sub-app-vue',
    entry: '//localhost:8081', // 你的 Vue 应用端口
    container: '#subapp-container',
    activeRule: '/vue-app',
  },
  {
    name: 'sub-app-react',
    entry: '//localhost:3000', // 你的 React 应用端口
    container: '#subapp-container',
    activeRule: '/react-app',
  },
];

export const setupQiankun = () => {
  registerMicroApps(apps);
  start();
};
  1. main.js 中启动 qiankun(对应qiankun开始监听路由):
js 复制代码
// ... existing code ...
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { setupQiankun } from './qiankun-setup'; // 引入

createApp(App).use(router).mount('#app');

setupQiankun(); // 启动
  1. App.vue 中设置容器和导航:
js 复制代码
<template>
  <div id="main-app">
    <header>
      <router-link to="/">主页</router-link> |
      <router-link to="/vue-app">访问Vue子应用</router-link> |
      <router-link to="/react-app">访问React子应用</router-link>
    </header>
    <!-- 主应用路由渲染 -->
    <router-view />
    <!-- 子应用容器 -->
    <div id="subapp-container"></div>
  </div>
</template>
  // ... existing code ...

第二步:Vue 子应用 (sub-app-vue) 改造

  1. 配置 vue.config.js 以支持跨域和 qiankun 的模块规范。
js 复制代码
const { defineConfig } = require('@vue/cli-service');
const { name } = require('./package.json');

module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 8081,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`, // 配合libraryTarget使用,挂载到全局时的名字
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式,便于将导出的 bootstrap、mount、unmount 等函数挂载到全局
      chunkLoadingGlobal: `webpackJsonp_${name}`,// 懒加载函数名字,防止不同应用间在懒加载时的冲突
    },
  },
});

为接入 qiankun,需在 vue.config.js 中配置 Webpack:

  1. output.libraryTarget: 'umd':将子应用打包成通用模块,以便 qiankun 能识别并获取其导出的生命周期函数。
  2. output.library: '${name}-[name]':为打包后的库设置唯一名称,防止多子应用在全局 window 上的命名冲突。
  3. output.chunkLoadingGlobal: 'webpackJsonp_${name}':为 Webpack 懒加载函数设置唯一名称,避免不同应用间的 chunk 加载逻辑互相覆盖。
  1. 添加入口文件 public-path.js: sub-app-vue/src/public-path.js
js 复制代码
if (window.__POWERED_BY_QIANKUN__) {
// 运行时动态修改所有静态资源路径
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

在 qiankun 架构中,子应用不知道自己的静态资源(如图片、JS chunk)应从何处加载,因为浏览器默认会从主应用域名请求,导致 404 错误。
为解决此问题,qiankun 向子应用注入了 window.__POWERED_BY_QIANKUN__ 标志位和 window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ 变量(值为子应用的 entry 地址)。子应用在入口文件判断若在 qiankun 环境中,就将这个注入的路径赋值给 Webpack 的 __webpack_public_path__。这样,所有资源请求的 URL 前缀都会在运行时被动态修正,从而确保能正确加载。

  1. 改造 main.js,导出 qiankun 需要的生命周期。
js 复制代码
import './public-path'; // 必须放在顶部
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

let instance = null;

function render(props = {}) {
  const { container } = props;
  instance = createApp(App);
  instance
    .use(router)
    .mount(container ? container.querySelector('#app') : '#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

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.unmount();
  instance = null;
}

第三步:React 子应用 (sub-app-react) 改造

  1. 安装 react-app-rewired
css 复制代码
npm install react-app-rewired --save-dev

react-app-rewired 能在不执行 "eject"(暴露配置)的前提下,通过创建 config-overrides.js 文件来覆盖 Webpack 配置。只需将 package.json 中的 react-scripts 命令替换为 react-app-rewired,即可轻松实现 outputdevServer 的定制,是 CRA 项目接入 qiankun 的必备工具。

  1. 修改 package.json scripts:
json 复制代码
// ... existing code ...
"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-scripts eject"
},
// ... existing code ...
  1. 创建 config-overrides.js: sub-app-react/config-overrides.js
js 复制代码
const { name } = require('./package.json');

module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
    return config;
  },
  devServer: (configFunction) => {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.headers = {
        'Access-Control-Allow-Origin': '*',
      };
      return config;
    };
  },
};
  1. 添加入口文件 public-path.js: sub-app-react/src/public-path.js
js 复制代码
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 改造 index.js,导出生命周期。
js 复制代码
import './public-path'; // 必须放在顶部
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

let root = null;

function render(props) {
  const { container } = props;
  const dom = container ? container.querySelector('#root') : document.getElementById('root');
  root = ReactDOM.createRoot(dom);
  root.render(
    <React.StrictMode>
    <App />
    </React.StrictMode>
  );
}

if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

export async function bootstrap() {
  console.log('[react] react app bootstraped');
}

export async function mount(props) {
  console.log('[react] props from main framework', props);
  render(props);
}

export async function unmount() {
  if (root) {
    root.unmount();
    root = null;
  }
}

六. JS 沙箱验证

JS 沙箱验证

Proxy Sandbox 是如何在不污染全局 window 的情况下工作的?我们来验证一下。

  1. 在 Vue 子应用中设置全局变量 : sub-app-vue/src/views/HomeView.vue
js 复制代码
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
  window.myGlobalVar = '我来自Vue子应用';
  console.log('【Vue子应用】设置了全局变量 window.myGlobalVar');
});
</script>
  1. 在 React 子应用中读取该变量 : sub-app-react/src/App.js
js 复制代码
import React, { useEffect } from 'react';
// ...
function App(props) {
  useEffect(() => {
    console.log('【React子应用】尝试读取 window.myGlobalVar:', window.myGlobalVar);
  }, []);
  // ...
}
export default App;

实验步骤与结论

  • 先访问 Vue 子应用,控制台打印"设置了全局变量"。
  • 再切换到 React 子应用,控制台打印"尝试读取 window.myGlobalVar: undefined"。
  • 结论 :Vue 子应用对 window 的写入被Proxy沙箱拦截,并储存在了自己的"独立化妆间"里,并未影响到真实的 window,因此 React 子应用无法读取到它。 样式隔离验证

七. CSS 沙箱验证

qiankun 通过三种不同的策略来解决样式冲突问题,我们来逐一讲解。

1. 动态样式隔离

这是 qiankun 默认启用的样式隔离方案。它的工作原理非常简单:

  1. 加载时qiankun 会遍历子应用 HTML 中的所有 <style><link> 标签,将 CSS 内容记录下来,然后动态创建新的 <style> 标签,把这些内容插入到主应用的 <head> 中。
  2. 卸载时qiankun 会找到并移除之前为该子应用添加的所有 <style> 标签。

它确保了在"单实例"场景下(即任何时候只有一个子应用在运行),后加载的应用样式不会被先前的应用样式污染。

缺点(这也是最关键的)

  1. 无法隔离主应用与子应用 :由于子应用的样式被直接插入主文档,如果选择器命名不当(例如,.btn),主应用的样式很容易被子应用覆盖,反之亦然。
  2. 无法隔离多实例子应用:如果同一个子应用需要被实例化多次并同时展示,它们的样式会相互冲突,因为它们共享相同的 CSS 规则。

验证默认隔离的局限性

我们来复现一下主应用被污染的场景。

  1. 在主应用中定义一个全局样式 : main-app/src/App.vue
html 复制代码
<template>
  <div id="main-app">
    <header>
      <!-- ... -->
      <button class="common-button">主应用按钮</button>
    </header>
    <!-- ... -->
    <div id="subapp-container"></div>
  </div>
</template>

<style>
/* 一个非常通用的类名 */
.common-button {
  background-color: #42b983; /* 绿色 */
  color: white;
  border: none;
  padding: 10px 20px;
  cursor: pointer;
}
</style>
  1. 在 Vue 子应用中定义一个同名但样式不同的类 : sub-app-vue/src/views/HomeView.vue
html 复制代码
<template>
  <div>
    <h3>Vue 子应用</h3>
    <button class="common-button">Vue子应用按钮</button>
  </div>
</template>

<style>
/* 子应用也定义了一个同名类 */
.common-button {
  background-color: #f0ad4e; /* 橙色 */
  border-radius: 8px;
}
</style>

实验步骤与结论

  1. 启动主应用,主应用的按钮是绿色的。
  2. 切换到 Vue 子应用,你会发现子应用的按钮是橙色的,并且有圆角。
  3. 问题出现了 :此时你再回头看主应用的按钮,它也变成了橙色圆角

所以该方案并不推荐使用


2. Shadow DOM 沙箱

这是一种需要手动开启的、隔离性最强的方案。

  • 原理qiankun 会为承载子应用的 container 元素创建一个 Shadow DOM。子应用的所有 DOM 节点和 <style> 标签都会被添加到这个 Shadow DOM 内部。
  • 效果 :Shadow DOM 提供了一个完全封装的"影子"文档树。内部的样式规则绝对不会 泄露到外部主文档,外部的全局样式也绝对不会影响到内部。

如何开启

在主应用注册子应用时,设置 sandbox 配置项:

main-app/src/qiankun-setup.js

js 复制代码
const apps = [
  {
    name: 'sub-app-vue',
    entry: '//localhost:8081',
    container: '#subapp-container',
    activeRule: '/vue-app',
    props: {
      // 在这里向子应用传递参数
    }
  },
  // ... 其他应用
];

export const setupQiankun = () => {
  registerMicroApps(apps);
  start({
    sandbox: {
      strictStyleIsolation: true // 此处开启 Shadow DOM
    }
  });
};

验证 Shadow DOM

开启 strictStyleIsolation: true 后,重复上面的实验:

  1. 启动主应用,按钮是绿色
  2. 切换到 Vue 子应用,子应用的按钮是橙色圆角
  3. 此时,主应用的按钮依然是绿色,未受任何影响。

打开开发者工具,你会看到 subapp-container 内部结构发生了变化:

html 复制代码
<div id="subapp-container">
  #shadow-root (open)
  |-- <!-- 子应用的所有内容和样式都在这里 -->
  |-- <style> .common-button { ... } </style>
  |-- <div id="app">
  |     <button class="common-button">Vue子应用按钮</button>
  |   </div>
</div>

结论:Shadow DOM 提供了完美的样式隔离。

缺点

  • 兼容性:一些旧浏览器不支持 Shadow DOM。
  • 弹出层问题 :子应用中的 modaldialog 等需要挂载到 document.body 的组件,其 DOM 会被渲染到 Shadow DOM 之外,导致无法应用 Shadow DOM 内部的样式。你需要额外处理这类组件的挂载节点。
  • 事件冒泡:某些事件可能无法穿透 Shadow DOM 边界,需要手动处理。

3. 作用域沙箱(Scoped CSS)

这是 qiankun 提供的另一种手动开启的折中方案。它不像 Shadow DOM 那样彻底,但能解决大部分问题且没有 Shadow DOM 的那些缺点。

  • 原理qiankun 会在运行时,为子应用动态添加的每个样式规则,自动增加一个基于 data-qiankun 属性的前缀选择器。
  • 效果 :例如,子应用的规则 .common-button { color: orange; } 会被改写成 div[data-qiankun="sub-app-vue"] .common-button { color: orange; }qiankun 会确保子应用的容器 div 上有 data-qiankun="sub-app-vue" 这个属性。这样,样式规则就只对该子应用容器内的元素生效了。

如何开启

关闭 Shadow DOM(experimentalStyleIsolation: false 或不设置),然后在 start 函数中配置 sandbox

main-app/src/qiankun-setup.js

js 复制代码
export const setupQiankun = () => {
  registerMicroApps(apps);
  start({
    sandbox: {
        experimentalStyleIsolation: true, // 这里开启
    }
  });
}

验证 Scoped CSS

使用上述配置,再次进行实验:

  1. 启动主应用,按钮是绿色
  2. 切换到 Vue 子应用,子应用的按钮是橙色圆角
  3. 主应用的按钮依然是绿色

打开开发者工具,检查 Vue 子应用的样式:

  1. 子应用的容器 div 会被添加一个属性:<div id="subapp-container" data-qiankun="sub-app-vue">...</div>

  2. <head> 中,你会找到被 qiankun 改写后的样式:

    css 复制代码
    div[data-qiankun="sub-app-vue"] .common-button {
      background-color: #f0ad4e;
      border-radius: 8px;
    }

结论:Scoped CSS 通过动态添加属性选择器,实现了子应用样式的作用域限制,成功隔离了主应用和子应用、以及子应用之间的样式,且没有 Shadow DOM 的那些副作用。

缺点

  • 性能开销:相比默认策略,它需要遍历和改写所有 CSS 规则,有轻微的性能开销。
  • 复杂选择器:在极少数情况下,对于非常复杂的 CSS 选择器,改写可能会失败或产生非预期的效果。

八. 应用间常用通信方法

前两种基本够用,但有局限性 最好了解每种通信方式的优劣

1. 基于 initGlobalState 的官方通信方案 (推荐)

这是 qiankun 官方推荐并内置的通信机制,基于 发布-订阅模式 实现,适用于主子应用、子子应用间的复杂状态同步。

  • 原理qiankun 在内部维护一个全局状态对象。主应用通过 initGlobalState 初始化这个状态,并返回一个 actions 对象。子应用在 mount 生命周期钩子中通过 props 获取这个 actions 对象,然后可以使用 onGlobalStateChange 监听状态变化,或使用 setGlobalState 修改状态。任何一方的修改都会通知所有监听者。

使用步骤

  1. 主应用:初始化和修改状态 main-app/src/qiankun-setup.js
js 复制代码
import { initGlobalState, registerMicroApps, start } from 'qiankun';

// 1. 定义一个初始 state
const initialState = {
	theme: "light",
	userInfo: {
		name: "Admin",
		role: "administrator",
	},
	event: null,
};


// 2. 初始化 state,返回 actions 对象
const actions = initGlobalState(initialState);

// 3. 监听 state 变化,并做出响应
actions.onGlobalStateChange((state, prev) => {
  // state: 当前最新的 state
  // prev: 变化前的 state
  console.log('【主应用】监听到全局状态变化:', state);
});


// 注册和启动 qiankun
export const setupQiankun = () => {
  registerMicroApps(/*...*/);
  start();
};
  1. 子应用:监听和修改状态 sub-app-vue/src/main.js
js 复制代码
// ...
let instance = null;

function render(props) {
  const { container } = props;
  
 	// 将 actions 保存下来,方便其他组件使用
	// 方案一:挂载到全局 (简单但不推荐)
	// app.config.globalProperties.$actions = actions;
	// 方案二:通过 provide/inject (推荐)
	instance = createApp(App);

	if (actions) {
		instance.provide("actions", actions);
	}
  instance.use(router).mount(container ? container.querySelector('#app') : '#app');
}

// ...
export async function mount(props) {
  console.log('[vue] props from main framework', props);
  
  // 1. 在 mount 时注册监听
  props.onGlobalStateChange((state, prev) => {
    console.log('[vue-sub] 监听到全局状态变化:', state);
    // 更新子应用自己的状态...
    // 例如,如果子应用也有主题设置,可以根据全局 appTheme 来更新
    if (state.appTheme !== prev.appTheme) {
      // 执行子应用内部的主题切换逻辑
    }
  });

  render(props);
}

// ...
  1. 子应用组件内使用 sub-app-vue/src/views/HomeView.vue
html 复制代码
<template>
  <div>
    <button @click="changeGlobalState">从Vue子应用修改全局状态</button>
    <button @click="toggleTheme">从Vue子应用切换主题</button>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, inject } from "vue";

// 通过 inject 获取全局 actions
const actions = inject("actions");

const theme = ref("");

// 监听全局状态变化的处理器
const stateChangeHandler = (state, prevState) => {
	console.log("【Vue子应用】监听到全局状态变化:", state);
	theme.value = state?.theme;
};

onMounted(() => {
	// 组件挂载时,注册 onGlobalStateChange 监听
	if (actions) {
		// console.log("【Vue子应用】注册全局状态监听", actions);
		// 初始化时,先主动获取一次当前 state
		const currentState = actions.getGlobalState?.();
		theme.value = currentState?.theme;
		actions.onGlobalStateChange(stateChangeHandler, true); // 第二个参数为 true,立即执行一次
	} 
});

onUnmounted(() => {
	// 组件卸载时,注销监听
	if (actions) {
		actions.offGlobalStateChange(stateChangeHandler);
	}
});
</script>

看效果:

2. 基于 props 的单向通信

这是最简单的通信方式,用于主应用向子应用单向传递数据或方法。

  • 原理 :在主应用 registerMicroApps 时,可以通过 props 属性传递任意数据或函数给子应用。子应用在 mount 生命周期钩子中,通过参数 props 接收这些数据。

使用步骤

  1. 主应用:通过 props 传递数据和函数 main-app/src/qiankun-setup.js
js 复制代码
function showAlert(msg) {
  alert(`[主应用消息]: ${msg}`);
}

const apps = [
  {
    name: 'sub-app-react',
    entry: '//localhost:3000',
    container: '#subapp-container',
    activeRule: '/react-app',
    props: {
      // 传递静态数据
      mainAppName: 'MyMainApp',
      // 传递方法,让子应用可以调用主应用的能力
      showAlert,
      // 传递一个对象,子应用可以根据需要解构
      config: {
        apiBaseUrl: '/api',
        // ...
      }
    }
  },
  // ...
];

export const setupQiankun = () => {
  registerMicroApps(apps);
  start();
};
  1. 子应用:在 mount 时接收和使用 sub-app-react/src/index.js
js 复制代码
// ...
function render(props) {
  const { container, mainAppName, showAlert, config } = props;
  
  // 可以在这里将 props 传递给根组件
  const dom = container ? container.querySelector('#root') : document.getElementById('root');
  root = ReactDOM.createRoot(dom);
  root.render(
    <React.StrictMode>
      <App mainAppName={mainAppName} showAlert={showAlert} config={config} />
    </React.StrictMode>
  );
}

// ...
export async function mount(props) {
  console.log('[react] props from main framework', props);
  render(props);
}
// ...
  1. 子应用组件内使用 sub-app-react/src/App.js
js 复制代码
function App({ mainAppName, showAlert, config }) {
  const handleClick = () => {
    // 调用从主应用传来的方法
    showAlert(`Hello from ${mainAppName}'s React child! API URL: ${config.apiBaseUrl}`);
  };

  return (
    <div>
      <button onClick={handleClick}>调用主应用方法并显示配置</button>
    </div>
  );
}

3. 基于路由参数或路径

这是最简单、最通用的 Web 页面间通信方式。

  • 原理 :将需要传递的信息编码到 URL 中。可以是 query string (?id=123),也可以是 path (/user/123)。主应用通过修改 URL 来"通知"子应用,子应用监听 URL 变化并解析参数来获取信息。

使用步骤

  1. 主应用:通过路由导航传递参数 main-app/src/App.vue
html 复制代码
<template>
  <div>
    <!-- ... -->
    <router-link to="/vue-app?userId=1001&userName=Alice">访问Vue子应用并传参</router-link> |
    <router-link to="/react-app/detail/2002">访问React子应用并传路径参数</router-link>
  </div>
</template>
  1. 子应用:在组件内解析路由参数 sub-app-vue/src/views/HomeView.vue
html 复制代码
<script setup>
import { useRoute } from 'vue-router';
import { onMounted } from 'vue';

const route = useRoute();

onMounted(() => {
  const userId = route.query.userId;
  const userName = route.query.userName;
  console.log('【Vue子应用】从 URL 获取到的 userId:', userId, 'userName:', userName); // "1001", "Alice"
});
</script>

sub-app-react/src/App.js (假设React路由已配置 '/detail/:id'

js 复制代码
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom'; // 假设使用 react-router-dom

function App() {
  const { id } = useParams();

  useEffect(() => {
    console.log('【React子应用】从 URL 获取到的路径参数 id:', id); // "2002"
  }, [id]);

  return (
    <div>
      <h3>React子应用</h3>
      {/* ... */}
    </div>
  );
}

4. 基于 localStorage / sessionStorage

通过浏览器提供的本地存储机制,实现不同应用间的数据共享。

  • 原理 :一个应用将数据存入 localStorage,另一个应用从 localStorage 读取数据。由于 localStorage 具备同源共享特性,因此在同一个主域名下,主应用和所有子应用都能访问到相同的数据。

使用步骤

  1. 发送方(例如主应用):存储数据 main-app/src/App.vue
html 复制代码
<template>
  <button @click="saveDataToLocalStorage">主应用存储数据到 localStorage</button>
</template>
<script setup>
function saveDataToLocalStorage() {
  localStorage.setItem('shared_data_from_main', JSON.stringify({ userId: 'main_user_001', role: 'admin' }));
  console.log('【主应用】数据已存储到 localStorage');
  // 可以触发一个事件通知其他应用数据已更新
  window.dispatchEvent(new CustomEvent('localStorageUpdate', { detail: { key: 'shared_data_from_main' } }));
}
</script>
  1. 接收方(例如 Vue 子应用):读取数据 sub-app-vue/src/views/HomeView.vue
html 复制代码
<template>
  <div>
    <button @click="readDataFromLocalStorage">Vue子应用读取 localStorage</button>
    <p v-if="sharedData">从 localStorage 读取到的数据: {{ sharedData }}</p>
  </div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const sharedData = ref(null);

function readDataFromLocalStorage() {
  const data = localStorage.getItem('shared_data_from_main');
  sharedData.value = data ? JSON.parse(data) : null;
  console.log('【Vue子应用】读取到 localStorage 数据:', sharedData.value);
}

// 为了实现响应式,监听 localStorage 的变化(非所有浏览器都支持 storage 事件监听所有 key)
// 更好的做法是结合 CustomEvent
const handleStorageUpdate = (event) => {
  if (event.detail && event.detail.key === 'shared_data_from_main') {
    readDataFromLocalStorage();
  }
};

onMounted(() => {
  readDataFromLocalStorage(); // 首次加载时读取
  window.addEventListener('localStorageUpdate', handleStorageUpdate);
});

onUnmounted(() => {
  window.removeEventListener('localStorageUpdate', handleStorageUpdate);
});
</script>

5. 基于状态管理工具 (Pinia/Vuex, Redux, Zustand等)

如果所有子应用都使用相同的前端框架(例如都是 Vue),并且都使用了相同的状态管理库,那么可以考虑使用一个共享的状态管理实例。

  • 原理 :在主应用中初始化一个状态管理实例,并通过 propsinitGlobalState 将其传递给子应用。子应用接收后,可以直接使用这个共享的实例来读写状态。

使用步骤

  1. 主应用:初始化状态管理实例并传递 (以 Vue/Pinia 为例) main-app/src/main.js
js 复制代码
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { setupQiankun } from './qiankun-setup';
import { createPinia } from 'pinia';

const app = createApp(App);
const pinia = createPinia(); // 创建一个 Pinia 实例

app.use(router).use(pinia).mount('#app');

// 将 Pinia 实例或特定的 Store 传递给 qiankun
setupQiankun(pinia); // 修改 qiankun-setup.js,使其能接收 pinia 实例

main-app/src/qiankun-setup.js

js 复制代码
// ...
export const setupQiankun = (mainPiniaInstance) => { // 接收 pinia 实例
  registerMicroApps(apps.map(app => ({
    ...app,
    props: {
      ...app.props,
      mainPiniaInstance, // 将主应用的 Pinia 实例作为 props 传递
    },
  })));
  start();
};
  1. 子应用:接收并使用共享实例 sub-app-vue/src/main.js
js 复制代码
// ...
import { createPinia } from 'pinia'; // 子应用也需要 Pinia

let instance = null;
let piniaInstance = null; // 存储 Pinia 实例

function render(props) {
  const { container, mainPiniaInstance } = props;

  instance = createApp(App);
  if (mainPiniaInstance) {
    // 如果主应用传递了 Pinia 实例,则使用它
    piniaInstance = mainPiniaInstance;
  } else {
    // 否则,子应用自己创建一个(独立运行或非 Vue 主应用时)
    piniaInstance = createPinia();
  }
  instance.use(piniaInstance).use(router).mount(container ? container.querySelector('#app') : '#app');
}
// ...
  1. 子应用组件内使用共享 Store sub-app-vue/src/components/MyComponent.vue
html 复制代码
<template>
  <div>
    <p>共享计数器: {{ sharedCounter }}</p>
    <button @click="incrementSharedCounter">增加共享计数器</button>
  </div>
</template>
<script setup>
import { useSharedStore } from './stores/sharedStore'; // 假设定义了一个共享的 store

const sharedStore = useSharedStore();
const sharedCounter = computed(() => sharedStore.counter);

function incrementSharedCounter() {
  sharedStore.increment();
}
</script>

结论:此方案适用于技术栈同构的场景。它能提供非常细粒度的状态管理和响应性。缺点是强耦合于特定框架的状态管理库,限制了技术栈异构的优势。

推荐优先级

根据实践经验,通信方案的选择应遵循以下优先级:

  1. initGlobalState(最高优先级) :官方方案,功能强大且规范,适用于所有需要双向、响应式数据同步的场景。是构建健壮微前端通信系统的首选。
  2. props:适用于主应用向子应用进行初始化和能力注入的场景,简单明了,职责清晰。
  3. URL 参数:适用于传递与页面定位相关的简单状态,符合 Web 标准,对用户友好。
  4. BroadcastChannel / SharedWorker:适用于跨多个 Tab 或需要独立线程处理的复杂通信场景,提供更强大的解耦和性能优势,但实现相对复杂。
  5. localStorage / sessionStorage:适用于存储不频繁变动且需要持久化的简单数据,通常需要配合事件机制来通知变化。
  6. 状态管理工具 (Pinia/Vuex, Redux等) :适用于技术栈同构的场景,提供细粒度的响应式状态管理,但牺牲了技术栈异构的灵活性。
相关推荐
高端章鱼哥2 小时前
前端新人最怕的“居中问题”,八种CSS实现居中的方法一次搞懂!
前端
冷亿!2 小时前
Html爱心代码动态(可修改内容+带源码)
前端·html
Predestination王瀞潞2 小时前
Java EE开发技术(第六章:EL表达式)
前端·javascript·java-ee
掘金012 小时前
在 Vue 3 项目中使用 MQTT 获取数据
前端·javascript·vue.js
QuantumLeap丶2 小时前
《uni-app跨平台开发完全指南》- 03 - Vue.js基础入门
前端·vue.js·uni-app
一 乐3 小时前
个人理财系统|基于java+小程序+APP的个人理财系统设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·后端·小程序
wyzqhhhh3 小时前
同时打开两个浏览器页面,关闭 A 页面的时候,要求 B 页面同时关闭,怎么实现?
前端·javascript·react.js
晴殇i3 小时前
从 WebSocket 到 SSE:实时通信的轻量化演进
前端·javascript
网络点点滴3 小时前
reactive创建对象类型的响应式数据
前端·javascript·vue.js