Skip to content
页面导航
精简

一、无界框架是什么?

在深入探讨了 qiankun 之后,再来看 无界(Wujie),你会感受到一种架构思路上“降维打击”般的精妙。

无界(Wujie) 是由腾讯(Tencent)开源的一款基于 **Web Components + iframe** 的极速微前端框架。

如果说 qiankun 的思路是“在同一个房间里,用极其复杂的魔法(Proxy 沙箱)努力让大家互相看不见”;那么无界的思路就是“直接给每个人建一间绝对隔音的铁屋子(iframe),然后把他们屋子里的全息投影(DOM)投射到大厅(Web Component)里”。


核心架构原理:JS 与 DOM 的“身首分离”

无界之所以能被称为下一代微前端框架,是因为它巧妙地结合了两种浏览器原生技术的优势,并用一层极薄的 Proxy 抹平了它们之间的鸿沟。

1. JS 执行环境:纯正的 iframe(绝对的物理隔离)

无界在基座应用中会悄悄创建一个隐藏的 iframe,用来运行微应用的 JavaScript 代码。

  • 优势: 既然是 iframe,那就意味着天然拥有完美的 JS 沙箱。不管你怎么修改 window,怎么定义全局变量,怎么搞原型链污染,都只会留在那个 iframe 里面,绝对不可能泄漏到基座。多实例运行、各种老旧代码兼容,统统不在话下。

2. 渲染环境:Web Components(完美的样式隔离)

无界会在基座的真实 DOM 树中创建一个自定义标签(Web Component,内置 Shadow DOM),作为微应用的视图容器。

  • 优势: Shadow DOM 提供了原生的 CSS 隔离,微应用的样式绝不会影响基座。

3. 核心魔法:跨越次元壁的 Proxy 拦截

既然 JS 代码跑在隐藏的 iframe 里,那它执行 document.body.appendChild 的时候,DOM 岂不是全画在隐藏的 iframe 里了,用户根本看不到? 这就是无界最核心的魔法:代理与重定向

  • 无界利用 Proxy 拦截了 iframe 内部的 document 对象。
  • 当微应用的 React/Vue 代码在 iframe 里试图操作 DOM 时(比如新增一个节点、弹出一个 Modal),无界会拦截这个动作,并把这个节点强行插入到外面基座的 Web Component(Shadow DOM)中
  • 同理,当用户在 Web Component 里触发了点击事件,无界会把这个事件代理回 iframe 内部。

相比 qiankun,无界解决了哪些痛点?

在实施复杂的企业级微前端项目时,无界展现出了碾压级的接入体验:

1. 极低的接入成本(几乎零改造)

  • qiankun: 微应用必须导出 bootstrapmountunmount 生命周期,必须配置跨域(CORS),如果用了 Webpack/Vite 还要疯狂修改 publicPath 和打包配置。
  • 无界: 根本不需要微应用做任何生命周期改造。哪怕是一个写死的 HTML 老项目,只要给个 URL 就能直接跑起来,真正做到了“拔插式”体验。

2. 完美的弹窗与事件兼容性

我们上一轮聊过 qiankun 开启 Shadow DOM 后,React 事件丢失、Ant Design 弹窗飞到外面的问题。

  • 无界: 因为无界劫持了 iframedocument,所以微应用弹出的 Modal 虽然在视觉上挂载在了外部,但它的逻辑层依然被牢牢锁在 iframe 的上下文中,各种事件代理完美生效,不需要打任何补丁。

3. 原生级别的状态保留(保活 / Keep-Alive)

  • qiankun: 切换微应用时,通常是把整个 DOM 和实例全部销毁(Unmount)。切回来又得重新执行一遍完整的生命周期,遇到重型数据大盘页面,体验极差。
  • 无界: 既然本质是 iframe,那么切换微应用只是把 Web Component 的 DOM 从页面上隐藏(display: none)。它背后的 iframe 依然在默默运行,JS 上下文、状态、甚至定时器都完好无损。切回来瞬间展现,体验丝滑。

客观来看,无界的缺点与挑战

没有银弹。基于 iframe 也会带来其固有的物理限制:

  1. 内存开销较大: 每一个微应用都需要实例化一个完整的浏览器 iframe 上下文,如果基座同时加载十几个无界微应用,内存占用会明显高于 qiankun。
  2. 路由同步的复杂性: 虽然无界在底层努力同步了 iframe 的 URL 和基座浏览器的 URL,但在处理一些极其复杂的嵌套路由或浏览器前进后退历史栈时,偶尔还是会遇到需要特殊处理的边界 Bug。

二、隐藏iframe和web components的关系

理解无界(Wujie)框架的核心,就在于理解它是如何巧妙地将“JavaScript 的执行”“DOM 的渲染”这两个原本在浏览器里是一体的事情,硬生生地拆分开来的。

如果用一个非常生动的比喻来形容无界的核心架构,那就是:木偶戏(操纵者与木偶分离)

  • **隐藏的 iframe** 就是躲在幕布后面的“操纵者(大脑)”。
  • Web Component (Shadow DOM) 就是站在前台舞台上的“木偶(肉体)”。
  • Proxy 代理机制 则是连接操纵者和木偶的“提线(神经系统)”。

下面详细拆解它们三者之间的奇妙关系。

第一层:为什么要让“灵魂与肉体分离”?

在传统的 Web 开发中,一段 JS 代码(比如 document.body.appendChild(...))运行在哪个 window 下,它生成的 DOM 节点自然就渲染在哪个页面的可视区域里。

无界团队在设计时面临一个两难的境地:

  1. 要想 JS 变量绝对不污染基座,最好的办法是把代码放进一个看不见的 iframe 里运行(完美的天然 JS 沙箱)。
  2. 要想 UI 完美融合且样式不冲突,最好的办法是利用 Web Component 的 Shadow DOM 将内容渲染在基座的 DOM 树中。

矛盾点来了: 如果把微应用的 JS 代码放进隐藏的 iframe 里跑,那它 appendChild 创建的弹窗、按钮,全都会画在那个看不见的 iframe 的文档里,用户在外面根本看不见!

为了打破这个屏障,无界引入了底层劫持机制(Proxy 代理)

第二层:Proxy 是如何跨越次元壁的?

当微应用的 JS 代码在隐藏的 iframe 中运行时,无界在底层对 iframe 里的 document 对象进行了极其深度的 Proxy 代理拦截。

1. DOM 创建与插入的重定向 (Iframe -> Web Component)

假设微应用的 Vue/React 代码在 iframe 里执行了这样一句话: const btn = document.createElement('button');document.body.appendChild(btn);

  • 常规情况: 按钮会被塞进 iframe 内部的 <body> 里,外部不可见。
  • 无界代理后: 无界的 Proxy 拦截到了 appendChild 这个动作。它会对 iframe 说:“好的,我帮你放进去。” 但实际上,无界拿着这个 btn 节点,偷偷地跑到了基座页面上,把它塞进了那个可见的 Web Component (Shadow DOM) 里面。注意:只会在 Web Component(也就是基座的真实 DOM 树)中生成一份物理节点。 隐藏的 iframe 的 DOM 树内部,是完全没有任何 UI 节点的。
  • 结果: 逻辑是 iframe 里的代码执行的,但最终的 DOM 节点却渲染在了外部的 Web Component 中,用户清晰可见,且样式被 Shadow DOM 完美隔离。
  • 注意: 无界重写了 iframe 内部 document.body 的 appendChild、insertBefore 等所有涉及 DOM 变异的 API。无界拦截到这类操作后,会对 编辑的btn 节点说:“虽然你是在 iframe 里出生的,但你不能呆在这里。” 然后,无界在底层通过 JavaScript 的引用,直接将这个 btn 节点插入到了外层基座页面准备好的 Web Component(Shadow DOM)中。不仅仅拦截了插入操作,它还把查询操作也全盘拦截了,所以查询时候,无界会把这个查询请求转发到外面的 Web Component 里去查找,找到后再把那个节点的引用返回给 iframe。
为什么不能在两边各生成一份?(底层铁律)

这是浏览器的底层铁律决定的:一个 DOM 节点实例对象(比如我们刚刚 new 出来的 btn),在同一时刻,只能存在于 DOM 树的一个位置,并且只能有一个父节点(parentNode)。

如果各自保留保留一份,那就必须调用 cloneNode() 克隆一个新节点。一旦克隆,微应用框架(比如 React 或 Vue)手里拿到的 btn 引用,和页面上真实渲染的 btn 就不再是同一个对象了。 这将导致灾难性的后果:React 执行 btn.style.color = 'red' 时,只会修改 iframe 里那个假按钮的颜色,而用户在外面看到的真按钮毫无反应。

因此,无界必须保证肉体唯一:始终只有那一个真实的 DOM 节点,并且它必须被挂载在外面让用户看到。

2. 事件绑定的逆向回传 (Web Component -> Iframe)

DOM 渲染出来了,但用户点击这个按钮时,事件是怎么传回给 iframe 里的业务逻辑的呢?

  • 当你在基座上的 Web Component 里点击了那个按钮,浏览器原生触发了 Click 事件。
  • 无界的事件代理机制捕获到了这个点击。它会找到这个 DOM 节点对应的 iframe 内部的原始映射,然后iframe 的上下文中,手动派发(Dispatch)一个相同的 Click 事件
  • 结果: iframe 里的 React/Vue 绑定的 onClick 回调函数被完美触发,仿佛用户真的点击了 iframe 里面的节点一样。

3. 位置与样式的同步计算

除了 DOM 和事件,无界还要处理极其复杂的坐标同步。 比如你在微应用里写了 ele.getBoundingClientRect() 来获取一个弹窗的位置。

  • 因为节点实际挂载在 Web Component(外部)中,无界会代理这个 API,去外部测量真实节点的屏幕坐标,然后把数据传回给 iframe 里的代码。这保证了下拉菜单、Tooltip 等组件的绝对定位能够精准无误。

总结

无界的这套设计,是一次绝妙的技术妥协与创新:

  • 它利用了 iframe 最无懈可击的 JS 隔离能力(无需自己写复杂的 AST 解析或 Proxy Sandbox 去清理副作用)。
  • 它绕过了 iframe 最令人头疼的 UI 割裂问题(弹窗出不来、滚动条双重、路由不同步),通过 Web Component 把 UI 完美融入基座。

正是因为这套架构不需要对微应用的代码上下文做任何运行时的词法替换,所以它的接入成本无限趋近于零,无论是多老的 jQuery 代码还是最新的 Vite/Vue3 项目,都能直接跑起来。

三、无界的跨域机制处理

在真实的微前端落地中,基座应用和微应用几乎 100% 都是部署在不同域名的。

无界解决这个问题的核心思想,可以用一句话来概括:将“浏览器层面的 iframe 跨域问题”,降维转换为“网络层面的 AJAX(CORS)跨域问题”。

浏览器对于跨域 iframe 的 DOM 访问是绝对封死的(这叫同源策略,神仙难救)。无界的做法是:根本不使用 iframe 原生的 src 属性去跨域加载页面。

下面是这套“移花接木”魔法的具体执行步骤:

1. 放弃 src,改用 fetch 拉取源码(首要前提)

当你在无界中配置了子应用的地址 url: "https://b.com" 时,无界并不会写一个 <iframe src="https://b.com">。 相反,它会在基座应用(https://a.com)中,使用原生的 window.fetch 发起一个普通的 AJAX 请求,去下载 b.com 上的 HTML、JS 和 CSS 的文本内容

⚠️ 这里有一个极其重要的架构前提: 既然是 AJAX 请求,就必然受到跨域限制。因此,无界框架对微应用提出了唯一的、强制的改造要求:微应用 b.com 的 Nginx 或 Node 服务器,必须配置跨域头 Access-Control-Allow-Origin: * (或指定 a.com

  • 以下是未开启跨域和开启跨域的 iframe 的对比图:2026-06-05-092613.png

打开跨域后

2026-06-05-092424.png

2. 准备容器:创建同源的空 iframe

代码拿到了,接下来需要在哪里执行呢? 无界会在基座页面上动态创建一个空壳 iframe。因为没有设置跨域的 src,这个 iframe 默认继承了基座的域名,它是 绝对同源 的。 此时,基座应用拥有对这个 iframe document 的 100% 控制权。

3. 注入与重生(Eval & Inject)

接下来,无界对刚刚 fetch 回来的 HTML 文本进行正则解析,把里面的 <script><style> 提取出来。 然后,将这些 JS 代码的字符串,通过 eval 或新建 <script> 标签的方式,直接塞进那个同源的空壳 iframe 中运行

  • 从物理位置看: 代码运行在 https://a.com 的 iframe 里(同源,无界可以尽情施展 Proxy 魔法)。
  • 从逻辑内容看: 这些代码完全是 https://b.com 写的业务逻辑。

4. 终极挑战:修复“迷失的路径”(Path Patching)

代码虽然成功跑起来了,但会引发一个致命的副作用。 假设子应用代码里有一句:fetch('/api/getUser') 或者 <img src="/logo.png">。 因为现在代码实际运行在 a.com 的 iframe 里,浏览器会自动把这个相对路径拼接成 https://a.com/api/getUser,导致接口 404!

无界为了填平这个坑,在底层做了极其精细的路径重写与劫持

  • 它劫持了 iframe 里的 XMLHttpRequestfetch,一旦发现微应用发起了相对路径的请求,无界会自动强行把它补全为 https://b.com/api/getUser
  • 同时,利用 Webpack 的 __webpack_public_path__ 机制,或者劫持 createElement('script') 等 DOM 操作,将所有静态资源(图片、字体、分包 JS)的请求地址,全部重定向回真实的微应用域名。

四、EventBus 通信机制

首先揭示一个微前端领域极其容易踩坑的认知陷阱

如果在面试中,面试官问你“无界是如何跨越 iframe 跨域限制进行通信的”,而你回答了 postMessage,那么你就直接掉进了陷阱。

真相是:无界底层的 iframe 根本不存在跨域限制! 它的 EventBus 机制也完全没有使用 postMessage

让我们一层一层来拆解无界是如何用一套“瞒天过海”的架构,实现极其高效的数据流转的。

1. 架构的欺骗:为什么无界的 iframe 不跨域?

在传统的认知里,如果基座在 https://a.com,微应用在 https://b.com,我们直接写 <iframe src="https://b.com">,那这就产生了一个跨域 iframe,里面的 JS 无法访问外面的 window,必须被迫使用缓慢的 postMessage

但无界的设计非常巧妙,它彻底抛弃了 iframe 的 src 属性加载方式

  1. 第一步:fetch 获取源码。 当基座需要加载微应用时,无界会直接使用浏览器原生的 window.fetch 去请求 https://b.com 的 HTML、JS 和 CSS 文本。 (注:这就要求微应用的 Nginx/Node 服务器必须配置 CORS 跨域头 Access-Control-Allow-Origin: *,这是无界唯一的强制跨域要求)。
  2. 第二步:创建“同源”的空 iframe。 无界会在基座页面中动态创建一个空的 iframe,但不设置跨域的 src(默认就是同源的,类似于 about:blank,或者是与基座同域的空 URL)。
  3. 第三步:将代码“注入”执行。 无界将刚才 fetch 拉取到的 JS 代码,直接注入到这个同源的 iframe 内部执行。

核心结论: 因为这个 iframe 是同源的,所以基座的 window 和 iframe 内部的 contentWindow 拥有最高级别的互相访问权限。它们之间是一片坦途,没有任何浏览器的跨域安全屏障!

2. EventBus 的底层真面目:极简的“共享内存”

既然没有了跨域限制,那所谓的 EventBus 通信机制,在底层实现起来就变得极其简单、粗暴且高效。

无界的 EventBus 并不是什么黑魔法,它本质上就是一个全局共享的 EventEmitter(发布订阅对象)

1. 实例的挂载

无界在初始化时,会在基座的全局上下文和子应用的全局上下文之间建立一座桥梁。 子应用内部可以通过 window.$wujie.bus 直接拿到基座分配给它的那个事件总线实例内存引用。

2. 通信流程

  • 基座监听:
javascript
// 基座应用
import { bus } from "wujie";
bus.$on("sub-app-msg", (data) => {
  console.log("收到子应用消息", data);
});
  • 微应用发送:
javascript
// 微应用 (在 iframe 内)
window.$wujie.bus.$emit("sub-app-msg", { user: 'Admin' });

整个过程,完全是在同一个 JavaScript 内存堆中进行的方法调用


3. 相比 postMessage,EventBus 的降维打击

既然明白了它是共享内存,你就能体会到无界 EventBus 相比传统 iframe postMessage 的巨大优势:

  1. 零序列化开销(极速):postMessage 在传递数据时,浏览器底层必须执行“结构化克隆(Structured Clone)”算法,将数据序列化再反序列化。如果传递的是几 MB 的庞大业务报表 JSON,postMessage 会导致明显的卡顿。而无界的 EventBus 传递的是对象引用,速度是纳秒级的。
  2. 突破数据类型限制(支持传递函数): 这是最致命的差异!postMessage 绝对无法传递 Function(函数)、Promise 或 Symbol。 但在无界中,微应用甚至可以把自己的一个内部回调函数、或者一个未 resolve 的 Promise 直接通过 EventBus 传给基座。基座拿到这个函数后可以直接调用,完美实现了“跨应用的组件级联动”。

无界通过 fetch + 同源 iframe 的设计,不仅解决了通信效率的痛点,还保留了完美的 JS 隔离。

4. 架构师级警告:不解绑必然导致内存泄漏!

在实际企业级开发中,这是使用 EventBus 最容易踩的巨坑。

前面我们聊过,无界的 EventBus 是存在于主应用内存里的。如果你的子应用在使用 Vue 或 React 开发,并且在某个页面组件内部使用了 $on,那么当这个页面组件销毁时,**你必须手动调用 $off**

如果不注销,下次用户再切回这个页面,组件又会执行一次 $on。不仅会导致内存泄漏,还会导致事件触发时,同一个回调函数被疯狂执行多次(俗称“多重影分身 Bug”)。

标准的 React 闭环写法(必背模板):

javascript
import { useEffect } from 'react';

function UserProfile() {
  useEffect(() => {
    // 1. 定义回调函数引用 (保证 $on 和 $off 引用的是同一个函数地址)
    const handleMessage = (data) => {
      console.log('收到数据', data);
    };

    // 2. 组件挂载时:注册监听
    window.$wujie?.bus?.$on('update-profile', handleMessage);

    // 3. 组件卸载时:必须解绑!
    return () => {
      window.$wujie?.bus?.$off('update-profile', handleMessage);
    };
  }, []); // 空依赖数组,确保只在挂载和卸载时执行

  return <div>用户信息页</div>;
}

(Vue 的写法同理,在 onMounted$on,在 onBeforeUnmount$off。)