前言:深入腾讯无界微前端框架源码,解析它如何用隐藏iframe沙箱承载子应用JS、将DOM渲染进主应用Shadow DOM,实现弹窗全屏遮罩与子应用保活不丢状态;再拆解srcdoc同源继承与proxyLocation伪造location的双层欺骗机制,让传统iframe的四大痛点在微前端场景下被逐一化解。
我把腾讯无界的源码翻了个底朝天,发现它把浏览器给骗了
前两天跟一个做前端的老朋友吃饭,他跟我吐槽,说他们公司的中台系统里塞了七八个子应用,全是用 iframe 糊的,弹窗永远只能弹在自己那一小块框里,遮罩盖不住顶部的导航栏,用户天天投诉,领导天天问他什么时候能重构。
我说你听说过无界吗,腾讯开源的那个微前端框架,就是拿 iframe 做的。
他筷子直接放下了。
拿 iframe 做微前端???
我特别理解他的反应。如果你关注前端这个领域的话,iframe 在微前端圈子里名声一直不咋地。隔离是真的隔离,js、css、dom 全给你焊死在框里,但代价也是真的大。路由不同步,用户在 iframe 里点了半天,浏览器地址栏纹丝不动,一刷新全回到解放前。弹窗割裂,fixed 定位只能相对 iframe 自己,遮罩永远盖不住全屏。通信麻烦,主应用和子应用之间只能靠 postMessage 传字符串,跟两个隔着玻璃罩子的人打手势似的。还有慢,每次打开都是一次浏览器上下文的重建,白屏时间长得让人怀疑人生。
所以圈内人的共识基本是,微前端想做好,就得绕开 iframe,用 Proxy 沙箱那种软隔离去硬搓。
然后腾讯的工程师反手就把 iframe 捡回来了。
我第一反应跟那位朋友一样,不信。所以我鬼使神差的把 wujie 的源码 clone 了下来,v2.1.0,packages/wujie-core 这个目录,一个文件一个文件的翻。翻了两天,我的结论是,之前看到的那份介绍材料基本属实,而且真实实现比材料里写的还要骚。
它干的事情,一句话就能讲清楚。
把Web子应用的灵魂和肉体拆开

子应用的 JS,跑在一个 display none 的隐藏 iframe 里,纯粹拿它当沙箱用,iframe 有浏览器原生的 window、完整的 history 和 location,隔离性拉满,一个字都不用改。而子应用的 DOM,被抽出来塞进主应用的一个 Web Component 里,也就是一个叫 wujie-app 的自定义标签,内部套着 Shadow DOM。
灵魂在框里,肉体在框外。
这一拆,传统 iframe 的几宗罪直接消掉一大半。弹窗为什么只能弹在框里?因为 DOM 长在 iframe 里。而现在 DOM 搬到主应用的文档里了,子应用写一个 fixed 定位,它相对的就是整个浏览器窗口,遮罩想盖多大盖多大。子应用里 document.getElementById 找不到元素怎么办?无界用 Proxy 把 iframe 的 document 整个代理了,querySelector、getElementById、head、body 这些查询类的接口,全部偷偷重定向到主应用的 Shadow Root 上。子应用毫无察觉,它以为自己摸的是自己的 document,其实摸的是主应用家里那棵 DOM 树。
说实话我看到这个设计的时候,第一反应是这也太损了,但太牛逼了。
不过真正让我把源码翻出味道来的,是另外两件事。一件是保活,一件是同源。这两块是那份材料里着墨最多的地方,我逐行对了一下,属实,而且细节比想象中精巧。
先说保活这块。
用过微前端的朋友都知道,子应用切换最恶心的就是状态丢失,用户填了一半的表单,切个菜单回来,全没了。Vue 有 keep-alive 这种保活方案,但微前端的场景下很难做,因为应用切换的时候整个实例都被销毁了。
无界的做法是,离开的时候,不断电。
具体是这样。主应用路由切走,wujie-app 这个自定义标签从页面里被拔出来,Web Component 原生的 disconnectedCallback 钩子触发,源码在 packages/wujie-core/src/shadow.ts 里,就几行,转手交给 handleWujieAppDisconnect 处理。这个函数里有一个关键判断,如果这个子应用没开保活,就调 destroy,全销毁,该清理清理。但如果开了 alive,走的是另一个分支,只调 unmount。
而 unmount 这个函数,我盯着看了很久。它里面到处都是「不」。
不调用子应用的销毁钩子。子应用那个 React 根组件、Vue 根实例,原封不动留在 iframe 的内存里,定时器还在跑,响应式变量还在,网络请求该回来回来。
不清空 Shadow DOM。整棵已经渲染好的 DOM 树,连着那个 wujie-app 节点,被 sandbox 对象攥在手里,常驻 JS 堆内存。它只是不在页面上了,不是不在了。
也不动 iframe。那个隐藏的 iframe 从头到尾挂在主应用 document.body 的最底下,安安静静,谁也不许碰它。
然后用户切回来了。startApp 一进来就检查,哦这实例还在,还是保活模式,直接走 active。active 里最核心的就一步,把内存里那个 wujie-app 节点,用原生 appendChild 插回页面容器,然后 if (this.alive) return,流程结束。
不请求 HTML,不解析样式,不执行任何 JS。
就是插回去。

你想想看,用户视角是什么体验。切走再切回,页面瞬间出现,表单还在,滚动位置还在,连子应用里那个 setInterval 计时器都没跳针。这不是加载快,这是压根没加载,就是从内存里把一块 DOM 拿出来拍回去了。
聊到这我想起一个哲学梗,忒修斯之船。一艘船的木板被一块块换掉,它还是原来那艘船吗。无界给出的答案挺暴力,只要灵魂不断电,肉体拔下来再插回去,它就还是原来那个应用!
但真正的高潮在后面。
顺着保活往下翻,我撞上了整个框架里我觉得最NB的部分。前面说了,子应用的 JS 跑在 iframe 里,DOM 挂在主应用里,那主应用就得能随时摸到 iframe 的内部,代理它的 document,注入它的 window。但浏览器的同源策略不是吃素的,一旦 iframe 跟主应用跨了域,任何这种跨框操作直接给你抛 SecurityError,一个字都不让你看。
问题来了。子应用明明部署在自己的域名下,比如 sub.domain.com,主应用在 main.domain.com,这个 iframe 凭什么能跟主应用同源?
我翻到 packages/wujie-core/src/iframe.ts 的 iframeGenerator 函数,看到创建 iframe 那段代码的时候,整个人是懵的。
它压根没给 iframe 设置 src。
它设置的是 srcdoc,内容就是一个空白的 HTML 壳,head 是空的,body 也是空的。

这是 HTML 规范里一个特别冷门的特性,通过 srcdoc 生成的文档,它的 origin 会强制继承自宿主,也就是主应用。零网络请求,天然同源。早期要是直接把 src 指到主应用的地址,浏览器会真的去加载一遍主应用的页面,脚本再跑一遍,直接递归死循环。srcdoc 一上,请求没有,同源白嫖。
但事情没完。srcdoc 文档的地址是个诡异的 about srcdoc,子应用里的相对路由、fetch 全会出幺蛾子。无界接着用 document open 再 document close 这个老掉牙的 API,把文档的 URL 规整回主应用的域名下,再用 history replaceState 把子应用的路由路径拼在主应用域名后面写进去。注意,写的永远是主应用域名加子应用路径,因为域名是主应用的,浏览器才放行。
那子应用代码里读 window.location.host 的时候不就穿帮了吗?它明明以为自己跑在 sub.domain.com。
这就是我觉得最骚的一层。无界用 Proxy 造了一个假的 location,叫 proxyLocation,然后通过一个立即执行函数,在子应用代码执行之前,把全局的 location 偷偷换成了这个假货。子应用读 host、hostname、origin,返回的是子应用自己的域名。读 href,底层那串主应用域名的真实地址,被 replace 一道,把主应用域名还原成子应用域名再给你。子应用调 location.replace 想跳转,参数先被反向替换成主应用域名才真正执行。
一层”骗“浏览器,一层”骗“子应用。
对浏览器来说,这个 iframe 从头到尾都是主应用的一部分,同源,随便操作。对子应用来说,我明明就跑在我自己的域名下,我的 origin 没问题,我的路由没问题。两边都被蒙在鼓里,只有无界在中间两头传话。
我当时看到 proxyLocation 那段代码,脑子里蹦出来的词是庄周梦蝶。那个 iframe 里的子应用,一直以为自己活在 sub.domain.com,其实它从头到尾都活在主应用给它造的一场梦里。可你要较真说它活在梦里吧,它渲染出来的 DOM、发起的请求、维护的状态,又全是真的。
还有一堆收尾的细节,比如往 iframe 的 head 里插一个 base 标签,把静态资源的解析基准指回子应用的服务器,不然子应用里一个相对路径的 logo 图片就会跑去主应用的域名下找,404 给你看。材料里提到的那些技巧,我逐个对过源码,都有对应的实现,没有一个是吹的。
坦率的讲,翻完这一圈,我对「用 iframe 做微前端」这个想法的印象彻底反转了。iframe 的问题从来不是 iframe 本身烂,而是大家只把它当一个容器用。而无界把它拆成了两半,拿它最硬的那一面做沙箱,拿 Web Components 去补它最软的那一面做渲染。隔离是原生的,性能没有 Proxy 沙箱那种 with 包一层的损耗,子应用几乎零改造就能接入,连 Vite 这种 ESM 的产物都能直接跑。
当然也不是没有代价。同源这套魔法的前提是子应用和主应用能被摆到同一个源下,如果子应用域名真的完全独立、跨域到没法处理,这套把戏就玩不下去,得走降级方案。而且保活模式下子应用的 JS 是永远在后台活着的,内存和定时器都占着,一口气开太多子应用一样会有性能压力。这些材料里没写,我觉得得替它补上,真诚是唯一的捷径嘛。
子应用独立域名解决方案
| 场景需求 | 核心痛点 | 最佳解决手段 | 实施侧 |
|---|---|---|---|
| 自有独立域名子应用 | 入口及 JS/CSS 跨域 | 子应用 Nginx / CDN 配置 Access-Control-Allow-Origin |
子应用部署层 |
| 独立域名身份鉴权 | 跨站 Cookie 被浏览器丢弃 | 废弃 Cookie 鉴权,改用 Authorization: Bearer <Token> Header |
主子应用业务代码 |
| 持久化存储防冲突 | 读写主应用 Storage 产生键冲突 | 配置无界插件劫持 localStorage 添加 App 前缀 |
主应用无界配置 |
| 外部不可控第三方系统 | 无法配 CORS / 拒绝代码改造 | 1. 无界配置 degrade: true2. 主应用 Nginx 代理并清洗 Header |
主应用配置 / 网关层 |
对了,还有个细节我特别喜欢。无界的官方文档里写着,这个方案的灵感,来自 qiankun 的 issue 区里一个开发者提的问题,能不能用 iframe 来做 JS 沙箱。一个 issue,一个当时看起来有点异想天开的问题,最后长成了一个框架。
你说巧不巧,我朋友那个天天被 iframe 弹窗折磨的中台,上周开始试无界了。他跟我说,弹窗能盖住全屏的那一刻,他愣了几秒。
那种感觉,大概就是,原来框住你的东西,换个用法就是保护你的东西。
磨平一些信息差,共勉。
/ 注意,本项目仅在 SyncMein 的500人小群分享。
/ 谢谢你看完了我的文章,我们下次再见吧。
/ 作者:明察
/ 投稿或爆料,请联系邮箱:mingcha@gqmg.com














暂无评论内容