微前端Qiankun核心原理

一、Qiankun 简介与微前端背景

微前端的核心目标是将一个大型的单体前端应用拆分成多个更小、更独立、可以由不同团队并行开发和部署的子应用。主应用(基座)负责承载这些子应用,管理它们的路由、加载和生命周期。

Qiankun 就是一个流行的微前端解决方案,它基于 single-spa,提供了更开箱即用的 API 和更完善的沙箱隔离机制。

二、Qiankun 整体架构

  1. 主应用(基座 Base App) :

    • 负责注册子应用信息(名称、入口 entry、激活规则 activeRule、容器 container 等)。
    • 监听路由变化,根据 activeRule 匹配决定加载哪个子应用。
    • 提供一个 DOM 容器给子应用渲染。
    • 调用 qiankun 的 registerMicroAppsstart API。
    • 可选:提供全局状态、公共库或工具函数给子应用。
  2. 子应用(Micro App) :

    • 独立开发、独立部署的前端应用(Vue, React, Angular, Static HTML 均可)。
    • 需要导出 qiankun 需要的生命周期钩子:bootstrap, mount, unmount
    • 打包配置需要支持 qiankun 加载(通常是 UMD 格式,并设置允许跨域)。
    • 在 qiankun 环境下运行时,其 JS 和 CSS 会被隔离。
  3. Qiankun 库:

    • 核心:资源加载器(import-html-entry)、JS 沙箱、CSS 隔离器、应用间通信机制、生命周期管理。

三、JS 隔离核心原理 (JS Sandbox)

目标 :防止不同子应用之间以及子应用与主应用之间的全局变量冲突、window 对象污染、定时器/事件监听器泄漏等问题。

Qiankun 主要提供了两种 JS 沙箱:

  1. SnapshotSandbox (快照沙箱 - 旧版,兼容性好但性能稍差)
  2. ProxySandbox (代理沙箱 - 现代,性能好但依赖 Proxy API)

目前 ProxySandbox 是主流且默认的选择。我们重点讲解 ProxySandbox

ProxySandbox 原理详解

ProxySandbox 利用 ES6 的 Proxy 对象来劫持子应用对 window 对象的访问和修改。

核心思想

  1. 创建代理 window :为每个子应用实例创建一个 Proxy 对象,这个 Proxy 模拟了 window 的行为。

  2. 拦截操作 :通过 Proxyget, set, has, deleteProperty 等处理器(traps),拦截子应用对全局变量的读写操作。

  3. 隔离修改

    • 写入 (set) :当子应用尝试设置 window.abc = 123 时,set 处理器会拦截这个操作,并将 abc 及其值 123 存储在沙箱内部的一个专门对象(例如 updatedPropsMap)中,而不是直接修改真实的 window 对象。
    • 读取 (get) :当子应用尝试读取 window.abc 时,get 处理器会优先从沙箱内部的 updatedPropsMap 中查找。如果找到,则返回沙箱内部的值;如果找不到,则向上(或说向外)查找真实的 window 对象上的属性。这样保证了子应用能访问到真实的浏览器 API(如 location, document 等),也能访问到自己设置的全局变量,但这些设置被隔离了。
    • 检查 (has) :类似 get,先检查沙箱内部,再检查真实 window
    • 删除 (deleteProperty) :在沙箱内部记录删除操作,阻止其影响真实 window
  4. 激活与失活

    • 当子应用 mount 时,沙箱被激活 (sandbox.activate()),Proxy 开始拦截并记录修改。
    • 当子应用 unmount 时,沙箱被失活 (sandbox.deactivate())。Qiankun 的 ProxySandbox 设计得比较巧妙,失活时并不会 清除 updatedPropsMap。这样,当同一个子应用下次再 mount 时,之前的全局变量状态可以恢复,提供了类似"快照"的效果,同时避免了 SnapshotSandbox 遍历 window 的性能开销。新的修改会继续记录在 updatedPropsMap 中。
  5. this 指向修正 :在 get 代理中,当获取到的属性是 window 上的函数时(如 window.fetch),需要确保返回的函数在执行时 this 仍然指向真实的 window,通常通过 Function.prototype.bind(window) 实现。

ProxySandbox 简化代码示例 (概念性)

JavaScript 复制代码
// 这是一个高度简化的 ProxySandbox 概念模型,实际 qiankun 代码更复杂健壮

class SimpleProxySandbox {
    constructor(name) {
        this.name = name;
        this.sandboxRunning = false; // 沙箱是否激活
        this.updatedPropsMap = new Map(); // 存储子应用对 window 的修改

        // 创建一个空的 'fakeWindow' 对象,作为代理的目标
        // Qiankun 实际实现会更复杂,可能需要处理原型链等
        const fakeWindow = Object.create(null);

        const { updatedPropsMap } = this; // 闭包引用

        // 创建代理
        this.proxy = new Proxy(fakeWindow, {
            /**
             * 拦截设置属性操作 (window.prop = value)
             * @param target - fakeWindow
             * @param prop - 属性名 (string | symbol)
             * @param value - 属性值
             * @param receiver - 代理对象本身 (this.proxy)
             * @returns boolean - 是否设置成功
             */
            set: (target, prop, value, receiver) => {
                if (this.sandboxRunning) {
                    // 如果沙箱在运行,将修改记录在内部 Map 中
                    updatedPropsMap.set(prop, value);
                    // console.log(`[Sandbox ${this.name}] SET: ${prop.toString()} =`, value);
                    // 可选:也在 fakeWindow 上设置一份,用于 has 等操作的快速判断
                    target[prop] = value;
                } else {
                    // 如果沙箱未激活,理论上不应该发生 (Qiankun 会控制执行时机)
                    // 或者可以根据策略决定是否直接写到 real window (一般不推荐)
                    console.warn(`[Sandbox ${this.name}] Setting prop '${prop.toString()}' while sandbox inactive!`);
                }
                return true; // 表示设置成功
            },

            /**
             * 拦截获取属性操作 (window.prop)
             * @param target - fakeWindow
             * @param prop - 属性名
             * @param receiver - 代理对象
             * @returns 属性值
             */
            get: (target, prop, receiver) => {
                // console.log(`[Sandbox ${this.name}] GET: ${prop.toString()}`);

                // 1. 优先从 fakeWindow 自身获取 (如果 set 时也设置了 target[prop])
                // if (prop in target) {
                //    return target[prop];
                // }
                // 这种检查可能导致问题,比如访问原型链上的属性。Qiankun 优先检查 updatedPropsMap

                // 2. 优先从沙箱记录的修改中获取
                if (updatedPropsMap.has(prop)) {
                    // console.log(`[Sandbox ${this.name}]   -> from sandbox cache`);
                    return updatedPropsMap.get(prop);
                }

                // 3. 如果沙箱没有记录,则从真实的 window 对象获取
                const realValue = window[prop];
                // console.log(`[Sandbox ${this.name}]   -> from real window`);

                // 重要:处理 window 上的函数,确保 this 指向正确
                // 如果直接返回 realValue (如 window.fetch),在子应用中调用时 this 可能是代理对象 proxy
                // 这会导致 'Illegal invocation' 错误
                if (typeof realValue === 'function') {
                    // 使用 Function.prototype.bind 绑定正确的 this (window)
                    // 需要处理构造函数、Symbol 属性等复杂情况 (Qiankun 有更完善的处理)
                    // 简化处理:
                    const boundFunction = realValue.bind(window);

                    // 为了防止原型链污染,可能需要进一步处理
                    // 例如,将函数的原型链方法也代理或特殊处理
                    // Qiankun 源码中对 Function 类型的属性有更复杂的绑定和缓存逻辑
                    for (const key in realValue) {
                        // eslint-disable-next-line no-prototype-builtins
                        if (realValue.hasOwnProperty(key)) {
                           boundFunction[key] = realValue[key];
                        }
                    }
                    // 如果是构造函数,可能需要特殊处理 new 操作符

                    return boundFunction;
                }

                return realValue;
            },

            /**
             * 拦截 in 操作符 (prop in window)
             * @param target - fakeWindow
             * @param prop - 属性名
             * @returns boolean
             */
            has: (target, prop, receiver) => {
                // 检查属性是否在沙箱记录中,或者在真实 window 中
                return updatedPropsMap.has(prop) || prop in window;
            },

            /**
             * 拦截 delete 操作 (delete window.prop)
             * @param target - fakeWindow
             * @param prop - 属性名
             * @returns boolean
             */
            deleteProperty: (target, prop) => {
                if (this.sandboxRunning) {
                    if (updatedPropsMap.has(prop)) {
                        updatedPropsMap.delete(prop);
                        // console.log(`[Sandbox ${this.name}] DELETE: ${prop.toString()} from sandbox`);
                        // 也从 fakeWindow 删除 (如果 set 时设置了)
                        delete target[prop];
                        return true;
                    }
                    // 如果要删除的属性只在真实 window 上,不允许删除 (保护全局环境)
                    // 或者标记为"已删除",在 get 时返回 undefined?Qiankun 的策略是阻止删除真实 window 属性。
                     console.warn(`[Sandbox ${this.name}] Attempted to delete non-sandboxed prop '${prop.toString()}'`);
                     return false; // 阻止删除真实 window 属性
                }
                return false;
            },

            // Qiankun 还实现了其他 traps,如 ownKeys, getOwnPropertyDescriptor 等,
            // 以便更全面地模拟 window 行为 (例如 Object.keys(window))
            // ... 其他处理器 ...
        });

        // 附加到沙箱实例,以便子应用代码能访问
        // 实际 qiankun 中,子应用的 JS 代码会在特定上下文中执行,
        // 使得代码内部的 'window' 引用指向这个 proxy
        // 这通常通过 Function 构造器或 eval + with 语句 (with 不推荐) 实现
        // 或者更现代的方式是修改 JS 加载和执行逻辑
    }

    /**
     * 激活沙箱
     */
    activate() {
        if (!this.sandboxRunning) {
            this.sandboxRunning = true;
            console.log(`[Sandbox ${this.name}] Activated.`);
            // 可能需要恢复之前的快照状态 (ProxySandbox 不需要,状态保留在 updatedPropsMap)
        }
    }

    /**
     * 失活沙箱
     */
    deactivate() {
        if (this.sandboxRunning) {
            this.sandboxRunning = false;
            console.log(`[Sandbox ${this.name}] Deactivated.`);
            // ProxySandbox 通常不需要在失活时清理 updatedPropsMap
            // 这使得下次激活时能恢复状态
            // 但可能需要清理一些副作用,如事件监听器、定时器 (Qiankun 有单独机制处理)
        }
    }
}

// ---- 如何使用 (概念) ----
// const subAppSandbox = new SimpleProxySandbox('mySubApp');
// subAppSandbox.activate();

// // 假设这是子应用的代码执行环境
// // (Qiankun 通过 import-html-entry 加载和处理子应用 JS)
// // 在这个环境中,全局的 'window' 变量被替换为 subAppSandbox.proxy
// // let window = subAppSandbox.proxy; (这是错误的演示,实际不是这样简单替换)

// // 更接近 qiankun 的方式 (简化):
// function runCodeInSandbox(code, sandboxProxy) {
//     // 使用 Function 构造器创建一个在特定作用域下执行的函数
//     // 'window' 在函数内部会指向 sandboxProxy
//     // 这是有安全风险和性能影响的,qiankun 的实际实现更复杂
//     const sandboxFunc = new Function('window', `try { ${code} } catch(e) { console.error('Sandbox Error:', e); }`);
//     sandboxFunc.call(sandboxProxy, sandboxProxy); // 设置 this 和第一个参数 'window' 都为 proxy
// }

// const subAppCode = `
//   console.log('SubApp: window.location.href =', window.location.href); // 读取真实 window 属性
//   window.mySubAppGlobalVar = 'Hello from SubApp'; // 设置属性,被代理捕获
//   console.log('SubApp: window.mySubAppGlobalVar =', window.mySubAppGlobalVar); // 读取沙箱内的属性
//   console.log('SubApp: typeof window.fetch =', typeof window.fetch); // 读取真实 window 函数 (bind 修正 this)
//   // window.addEventListener(...) // Qiankun 会记录并清理事件监听器
//   // setTimeout(...) // Qiankun 会记录并清理定时器
// `;

// runCodeInSandbox(subAppCode, subAppSandbox.proxy);

// console.log('Host: window.mySubAppGlobalVar =', window.mySubAppGlobalVar); // undefined (主应用访问不到)

// subAppSandbox.deactivate();

实际执行环境 : Qiankun 使用 import-html-entry 库来加载子应用的 HTML 和 JS。它会解析 HTML,提取 JS 脚本,然后通过某种方式(可能是 Function 构造器、修改 eval 行为,或者更底层的 JS 执行上下文控制)来执行这些脚本,并确保在执行时,代码内部对 window 的引用指向的是对应子应用的 ProxySandbox 实例的 proxy 对象。

副作用处理: 除了全局变量,JS 运行还会产生副作用,如:

  • setInterval, setTimeout: Qiankun 会记录子应用创建的定时器 ID,在 unmount 时自动 clearInterval/clearTimeout
  • window.addEventListener: Qiankun 会代理 addEventListenerremoveEventListener,记录下子应用添加的事件监听器,在 unmount 时自动移除,防止内存泄漏。

四、样式隔离核心原理 (CSS Isolation)

目标:防止子应用的 CSS 规则影响主应用或其他子应用,也防止主应用的 CSS 污染子应用。

Qiankun 提供两种主要的样式隔离方案:

  1. Strict Style Isolation (严格样式隔离 - 基于 Shadow DOM)
  2. Experimental Style Isolation (实验性样式隔离 - 基于 Scoped CSS) (现在已成为默认且稳定的方案)

1. 严格样式隔离 (Shadow DOM)

  • 原理 :利用 Web Components 的 Shadow DOM 特性。Qiankun 会为子应用创建一个 Shadow Root,并将子应用的整个 DOM 结构和样式(<style><link>) 放入这个 Shadow Root 中。

  • 优点 :浏览器原生提供最强的隔离性,内部样式完全不会泄漏到外部,外部样式也几乎不会影响内部(除非使用 CSS 继承的属性或 CSS 自定义属性 --var)。

  • 缺点

    • 某些第三方库的弹窗、下拉菜单等可能挂载到 document.body 而不是 Shadow Root 内部,导致样式丢失。
    • 跨越 Shadow Boundary 的事件处理可能复杂化。
    • 老旧浏览器兼容性问题(不过现代浏览器支持良好)。
    • 主应用想覆盖子应用样式比较困难(需要使用 CSS 自定义属性或 ::part 等特定选择器)。

2. 实验性/默认样式隔离 (Scoped CSS / Runtime CSS Transformer)

  • 原理:运行时动态修改子应用的 CSS 规则,为其添加范围限制。

  • 核心思想

    1. 打标记 :当子应用挂载时,Qiankun 会给子应用的根容器 DOM 元素添加一个独特的属性,例如 div[data-qiankun="subAppName"]

    2. 拦截与改写 :Qiankun 会拦截子应用动态添加到 <head><style> 标签或 <link rel="stylesheet"> 标签。

      • 对于 <style> 标签:获取其 CSS 文本内容,使用 CSS 解析器(如 postcss 的子集或自定义解析器)遍历所有 CSS 规则。对每个选择器,在其前面添加上一步的属性选择器作为前缀。例如,.my-button { color: red; } 会被改写成 div[data-qiankun="subAppName"] .my-button { color: red; }。这样,这个样式就只会作用于特定子应用容器内部的 .my-button 元素了。
      • 对于 <link> 标签:Qiankun 会 Workspace 这个 CSS 文件,获取内容,进行同样的改写,然后将改写后的 CSS 文本注入到一个新的 <style> 标签中,替换掉原来的 <link>
    3. DOM 操作劫持 :为了确保子应用通过 JS 动态创建的 <style><link> 也能被处理,Qiankun 会重写 (monkey-patch) HTMLHeadElement.prototype.appendChild, insertBefore 等方法。当检测到是添加样式元素时,执行上述的改写逻辑。

    4. 卸载时清理 :当子应用 unmount 时,Qiankun 会移除所有由它添加或改写过的 <style> 标签,清理样式。

  • 优点

    • 兼容性好,不依赖 Shadow DOM。
    • 允许主应用更容易地覆盖子应用的样式(通过更高优先级的选择器)。
    • 弹窗等挂载到 body 的元素样式问题较少(因为全局样式依然存在,只是子应用内部样式被限定了范围)。
  • 缺点

    • 隔离性不如 Shadow DOM 完美,仍有可能因子应用 CSS 写法不规范(如直接修改 body, html 标签样式)或 CSS 优先级问题导致一些意想不到的样式冲突。
    • 需要运行时解析和重写 CSS,有一定性能开销(但 qiankun 做了优化)。
    • 需要处理复杂的 CSS 选择器(如 :root, @keyframes, 属性选择器等)的改写逻辑。

Scoped CSS 简化代码示例 (概念性 - CSS 改写)

JavaScript 复制代码
// 高度简化的 CSS 改写逻辑,实际 Qiankun 使用更健壮的 CSS 解析库

/**
 * 给 CSS 规则选择器添加范围前缀
 * @param cssText 原始 CSS 字符串
 * @param appName 子应用名称,用于生成唯一前缀
 * @param containerSelectorPrefix 例如 'div[data-qiankun]'
 * @returns 改写后的 CSS 字符串
 */
function scopeCss(cssText, appName, containerSelectorPrefix = 'div[data-qiankun]') {
    const prefix = `${containerSelectorPrefix}="${appName}"`; // 如: div[data-qiankun="mySubApp"]

    // 这是一个非常基础的正则替换,无法处理所有 CSS 复杂情况
    // 实际需要 AST 解析器来精确处理选择器、@规则等
    const rewriteRule = (rule) => {
        // 尝试匹配选择器部分和 { ... } 块
        // 这个正则非常不完善,仅为演示
        const ruleRegex = /([^{]+)({[^}]*})/g;
        return rule.replace(ruleRegex, (match, selectorPart, stylePart) => {
            selectorPart = selectorPart.trim();

            // 忽略 @keyframes, @font-face, @media 等 (需要更精细判断)
            if (selectorPart.startsWith('@')) {
                return match;
            }

            // 忽略 html, body, :root 等全局选择器 (根据策略决定)
            // Qiankun 可能会允许子应用修改某些全局样式,但有风险
            if (selectorPart === 'html' || selectorPart === 'body' || selectorPart === ':root') {
                // 可以选择移除这些规则,或保留但不加前缀,或尝试加前缀 (可能有问题)
                // console.warn(`[CSS Scoping] Global selector detected: ${selectorPart}`);
                // return match; // 示例:保留但不加前缀
                 return ''; // 示例:直接移除该规则
            }

            // 处理逗号分隔的多个选择器
            const scopedSelectors = selectorPart
                .split(',')
                .map(selector => {
                    selector = selector.trim();
                    if (selector) {
                        // 基本前缀添加,需要处理特殊情况如 ID 选择器优先级、属性选择器组合等
                        // 实际场景可能需要更复杂的逻辑判断如何组合前缀
                        return `${prefix} ${selector}`;
                    }
                    return '';
                })
                .filter(Boolean)
                .join(', ');

            return `${scopedSelectors} ${stylePart}`;
        });
    };

    // 简单按规则块分割 (非常粗糙)
    const rules = cssText.split('}');
    const scopedCss = rules.map(rule => {
        if (rule.trim()) {
            return rewriteRule(rule + '}'); // 加回 '}'
        }
        return '';
    }).join('\n');

    return scopedCss;
}

// ---- 如何使用 (概念) ----

// 假设这是从子应用的 <style> 或 fetch 的 <link> 获取的 CSS
const originalCss = `
  .button { color: blue; padding: 10px; }
  #app-container > p { font-size: 14px; }
  body { background-color: #eee; } /* 全局样式 */
  @keyframes my-animation { 0% { opacity: 0; } 100% { opacity: 1; } }
`;

const appName = 'reactApp';
const scopedCssResult = scopeCss(originalCss, appName);

console.log("---- Original CSS ----");
console.log(originalCss);
console.log("\n---- Scoped CSS ----");
console.log(scopedCssResult);
/*
可能的输出 (根据上面简化逻辑):

---- Scoped CSS ----

div[data-qiankun="reactApp"] .button { color: blue; padding: 10px; }

div[data-qiankun="reactApp"] #app-container > p { font-size: 14px; }


@keyframes my-animation { 0% { opacity: 0; } 100% { opacity: 1; } }

*/

// ---- 运行时 DOM 操作劫持 (概念) ----
const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
HTMLHeadElement.prototype.appendChild = function(node) {
    const appName = window.__CURRENT_SUB_APP_NAME__; // 假设 Qiankun 注入了当前激活的子应用名

    if (appName && node instanceof HTMLStyleElement && node.tagName === 'STYLE') {
        // console.log(`[CSS Patch] Intercepting style tag append for app: ${appName}`);
        try {
            const scopedCssText = scopeCss(node.textContent, appName);
            node.textContent = scopedCssText;
            // 添加标记,以便卸载时识别和移除
            node.setAttribute('data-qiankun-app', appName);
        } catch (e) {
            console.error(`[CSS Patch] Failed to scope CSS for app ${appName}:`, e);
        }
    } else if (appName && node instanceof HTMLLinkElement && node.rel === 'stylesheet' && node.href) {
        // console.log(`[CSS Patch] Intercepting link tag append for app: ${appName}`);
        // 实际 Qiankun 会 fetch CSS 内容,改写,然后插入 <style>
        // 这里简化处理,仅作示意
        const originalHref = node.href;
        console.warn(`[CSS Patch] <link> tag handling needs async fetch & rewrite for ${originalHref}`);
        // 阻止直接添加 link,或者 fetch 后再添加 style (异步)
        // return node; // 或者返回一个注释节点占位?
    }

    // 调用原始的 appendChild 方法
    return rawHeadAppendChild.call(this, node);
};

// 还需要劫持 insertBefore 等其他 DOM 修改方法
// 以及处理 <style> 内容动态修改的情况

五、子应用通信

Qiankun 提供了几种应用间通信的方式:

  1. 基于 Props 的单向数据流 (主 -> 子)

    • 主应用在 registerMicroApps 时,可以给每个子应用传递 props 数据。

    • 这些 props 会在子应用的 mount 生命周期钩子中接收到。

    • 主应用可以通过更新这些 props(需要重新加载或特定机制)来向子应用传递信息。

    • 优点:简单直接,符合组件化思维。

    • 缺点 :通常是单向的,子应用直接修改 props 不符合规范,且通信不实时(除非结合其他机制)。

    JavaScript 复制代码
    // --- 主应用 ---
    registerMicroApps([
      {
        name: 'vueApp',
        entry: '//localhost:8081',
        container: '#subapp-viewport',
        activeRule: '/vue',
        props: {
          msgFromMain: 'Hello from main application!',
          sharedUtils: { /* ... */ },
          // 可以传递一个回调函数让子应用调用
          onSubAppEvent: (payload) => {
            console.log('Event from vueApp:', payload);
          }
        }
      },
    ]);
    start();
    
    // --- 子应用 (Vue) ---
    export async function mount(props) {
      console.log('Props from main app:', props);
      // props.msgFromMain
      // props.sharedUtils
      // 调用主应用传来的回调
      // props.onSubAppEvent({ data: 'some data' });
      // ... 创建 Vue 实例,并将 props 注入 ...
      new Vue({
         // ...
         props: { // 或者通过 provide/inject, event bus 等方式传递 props
            mainProps: { type: Object, default: () => props }
         }
      }).$mount('#app');
    }
  2. 基于 initGlobalState 的全局状态管理 (推荐)

    • Qiankun 提供 initGlobalState(initialState) API,用于创建一个全局共享的状态。

    • 该 API 返回一个 MicroAppStateActions 对象,包含:

      • setGlobalState(state): 用于修改全局状态。主应用和子应用都可以调用。
      • onGlobalStateChange(callback, fireImmediately): 用于监听全局状态变化。主应用和子应用都可以注册监听。fireImmediately (可选,默认为 false) 参数决定是否在注册监听时立即触发一次回调。
    • Qiankun 会自动将这些 actions 通过 props 注入到每个子应用的 mount 钩子中。

    • 优点:实现了任意应用间的双向、实时通信,API 简洁易用。

    • 缺点:所有应用共享一个状态对象,需要规划好状态结构,避免命名冲突和滥用。

    JavaScript 复制代码
    // --- 主应用 ---
    import { initGlobalState, registerMicroApps, start } from 'qiankun';
    
    const initialState = { user: null, theme: 'light', lang: 'en' };
    const actions = initGlobalState(initialState);
    
    // 监听状态变化
    actions.onGlobalStateChange((state, prevState) => {
      console.log('[MainApp] Global state changed:', state);
      // 可以根据 state 更新主应用 UI, 如 state.theme
      document.body.className = `theme-${state.theme}`;
    }, true); // true: 立即触发一次
    
    // 修改状态 (示例:模拟登录后)
    setTimeout(() => {
      actions.setGlobalState({ ...initialState, user: { id: 1, name: 'Admin' } });
    }, 3000);
    
    // 修改状态 (示例:切换语言)
    function switchLang(lang) {
        actions.setGlobalState({ ...actions.getGlobalState(), lang }); // getGlobalState() 不是标准 API, 需要自己获取当前状态
        // 正确做法是读取上一个 state
        actions.setGlobalState({ ...(window._qiankun_global_state || initialState), lang }); // 假设用某种方式存储了最新 state
        // 或者直接从监听回调的 state 参数获取最新状态来合并
    }
    
    registerMicroApps([/* ... */]);
    start();
    
    
    // --- 子应用 (React) ---
    import React, { useState, useEffect } from 'react';
    import ReactDOM from 'react-dom';
    
    function SubAppRoot(props) {
        // props 会包含 qiankun 注入的 onGlobalStateChange, setGlobalState
        const { name, onGlobalStateChange, setGlobalState } = props;
        const [globalState, setGlobalStateLocal] = useState(() => {
             // 尝试获取初始全局状态 (如果 qiankun 提供了同步获取方法,或者从 props 传递)
             // 这里假设初始状态需要通过监听获取
            return {};
        });
    
        useEffect(() => {
            // 注册全局状态监听
            const handleChange = (state, prevState) => {
                console.log(`[SubApp ${name}] Global state received:`, state);
                setGlobalStateLocal(state);
            };
            onGlobalStateChange(handleChange, true); // true: 立即获取一次当前状态
    
            // 组件卸载时无需手动移除监听,qiankun 会在 unmount 时处理
            // (但如果是在组件内部动态添加的监听,最好返回一个清理函数)
            // return () => { /* 如果需要手动清理 */ };
        }, [onGlobalStateChange, name]);
    
        const toggleTheme = () => {
            const newTheme = globalState.theme === 'light' ? 'dark' : 'light';
            console.log(`[SubApp ${name}] Setting theme to:`, newTheme);
            // 修改全局状态
            setGlobalState({ ...globalState, theme: newTheme });
        };
    
        return (
            <div>
                <h2>Sub App: {name}</h2>
                <p>Global User: {globalState.user ? globalState.user.name : 'Loading...'}</p>
                <p>Global Theme: {globalState.theme || 'Loading...'}</p>
                <p>Global Lang: {globalState.lang || 'Loading...'}</p>
                <button onClick={toggleTheme}>Toggle Theme</button>
            </div>
        );
    }
    
    export async function mount(props) {
      console.log(`[SubApp ${props.name}] Mounted with props:`, props);
      ReactDOM.render(
        <SubAppRoot {...props} />,
        props.container ? props.container.querySelector('#root') : document.getElementById('root')
      );
    }
    
    export async function unmount(props) {
       ReactDOM.unmountComponentAtNode(
        props.container ? props.container.querySelector('#root') : document.getElementById('root')
      );
    }
    
    // 其他生命周期... bootstrap, update (如果需要)
  3. 其他方式

    • 发布/订阅模式 (Event Bus) :可以使用 CustomEvent, 或者引入 mitt, rxjs 等库,在主应用和子应用之间共享一个事件总线实例,通过发布和订阅事件来进行通信。
    • 共享模块/库: 通过 webpack 的 Module Federation 或其他共享依赖机制,共享状态管理库(如 Redux Store, Zustand Store)的实例。
    • window 对象 (不推荐) :虽然 JS 沙箱隔离了直接的 window 修改,但可以通过在主应用 window 上挂载明确的通信接口(如 window.mainAppBridge = { ... }),子应用通过 window.mainAppBridge 调用。这种方式破坏了隔离性,应谨慎使用。
    • localStorage/sessionStorage: 简单场景下可以用来传递少量非实时数据,但监听变化不方便且有性能/容量限制。

六、生命周期管理

Qiankun 建立在 single-spa 之上,也遵循其生命周期概念:

  1. bootstrap: 子应用首次加载时执行一次。适合执行一些全局初始化操作,如设置全局配置、初始化路由实例等。只执行一次,除非应用被完全销毁后重新加载。
  2. mount: 子应用被激活(路由匹配)并挂载到 DOM 时执行。适合执行创建应用实例、渲染 DOM、设置监听器/定时器等操作。每次激活都会执行。
  3. unmount: 子应用失活(路由切换走)或被卸载时执行。适合执行销毁应用实例、清理 DOM、移除监听器/定时器、释放资源等操作。每次失活都会执行。
  4. update (可选) : 主应用可以通过 props 更新子应用时调用(较少使用,通常重新 mount/unmount 更常见)。

子应用必须在其入口 JS 文件中导出这三个(或四个)函数,通常是 UMD 格式导出。

JavaScript 复制代码
// --- 子应用入口 (例如 main.js/index.js) ---

// 假设使用 Vue
import Vue from 'vue';
import App from './App.vue';
import router from './router'; // 子应用自己的路由

let instance = null;

function render(props = {}) {
  const { container } = props;
  instance = new Vue({
    router, // 使用子应用自己的 router
    render: h => h(App),
    // 可以将 props 注入到 Vue 实例中
    data: { mainAppProps: props }
  }).$mount(container ? container.querySelector('#app') : '#app'); // 挂载到 qiankun 指定的容器或默认 #app
}

// -------- 生命周期钩子 --------

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log('[SubApp] bootstraped');
  // 初始化一些全局配置,只执行一次
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log('[SubApp] props from main framework', props);
  // 可以在这里获取主应用传递的 props,包括通信方法等
  // 记录 props,或者传递给 render 函数
  window.__SUB_APP_PROPS__ = props; // 示例:全局存储(注意沙箱影响)

  // 调用渲染方法
  render(props);

  // 可以在这里启动定时器、添加事件监听器等
  // props.onGlobalStateChange(...) // 注册全局状态监听
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount(props) {
  console.log('[SubApp] unmount');
  if (instance) {
    instance.$destroy(); // 销毁 Vue 实例
    instance.$el.innerHTML = ''; // 清空 DOM
    instance = null;
    // router = null; // 如果路由需要重置
  }
  // 清理定时器、事件监听器等 (Qiankun 会自动处理部分,但自定义的需要手动清理)
  // delete window.__SUB_APP_PROPS__;
}

/**
 * 可选生命周期钩子,当主应用 props 更新时触发
 * @param props
 */
// export async function update(props) {
//   console.log('[SubApp] update props', props);
//   // 更新应用逻辑,可能需要重新渲染或更新组件状态
// }


// -------- 处理独立运行时 --------
// 为了方便开发调试,通常需要让子应用可以独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  console.log('[SubApp] Running independently');
  render(); // 独立运行时,直接渲染
}

// -------- Webpack UMD 配置 (webpack.config.js) --------
/*
module.exports = {
  // ... 其他配置
  output: {
    library: `${packageName}-[name]`, // 必须唯一,例如 'vueApp-main'
    libraryTarget: 'umd', // 必须是 umd
    jsonpFunction: `webpackJsonp_${packageName}`, // webpack 5 使用 chunkLoadingGlobal
    globalObject: 'window', // 或者 'this'
    publicPath: process.env.NODE_ENV === 'production' ? '/your-cdn-path/' : '/js/', // 资源公共路径
  },
  devServer: {
     headers: {
       'Access-Control-Allow-Origin': '*', // 允许跨域
     },
     // ... 其他 devServer 配置
  },
};
*/

总结

Qiankun 通过巧妙地运用 Proxy (JS 沙箱)、运行时 CSS 重写或 Shadow DOM (CSS 隔离) 以及提供明确的通信机制 (initGlobalState, props),有效地解决了微前端架构中的核心痛点,使得多个独立开发部署的子应用能够平滑地集成在同一个主应用中运行,同时保持各自的独立性。

以上代码示例旨在阐明核心原理,Qiankun 的实际源码要复杂得多,包含了大量的边界处理、性能优化、错误处理和浏览器兼容性代码。理解了这些核心概念后,再去阅读 qiankun 源码会更有方向。

相关推荐
liuyang___1 小时前
第一次经历项目上线
前端·typescript
西哥写代码1 小时前
基于cornerstone3D的dicom影像浏览器 第十八章 自定义序列自动播放条
前端·javascript·vue
清风细雨_林木木1 小时前
Vue 中生成源码映射文件,配置 map
前端·javascript·vue.js
FungLeo2 小时前
node 后端和浏览器前端,有关 RSA 非对称加密的完整实践, 前后端匹配的代码演示
前端·非对称加密·rsa 加密·node 后端
不灭锦鲤2 小时前
xss-labs靶场第11-14关基础详解
前端·xss
不是吧这都有重名2 小时前
利用systemd启动部署在服务器上的web应用
运维·服务器·前端
霸王蟹2 小时前
React中巧妙使用异步组件Suspense优化页面性能。
前端·笔记·学习·react.js·前端框架
Maỿbe2 小时前
利用html制作简历网页和求职信息网页
前端·html
森叶3 小时前
Electron 主进程中使用Worker来创建不同间隔的定时器实现过程
前端·javascript·electron
霸王蟹3 小时前
React 19 中的useRef得到了进一步加强。
前端·javascript·笔记·学习·react.js·ts