// 输入:
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,
};
-
t.identifier("_p"):- 创建一个标识符节点,表示变量
_p
- 创建一个标识符节点,表示变量
-
t.identifier(functionName):- 创建一个标识符节点,表示变量
functionName,functionName是一个变量,应该包含你自定义的运算符函数名,例如add
- 创建一个标识符节点,表示变量
-
t.memberExpression(t.identifier("_p"), t.identifier(functionName)):- 创建一个成员表达式节点,表示
_p.functionName,这里_p是一个对象,functionName是这个对象的方法 - 举例来说,如果
functionName是add,那么这个成员表达式表示_p.add
- 创建一个成员表达式节点,表示
-
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