微前端已经成为前端领域比较火爆的话题,在技术方面,微前端有一个始终绕不过去的话题就是前端沙箱
Sandboxie(又叫沙箱、沙盘)即是一个虚拟系统程序,允许你在沙盘环境中运行浏览器或其他程序,因此运行所产生的变化可以随后删除。它创造了一个类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。 在网络安全中,沙箱指在隔离环境中,用以测试不受信任的文件或应用程序等行为的工具
简单来说沙箱(sandbox)就是与外界隔绝的一个环境,内外环境互不影响,外界无法修改该环境内任何信息,沙箱内的东西单独属于一个世界。
对于 JavaScript 来说,沙箱并非传统意义上的沙箱,它只是一种语法上的 Hack 写法,沙箱是一种安全机制,把一些不信任的代码运行在沙箱之内,使其不能访问沙箱之外的代码。当需要解析或着执行不可信的 JavaScript 的时候,需要隔离被执行代码的执行环境的时候,需要对执行代码中可访问对象进行限制,通常开始可以把 JavaScript 中处理模块依赖关系的闭包称之为沙箱。
我们大致可以把沙箱的实现总体分为两个部分:
构建一个闭包环境
模拟原生浏览器对象
我们知道 JavaScript 中,关于作用域(scope),只有全局作用域(global scope)、函数作用域(function scope)以及从 ES6 开始才有的块级作用域(block scope)。如果要将一段代码中的变量、函数等的定义隔离出来,受限于 JavaScript 对作用域的控制,只能将这段代码封装到一个 Function 中,通过使用 function scope 来达到作用域隔离的目的。也因为需要这种使用函数来达到作用域隔离的目的方式,于是就有 IIFE(立即调用函数表达式),这是一个被称为 自执行匿名函数的设计模式
当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问,它拥有独立的词法作用域。不仅避免了外界访问 IIFE 中的变量,而且又不会污染全局作用域,弥补了 JavaScript 在 scope 方面的缺陷。一般常见于写插件和类库时,如 JQuery 当中的沙箱模式
当将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。
模拟原生浏览器对象的目的是为了,防止闭包环境,操作原生对象。篡改污染原生环境;完成模拟浏览器对象之前我们需要先关注几个不常用的 API。
eval 函数可将字符串转换为代码执行,并返回一个或多个值
由于 eval 执行的代码可以访问闭包和全局范围,因此就导致了代码注入的安全问题,因为代码内部可以沿着作用域链往上找,篡改全局变量,这是我们不希望的
Function 构造函数创建一个新的 Function 对象。直接调用这个构造函数可用动态创建函数
语法
<code>new Function ([arg1[, arg2[, ...argN]],] functionBody)</code>
arg1, arg2, ... argN 被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的 JavaScript 标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”,“theValue”,或“a,b”。
functionBody
一个含有包括函数定义的 JavaScript 语句的字符串。
同样也会遇到和 eval 类似的的安全问题和相对较小的性能问题。
与 eval 不同的是 Function 创建的函数只能在全局作用域中运行。它无法访问局部闭包变量,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造器创建时所在的作用域的变量;但是,它仍然可以访问全局范围。new Function()是 eval()更好替代方案。它具有卓越的性能和安全性,但仍没有解决访问全局的问题。
with 是 JavaScript 中一个关键字,扩展一个语句的作用域链。它允许半沙盒执行。那什么叫半沙盒?语句将某个对象添加到作用域链的顶部,如果在沙盒中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出 ReferenceError。
究其原理,<code>with</code>在内部使用<code>in</code>运算符。对于块内的每个变量访问,它都在沙盒条件下计算变量。如果条件是 true,它将从沙盒中检索变量。否则,就在全局范围内查找变量。但是 with 语句使程序在查找变量值时,都是先在指定的对象中查找。所以对于那些本来不是这个对象的属性的变量,查找起来会很慢,对于有性能要求的程序不适合(JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。)。with 也会导致数据泄漏(在非严格模式下,会自动在全局作用域创建一个全局变量)
in 运算符能够检测左侧操作数是否为右侧操作数的成员。其中,左侧操作数是一个字符串,或者可以转换为字符串的表达式,右侧操作数是一个对象或数组。
配合 with 用法可以稍微限制沙盒作用域,先从当前的 with 提供对象查找,但是如果查找不到依然还能从上获取,污染或篡改全局环境。
由上部分内容思考,假如可以做到在使用<code>with</code>对于块内的每个变量访问都限制在沙盒条件下计算变量,从沙盒中检索变量。那么是否可以完美的解决JavaScript沙箱机制。
使用 with 再加上 proxy 实现 JavaScript 沙箱
ES6 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种“元编程”(meta programming)
我们前面提到<code>with</code>在内部使用<code>in</code>运算符来计算变量,如果条件是 true,它将从沙盒中检索变量。理想状态下没有问题,但也总有些特例独行的存在,比如 Symbol.unscopables。
Symbol.unscopables
Symbol.unscopables 对象的 Symbol.unscopables 属性,指向一个对象。该对象指定了使用 with 关键字时,哪些属性会被 with 环境排除。
上面代码说明,数组有 6 个属性,会被 with 命令排除。

由此我们的代码还需要修改如下:
Symbol.unscopables 定义对象的不可作用属性。Unscopeable 属性永远不会从 with 语句中的沙箱对象中检索,而是直接从闭包或全局范围中检索。
以下是 qiankun 的 snapshotSandbox 的源码,这里为了帮助理解做部分精简及注释。
快照沙箱实现来说比较简单,主要用于不支持 Proxy 的低版本浏览器,原理是基于<code>diff</code>来实现的,在子应用激活或者卸载时分别去通过快照的形式记录或还原状态来实现沙箱,snapshotSandbox 会污染全局 window。
qiankun 框架 singular 模式下 proxy 沙箱实现,为了便于理解,这里做了部分代码的精简和注释。
legacySandBox 还是会操作 window 对象,但是他通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离的,同样会对 window 造成污染,但是性能比快照沙箱好,不用遍历 window 对象。
在 qiankun 的沙箱 proxySandbox 源码里面是对 fakeWindow 这个对象进行了代理,而这个对象是通过 createFakeWindow 方法得到的,这个方法是将 window 的 document、location、top、window 等等属性拷贝一份,给到 fakeWindow。
源码展示:
proxySandbox 由于是拷贝复制了一份 fakeWindow,不会污染全局 window,同时支持多个子应用同时加载。
详细源码请查看:proxySandbox
常见的有:
CSS Module
namespace
Dynamic StyleSheet
css in js
Shadow DOM
常见的我们这边不再赘述,这里我们重点提一下Shadow DO。
Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。