在前面的内容中,我们讲了在线ide 的内容种类,状况,以及如何选择ide 的代码编辑器, 我们从
市面上的各种高端的ide 实现套路,说到了他的简单的原理,从
monaco-editor
讲到了 vue-codemirror
对比了他们优劣,简单的讲了他们的使用方式
但是没有什么人看,因为我相信很多这个行当的人,终其一生,都不会用到,
学会这个东西,对他升职加薪,没有任何帮助,
所以,他也不是什么高流量的内容,阅读量可谓惨淡,尽管运营老哥,给我疯狂推流量,但是依然吸引不了眼球,可见此类内容,在jym
的眼里远没有 一个面试文章来的立竿见影
这两天我就在反思,我这个系列文章,为什么要选一个这么拉胯的题目?东家回头看见这点流量反悔了不结账怎么办? 我都三十了,再不火可就过气了,明知道这是个流量为王的年代,为什么还要选个冷门的,我应该选vue 啊
明知道,大家在这个快节奏的快餐时代,大家都想要立竿见影,注重修炼外功,他们其实想学,独孤九剑
,我偏要说乾坤大挪移
就在我还在比较内耗的时候,
我偶然看到了,明朝那些事
中,当年明月对于徐霞客
的描述
当年明月说:“我之所以写徐霞客,是想告诉你:所谓百年功名、千秋霸业、万古流芳,与一件事情相比,其实算不了什么。这件事情就是——用你喜欢的方式度过一生。”
当我读完明朝那些事, 我有了一个最大的感触
位极人臣 ,功名利禄,最后也不过是一抔黄土,倒不如黄山上徐霞客兀自听雪,才是美好的人生
说道这,我都能想象到徐霞客
的惬意
山下,灯火辉煌,喧嚣成海。
徐霞客却端坐山顶,表情淡然。
他举头眺望星空,身心愉悦。
这才是我们应该有的状态
突然间我释怀了,什么流量,什么热点,什么成名,通通滚蛋
我就要写我想写的,我喜欢的
坦率的讲,高端的IDE一直是我喜欢研究的对象,因为在我看来,他们就是前端清华
,因为他足够装x
技术的本质,除了挣钱,不就是装x
吗?当然我也有一个梦想--用技术改变世界!
尽管,很多人,只是停留在挣钱的这个阶段,所以装x
的东西对他来说,总是显得华而不实
,
但那又怎样,我痛快了也行,毕竟东家还给盒饭
呢
我还有兜底
写到这,很多人,可能内心一团火,仿佛要爆炸,踌躇满志,双拳紧握,怒目圆睁,头发都立着,
他们仿佛被我点燃了,他们要用自己喜欢的方式度过一一生,要同时的喊出那句口号—— 老子要辞职老子,老子最喜欢的就是躺平
额,jym
别这样,一说一乐
这个世界,的很多人,说的和做的,不能说是,一模一样
,简直是大相径庭
,
所以你朋友圈里,那些位天天发文教你励志,教你学习的所谓的技术大佬,很可能他是在无节操的卖课
,他在现实生活中也不一定是个爱学习的人。人家可能只是生活所迫。
放到咱这也是一样,你以为,我是想要用自己喜欢的方式度过一生
?
其实,我这是为了完成任务,领东家的盒饭
哈哈哈,扯了半天蛋了,希望对各位jym
有些许启发 !
好了,闲言少叙,多放白糖,我们正式开始,码上掘金系列之—— 沙箱环境
在开始之前我们需要先具备几个前置条件
在传统的描述中Sandbox
(又叫沙箱)即是一个虚拟系统程序,允许你在沙箱环境中运行浏览器或其他程序,因此运行所产生的变化可以随后删除。它创造了一个类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。 在网络安全中,沙箱指在隔离环境中,用以测试不受信任的文件或应用程序等行为的工具。
而在我们浏览器中,所谓的沙箱,就是一个能够不受外界干扰的js 运行环境
,
在前端
飞速发展的今天,沙箱的应用已经非常普遍,你比如说,微前端
、iframe
等等
当然,还有我们今天的重头戏—— 沙箱编译,接下来我们简单的细数一下现在市面上的几种沙箱模式
我们知道,在浏览器中有一个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 语句,它能够扩展一个语句的作用域链。
他的作用就是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))])
]
)
}
})
但是官方不建议使用
于是现在的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 的单例沙箱 他的实现思路同样的还是操作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
中找,这样既保证了,每个引用的不同部分的隔离,有保证了,相同部分的公用,于是我们将单例沙箱来做一个改造即可
代码如下
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
的沙箱的主流解决方案,
上述的沙箱解决方案,由于都是在同一个环境中去执行,只是去模拟沙箱的模式,虽然,能在一定程度上解决问题,但是总是不彻底,于是在我们在线IDE界
通常就会使用一个彻底的解决方案,iframe
因为你总归要在ifarme
中去渲染视图,并且具有天然的样式隔离
所以在现在市面上主流的编辑器中,都是采用的这个方案
iframe
自不用过多介绍,这个标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离。
我们在通过 Window.postMessage
实现沙箱和编辑器的通信
由于是我们整个在线IDE
最重要的部分就是编译
和渲染
,于是沙箱和外接的通信尤为重要
他要具备几个步骤
在这个系列文章的前三篇文章中,我们介绍了运行环境,和编辑器等这些基础内容的选型,主要是为了让大家先了解整个IDE 基础构成,以及他的实现前提
接下来,我们继续介绍他最神秘的部分,通信以及编译,请大家敬请期待吧!