前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手摸手打造类码上掘金在线IDE(三)——沙箱环境

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

作者头像
用户7413032
发布2022-11-18 16:35:58
7450
发布2022-11-18 16:35:58
举报
文章被收录于专栏:佛曰不可说丶佛曰不可说丶
image.png
image.png

前言

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

这才是我们应该有的状态

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

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

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

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

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

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

我还有兜底

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

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

额,jym 别这样,一说一乐

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

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

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

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

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

好了,闲言少叙,多放白糖,我们正式开始,码上掘金系列之—— 沙箱环境

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

沙箱

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

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

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

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

自执行的匿名函数IIEF

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

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

我举个简单的例子

代码语言:javascript
复制
var iifeObj = {
    a: 1,
    b:1
}
iifeObj.b=2

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

代码语言:javascript
复制
const iife = function () {
  var a = 1;
  var b = 2;
  var c = a + b;
  return c
}

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

代码语言:javascript
复制
(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异常。

举个例子

代码语言:javascript
复制
// 一个这样的语句
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

代码语言:javascript
复制
  (function anonymous() {
        with (this) {
            return _c('div', {
                attrs: {
                    "id": "app"
                }
            },
                [
                    _c('p', [_v(_s(msg))])
                ]
            )
        }
    })

但是官方不建议使用

image.png
image.png

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

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

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

vue3用的就是他

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

代码如下:

代码语言:javascript
复制
 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对象不就行了吗,于是沙箱快照就诞生了

代码如下

代码语言:javascript
复制
        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 污染

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

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

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

proxy 的单例沙箱

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

代码如下

代码语言:javascript
复制
        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中找,这样既保证了,每个引用的不同部分的隔离,有保证了,相同部分的公用,于是我们将单例沙箱来做一个改造即可

代码如下

代码语言:javascript
复制
    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 基础构成,以及他的实现前提

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-11-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 沙箱
    • 自执行的匿名函数IIEF
      • with + new Function +Proxy沙箱模式
        • 沙箱快照
          • proxy 的单例沙箱
            • 不会污染全局window的沙箱
              • iframe
                • iframe 通信事件设计
            • 最后
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档