通过Babel重写加减乘除运算符,支持用户配置公式规则计算数组数据

214 阅读5分钟
// 输入:
const a = 1;
const b = 2;
const c = a + b;
console.log(c); // 3

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = arr1 + arr2;
console.log(arr3); // ??

在一些低代码的业务逻辑里面,通过配置一个公式支持用户直观上的数据运算,比如数组的加法。这里通过Babel插件替换JavaScript中的基本运算符,最终执行自定义的操作来实现这个功能

本文介绍了如何通过Babel插件重写JavaScript中的加减乘除运算符,以支持自定义的运算逻辑,如数组的运算等

一个有用的示例(完整代码附文末):

实现

Babel插件通过修改AST,允许开发者在源代码中使用自定义的运算符

原始代码test.js如下:

// 加法示例
const num1 = 1;
const num2 = 2;
const addResult1 = num1 + num2;
const addResult2 = num1 + (num2 + 3);
console.log(addResult1); // 3
console.log(addResult2); // 6

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arrSumResult = arr1 + arr2;
console.log(arrSumResult); // 5, 7, 9

// 乘法示例
const mulResult = num1 * num2;
console.log(mulResult); // 2

const arrMulResult = arr1 * arr2;
const arrMulResult2 = arr1 * 2;
const arrMulResult3 = arr1 * [2];
console.log(arrMulResult); // 4, 10, 18
console.log(arrMulResult2); // 2, 4, 6
console.log(arrMulResult3); // 2, 2, 3

// 复合运算
const result = ([num1 + num2 * 2] + arr1) * 2;
console.log(result); // 5

上面是将想要运行的目标代码,其中可以看见一些数组的加法和乘法,这些运算直接运行是不支持的,下面看如何重写来运行

主流程代码transform.js:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const { visitor } = require("./pluginCustomOperators");
const fs = require("fs");
const path = require("path");

function readFileSyncSafe(filePath) {
  try {
    return fs.readFileSync(filePath, "utf-8");
  } catch (error) {
    console.error("读取文件失败", error);
    throw error;
  }
}

function main() {
  const codePath = path.resolve(__dirname, "./test.js");
  const code = readFileSyncSafe(codePath);
  // 解析代码为 AST
  const ast = parser.parse(code);
  // 遍历并替换 AST
  traverse(ast, visitor());
  // 生成新代码
  const newCode = generate(ast).code;
  console.log("---------函数体----------");
  console.log(newCode);

  const businessLogicPath = path.resolve(__dirname, "./businessLogic.js");
  const businessLogic = readFileSyncSafe(businessLogicPath);

  const func = new Function(`
    ${businessLogic}
    ${newCode}
  `);
  console.log("---------执行函数----------");
  func();
}

main();

代码阅读上非常的简单,先是读取文件,然后解析代码为AST,遍历并替换AST,生成新代码,然后执行新代码

新代码就是我们最终运行的代码,里面的运算符已经被替换成了我们自定义的运算符

详看使用到的一个插件pluginCustomOperators.js,里面做了运算符的替换操作:

const t = require("@babel/types");

function visitor() {
  return {
    BinaryExpression(path) {
      const { node } = path;
      const operatorMap = {
        "+": "add",
        "-": "sub",
        "*": "mul",
        "/": "div",
      };

      const functionName = operatorMap[node.operator];
      if (functionName) {
        const customFunctionCall = t.callExpression(
          t.memberExpression(t.identifier("_p"), t.identifier(functionName)),
          [node.left, node.right]
        );
        path.replaceWith(customFunctionCall);
      }
    },
  };
}

module.exports = {
  visitor,
};

  1. t.identifier("_p")

    • 创建一个标识符节点,表示变量 _p
  2. t.identifier(functionName)

    • 创建一个标识符节点,表示变量 functionNamefunctionName 是一个变量,应该包含你自定义的运算符函数名,例如 add
  3. t.memberExpression(t.identifier("_p"), t.identifier(functionName))

    • 创建一个成员表达式节点,表示 _p.functionName,这里 _p 是一个对象,functionName 是这个对象的方法
    • 举例来说,如果 functionNameadd,那么这个成员表达式表示 _p.add
  4. t.callExpression(...)

    • 创建一个函数调用表达式,表示对 _p.add 的调用
    • 第一个参数是被调用的函数,即 _p.add
    • 第二个参数是传递给函数的参数数组 [node.left, node.right],表示运算符左侧和右侧的操作数

在pluginCustomOperators.js中,visitor函数返回一个对象,该对象包含一个BinaryExpression方法。这个方法负责替换表达式中的运算符。replacePath函数负责替换表达式中的运算符,它用一个映射表来查找需要替换的运算符,并将其替换为对应的_Op对象中的方法调用,这就是替换代码的核心逻辑

执行完之后,原始的代码将会被替换成如下代码:

// 加法示例
const num1 = 1;
const num2 = 2;
const addResult1 = _p.add(num1, num2);
const addResult2 = _p.add(num1, _p.add(num2, 3));
console.log(addResult1); // 3
console.log(addResult2); // 6

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arrSumResult = _p.add(arr1, arr2);
console.log(arrSumResult); // 5, 7, 9

// 乘法示例
const mulResult = _p.mul(num1, num2);
console.log(mulResult); // 2

const arrMulResult = _p.mul(arr1, arr2);
const arrMulResult2 = _p.mul(arr1, 2);
const arrMulResult3 = _p.mul(arr1, [2]);
console.log(arrMulResult); // 4, 10, 18
console.log(arrMulResult2); // 2, 4, 6
console.log(arrMulResult3); // 2, 2, 3

// 复合运算
const result = _p.mul(_p.add([_p.add(num1, _p.mul(num2, 2))], arr1), 2);
console.log(result); // 5

当前代码 + * 的运算符被替换为了_p.add、_p.mul,但是代码的上下文中没有这些方法,所有我们需要定义并且加入到最终的执行上下文中 在transform.js中,我们通过new Function的方式执行了新代码,而自定义操作符代码在businessLogic.js中:

const _p = {
  add: (left, right) => {
    if (isNumber(left) && isNumber(right)) {
      return left + right;
    }
    if (Array.isArray(left) && Array.isArray(right)) {
      const len = Math.max(left.length, right.length);
      const result = [];
      for (let i = 0; i < len; i++) {
        const leftValue = left[i] !== undefined ? left[i] : 0;
        const rightValue = right[i] !== undefined ? right[i] : 0;
        result.push(leftValue + rightValue);
      }
      return result;
    }
    throw new Error("无效输入:两个参数必须都是数字或数字数组");
  },
  sub: (left, right) => left - right,
  mul: (left, right) => {
    if (isNumber(left) && isNumber(right)) {
      return left * right;
    }
    if (Array.isArray(left) && isNumber(right)) {
      return left.map((item) => item * right);
    }
    if (Array.isArray(right) && isNumber(left)) {
      return right.map((item) => item * left);
    }
    if (Array.isArray(left) && Array.isArray(right)) {
      const len = Math.max(left.length, right.length);
      const result = [];

      for (let i = 0; i < len; i++) {
        const leftValue = left[i] !== undefined ? left[i] : 1;
        const rightValue = right[i] !== undefined ? right[i] : 1;
        result.push(leftValue * rightValue);
      }

      return result;
    }
    throw new Error("无效输入:参数必须是数字或数字数组");
  },
  div: (left, right) => left / right,
};

function isNumber(value) {
  return typeof value === "number" && isFinite(value);
}

最后需要将两段代码合并在一起执行,这样达到了重定义运算符的效果,就是这样简单,快打开代码试试吧

总结

该逻辑是低代码公式规则执行的部分核心逻辑设计demo,希望对你有所帮助,如果有任何问题欢迎留言讨论


示例源码地址:github.com/Aoyia/nqbi