如今,微前端已经成为前端领域的热门话题。在技??术方面,微前端总有一个绕不开的话题,就是前端沙箱。本文将分享阿里云开放平台微前端解决方案的沙箱实现原理,具体探讨如何在微前端领域实现前端沙箱。背景应用沙箱可能是微前端技术体系中最有趣的部分。一般来说,沙盒在微前端技术体系中并不是必须的,因为如果规范做得足够好,可以避免一些读写上的变量冲突,CSS样式冲突。但是如果你在一个足够大的系统中,仅仅通过规范是无法保证应用的可靠性的,还需要技术手段来管理运行时的一些冲突。这也是沙盒解决方案成为微前端技术体系的部分原因。首先,综观各种技术方案,有一个大前提决定了这个沙箱是如何工作的:微应用到底是单实例还是多实例存在于宿主应用中。这直接决定了沙盒的复杂程度和技术方案。单实例:同一时刻只有一个微应用实例存在。此时,浏览器的所有浏览器资源都是这个应用程序独享的。解决方案很大程度上是应用切换时的清理和现场恢复。它相对轻量级且易于实现。多实例:资源不独占应用,需要解决路由、样式、全局变量读写、DOM等资源共享的情况。可能需要考虑的情况更多,实现起来也更复杂。一开始我们的想法是:从业务场景来看:我们可能会出现这样一种情况,当用户操作一个产品A,另一个产品B有关联操作,需要唤醒应用B来做操作。虽然从产品维度上可以避免,比如先切换到B再切换回A,但某种程度上,由于技术原因,我们限制了产品交互的使用。从技术角度:解决了多实例。当然,单例场景不是问题,单例方案给编码带来了一定的复杂度。比如业务代码需要自己切换业务上下文。最近乾坤2也换了个思路,从支持单实例到支持多实例,这或多或少说明多实例是一个值得投入和技术攻克的场景。基于以上考虑,我们开始探索我们的BrowserVM沙箱的实现。总结一下,可以用下图来表示:JavaScript沙箱实现沙箱环境搭建实现沙箱需要对浏览器原生对象进行隔离,但是如何隔离和搭建沙箱环境呢?Node中有一个vm模块来实现类似的能力,但是浏览器不行,但是我们可以利用闭包和变量作用域的能力来模拟一个沙箱环??境,比如下面的代码:functionfoo(window){console.log(窗口.文件);}foo({文档:{};});例如,这段代码的输出必须是{},而不是原生浏览器的文档。因此,ConsoleOS(阿里云管理系统的微前端解决方案)实现了一个Wepback插件,在构建应用代码时,为子应用代码添加一层wrap代码,创建一个闭包,存储需要隔离的本机浏览器对象。它变成了从下面的函数闭包中获取,这样我们就可以在应用加载的时候传入模拟窗口、文档等对象。//包代码__CONSOLE_OS_GLOBAL_HOOK__(id,function(require,module,exports,{window,document,location,history}){/*包代码*/})function__CONSOLE_OS_GLOBAL_HOOK__(id,entry){entry(require,module,exports,{window,document,location,history})}当然也可以不用工程手段实现,或者通过请求脚本,然后在运行时拼接这段代码,再通过eval或者new函数来达到同样的目的。有了原生对象模拟沙箱隔离能力,剩下的问题就是如何实现这一堆浏览器的原生对象了。最初的想法是我们按照ECMA规范来实现(现在还有类似的想法),但是发现成本太高了。但是,经过我们的各种实验,我们发现了一个非常“刁钻”的做法。我们可以新建一个iframe对象,通过contentWindow提取里面的原生浏览器对象。因为这些对象是天然隔离的,所以节省了自己实现的成本。.constiframe=document.createElement('iframe');当然还有很多细节需要考虑,比如:只有同域的iframe才能得到对应的contentWindow。因此,您需要提供一个空的宿主应用程序的同域URL作为iframe的初始加载URL。当然,按照HTML规范,如果这个URL使用about:blank,必须保证同一个域,不会发生资源加载,但是这个iframe关联的历史是无法被操作的。这时候路由改造只能变成hash模式。如下图所示,我们在对应的iframe中取出原始对象后,会为具体需要隔离的对象生成对应的Proxy,然后对一些属性的获取和属性设置进行一些具体的设置,比如因为window.document需要返回一个特定的沙箱文档而不是当前的浏览器文档。classWindow{constructor(options,context,frame){returnnewProxy(frame.contentWindow,{set(target,name,value){target[name]=value;returntrue;},get(target,name){switch(name){case'document':returncontext.document;default:}if(typeoftarget[name]==='function'&/^[a-z]/.test(name)){returntarget[name].bind&&target[name].bind(target);}else{returntarget[name];}}});}}各个对象的实现,这里就不细说了。有兴趣的可以看一下我们开源的代码:https://github.com/aliyun/alibabacloud-console-os/tree/master/packages/browser-vm但是为了让文件加载到同样的DOM树,对于文档来说,DOM操作的大部分属性和方法都是在宿主浏览器中直接使用文档的属性和方法。由于子应用有自己的沙箱环境,所有以前独占的资源现在都是应用独有的(尤其是位置、历史),所以子应用也可以同时加载。而对于一些变量,我们也可以在proxy中设置一些访问权限来限制子应用的能力,比如Cookie、LocalStoage的读写。当iframe被移除的时候,window里面写的变量和一些timeouts的设置也会被移除(当然DOM事件需要沙箱化,然后在host里面移除)。综上所述,我们的sandbox可以实现以下几个特性:CSS隔离CSS隔离方案比较常规,常见的有:CSSModuleAddCSSnamespaceDynamicStyleSheetShadowDOMCSSModuleorCSSNamespace通过修改基础组件的样式前缀框架而微应用依赖基础组件样式的隔离(依赖于工程中CSS的预处理器编译和运行时基础组件库的配置),同时避免全局样式的编写(依赖于约定或工程lint方法)。DynamicStyleSheet隔离方式是通过JS运行时动态加载和卸载微应用样式表,避免样式冲突。第一个限制是站点框架本身或其组件(页眉/菜单/页脚)与当前运行的微应用之间仍然存在样式冲突的可能性,第二个是无法支持多个微应用同时运行的应用程序。ShadowDOM的优势在于浏览器层面提供的样式隔离能力,可以做到完全隔离。缺点是目前兼容性还不是很好,改造会涉及到旧应用业务代码的改造,对子应用的侵入性比较大。最后经过实践,我们选择的方式是CSSModule+namespace来添加CSS。CSS模块保证应用业务风格不冲突,Namespace保证公库不冲突。我们实现了一个postcss插件,在构建应用时会为所有样式添加应用前缀,包括应用公共库的CSS(这样方便同一个组件库新旧版本的兼容)。如下图所示://hosthostapp.next-btn{color:#eee;}//subappaliyun-slb.next-btn{color:#eee;}//host中生成的节点
