简介
由于自己参与过低代码平台开发,所以希望能把我自己开发低代码中遇到的问题或者一些设计思路进行总结汇总,这是开始写的第一篇,也是比较基础的一篇,关于低代码平台的介绍会放在介绍篇章,这篇就不做过多介绍。
这里为什么会一开始介绍js沙箱设计呢?
因为低代码平台,会运行用户本身自己编写的业务逻辑代码,这里就需要平台去运行用户写的js代码,但是js代码保存到数据库是一个字符串,那么平台应该怎么运行呢?
答案是js沙箱,那么如何设计一个沙箱呢?按照低代码平台的需要特性,主要以下几方面:
- 隔离,隔离是为了保证当前执行代码不影响整个平台的代码
- 插入,沙箱允许插入平台的内置对象
- 容错,沙箱内代码即使有错误,也不影响整个平台执行
沙箱
在设计沙箱之前,我们先对沙箱有个了解:
在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行。
通俗的讲,就是由我们主程序自己设定一个区域,用来执行代码,且这段代码如何执行都不会影响到外部的主程序。
举几个我们开发中经常会用的沙箱:
- Vue template里的表达式,如:
,执行{{ 1+1 }}
1+1
就是Vue设计的一个沙箱机制 - 开发Chrome插件,插件里的代码有很多限制条件,循序Chrome插件规则,那么插件的运行环境和规则也是一个沙箱
- 在线代码编辑器, CodeSanbox在执行脚本也会单独成立一个沙箱去隔离执行代码,防止代码访问或影响主页面
- 微前端
qiangun
或single-spa
框架里主应用和子应用之间的完全隔离,也是一种沙箱机制,如: 应用之间CSS样式不能互相影响
在了解完沙箱是什么后,那么在JavaScript语言里如何实现沙箱呢?主要有以下几种方式:
- 使用 with 声明
- 使用 new Function 声明
- 基于 Proxy实现
- 基于属性 diff实现
- 基于 iframe实现
- 基于 ES 提案 ShadowRealm API
前置知识
with关键字
with 扩展一个语句的作用域链。 —— MDN withJavaScript 查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的 context 或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在 statement 中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出ReferenceError异常。
按照个人比较容易理解的意思,就是给一段代码加上指定对象为该作用的全局变量。示例代码如下:
Math.floor(1.1) // 1 // 使用with with(Math){ floor(1.1) // 1 }
new Function
new Function(argStr, codeStr)
是能将字符串代码转换为可执行的函数。具体示例如下:
const name = 'test'; const test = new Function('arg', 'console.log(arg)'); // 这里等于 test = (arg)=> {console.log(arg)}; test(name); // test
Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy MDN
Proxy只能代理object
类型的变量,针对基础类型的代理只能将其封装到对象里再进行代理监听。
Proxy代理方法如下:
getPrototypeOf(target)
代理获取原型的方法setPrototypeOf(target, newProto)
设置原型,如果不想设置原型,可以return false
isExtensible(target)
拦截对对象的Object.isExtensible()
,必须返回一个 Boolean 值,判断一个对象是否是可扩展的preventExtensions(target)
拦截Object.preventExtensions()
,让一个对象变的不可扩展,也就是永远不能再添加新的属性getOwnPropertyDescriptor(target, prop)
拦截Object.getOwnPropertyDescriptor()
,拦截获取对象属性的描述符defineProperty(target, property, descriptor)
拦截Object.defineProperty()
has(target, key)
,针对 in 操作符的代理方法get(target, property, receiver)
,用于拦截对象的读取属性操作set()
,设置属性值操作的捕获器。construct()
,用于拦截 new 操作符
与Object.defineProperty
主要区别(可拦截方法比Object.defineProperty
多):
- Proxy代理的是整个对象,Object.defineProperty只代理对象上的某个属性,如果是多层嵌套的数据需要循环递归绑定;
- 对象上定义新属性时,Proxy可以监听到,Object.defineProperty监听不到,需要借助$set方法;
- 数组的某些方法(push、unshift和splice)Object.defineProperty监听不到,Proxy可以监听到;
Symbol.unscopables
指用于指定对象值,其对象自身和继承的从关联对象的 with 环境绑定中排除的属性名称。
可以这么理解,就是为了防止with
添加作用域的时候,将某个属性从作用域中排除掉,代码如下:
a = { p: 1, b: 2 } // 禁止将a.p放到with作用域中 a[Symbol.unscopables] = {p: true} with(a){ console.log(p) // 报错 p not defined console.log(b) // 正常输出 }
因此很多内置对象都设置该值为true,从而降低with的侵入,具体如下:
Array.prototype[Symbol.unscopables]; /*{ copyWithin: true, entries: true, fill: true, find: true, findIndex: true, flat: true, flatMap: true, includes: true, keys: true, values: true, }*/