古老的 Iframe
一切都从iframe 开始。无论如何,这是一个美观的解决方案。没有实际使用过的人肯定会想象到这一点。但有些人可能直到你真正通过iframe 进行全面聚合后才知道。简单的iframe聚合非常麻烦,需要花费大量的人力来修复泄漏。
旧的iframe方案可以在一定程度上解决耦合问题。具体来说,一个网站页面被分割成N个框架,每个框架运行一个独立的域名。
因为一个完整的项目包含大量的常用功能和代码,比如登录身份、站点消息等,业务模块只是其中的一部分。这部分完全利用跨窗口通信来实现是非常耗时耗力的,而且在单页面应用React或者类似的加载技术之后,iframe的效果也逊色很多。突破这些限制有很多困难。
古老的困难
第一点不用说大家都会想到deeplinking的问题吧?至少必须做到这一点才算是一个项目,特别是路由从MVC时代以来就非常重要。
还有各种共享的东西,比如如何共享登录。当然iframe也不是不可能。就像前面和下面提到的许多问题一样,不是不可能,而是非常麻烦。有很多困难需要为他解决。从效果上来说,最终可以形成一个好的iframe沙箱。
另一个明显的困难是组件库和组件样式的父子传递,以及React VUE等渲染引擎的底层代码和内存对象的传递。最初的实现是增加分片和打包功能,将公共chunk切分并独立部署在CDN上,最后在加载时通过浏览器自身的缓存能力加速访问。然而,运行时内存不是共享的,并且运行时对包的修改很难重用。
还有数据层、数据存储等的设计。数据层至少要有一定的功能来打通事件。如果不行的话,整个系统都会受到影响,一个需求一变,就得发布四五个项目。
2. 沙盒应该像什么
虚拟化、容器化、Docker
我们先来说说这里的美丽风景吧,docker。从解耦的角度来看,服务器端微服务主要通过docker技术实现对虚拟化的底层支持,导致服务开发者无法意识到环境的差异,抹平运行时的差异。可以说,对于微服务来说,docker是近年来取得如此发展的基石。
简单来说,微服务这个概念本身很早以前就有人提出来了,就有了面向服务编程的理论。不过开发还少,实现起来还比较困难,搭建虚拟机也比较麻烦。它还包括开发经验。我打包的镜像——如果要一致交付,是否需要包含整个操作系统?这对开发体验造成了不好的影响。
在docker广泛使用之前,服务器端微服务的使用主要基于虚拟机。相比之下,使用起来非常复杂,维护成本也增加。且不说虚拟机有多麻烦,大家都明白。它所消耗的资源与容器化技术不是一个级别的。例如,当您想要拍摄快照时,它会耗尽磁盘。而且,多种业务之间的资源协调和有效配置也很难实现。
很多扩大的成本问题直到Docker的沙箱系统才得到解决。微服务刚刚成为一种趋势。不幸的是,前端浏览器内的运行时尚不存在这样的容器环境。
3. 沙盒应该怎么做
那么如何制作我们所说的这种沙箱,这种强调组件间协作通信并且非常节省资源的轻量级沙箱呢?下面我从三个方向来介绍。 (目前还没有具体实现,我们主要从可能性的角度来分析如何在浏览器中创建沙箱。)
3.1 单进程与多进程
指的是单核和操作系统进程来模拟进程切换策略。我们的沙箱本质上允许一个浏览器运行多个“独立”的应用程序,因此操作系统的模仿和最终的融合必然是不可避免的。从这个角度来看,与其他语言相比,JavaScript 利用了一个独特的执行功能:它本身是单线程的。无论我做什么,本质上都是在一个线程内。这相当于我们的操作系统从一开始就限制了只有一个单核的能力。
那么操作系统如何才能实现多进程并行呢?通过根路由和其他规则可以简单地控制单个进程。一次只能激活一个,每个人都可以切换上下文。多进程并行正好可以利用JavaScript的特性。我可以封装每个独立的事件循环。例如,对于setTimeout和各种事件回调处理程序,我们首先将上下文切换到实际函数之外,然后执行您原本想要绑定的函数。这是线程安全的。总结起来就是以下两点:
使用路由交换封装来模拟单核、单进程。利用事件循环的整体封装来模拟单核、多进程。
3.2 Context 切换
使用上下文切换来模拟线程安全。具体含义是,当每个孤立的子应用“进程”即将被激活时,首先找到当前激活的以及其他子应用,然后记录即将退出的应用。 “操作系统”的完整站点状态被保存为其上下文。最后,恢复并创建自己的上下文以激活新的“流程”。
如上所述,我将当前状态记录为上下文,以确保每个子应用程序都适用于自己的上下文,并且不会影响或改变其他人的上下文。该操作全部由承载子应用程序的父系统统一切换。
意味着删除key必须遍历两次,才能保证每个对象都被遍历一次。这里需要强调一点。当你比较新旧对象时,遍历一个对象的键并在另一个对象中找到它是不够的,因为某些东西可能会被删除。删除导致key消失,自然就无法遍历了。为了反映这个删除,你必须遍历两次,旧的和新的对象,才能知道谁的多,谁的少。尤其是当人们将“空闲”与新打开的沙箱进行比较时,很容易忘记这个细节。
上下文切换的性能好不好?我们先来说说这个快照的空间表现。如果有N个沙箱,需要多少种切换组合?是否需要完整存储上下文的全文,或者任意两个沙箱之间的上下文差异?其实没有。我们只需要记录差异、上下文变化以及仅到“空闲”状态的差异。比如A、B、C、D、E、F、G。我们不需要记录AB AC的切换,而是记录一个虚拟的空闲状态:O,都是AO、BO ,并且只保存它们与O的差值。需要记录和比较的变量个数从子应用个数的乘法变成了加法。编写一个循环来快速比较更改。
综上所述,各个子应用的开始和结束以及相互之间的切换都回到虚拟的“初始状态”,场景恢复,然后进入激活的沙箱状态。每个交换机只记录一个沙箱信息。这样避免了切换算法计算笛卡尔平方积,导致沙箱切换信息比较和保存过大的问题。
4. 字节跳动的沙盒采取的方案
4.1 CSS 沙盒
我们先来说说CSS沙箱。 webComponent这块已经开发了不少了。这里不能不说,Web标准中有一个内容非常吸引我,让我觉得很有趣:scoped css——,也就是说添加一个Attribute可以结合DOM树来限制CSS的范围。后来这个标准被取消了。因为它让位于ShadowDOM 系统。
我不太明白这一点:因为作用域CSS允许外部规则进来而内部规则不出去,但shadowDOM是完全分离的。这种巨大的差异使得它们的工程意义截然不同。我们稍后会谈到css模块,它的性能显然与scoped style相同,而与Shadow不同。
CSS模块和JS中的CSS都是将样式编写或编译成脚本,并在脚本生成的DOM的最外层添加一个nonounce属性;然后将此“属性”应用于所有受控CSS 规则。缺点是比较麻烦一点,需要完全控制所有的DOM 创建。在前端框架中,Angular 很自然地做到了这一点。
稍后会提到,该领域最流行的NPM 软件包具有有趣的功能,可能会导致意外的错误。
我们使用DOM 沙箱来保护head 中的标签。这样的样式和链接本身可以通过沙箱统一保护。在实际应用中,我们的子应用开发者在业务组件中也使用了CSS模块,我们不需要担心——。无论如何,去掉标签是最安全的事情。
DOM 沙箱负责处理特定的DOM 标签。如果有人想改的话,切换沙箱的时候就会改回来。在大多数情况下,它对于绑定样式和链接标记有效。但这仅限于单进程场景。
如果是上面提到的多进程情况(理解为有N个沙箱同时并行运行在一起的系统)。那么CSS肯定不能做与JavaScript的单线程运行时相同的事情,所以你必须使用模块化的CSS。这并不难,有很多开源库可用。即使每个人都引用同一组件库的不同版本、黑客攻击或意外创建某些内容,也不要害怕,因为它们都是经过编译的并且范围有限。
在NPM上使用styled-component包时要小心,他们会根据环境变量来确定环境;然后为prod环境启用一种名为“speedy”的模式,该模式不会使用innerText来编写样式规则,而是使用addRules这组API。然而,这套标准似乎并没有明确定义这个标签从文档DOM 树中删除时的行为和性能,也许是因为很明显规则也应该一起删除。但当我们回来时,这种歧义就消失了。浏览器的实际表现是,去掉再插入的标签规则就没有了。这显然需要我们进行额外的处理。
4.2 全局变量沙盒
另一个重要问题是全局变量干扰。 Polyfill等与运行环境相关的全局对象和环境变量的具体实现存在非常大的差异,并且都具有全局作用。对于子应用程序和模块化子组件来说,它们属于自己的全局外部环境。
这是微前端实现的一大重点。我个人是这么认为的。看得出来,没有人很相信。 “谁不知道不要写全局变量呢?不会有这么不靠谱的人。”事实上,只有你真正尝试过,你才会发现有很多。例如,今日头条使用插件库来裁剪图片。它是一个非常完整、体面、经典的包,同时支持React 和JQuery。它为整个世界编写了一个单例实现。而且在开发和调试过程中,我们不同业务线的团队实际上使用了这个包的不同版本。
当然这并不重要,也不会造成问题。更严重的例子就是这个—— reGeneratorRuntime。它用于编译异步语法。 Babel 在某个配置下会删除这个对象。我不知道原理是什么,也不需要详细解释,但是很确定会冲突,产生问题。这种冲突发生在我们西瓜号团队的polyfill规则和另一个业务线之间。所以比较删除、取消删除、切换回西瓜再删除。
标识符是另一个值得关注的点。您完全了解标识符是什么吗?标识符是在一定范围内工作的变量的名称,包括function、let、class、const。只有var里面的东西比较特殊,不会占用Identifier。以上物品占用后将无法重复使用。
首先,你不能遍历这些东西,没有枚举器;其次,它们不是对象的成员,而只是编译级别的名称。一旦生成就无法删除。
当在全局作用域中使用var a 时,实际上会生成一个跨作用域标识符,并在全局作用域上创建一个具有相同名称的附加键,指向相同的地址。这是var 语句的附加操作。这允许我们通过遍历窗口来处理全局变量。
总之,不要对这件事想太多,用新函数包装它几乎是必不可少的。还可以传入setTimeout等输入参数来控制“多进程”并行的异步实现。
还有一个位置。不要移动它,页面会刷新。将其列入黑名单。
另一个有趣的事情是:函数和var 一样,会向window 添加一个额外的键。该属性可配置为false——,即不能删除。但可以给它赋值。
所以如果你只是var a,你可以删除window.a;如果你再写一次,它将是未定义的。写一个函数a,然后再写一个delete a,是无效的。但是如果你写一个函数a,然后写一个var a=1,那效果是什么呢?你给窗口绑定了一个无法删除的数字,延续了function a的不可删除属性和var a的值。
比较有意思的是class,你用class B{},然后控制台日志window.B,怎么样,undefind。再写一次B=1 怎么样;然后看window.B?继续未定义,B=1 无效。
它表明一种潜在的机制在执行class关键字时将一个名为B的属性绑定到global,但它是一个无法枚举和访问的属性。该属性具有除writable true、enumerable: true 和configurable: true 之外的隐藏属性。
4.3 其他
有很多对象需要进程安全,例如cookie,但这并不是特别重要。只需同意一条路径并使用它即可。 —— 除了设置域外,还可以设置路径。只是大多数人不设置(即设置到根目录“/”)。
localStorage也可以受到保护。取决于您的业务。因为这些是windows的全局变量,所以实现一个包装类集成并模拟localStorage的原始行为就足够了。让其所有方法先给key加上前缀,然后再执行该方法的super。这个前缀可以简单地写入当前沙箱的uuid,因为window.localStorage作为全局变量本身就在沙箱的保护范围内。
5. 沙盒的其他功能
下面是最后一章,会讲沙箱下的一些特殊的东西,它们都需要额外的处理。重点是要讲埋点。在大多数微前端项目中,一个页面中的埋点已经属于不同的项目了。这种情况下,就需要弄清楚具体是哪些子应用,使用了哪些统计代码,需要处理什么级别的缓存。
5.1 埋点缓存系统
如前所述,将所有存储缓存包装在沙箱中对于隐藏系统来说是不够的。绝大多数隐藏的系统事件都是异步发送的,并且仅在网络空闲时发送。而这些源代码通常都在SDK中,而不是在父项目直接控制的代码中。所以没有太大的回旋余地。其实我们只能把缓存数据和项目信息保留好,然后在数据生成时将采集数据的缓存与沙箱状态进行匹配。
5.2 console
沙箱可能包含一层或多层运行时,因此控制台读取会很累。开发过程中可以进行额外的处理,为开发者提供方便。而且,在现在的前端项目中,在线用户希望控制台打印尽可能少,内容尽可能正式,调试时非常忌讳别人留下的打印干扰。这些都是沙箱可以做的事情。我们甚至制作了一个上传日志,将内容直接连接到收集系统。
具体来说,new Error用于将callstack注入到日志中。这样就可以通过error.stack获取调用堆栈。这个值直接是一个字符串,是用换行符分隔的Markdown。可以写一个链接进去,也可以对应调试窗口中的源代码。
同理,当遇到真正的异常时,也应该这样来控制。从捕获到异常以及再次抛出之前,您可以破解所有error.stack 值。删除不必要的stack——,例如您包含的new Function 层,并删除该行。提示的内容也可以更改。
5.3 sourceMapping
sourceMapping是Googleclosure发明的东西,现在是ES6标准。其原理是字符位置到字符位置的映射。那么可以在new Function下的沙箱中使用吗?当然。
我们先来说说new Function的性能。在chrome中,是调试时新的匿名环境:anonymous。字符行和列位置是从函数字符串开头的第一行开始计算的。如果将编译生成的sourceMap包放入新函数中执行,这个位置是完全对应的,不需要做任何额外的hacking。
用户评论
字节跳动这次微前端沙箱实践真的很赞,我一直在关注这个领域,他们的方案看起来很成熟,希望能学到更多。
有5位网友表示赞同!
微前端沙箱实践,字节跳动果然是行业领头羊,这样的技术分享太有价值了,希望更多企业能跟上。
有15位网友表示赞同!
看了字节跳动的微前端沙箱实践,感觉挺有意思的,不过对于新手来说,理解起来可能有点困难。
有13位网友表示赞同!
字节跳动这次微前端沙箱实践,让我对微前端有了更深的理解,特别是沙箱机制,太实用了。
有15位网友表示赞同!
微前端沙箱实践,字节跳动是不是在搞大新闻?不过我还是很期待看到他们的成果。
有6位网友表示赞同!
微前端沙箱实践,这个标题太吸引人了,我一定要仔细研究一下字节跳动的做法。
有5位网友表示赞同!
感觉字节跳动的微前端沙箱实践有点过于复杂,不太适合小团队或者初创公司。
有18位网友表示赞同!
字节跳动在微前端沙箱实践上下了不少功夫,但我觉得他们的文档可以更详细一些。
有15位网友表示赞同!
微前端沙箱实践,这个话题很热门,但我对字节跳动的方案还是持保留态度。
有5位网友表示赞同!
看了字节跳动的微前端沙箱实践,感觉他们的技术实力真的很强,但我更关心实际应用的效果。
有19位网友表示赞同!
微前端沙箱实践,这个标题让我想起了自己之前的一个项目,字节跳动的方案给了我很多启发。
有19位网友表示赞同!
字节跳动这次微前端沙箱实践,让我看到了微前端技术的巨大潜力,期待更多创新。
有14位网友表示赞同!
微前端沙箱实践,我觉得字节跳动可以分享更多细节,比如沙箱的具体实现方法。
有9位网友表示赞同!
字节跳动在微前端沙箱实践上做得不错,但我还是觉得他们的方案有点过于理论化。
有16位网友表示赞同!
微前端沙箱实践,这个话题让我想起了之前的一个技术难题,字节跳动的解决方案很有参考价值。
有13位网友表示赞同!
字节跳动这次微前端沙箱实践,让我对微前端有了全新的认识,感谢他们的分享。
有10位网友表示赞同!
微前端沙箱实践,字节跳动是不是在引领行业潮流?他们的做法值得其他企业学习。
有5位网友表示赞同!
字节跳动的微前端沙箱实践让我大开眼界,没想到沙箱可以这样用,太酷了。
有20位网友表示赞同!
微前端沙箱实践,这个话题让我对字节跳动的技术团队充满了敬意,希望他们的方案能推广开来。
有5位网友表示赞同!