likes
comments
collection

手摸手打造类码上掘金在线IDE(三)——沙箱环境

作者站长头像
站长
· 阅读数 11

手摸手打造类码上掘金在线IDE(三)——沙箱环境

前言

在前面的内容中,我们讲了在线ide 的内容种类,状况,以及如何选择ide 的代码编辑器, 我们从

市面上的各种高端的ide 实现套路,说到了他的简单的原理,从

monaco-editor讲到了 vue-codemirror 对比了他们优劣,简单的讲了他们的使用方式

但是没有什么人看,因为我相信很多这个行当的人,终其一生,都不会用到,

学会这个东西,对他升职加薪,没有任何帮助,

所以,他也不是什么高流量的内容,阅读量可谓惨淡,尽管运营老哥,给我疯狂推流量,但是依然吸引不了眼球,可见此类内容,在jym 的眼里远没有 一个面试文章来的立竿见影

这两天我就在反思,我这个系列文章,为什么要选一个这么拉胯的题目?东家回头看见这点流量反悔了不结账怎么办? 我都三十了,再不火可就过气了,明知道这是个流量为王的年代,为什么还要选个冷门的,我应该选vue 啊

明知道,大家在这个快节奏的快餐时代,大家都想要立竿见影,注重修炼外功,他们其实想学,独孤九剑,我偏要说乾坤大挪移

就在我还在比较内耗的时候,

我偶然看到了,明朝那些事中,当年明月对于徐霞客的描述

当年明月说:“我之所以写徐霞客,是想告诉你:所谓百年功名、千秋霸业、万古流芳,与一件事情相比,其实算不了什么。这件事情就是——用你喜欢的方式度过一生。”

当我读完明朝那些事, 我有了一个最大的感触

位极人臣 ,功名利禄,最后也不过是一抔黄土,倒不如黄山上徐霞客兀自听雪,才是美好的人生

说道这,我都能想象到徐霞客的惬意

山下,灯火辉煌,喧嚣成海。

徐霞客却端坐山顶,表情淡然。

他举头眺望星空,身心愉悦。

这才是我们应该有的状态

突然间我释怀了,什么流量,什么热点,什么成名,通通滚蛋

我就要写我想写的,我喜欢的

坦率的讲,高端的IDE一直是我喜欢研究的对象,因为在我看来,他们就是前端清华,因为他足够装x

技术的本质,除了挣钱,不就是装x吗?当然我也有一个梦想--用技术改变世界!

尽管,很多人,只是停留在挣钱的这个阶段,所以装x的东西对他来说,总是显得华而不实

但那又怎样,我痛快了也行,毕竟东家还给盒饭

我还有兜底

写到这,很多人,可能内心一团火,仿佛要爆炸,踌躇满志,双拳紧握,怒目圆睁,头发都立着,

他们仿佛被我点燃了,他们要用自己喜欢的方式度过一一生,要同时的喊出那句口号—— 老子要辞职老子,老子最喜欢的就是躺平

额,jym 别这样,一说一乐

这个世界,的很多人,说的和做的,不能说是,一模一样,简直是大相径庭,

所以你朋友圈里,那些位天天发文教你励志,教你学习的所谓的技术大佬,很可能他是在无节操的卖课,他在现实生活中也不一定是个爱学习的人。人家可能只是生活所迫。

放到咱这也是一样,你以为,我是想要用自己喜欢的方式度过一生?

其实,我这是为了完成任务,领东家的盒饭

哈哈哈,扯了半天蛋了,希望对各位jym 有些许启发 !

在开始之前我们需要先具备几个前置条件

沙箱

在传统的描述中Sandbox(又叫沙箱)即是一个虚拟系统程序,允许你在沙箱环境中运行浏览器或其他程序,因此运行所产生的变化可以随后删除。它创造了一个类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。 在网络安全中,沙箱指在隔离环境中,用以测试不受信任的文件或应用程序等行为的工具。

而在我们浏览器中,所谓的沙箱,就是一个能够不受外界干扰的js 运行环境

前端飞速发展的今天,沙箱的应用已经非常普遍,你比如说,微前端iframe 等等

当然,还有我们今天的重头戏—— 沙箱编译,接下来我们简单的细数一下现在市面上的几种沙箱模式

自执行的匿名函数IIEF

我们知道,在浏览器中有一个window,我们的变量声明会或多或少的影响全局环境

但是人们发现,由于函数的特殊作用通过闭包的方式,可以将多余的变量,保存在闭包中,只留下个别变量挂在全局,并且全局 不能访问到闭包中的变量,这样就形成了一个简单的沙箱模式,防止,外部恶意的篡改,改变程序的运行轨迹

我举个简单的例子

var iifeObj = {
    a: 1,
    b:1
}
iifeObj.b=2

普通的对象模式,就能随意篡改

const iife = function () {
  var a = 1;
  var b = 2;
  var c = a + b;
  return c
}

而通过函数包裹,外部就无法访问到a和b ,在称霸行业很多年的Jquery 用的就是这个套路

(function (window) {
  var jQuery = function (selector, context) {
    return new jQuery.fn.init(selector, context);
  };
  jQuery.fn = jQuery.prototype = function () {
    //原型上的方法,即所有jQuery对象都可以共享的方法和属性
  };
  jQuery.fn.init.prototype = jQuery.fn;
  window.jQeury = window.$ = jQuery; // 暴露到外部的接口
})(window);
​

当然这是最低级的沙箱模式 ,因为作用域链的关系,外部变量也能被篡改

于是大佬们开始搜寻下一个招数

with + new Function +Proxy沙箱模式

所谓with 语句,它能够扩展一个语句的作用域链。

他的作用就是JavaScript 查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的 context 或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在 statement 中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出ReferenceError异常。

举个例子

// 一个这样的语句
var p = a.b.c; p.x = 1; p.y = 2; p.z = 3;
// 通过with 包装
with(a.b.c){ x = 1; y = 2; z = 3; }

有了他的加持,在早期可谓如鱼得水,就连早期的vue编译后的内容,也使用with

  (function anonymous() {
        with (this) {
            return _c('div', {
                attrs: {
                    "id": "app"
                }
            },
                [
                    _c('p', [_v(_s(msg))])
                ]
            )
        }
    })

但是官方不建议使用

手摸手打造类码上掘金在线IDE(三)——沙箱环境

于是现在的vue的render函数再也看不见with的影子

new Function自不用过多介绍,就是能将一段代码段,变成js 执行

Proxy 大家也很熟悉,对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

vue3用的就是他

当我们将他们三个放在一起使用却能实现一个简单的沙箱,它能够防止作用域链向上查找路径,从而阻断外界环境影响内部执行

代码如下:

 function sandbox(code) {
            code = 'with (sandbox) {' + code + '}'
            debugger
            const fn = new Function('sandbox', code)

            return function (sandbox) {
                const sandboxProxy = new Proxy(sandbox, {
                    has(target, key) {
                        return true
                    },
                    get(target, key) {
                        if (key === Symbol.unscopables) return undefined
                        return target[key]
                    }
                })
                return fn(sandboxProxy)
            }
        }
        var test = {
            a: 1,
            log() {
                console.log('11111')
            }
        }
        //当传入对象的时候 沙箱内部只执行 传入的的变量内部的成员的的代码
        //而你在code中传入的全局方法console.log 就会被拦截从而报错
        // 从而保证code代码执行的干净纯洁
        var code = 'log();console.log(a)' 
        sandbox(code)(test)

我们通过Proxy的拦截,来过滤掉, code代码执行过程中的由于作用域链等外部环境对于他的影响,从而实现了沙箱模式

然而他并没有什么卵用,为什么这么说呢?

1、你在code中执行的log 函数,还是能访问到全局内容,所以,所谓沙箱形同虚设,他也只是能隔离code代码中的一些变量

2、由于Proxy 的拦截限制,多层拦截,就凉了

所以,这个所谓的沙箱模式,并不能再真正的项目上投入使用,他也只是人们的探究而已

于是随着技术的发展,微前端出现,大大推动了沙箱模式的进化,因微前端是给两个项目攒到一块,所以必须实现全局window 的隔离 ,于是大佬们又开始了折腾之路

沙箱快照

最开始大家的方案很简单,既然是window 的隔离那我们深拷贝一份window对象不就行了吗,于是沙箱快照就诞生了

代码如下

        function iter(obj, callbackFn) {
            for (const prop in obj) {
                if (obj.hasOwnProperty(prop)) {
                    callbackFn(prop);
                }
            }
        }
        class SnapshotSandbox {
            constructor(name) {
                this.name = name;
                this.proxy = window;
                this.type = 'Snapshot';
                this.sandboxRunning = true;
                this.windowSnapshot = {};
                this.modifyPropsMap = {};
                this.active();
            }
            //激活
            active() {
                // 记录当前快照
                this.windowSnapshot = {};
                iter(window, (prop) => {
                    this.windowSnapshot[prop] = window[prop];
                });

                // 恢复之前的变更
                Object.keys(this.modifyPropsMap).forEach((p) => {
                    window[p] = this.modifyPropsMap[p];
                });

                this.sandboxRunning = true;
            }
            //还原
            inactive() {
                this.modifyPropsMap = {};

                iter(window, (prop) => {
                    if (window[prop] !== this.windowSnapshot[prop]) {
                        // 记录变更,恢复环境
                        this.modifyPropsMap[prop] = window[prop];
                      
                        window[prop] = this.windowSnapshot[prop];
                    }
                });
                this.sandboxRunning = false;
            }
        }
        let sandbox = new SnapshotSandbox();
        //test
        ((window) => {
            window.name = '张三'
            window.age = 18
            console.log(window.name, window.age) //    张三,18
            sandbox.inactive() //    还原
            console.log(window.name, window.age) //    undefined,undefined
            sandbox.active() //    激活
            console.log(window.name, window.age) //    张三,18
        })(sandbox.proxy);

快照沙箱,虽然简单粗暴,但是他却有一个致命缺点,造成windw 污染

//不断的激活和失活,就会导致 window被不断的赋值,导致并非纯净,总会意外包含很多变量
   Object.keys(this.modifyPropsMap).forEach((p) => {
                    window[p] = this.modifyPropsMap[p];
                });

而且在一般情况下每次切换都会发生赋值,性能上损耗较大,于是大佬们又开始琢磨

此时Proxy沙箱模式派上了用场,大佬们站在巨人的肩膀上,有搞出来可以实际使用的基于proxy的单例沙箱

proxy 的单例沙箱

proxy 的单例沙箱 他的实现思路同样的还是操作window 他们两者的本质并没有任何区别,唯一的区别就是解决了性能损耗的问题,因为通过代理的方式解决了 window的多次遍历赋值

代码如下

        const callableFnCacheMap = new WeakMap();

        function isCallable(fn) {
            if (callableFnCacheMap.has(fn)) {
                return true;
            }
            const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined';
            const callable = naughtySafari ? typeof fn === 'function' && typeof fn !== 'undefined' : typeof fn ===
                'function';
            if (callable) {
                callableFnCacheMap.set(fn, callable);
            }
            return callable;
        };

        function isPropConfigurable(target, prop) {
            const descriptor = Object.getOwnPropertyDescriptor(target, prop);
            return descriptor ? descriptor.configurable : true;
        }

        function setWindowProp(prop, value, toDelete) {
            if (value === undefined && toDelete) {
                delete window[prop];
            } else if (isPropConfigurable(window, prop) && typeof prop !== 'symbol') {
                Object.defineProperty(window, prop, {
                    writable: true,
                    configurable: true
                });
                window[prop] = value;
            }
        }


        function getTargetValue(target, value) {
            /*
              仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类。目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
              @warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常)
             */
            if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) {
                const boundValue = Function.prototype.bind.call(value, target);
                for (const key in value) {
                    boundValue[key] = value[key];
                }
                if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
                    Object.defineProperty(boundValue, 'prototype', {
                        value: value.prototype,
                        enumerable: false,
                        writable: true
                    });
                }

                return boundValue;
            }

            return value;
        }

        class SingularProxySandbox {
            /** 沙箱期间新增的全局变量 */
            addedPropsMapInSandbox = new Map();

            /** 沙箱期间更新的全局变量 */
            modifiedPropsOriginalValueMapInSandbox = new Map();

            /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
            currentUpdatedPropsValueMap = new Map();

            name;

            proxy;

            type = 'LegacyProxy';

            sandboxRunning = true;

            latestSetProp = null;

            active() {
                if (!this.sandboxRunning) {
                    this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
                }

                this.sandboxRunning = true;
            }

            inactive() {
                // console.log(' this.modifiedPropsOriginalValueMapInSandbox', this.modifiedPropsOriginalValueMapInSandbox)
                // console.log(' this.addedPropsMapInSandbox', this.addedPropsMapInSandbox)
                //删除添加的属性,修改已有的属性
                this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
                this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));

                this.sandboxRunning = false;
            }

            constructor(name) {
                this.name = name;
                const {
                    addedPropsMapInSandbox,
                    modifiedPropsOriginalValueMapInSandbox,
                    currentUpdatedPropsValueMap
                } = this;

                const rawWindow = window;
                //Object.create(null)的方式,传入一个不含有原型链的对象
                const fakeWindow = Object.create(null);

                const proxy = new Proxy(fakeWindow, {
                    set: (_, p, value) => {
                        if (this.sandboxRunning) {
                            if (!rawWindow.hasOwnProperty(p)) {
                                addedPropsMapInSandbox.set(p, value);
                            } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
                                // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
                                const originalValue = rawWindow[p];
                                modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
                            }

                            currentUpdatedPropsValueMap.set(p, value);
                            // 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据
                            rawWindow[p] = value;

                            this.latestSetProp = p;

                            return true;
                        }

                        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
                        return true;
                    },

                    get(_, p) {
                        //避免使用 window.window 或者 window.self 逃离沙箱环境,触发到真实环境
                        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
                            return proxy;
                        }
                        const value = rawWindow[p];
                        return getTargetValue(rawWindow, value);
                    },

                    has(_, p) { //返回boolean
                        return p in rawWindow;
                    },

                    getOwnPropertyDescriptor(_, p) {
                        const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
                        // 如果属性不作为目标对象的自身属性存在,则不能将其设置为不可配置
                        if (descriptor && !descriptor.configurable) {
                            descriptor.configurable = true;
                        }
                        return descriptor;
                    },
                });

                this.proxy = proxy;
            }
        }

        let sandbox = new SingularProxySandbox();

        ((window) => {
            // name 这是一个特殊变量,一旦赋值刷新不会消失
            window.name = '张三';
            window.age = 18;
            window.sex = '男';
            console.log(window.name, window.age, window.sex) //    张三,18,男
            sandbox.inactive() //    还原
            console.log(window.name, window.age, window.sex) //    张三,undefined,undefined
            sandbox.active() //    激活
            console.log(window.name, window.age, window.sex) //    张三,18,男
        })(sandbox.proxy); //test

上述代码中(当然这是前辈们的写的例子,我引用了一下),我们可以看出,他还是会操作,window,全局污染这个问题,如论如何都无法避免。于是,大佬们又开始思索,怎么能开发一个不会污染全局的window的沙箱呢?

不会污染全局window的沙箱

在大佬们的苦苦追寻下,终于找到了一个解决方案, 其实回过头来想,我们的诉求就是找到一个多个应用不互相干扰的环境,不论是快照沙箱也好,代理沙箱也好 ,我们都是为了保证沙箱激活后,我的window和之前的不共用,

那么问题就迎刃而解了,我只需要将每个应用的内容保存到一个对象中,如果在对象中,找不到的情况下,再去全局window中找,这样既保证了,每个引用的不同部分的隔离,有保证了,相同部分的公用,于是我们将单例沙箱来做一个改造即可

代码如下

    const rawWindow = window;
    // 将每个沙箱,单独加一个独立的对象并且去代理
    const fakeWindow = {};
    const proxy = new Proxy(fakeWindow, {
      set: (target, prop, value) => {
        // 只有沙箱开启的时候才操作 fakeWindow
        if (this.sandboxRunning) {
          // 对 window 的赋值,我们处理当前沙箱,从而实现隔离
          target[prop] = value;
          return true;
        }
      },
      get: (target, prop) => {
        // 先查找 fakeWindow,找不到再寻找 window
        let value = prop in target ? target[prop] : rawWindow[prop];
        return value;
      },
    });
    this.proxy = proxy;

如此一来,我们就解决了全局污染的问题,这也是现在qiankun的沙箱的主流解决方案,

iframe

上述的沙箱解决方案,由于都是在同一个环境中去执行,只是去模拟沙箱的模式,虽然,能在一定程度上解决问题,但是总是不彻底,于是在我们在线IDE界 通常就会使用一个彻底的解决方案,iframe

因为你总归要在ifarme 中去渲染视图,并且具有天然的样式隔离

所以在现在市面上主流的编辑器中,都是采用的这个方案

iframe 自不用过多介绍,这个标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离。

我们在通过 Window.postMessage实现沙箱和编辑器的通信

iframe 通信事件设计

由于是我们整个在线IDE最重要的部分就是编译渲染,于是沙箱和外接的通信尤为重要

他要具备几个步骤

  • 1、外界初始化Iframe,并传入沙箱内部
  • 2、内部初始化完成需要通知外界
  • 3、外界收到通知,需要通知沙箱启动编译
  • 4、编译完成启动启动渲染,挂载
  • 5、内容变化需要通知沙箱启动再次编译

最后

在这个系列文章的前三篇文章中,我们介绍了运行环境,和编辑器等这些基础内容的选型,主要是为了让大家先了解整个IDE 基础构成,以及他的实现前提

接下来,我们继续介绍他最神秘的部分,通信以及编译,请大家敬请期待吧!