estime

estime = ecmascript + runtime, in javascipt(es5) environment

Usage no npm install needed!

<script type="module">
  import estime from 'https://cdn.skypack.dev/estime';
</script>

README

estime

estime = ecmascript + runtime, in javascipt(es5) environment

基于 TypeScript 编写的 JavaScript 解释器,运行于es的环境,且原生支持es6\jsx等众多常用的新特性。独立、安全。

初版fork于eval5,目标是原生支持es2017(非严格)语法和JSX且修改bug,持续开发中,进度请查看最后的todoList

使用场景

  • 不支持eval Function的 JavaScript 运行环境:如 微信小程序。
  • 支持eval的Javascript环境,但是又担心eval的安全性问题。
  • 需要代码动态更新的场景。例如你的React应用需要热更新组件;你的规则系统需要动态下发规则脚本等等。
  • 研究/学习用

功能演示

安装

npm i estime -S

使用

import { Interpreter } from "estime";

const interpreter = new Interpreter({
  console,
  rt: (val) => (res = val)
});

try {
  let res;
  interpreter.evaluate(`
    class Test {
      name = 'default_test';
      setName = (name) => {
        this.name = name
      }
    }
    let t = new Test
    t.setName('hello')
    console.info(t.name)
    rt(t.name)
  `);
  console.info('the result is ', res)
} catch (e) {
    console.log(e);
}

参数

interface Options {
    // 根作用域,只读
    rootContext?: {} | null;
    globalContextInFunction?: any;
}

Example

import { Interpreter } from "estime";

const ctx = {};
const interpreter = new Interpreter(ctx, {
  rootContext: window,
});

interpreter.evaluate(`
  a = 100;
  console.log(a); // 100
`);

window.a; //undefined

Interpreter静态属性

global

默认值: {}

设置默认的全局作用域

Interpreter.global = window;
const interpreter = new Interpreter();
interpreter.evaluate('alert("hello estime")');

globalContextInFunction

默认值: undefined

estime 不支持 use strict 严格模式, 在非严格下的函数中this默认指向的是全局作用域,但在estime中是undefined, 你可以通过globalContextInFunction来设置默认指向。

Interpreter.globalContextInFunction = window;
const interpreter = new Interpreter();
interpreter.evaluate('alert("hello estime")');

ES2015 支持

import {Interpreter} from '../src/interpreter/main'

let inter = new Interpreter(null)

let res = inter.evaluate(`
let a = (function(){
    if(true){
        if(true){
            try{
                let a = b;
                let b = 123;
                var result = 456;
            }catch(e){
                // e is Cannot access 'b' before initialization
                result = 123;
            }
        }
    }
    return result
})()
a
`);
console.info(res)  // 123

JSX支持

其中不支持JSXFragments、JSXNamespacedName和JSXSpreadChild。JSX标准参考fb的jsx specification acorn-jsx不支持JSXSpreadChild,且用的比较少。例如下面两种content的用法,效果是一样的,为什么要我要去用spread呢?暂且不支持吧。

使用estime完成React组件动态更新的例子,非常灵活

let content = [1,2,3]
let t = <div>
  {content}
  {...content}
</div>

jsx使用例子:

let code = `
let props = {
   style: { border: '1px solid #333', color: 'red', borderRadius: 3, padding: 10, margin: 10 }
}

class Panel extends React.Component{
   render(){
       return <div {...props}>this is other Component: Panel</div>
   }
}
class TT extends React.Component{
  render(){
    return <div {...props}>
      hello world
      <input disabled style={{display: 'block', width: 300,}} />
      <Panel />
    </div>
  }
}
__rt(TT)
`
class Test {
  getCpt(code){
    let interRes;
    let inter = new Interpreter({
      __rt: val => (interRes = val),
      console,
      React: React,
    })
    try{
      inter.evaluate(code)
    }catch(e){
      console.info(e)
      return e.message
    }
    return interRes
  }
  render(){
    let C = this.state.C
    return <div>
      <button onClick={_=>{
        let Cpt = this.getCpt(code)
        this.setState({
          C: Cpt
        })
      }}>点击生成组件</button>
      {C && <div><C/></div>}
    </div>
  }
}

效果如下:

相关

异步队列方案

要在沙箱内部支持异步方法,就必须用js去模拟整个task的执行流程,task分为micro task和macro task。即我们说的大队和小队。Promise入的是小队,setTimeout入的是大队。这里是难点,这一套机制是整个异步流程的基石。

那么,怎么实现大小队呢?参考现有的promise pollyfill库,promise.js用的是asapasap底层是process.nextTick降级到queueMicrotask再降级到MutationObserver最后降级到setTimeout。那么对于一个沙箱环境,我们是否有必要做得这么复杂呢?

首先明确一点,在浏览器环境,用户可以任意编写方法调用setTimeout或是queueMicrotask或是Promise.resolve等等,浏览器一般并没有限制。所以,当在浏览器实现一个小队的polyfill时候,就需要判断各种各样的api接口是否可用,然后处理各种降级,为的就是当用户调用你的fakeMicroTask放入的函数必定比他自己调用setTimeout放入的函数后执行。

那么,如果是沙箱环境,任何函数的实际执行都是沙箱决定的,沙箱完全可以只提供一个虚拟的“队列”,无论是setTimeout还是queueMicrotask还是Promise.resolve都放入同一个虚拟队列中,只是他们优先级不一样而已。那么,只要我们沙箱外部环境拥有setTimeout的能力(这几乎是100%兼容的),我们就能够提供这样的虚拟队列,保证沙箱环境的各种不同优先级的异步方法执行;且这样做还有个好处,就是沙箱中的异步方法,永远都是优先级最低的setTimeout,不和外部环境抢小队的时间片。

实现虚拟的大小队的标准可以参照event-loop-processing-model

generator相关实现方法

generator的实现也是难点,但其功能又如此重要(异步方法语法糖的基础),不得不支持。目前正在纠结中,将generator的定义转换成es5可执行的同等函数,其工作量不亚于再写一个js解释器。有现成的npm包比如regenerator可以将generator的代码转换成es5的形式,但其包体大小压缩有都有足足1M,我是肯定不会用的。typescript的源码中也带有转换generator,但由于依赖挺多的,拆分出来的成本较大。经过一段时间的源码阅读,发现regenerator实际是基于babel-plugin写的一个ast替换插件,整体核心部分大致有2000+行代码,加上运行时600行代码,比较合理。不过regenerator基于babel-plugin,用到了babylon的语法分析能力,也基于babel-types和babel的travel能力,这部分代码庞大,需要去掉自己写;且babylon的语法分析结果和acorn.js的语法分析声明的都是遵守estree,不过两者最终输出的ast结构还是有差异的,estime基于acorn做语义分析,这部分适配工作也需要自己做。

整体思路很简单,如果遇到了async或generator函数,先进行ast的转译,然后再进行接下来的编译闭包工作。对ast的转译工作单独放在了这个库里:estime-resync

todoList

es2015\es2017等等申明,个人感觉是非严格的es规范支持声明。es的规范会经过不同stage的提案状态,有些特性还在stage-1等就已经放出来开始广泛使用了。所以对于es2015,你会看到有“对象解构”,但是实际上在2015年的时候,它还不是stage-4。我看acorn.js在es2018才支持解构,但是babel的文档上,es2015就已经包含解构了,这样的差异还真不好细究清楚,且深究也没有意义。所以,我没有局限在2015还是2017上,关注的是特性,需要支持的特性下面的todolist都会列出来。

相关特性可以看这里,并不一定全部实现。但常用的都会实现的。

  • 块级作用域
    • let
    • const
  • Class
    • 基础声明
    • extends
    • class fields
    • static property
  • 箭头函数
    • 基础执行支持
    • context绑定
  • 解构
    • 对象解构
    • 数组解构
    • 函数实参解构
  • Rest element
    • ObjectPattern
    • ArrayPattern
    • 函数形参rest
  • Map + Set + WeakMap + WeakSet 由外部提供支持,沙箱不做特殊支持
  • for-of
  • Template Strings
  • Computed property
  • Symbols
  • Array新增方法等
    • Array.from
    • Array.of
    • Array.prototype.entries
    • Array.prototype.values
    • Array.prototype.keys
    • Array.prototype.reverse
    • Array.prototype.find
    • Array.prototype.fill
    • Array.prototype.lastIndexOf
    • Array.prototype.findIndex
    • Array.prototype.copyWithin
    • Array.prototype.includes
    • Array.prototype.flat
    • Array.prototype.flatMap
    • Array.prototype.reduceRight
  • 异步函数
    • 虚拟大小队列core
    • 虚拟大小队列的自动销毁
    • setTimeout
    • setInterval 不支持,容易造成timer泄露,我鼓励自己用setTimeout来实现interval功能
    • Promise
    • queueMicrotask
  • Generators
  • async/await
    • Async generator functions 不支持
  • JSX支持,其中不支持JSXFragments、JSXNamespacedName和JSXSpreadChild。JSX标准参考fb的jsx specification
    • JSXElement
    • JSXIdentifier for React IntrinsicElement
    • SelfClosing
    • JSXExpressionContainer
    • JSXText
  • 抽离acorn.js的依赖。为网络传输提供可靠安全的基础。
    • AST的压缩(可能是二进制)表示形式
    • 源码打包器
    • 压缩AST解释器runtime

License

Mozilla Public License Version 2.0