用 Proxy 解析 JS 脚本

72 阅读2分钟

假设外部传递进来这样一段代码,其中 Riko 可以理解为自定义语法,通过自定义语法向外部传递代码意图。要想知道代码意图,就需要做代码解析,简单说就是要获取这段代码用到了哪些自定义语法函数并且传递了什么参数。

class Script extends Riko.Script {
  x = Riko.useNumber({
    name: 'cz的水平坐标62',
    tooltip: '节点的水平坐标',
    colSpan: 13,
  });
  onAwake() {
    console.log(this.target);
    // TODO
  }
}

常见的有下面两种方式:

  1. AST 解析
  • 开发复杂度高,需要对 AST 树型结构有充分的理解
  1. 正则匹配
  • 不够灵活,需要考虑很多边界问题(例如注释情况)

下面介绍另外一种方式,Proxy 代理。

Proxy 代理

const content = `
  class Script extends Riko.Script {
    x = Riko.useNumber({
      name: 'cz的水平坐标62',
      tooltip: '节点的水平坐标',
      colSpan: 13,
    });
    onAwake() {
      console.log(this.target);
      // TODO
    }
  }
`

const Script = eval('(' + content + ')')
new Script();

我们拿到 content 代码的内容后,使用 eval 去执行这段代码,很明显它会报 Riko is not defined 的错误。

那如果我们提前把 Riko 定义好呢?

const content = `
  class Script extends Riko.Script {
    x = Riko.useNumber({
      name: 'cz的水平坐标62',
      tooltip: '节点的水平坐标',
      colSpan: 13,
    });
    onAwake() {
      console.log(this.target);
      // TODO
    }
  }
`

const Riko = {};
Riko.Script = class {
  constructor(){}
};
Riko.useNumber = (...args) => {
  console.log('参数:', args);
}

const Script = eval('(' + content + ')');

new Script();

通过提前定义 Riko 方法,并执行字符串脚本,我们可以获取到 Riko.useNumber 的参数。那如果 Riko.useXXX 类型有很多种呢?难道我们每个类型都要去定义一次吗?

我们可以使用 JS 原生的 Proxy 语法,代理所有 Riko 对象上的操作。

const content = `
  class Script extends Riko.Script {
    x = Riko.useNumber({
      name: 'cz的水平坐标62',
      tooltip: '节点的水平坐标',
      colSpan: 13,
    });
    onAwake() {
      console.log(this.target);
      // TODO
    }
  }
`

// 用一个类来表示自定义语法属性
class PropsConfig {
  static Script = class {};
  config;
  constructor(callee, options) {
    this.config = {
      callee,
      ...options
    }
  }
}

const getProxy = (callee) => new Proxy(PropsConfig, {
  get(target, prop) {
    return getProxy(`${callee}.${prop}`); 
  },
  apply(target, thisArg, argumentsList) {
    return Reflect.construct(target, [callee, ...args]);
  },
})

const Riko = getProxy('Riko');

const Script = eval('(' + content + ')');

console.log(new Script()); 
/**
{
    "x": {
        "config": {
            "callee": "Riko.useNumber",
            "name": "cz的水平坐标62",
            "tooltip": "节点的水平坐标",
            "colSpan": 13
        }
    }
}
**/

通过这种方式实例化出来的对象,可以很直观的看到脚本内用到了哪些自定义语法,并且参数是什么。