字节跳动微前端架构沙盒应用解析
大家好,今天小编来为大家解答以下的问题,关于字节跳动微前端架构沙盒应用解析,这个很多人还不知道,现在让我们一起来看看吧!
一切都从 iframe 开始讲起了,反正是个看上去很美的解决方案。没有真的用过的人肯定都会这样想象。但有些可能等到你要真的搞一个通过 iframe 全面聚合的才知道。单纯的 iframe 聚合非常麻烦,需要很多补漏的劳动量。
旧的 iframe 的方案可以在一定程度上解决了耦合问题。具体是把一个站点页面拆成 N 个 frame,每个 frame 单独跑一个独立的域名。
因为一个完整的项目包含大量公用的功能和代码,例如登录身份、站内信,业务模块只是其中的一个部分。这部分完全用跨 window 通信实现起来很费时费力,并且单页应用了 React 或类似的加载技术展示之后,iframe 的效果也逊色很多。想要突破这些限制,困难就很多了。
古老的困难
第一点不用多说各位都会想到 deeplinking 的问题,对吧,至少这点得做到才能算是一个工程,尤其 MVC 时代以来路由一直非常重要。
还有就是各种共享的东西,比如登录怎么共享。iframe 当然也不是不行。和前面后面提到的诸多问题一样都不是不行、而是很麻烦,要针对他去解决很多困难。从效果上讲,最终也完全可以形成一个不错的 iframe 沙盒。
另一显然的困难是,组件库、组件风格的父子传递,以及 React VUE 等渲染引擎的底层代码、内存对象的传递。初步的实现是加入分片打包功能,拆 common chunk 并独立部署 CDN 上,最后在加载时,通过浏览器自身的缓存能力加速访问。但是运行时内存并不共享,对包的运行时修改也难以复用。
还有数据层的设计,数据 Store 等等。数据层至少要有一定的。。打通的功能。搞不好就牵一发动全身,改一个需求,不得不发布四五个项目。
2. 沙盒应该像什么
虚拟化、容器化、Docker
到这里开始讲到美好的景物了,docker。从解耦的角度看,服务端的微服务主要是通过 docker 技术实现虚拟化的底层支持,使服务开发者可以体会不到环境的区别、抹平运行时差异的。可以说对微服务来说,docker 是这些年能得到如此的发展的一个基石。
单纯说微服务的概念本身,很久之前也有这个概念的,有面向服务编程的理论。但是发展很少,兑现仍然很困难,搞虚拟机很麻烦。而且还包括开发体验的东西,我打包的镜像——要想交付一致得包含整个操作系统吗?这对开发体验影响很坏。
在 docker 得到普遍应用之前,微服务在服务端的使用主要基于虚机。相比之下使用非常复杂、维护成本提高。虚拟机不说多麻烦了,大家都懂。它吃掉的资源,和容器化技术比完全不在一个量级。还有比如当你想要打个快照连磁盘都吃掉。并且多个服务之间的资源协调和有效分配,实现起来也极端困难。
诸多扩大的成本问题直到随着 docker 的沙盒体系才得以解决。微服务才成为一个趋势。可惜的是这样的容器环境在前端浏览器内的运行时还不存在。
3. 沙盒应该怎么做
那么我们说的这种沙盒,这种轻量级、强调组件间协同沟通、非常节省资源的沙盒怎么做呢,下面我分 3 个方向分别介绍。(这块还不会有具体的实现,主要从可能性上分析在浏览器里怎么造沙盒。)
3.1 单进程与多进程
参考单核、操作系统进程,模拟进程切换策略。 我们的沙盒实质上在让一个浏览器去跑多个“独立”的应用,那么这里对操作系统的模仿、最终趋同一定避不开。在这个角度上,和其他语言相比 JavaScript 占了一个独特的执行特点的便宜:它自身是单线程的。我怎么做实质上也都是在一个线程内。相当于我们这个操作系统从一开始就限定了单核只有一个出力气。
那么一个操作系统,怎么做多进程并行呢, 单进程可简单通过根路由等等规则控制,每次只激活一个,大家做 context 切换即可;多进程并行就正好可以利用 JavaScript 的特性,我可以封装每个独立的。。循环。比如 setTimeout、各种。。回调的 handler,我们在实际 function 外面先切换 context,再执行你原本希望绑定的 function。这样是线程安全的。总结下来就会是下面两条:
用路由切换封装,模拟单核单进程。
用。。循环的总体封装,模拟单核多进程。
3.2 Context 切换

用 context 切换来模拟线程安全, 具体的意思是在每个隔离的子应用“进程”即将开始激活时,先查找当前被激活的、其他的子应用,然后为这个将要退出的应用录制“操作系统”的全现场状态,保存为它的 context。最后为即将激活的新“进程”恢复、新建它自己的 context。
如上面所说,我把当前状态记录为 context,保证每个子应用都适用在自己的 context 内,不影响和改变别人的 context。这个操作全部由托管了子应用的父系统统一来切换。
体现删除 key 必须要遍历两次, 才能保证每个对象都遍历到一遍。这块要强调一个点,当你拿到新旧两个对象比较,遍历其中一个的 key、到另一个里面找,只这样做是不够的,因为有可能又删掉的东西。删掉导致 key 没有了,自然也遍历不到。想体现这个删除,必须要遍历两次,新旧两个对象都遍历一遍,才能知道相比之下谁多了什么谁少了什么。尤其是大家做“空闲”到新开沙盒的比较的时候,特别容易忘了这个细节。
Context 切换的性能够好吗? 先说说这个快照的空间性能。如果你有 N 个沙盒需要有多少切换的组合呢,是不是 context 的全文、或者任意两个沙盒之间的 context 差异都要完整储存?实际上不用。我们只需要记录差别、context 的变化,并且只记录他对 “idle”状态的差别。例如 A、B、C、D、E、F、G。我们不需要记录 A→B A→C 这样的切换,而是虚拟一个空闲状态:O,都是 A→O,B→O,只保存他们和 O 之间的差别。需要记录比较的变量数量从子应用个数的乘法变成了加法。写一个循环就可以快速比较完变化。
综上所述就是让每个子应用的开始和结束、互相切换,都先回到一个虚拟的“初始状态”,恢复现场,再进入被激活的沙盒状态,每次切换仅记录一个沙盒信息。避免了切换算法计算笛卡尔平方积导致比较、和保存沙盒切换信息过大的问题。
4. 字节跳动的沙盒采取的方案
4.1 CSS 沙盒
先说 CSS 沙盒。 这块 webComponent 已经做了很多发展了很多了。这里忍不住要说,web 标准里一度有一个非常吸引我、让我感觉非常有意思的内容是 scoped css——就是加个 Attribute 就能结合 DOM 树限制 CSS 作用范围。后来这个标准被取消了。因为让路给了 ShadowDOM 体系。
这个我不是很理解:因为 scoped CSS 是外面的规则能进来、里面的规则不出去,但 shadowDOM 是完全的割裂。这个巨大的区别使得它们的工程意义相差甚远。后面我们会说到 css module,它的表现显然和 scoped style 一样,和 Shadow 不一样。
CSS module 和 CSS in JS 都是把样式写成或编译成脚本,同时把脚本生成的 DOM 的最外面一层加一个 nounce 的 attribute;然后再给所有受控的 CSS 规则都套上这个 "attribute"。缺点是相对麻烦了一点、并且要完全控制掌握所有 DOM 创建。在前端框架里,Angular 这样做很自然。
后面还会提到这方面最流行的 NPM 包有个好玩的 feature 可能会造成不小心的 bug。
我们采用的是 DOM 沙盒保护 head 内标签。 这样的 style 和 link 本身都可以受到沙盒统一保护。在实际应用中我们的子应用开发者在业务组件里也有用 CSS module 的,我们也不用管——反正去掉标签这个事情最安全。
DOM 沙盒就看管好某个 DOM 标签,谁要改了,沙盒切换时改回来。对绝大多数情况绑定的 style 和 link 标签都有效。但这个只限于单进程的情景。
如果如前面提到的多进程的情况(就是理解成同时有 N 个沙盒在一起运行、并行的系统)。那 CSS 肯定不能和 JavaScript 的单线程运行时一样那么搞,所以一定要用 moduled CSS。也不难搞,很多开源库可以用。即使出现大家引用同一个组件库的不同版本、各自 hack 过、失手创造了什么“幺蛾子”也不用怕,因为他们都是编译好、作用域有限了的。
用 NPM 上的 styled-component 包时要小心, 他们会根据环境变量判断环境;然后对 prod 环境启用一个叫“speedy”的模式,它将不用 innerText 写样式规则,而是用 addRules 那一整套 API。但是这套标准似乎没明确界定这个标签被从文档 DOM 树里移除时的行为和表现,也许因为显而易见 rules 也应当一起移除。但我们插回来时,这种含糊就乱套了。浏览器实际的表现是移除再插回的标签 rules 都没了。这里显然需要我们额外处理。
4.2 全局变量沙盒
另一个重要的是全局变量干扰问题。 Polyfill 等运行环境相关的全局对象、环境变量等具体实现上有非常大的差别,又全部作用于全局。它们对子应用、模块化的子组件来说,又属于自身全局外部的环境。
这块是微前端实施的一大重点。我个人觉得是这样的。可以看出来大家都不是很信。“谁不知道不要写全局变量啊?不会有这么不靠谱的人”。事实上真的试过才会发现会有好多。例如头条号里面用到了某个剪裁图片的插件库。他是个非常完善、正派、和古典的包,同时支持 React 和 JQuery。它给全局写了一个单例的实现。并且在开发调试过程中我们不同业务线的团队就真的用了这个包的不同版本。
当然这个不重要,也没有造成问题。一个比较严峻的例子是这个—— reGeneratorRuntime。它是编译 async 语法用的,在某个 config 下的 Babel 会 delete 这个对象。到底是啥原理不清楚也不需要多讲,但是非常肯定会冲突并造成问题。曾经我们的西瓜号团队的 polyfill 规则和另一个业务线就发生了这类冲突。所以要比较 delete,恢复删除,切换回去西瓜再删掉。
Identifier 是另一个关注点。 你是否完全清楚 Identifier 是什么?Identifier 就是在某个 scope 下起作用的变量名啊什么的,包括 function,let,class,const。只有 var 出来的东西特殊一些、不会占用 Identifier,以上几个会,占用后不可以重复用。
这些东西首先你遍历不了,没有枚举器;其次他们不是某个对象的成员,而仅是编译层面的名字。一旦产生了绝对删不掉。
在全局作用域下 var a 的时候,实际上是生成了一个超范围的 Identifier 并且额外在 global 上创建一个同名的 key,指向同一个地址。这是 var 语句额外的操作。这让我们可以用遍历 window 的方式来处置全局变量。

总之这个事不要多想,new function 包起来几乎必不可少。还可以传入如 setTimeout 这种入参,用来控制异步实现“多进程”并行。
还有个 location 不要挪, 会刷新页面。黑名单掉它。
还有个好玩的事:function 和 var 一样会额外在 window 上增加个 key。这个 property 的 configurable 是 false——也就是不能删除。但是可以赋值。
所以如果你如果光 var a,就可以 delete window.a;再写 a 就是 undefined。写个 function a,再写个 delete a 就无效。 但如果你写个 function a,再写个 var a = 1。啥效果呢,你给 window 上绑了个删不掉的数字,延续了 function a 的不可删除属性和 var a 的值。
更好玩的是 class,你 class B {} ,再 console log window.B,咋么样,undefind。再写 B = 1;然后再看 window.B 怎么样?继续 undefined 了, B = 1 没有效果。
说明潜在的某个机制在 class 关键字执行的时候,给 global 绑上了个叫 B 的 property,但是是个无法枚举和访问的 property,property 有 writable true, enumerable: true, configurable: true 之外的隐藏属性。
4.3 其他
还有好多需要进程安全的对象, 比如 cookie,但这个其实不特别重要,简单约定一个使用 path 就可以了——cookie 除了设置 domain 还可以设置 path。只不过大部分人都不设它(也就是设为根目录“/”)。
localStorage 可以也保护一下。 取决于你的业务。因为这些都属于 windows 的全局变量,所以实现一个包装过的 class 集成并模拟 localStorage 原本的行为就可以。让它所有方法都先给 key 加 prefix,再执行方法的 super。这个 prefix 可以简单地写死当前沙盒的 uuid 即可,因为 window.localStorage 作为全局变量本身就在沙盒保护之内。
5. 沙盒的其他功能
下面是最后一章节,会讲沙盒下有些特殊的东西,它们都需要额外处理。其中重点说一下埋点。多数微前端项目一个页面里的埋点已经属于不同项目了,这块就得搞清楚具体什么子应用、用的什么统计代码、需要处理哪些级别的缓存。
5.1 埋点缓存系统
像前面说的,把 Storage 缓存全部用沙盒包装过,对埋点体系来说还不算完。绝大多数埋点系统的。。发送都是异步、找网络空闲的。并且这些源码通常又在 SDK 里面、不在父工程直接控制的代码里。所以可操作余地不多。实际上只能是把缓存数据、项目信息都好好保存好,再把收集数据的缓存和产生数据时的沙盒状态对应起来。
5.2 console
沙盒可能会包一层或者多层运行时,所以 console 读会比较累。开发的时候可以额外处理一下,为开发者提供便利。 而且现在的前端项目,线上希望 console 打印越少越好、内容越正规越好,并且调试又非常忌讳别人遗留的打印干扰。这些都是沙盒可以做的。我们甚至做了把内容直接对接到采集系统里的上传 log。
具体来说,为 log 注入 callstack 是用 new Error 的方式。这样可以通过 error.stack 拿到调用堆栈。这个值直接是个字符串、是换行分割的 Markdown,可以写链接进去,也能对应到调试窗口的 source code。
同理的是当遇到真的 exception 也应当如此管控。从 catch 到异常、再次 throw 出去之前,可以给 error.stack 值全都 hack 掉。去掉不必要的 stack——比如你包的那层 new Function,删掉那一行。提示的内容也可以改。
5.3 sourceMapping
sourceMapping 是谷歌 closure 发明的一个、现在成为 ES6 标准的东西。原理是一个字符位置到字符位置的映射。那么在 new Function 下的沙盒里能不能用呢?当然可以。
我们先说说 new Function 的表现。在 chrome 里它在调试中是一个新的匿名环境:anonymous,字符行列位置就从函数字符串开头第一行开始算起。如果你是把编译并且生成了 sourceMap 后的 bundle 放到 new function 里执行,这个位置是完全对应的,不需要做任何额外的 hack。
这同时是因为 chrome 能正常识别 new Function 参数字符串末尾的 sourcemappingUrl= 的注释。对应在 callstack 里一切都对。有好多时候我们发现业务方会不放心这个事,会觉得我直接下载的 .js 被包了,不放心调试的 call stack 和 sourceMap。事实上没问题。这两方面都没问题。
结语
用户评论
看到这篇博文,突然明白为什么现在很多公司都对微前端很感兴趣了。比如字节跳动这种规模庞大的公司,用微前端来管理这么多业务线确实能提高开发效率和代码 Maintainability,太棒了!
有8位网友表示赞同!
我觉得沙盒这个模式真的很关键,毕竟在大型项目里,每个团队的业务逻辑都不一样,需要隔离性强。字节跳动的方法很有启发意义,让我对微前端有了更深的理解。
有15位网友表示赞同!
我一直好奇大公司是怎么管理这么庞大的代码库的,现在看来字节跳动的微前端沙盒实践给出了答案!高效、模块化,这种架构未来发展一定大有可为啊!
有12位网友表示赞同!
标题很吸引人,我就想看看字节跳动是怎么把微前端和沙盒模式结合起来的。看完这篇博文后感觉还是挺有用的,尤其是关于数据隔离机制的介绍,让我对如何更好地应用微前端有了些许想法。
有18位网友表示赞同!
我也有在尝试微前端,但是遇到的最大的问题是前端开发工具链的多样性,各个项目的依赖关系也很复杂。字节跳动的实践案例可以借鉴一下,希望能解决我的难题!
有10位网友表示赞同!
说实话,这种微前端沙盒的架构听起来挺复杂的,对团队协作和代码管理的要求很高。会不会开发成本反而增加呢?文章没提到这个问题,希望作者能够补充说明。
有18位网友表示赞同!
我觉得微前端最大的意义在于提高开发效率以及每个项目的独立性和可维护性。字节跳动的这篇文章说的很清楚,也让我看到了未来微前端发展的方向。
有17位网友表示赞同!
我是个前端开发者,对微前端一直比较感兴趣。但感觉这种沙盒模式的实施还是需要强大的技术团队和资源支持才能有效地落地吧?
有13位网友表示赞同!
这篇文章写的不错,把字节跳动的微前端实践讲得很详细也很透彻。让我对微前端有了更深入的了解,有学习借鉴的地方啊!
有11位网友表示赞同!
虽然我比较好奇微前端到底能带来哪些实际效果,但从文章中也能看出来,字节跳动这种大型公司确实需要一套更加高效的开发架构才能满足需求。
有15位网友表示赞同!
感觉文章有点过于技术深奥,对于入门级的程序员来说很难理解。如果能用更通俗易懂的语言解释一下,对像我这样的菜鸟更有帮助。
有7位网友表示赞同!
我觉得微前端的确是一种未来趋势,但是现在的实现方案还没有完全成熟,还有很多待完善的地方。需要更多的实践经验和技术的积累才能更好地推广。文章就提供了很好的切入点,让我们更清晰地看到了挑战和机遇。字节跳动的实践值得借鉴和参考!
有12位网友表示赞同!
一直在关注微前端的发展方向,感觉这种沙盒模式很有意思,可以更好地隔离不同的业务代码,提高维护效率。 希望未来能看到更多关于微前端技术的分享和探讨。
有17位网友表示赞同!
看文章感觉字节跳动微前端的实践经验非常丰富,特别是对于数据隔离机制的设计可以学习借鉴一下。不过对我们这种小型公司来说,可能用上微前端的技术门槛还是比较高的。
有18位网友表示赞同!
我对微前端一直感兴趣,但实际应用场景不太清楚。文章提到沙盒模式可以提高开发效率,具体有哪些例子呢?希望作者能补充一些更具体的应用案例。
有11位网友表示赞同!
感觉这篇文章写的有点理论性较强,缺少一些真实的案例和数据支持。如果能结合一些具体的实践经验,效果会更好!
有8位网友表示赞同!
字节跳动真是个厉害的公司,这种微前端的沙盒模式确实很新颖也很实用,希望会有更多公司借鉴他们的经验,推动微前端的发展!
有14位网友表示赞同!
看了这篇文章我更加坚定了做微前端的决心。未来技术的趋势就是越来越细化模块化,微前端在这个方向上走在最前线了。
有5位网友表示赞同!
对字节跳动这种大公司来说,微前端的应用确实能带来很多好处,如开发效率提升、代码结构更清晰等。不过对于我们这种小型团队来说,也许还是先看看微前端能不能完全满足我们的需求再考虑使用吧!
有8位网友表示赞同!