本文主要讲解“如何实现web微前端沙盒”。本文的解释简单明了,易学易懂。请跟随边肖的思路,一起学习学习《如何实现网页微前端沙盒》!
背景
沙盒应用可能是微前端技术系统中最有趣的部分。一般来说,在微前端技术系统中,沙盒是不必要的,因为如果规范做得足够好,可以避免一些变量、读写、CSS样式的冲突。但是,如果你在一个足够大的系统中,你不能仅仅通过规范来保证应用程序的可靠性,或者在运行时仍然需要技术手段来处理一些冲突,这也是沙盒方案成为微前端技术体系的一部分的原因。
首先,纵观各种技术方案,有一个大前提决定了这个沙盒怎么做:最终的微应用是单个实例或者多个实例存在于宿主应用中。这直接决定了这个沙盒的复杂度和技术方案。
单实例:同一时间只存在一个微应用实例。此时,浏览器的所有浏览器资源都是该应用程序专有的。解决方案很大程度上需要解决应用切换时的清理和现场恢复。重量轻,易于实施。
多实例:如果资源不是应用独占的,就要解决资源共享的情况,比如路由、样式、全局变量读写、DOM。可能有很多情况需要考虑,实现起来比较复杂。
起初,我们的想法是:
从业务场景来看:我们可能会有这样的情况,当用户操作一个产品A,另一个产品B时,需要唤醒应用B来做操作。虽然从产品维度可以避免,比如先切到B再切回A,但在某种程度上,由于技术原因,我们限制了产品之间的交互。
从技术角度来看,解决了多实例和单实例场景的问题,单实例方案在一定程度上给编码带来了一定的复杂性,比如业务代码需要自行切换业务上下文。
最近,钱坤2也转变了思路,从单实例支持转变为多实例支持,这或多或少说明了多实例是一个值得投资和技术的场景。
基于以上考虑,我们开始探索浏览器虚拟机沙箱的实现。综上所述,可以用下图来表示:
JavaScript 沙箱实现
沙箱环境构造
要实现沙盒,需要隔离浏览器的原生对象,但是如何隔离和构建沙盒环境呢?Node中有一个vm模块可以实现类似的功能,但是浏览器不会工作,但是我们可以利用闭包和变量作用域的能力来模拟一个沙盒环境,比如下面的代码:
function foo(window){ console . log(window . document);} foo({ document : } };});例如,此代码的输出必须是{},而不是本机浏览器的文档。
因此,ConsoleOS(阿里巴巴云管理系统的微前端解决方案)实现了一个Wepback的插件,在构建应用程序代码时给子应用程序代码增加一层包装代码,创建一个闭包,将需要隔离的浏览器原生对象改为下面的函数闭包,这样我们在加载应用程序时就可以传入窗口、文档等模拟对象。
//打包代码_ _ console _ OS _ global _ hook _ _ (ID,函数(require,module,exports,{window,document,location,History}){/*打包代码*/})Function _ _ console _ OS _ global _ hook _ _(ID,entry) {entry (require,module,exports,{window,document,location,history})}当然也可以不用工程手段实现。您还可以请求一个脚本,然后在运行时拼接代码,然后评估或新建一个函数来实现相同的目标。
原生对象模拟
有了沙盒隔离的能力,剩下的问题就是如何实现这一堆浏览器的原生对象。一开始的想法是按照ECMA规范来实现(现在还有类似的想法),但是发现成本太高了。然而,在我们的各种实验之后,我们发现了一种非常“棘手”的方法。我们可以使用新的iframe对象来匹配其中的本机浏览器。
象通过contentWindow 取出来,因为这些对象天然隔离,就省去了自己实现的成本。
const iframe = document.createElement( 'iframe' );
当然里面有很多的细节需要考量,比如:只有同域的 iframe 才能取出对应的的contentWindow。所以需要提供一个宿主应用空的同域 URL 来作为这个 iframe 初始加载的 URL。当然根据 HTML 的规范,这个 URL 用了 about:blank 一定保证同域,也不会发生资源加载,但是会发生和这个 iframe 中关联的 history 不能被操作,这个时候路由的变换只能变成 hash 模式。
如下图所示,我们取出对应的 iframe 中原生的对象之后,就会对特定需要隔离的对象生成对应的 Proxy, 然后对一些属性获取和属性设置,做一些特定的设置,比如 window.document 需要返回特定的沙箱 document 而不是当前浏览器的 document。
class Window { constructor(options, context, frame) { return new Proxy(frame.contentWindow, { set(target, name, value) { target[name] = value; return true; }, get(target, name) { switch( name ) { case 'document': return context.document; default: } if( typeof target[ name ] === 'function' && /^[a-z]/.test( name ) ){ return target[ name ].bind && target[ name ].bind( target ); }else{ return target[ name ]; } } }); } }
对于每一个对象的实现这里不讲细节了,有兴趣可以看看我们的开源之后的代码 :https://github.com/aliyun/alibabacloud-console-os/tree/master/packages/browser-vm
但是为了文档能够被加载在同一个 DOM 树上,对于 document,大部分的 DOM 操作的属性和方法还是直接用的宿主浏览器中的 document 的属性和方法。
由于子应用有自己的沙箱环境,之前所有独占式的资源现在都变成了应用独享(尤其是 location、history),所以子应用也能同时被加载。并且对于一些变量,我们还能在 proxy 中设置一些访问权限的事情,从而限制子应用的能力,比如 Cookie, LocalStoage 读写。
当这个 iframe 被移除时,写在 window 的变量和设置的一些 timeout 时间也会一并被移除(当然 DOM 事件需要沙箱记录,然后在宿主中移除)。
总结一下,我们的沙箱可以做到如下的特性:
CSS 隔离
CSS 隔离方案相对来说比较常规,常见的有:
-
CSS Module
-
添加 CSS 的 namespace
-
Dynamic StyleSheet
-
Shadow DOM
CSS Module or CSS Namespace
通过修改基础组件样式前缀来实现框架和微应用依赖基础组件样式的隔离性(依赖于工程上 CSS 的预处理器编译和运行时基础组件库配置),同时避免全局样式的书写(依赖于约定或工程 lint 手段)。
Dynamic StyleSheet
隔离方式是通过 JS 运行时动态加载卸载微应用样式表来避免样式的冲突,局限性一是对于站点框架本身或其部件(header/menu/footer)与当前运行的微应用间仍存在样式冲突的可能性,二是没有办法支持多个微应用同时运行显示的情况。
Shadow DOM
优点是浏览器级别提供的样式隔离能力,可以做到完全隔离。缺点在于,目前兼容性还是不太好,并且改造会涉及到旧应用的业务代码的改造,对子应用侵入性比较高。
最终经过实践,我们选择的方式是 CSS Module + 添加 CSS 的 namespace。CSS module 保证的是应用业务样式不冲突,Namespace 保证公共库不冲突。我们实现了一个 postcss 插件,会在应用构建的时候给所有的样式都加上应用前缀包括应用公共库的 CSS(这样方便做到同一个 组件库新旧版本样式的兼容)。如下图所示:
// 宿主 host app .next-btn { color: #eee; } // 子应用 sub app aliyun-slb .next-btn { color: #eee; } //宿主中生成的节点 <aliyun-slb> <!-- 子应用的节点 --> </aliyun-slb>
这样实现的好处在于:
-
每个应用都有 namespace,可以多实例共存。
-
不依赖特定的 CSS 预处理器。
-
对于同一个库不同版本的 CSS(如 fusion1 和 fusion2),可以做到彻底隔离。
-
鉴于上面 JS 沙箱的存在,对于一些弹窗类的组件,这个微应用获取的 body 实际上是宿主生成的节点,所以弹窗会被添加到微应用的节点(也就是上面的 aliyun-slb)这个节点,样式不会失效。
不过也会有一些问题,比如:
-
嵌套应用组件样式优先级的问题。由于 CSS module 的存在,一般只会发生在公共 CSS 样式中,这个就是只能尽量避免嵌套。
-
fusion 不同版本库公用字体的问题。目前的解决办法:比较 hack,使用工程化的手段替换掉 next 字体的名字。
如何和其他体系结合
如果看完上面的文章,觉得这个沙箱方案不错,但是又已经有自己的微前端体系了,想套用咋办?
目前 ConsoleOS 的代码已经在 Github 上开源:http://github.com/aliyun/alibabacloud-console-os,这里不妨可以尝试试用一下。
JS 沙箱部分
如果看懂了上面关于原理的介绍可以看到其实沙箱实现包括两个层面:
-
原生浏览器对象的模拟(Browser-VM)
-
如何构建一个闭包环境
Browser-VM 可以直接用起来,这部分完全是通用普适的。但是涉及到闭包构建的这部分,每个微前端体系不太一致,可能需要改造,比如:
import { createContext, removeContext } from '@alicloud/console-os-browser-vm'; const context = await createContext(); const run = window.eval(` (() => function({window, history, locaiton, document}) { window.test = 1; })() `) run(context); console.log(context.window.test); console.log(window.test); // 操作虚拟化浏览器对象 context.history.pushState(null, null, '/test'); context.locaiton.hash = 'foo' // 销毁一个 context await removeContext( context );
当然可以直接选择沙箱提供好的 evalScripts 方法:
import { evalScripts } from '@alicloud/console-os-browser-vm'; const context = evalScripts('window.test = 1;') console.log(window.test === undefined) // true
CSS 沙箱
如果用 Webpack 构建,可以直接配置如下:
const postcssWrap = require('@alicloud/console-toolkit-plugin-os/lib/postcssWrap') // 下面是 webpack config { test: /\.css$/, use: [ 'style-loader', { loader: 'postcss-loader', options: { plugins: [ // 加入插件 postcssWrap({ stackableRoot: '.prefix', repeat: 1 }) ], }, }, 'css-loader', ], exclude: /^node_modules$/, }
感谢各位的阅读,以上就是“如何实现web微前端沙箱”的内容了,经过本文的学习后,相信大家对如何实现web微前端沙箱这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/92289.html