面试官:qiankun如何实现Js与CSS隔离?

随着前端业务的快速发展,微前端架构 已经被广泛采用,其中 qiankun 作为主流解决方案也越来越受到关注。前几天面试时,我就被问到了一个高频问题:qiankun 是如何实现 JS 和 CSS 隔离的?

qiankun 的JS 沙箱

qiankun 的微前端场景是:主应用加载多个子应用 ,不同子应用可能依赖不同版本的库、全局变量,甚至可能会互相覆盖 window 上的属性。为了避免"全局污染",qiankun 提供了沙箱机制。

常见的JS 沙箱实现思路有下面三种:

SnapshotSandbox(快照沙箱)

快照沙箱是微前端里最直观的 JS 隔离方式之一:

  • 挂载应用前 → 对 window 对象做一次"快照",保存所有属性及其值。
  • 应用运行中 → 子应用可以随意修改全局变量。
  • 卸载应用时 → 把 window 恢复到挂载前的快照状态(新增的删掉、改过的还原)。

它的过程使用伪代码大致如下:

js 复制代码
/**
 * 快照沙箱
 * - 挂载前:拍快照(浅拷贝 window 属性)
 * - 卸载时:恢复快照(删除新增,还原修改)
 */
function createSnapshotSandbox() {
  const rawWindow = window;
  let snapshot = null;      // 存储拍下来的全局状态
  let modifiedProps = {};   // 存储运行过程中被修改的属性

  return {
    // 激活:拍下当前 window 状态
    activate() {
      snapshot = {};
      for (const key in rawWindow) {
        try {
          snapshot[key] = rawWindow[key];
        } catch (_) {
          // 某些属性可能不可访问,忽略即可
        }
      }
    },

    // 记录全局修改(手动写变量时调用)
    set(key, value) {
      modifiedProps[key] = rawWindow[key];
      rawWindow[key] = value;
    },

    // 失活:恢复 window 到快照
    deactivate() {
      for (const key in rawWindow) {
        if (!(key in snapshot)) {
          // 卸载后删除新增的
          delete rawWindow[key];
        } else if (rawWindow[key] !== snapshot[key]) {
          // 还原被修改的
          rawWindow[key] = snapshot[key];
        }
      }
      modifiedProps = {};
    }
  };
}

上述代码中,snapshot 是全局变量的"拍照备份",在 sandbox.activate() 时,会遍历一次 window,保存所有当前的属性和值。它用于记录挂载子应用之前的 window 状态,在卸载时(deactivate)时,拿这个备份和当前 window 对比,使 window 回到快照时的状态。

  • 删除新增属性(子应用新增的全局变量)。
  • 还原被修改的属性(子应用修改过的变量)。

modifiedProps 是运行时的"变更记录",使用它快速知道子应用改动了哪些属性,卸载时可以更高效地只恢复被改动过的,而不是全量比对。

使用示例:

js 复制代码
const sandbox = createSnapshotSandbox();

sandbox.activate();  // 挂载前,拍快照
window.foo = 123;    // 模拟子应用写全局
console.log(window.foo); // 123

sandbox.deactivate(); // 卸载后恢复
console.log(window.foo); // undefined(被删除)

LegacySandbox(单实例沙箱)

快照沙箱 (SnapshotSandbox) 虽然能恢复全局变量,但性能差,还不支持并行运行。

因此 qiankun 在 支持 Proxy 之前 ,实现了一个改进版的沙箱 ------ LegacySandbox

简化版代码示例:

js 复制代码
class LegacySandbox {
  constructor(name) {
    this.name = name;

    this.addedPropsMap = new Map();              // 记录新增的全局属性
    this.modifiedPropsOriginalMap = new Map();   // 记录修改前的原始值
    this.currentUpdatedPropsValueMap = new Map();// 记录当前子应用改动后的值
  }

  // 激活:恢复上次的运行环境
  activate() {
    this.currentUpdatedPropsValueMap.forEach((v, p) => {
      window[p] = v;
    });
  }

  // 失活:清理全局变量
  deactivate() {
    // 删除新增属性
    this.addedPropsMap.forEach((_, p) => {
      delete window[p];
    });
    // 恢复修改过的属性
    this.modifiedPropsOriginalMap.forEach((v, p) => {
      window[p] = v;
    });
  }

  // 设置全局变量时调用
  setWindowProp(prop, value) {
    if (!window.hasOwnProperty(prop)) {
      // 新增属性
      this.addedPropsMap.set(prop, value);
    } else if (!this.modifiedPropsOriginalMap.has(prop)) {
      // 第一次修改,记录原始值
      this.modifiedPropsOriginalMap.set(prop, window[prop]);
    }
    // 记录最新值
    this.currentUpdatedPropsValueMap.set(prop, value);
    window[prop] = value;
  }
}

LegacySandbox 的核心思路是:

  • 维护三份状态: addedPropsMap:记录子应用新增的全局属性。modifiedPropsOriginalMap:记录子应用修改前的原始值。currentUpdatedPropsValueMap:记录子应用修改后的值。
  • 激活(activate): 遍历 currentUpdatedPropsValueMap,恢复上次运行时的修改。
  • 运行中: 每当子应用往 window 上赋值时:如果是新增 → 记录到 addedPropsMap。如果是修改 → 记录原始值到 modifiedPropsOriginalMap,并把新值写到 currentUpdatedPropsValueMap
  • 失活(deactivate): 删除 addedPropsMap 中的属性(还原新增)。用 modifiedPropsOriginalMap 恢复被修改过的属性(还原修改)。

使用示例:

js 复制代码
const sandbox = new LegacySandbox("app1");

sandbox.activate();              // 激活应用
sandbox.setWindowProp("foo", 123);
console.log(window.foo);         // 123

sandbox.deactivate();            // 卸载应用
console.log(window.foo);         // undefined(被删除)

ProxySandbox(代理沙箱,多实例沙箱)

ProxySandbox 可以说是 qiankun 沙箱的"终极形态",现代浏览器环境下的主力方案。前面说的两种沙箱存在下面的问题

  • SnapshotSandbox:全量快照,对比恢复,性能差。
  • LegacySandbox:单实例(只能一个子应用同时运行),多个并行时会冲突。

为了解决 性能 + 并行运行 的问题,引入了 ProxySandbox

它的核心是 ES6 的 Proxy ,拦截对 window 的访问:

  • 给每个子应用创建一个「假的 window」对象(称为 fakeWindow)。
  • fakeWindow 的原型指向真正的 window,这样子应用能正常访问到全局属性。
  • 子应用对全局变量的 修改、删除、新增 都只会作用在 fakeWindow 上,而不会污染真实的 window
  • 不同子应用有不同的 fakeWindow,天然实现多实例隔离。
js 复制代码
// 1. 创建 ProxySandbox
function createProxySandbox() {
  // 创建一个空对象 没有原型链。
  const fakeWindow = Object.create(null);
  return new Proxy(fakeWindow, {
    get(target, prop) {
      if (prop in target) {
        return target[prop]; // 优先取子应用自己的
      }
      return window[prop];   // 否则取宿主的全局
    },
    set(target, prop, value) {
      target[prop] = value;  // 写只写在 fakeWindow 上
      return true;
    }
  });
}

// 2. 模拟子应用执行环境
function runInSandbox(code, sandbox) {
  const wrapper = new Function("window", `
    with(window) {
      ${code}
    }
  `);
  wrapper(sandbox); // 关键:传入 proxy
}

// 3. 使用
const sandbox1 = createProxySandbox();
const sandbox2 = createProxySandbox();

runInSandbox(`window.foo = "app1"; console.log("app1 foo =", window.foo);`, sandbox1);
runInSandbox(`window.foo = "app2"; console.log("app2 foo =", window.foo);`, sandbox2);

console.log("真实 window.foo =", window.foo); // undefined,没有污染

new Proxy(fakeWindow, handler)

这里的逻辑简化一下主演干了下面的事情:

  • get
    读属性时触发。优先取 fakeWindow,否则兜底真实 window
    👉 写过的值会"遮挡"宿主值。
  • set
    写属性时触发。只写入 fakeWindow,不污染真实 window
  • has
    with 语句查找变量时触发。返回 prop in fakeWindow || prop in window
    👉 确保像 consoledocument 这些全局在子应用里能被正常访问。
  • deleteProperty
    删除属性时触发。只删 fakeWindow 的内容,不影响真实 window

runInSandbox 是如何把子应用"绑"到 proxy 的

javascript 复制代码
const wrapper = new Function("window", `
  with(window) {
    ${code}
  }
`);
wrapper(proxy);
  • new Function("window", "with(window){ ... }") 创建了一个函数,函数参数名是 window
  • wrapper(proxy) 把我们造的 proxy 作为形参 window 传入。
  • with(window) { ... } 会把这个 window(即 proxy)加入当前作用域链,所以代码里的未限定标识符(比如 foo location document )会先在 proxy 上被查找/操作
  • 结合上面的 get/set/has,所有读取/写入都会被代理到 handler,从而实现拦截。

CSS 隔离原理

qiankun 没有强制启用某种隔离,而是给开发者提供了几种选择:

  • 默认:无强隔离, 子应用样式直接插入主应用 head,容易污染,但性能最好。
  • StrictStyleIsolation(严格隔离): 使用 Shadow DOM 把子应用包裹起来。
js 复制代码
registerMicroApps(apps, {
  sandbox: { strictStyleIsolation: true }
})

这种方式的优点是彻底隔离,但某些全局样式/第三方库不兼容

  • ExperimentalStyleIsolation(实验性隔离): 给子应用容器加 data-qiankun="xxx" 属性,然后动态给所有 CSS 规则加前缀。
js 复制代码
registerMicroApps(apps, {
  sandbox: { experimentalStyleIsolation: true }
})

这种范式类似 Vue 的 scoped CSS,兼容性比 Shadow DOM 更好。

相关推荐
前端进阶者23 分钟前
electron-vite_20外部依赖包上线后如何更新
前端·javascript·electron
晴空雨38 分钟前
💥 React 容器组件深度解析:从 Props 拦截到事件改写
前端·react.js·设计模式
Marshall357243 分钟前
前端水印防篡改原理及实现
前端
阿虎儿1 小时前
TypeScript 内置工具类型完全指南
前端·javascript·typescript
IT_陈寒1 小时前
Java性能优化实战:5个立竿见影的技巧让你的应用提速50%
前端·人工智能·后端
张努力2 小时前
从零开始的开发一个vite插件:一个程序员的"意外"之旅 🚀
前端·vue.js
远帆L2 小时前
前端批量导入内容——word模板方案实现
前端
Codebee2 小时前
OneCode3.0-RAD 可视化设计器 配置手册
前端·低代码
葡萄城技术团队2 小时前
【SpreadJS V18.2 新版本】设计器新特性:四大主题方案,助力 UI 个性化与品牌适配
前端