Babel插件开发小试:用Proxy让代码更易读与其他优化
一、遇到的问题
最近我在给运算符重载Babel
插件增加Typescript
支持,我们可以以如下方式声明同一个运算对多个类型的重载:
1 2 3 4 5 6
| const $operator = { plus: [ (l: number, r: number): number => l + r * 2, (l: string, r: string): number => l.length + r.length ] }
|
左值和右值类型匹配的加法会编译,不匹配则不编译:
1 2 3
| $operator.plus[0](1, 2); $operator.plus[1]("str1", "str2"); true + false;
|
用于构建其抽象语法树的代码如下:
1 2 3 4 5 6 7
| t.callExpression( t.memberExpression( t.memberExpression( t.identifier(operatorObjName), t.identifier(operator.toString()) ), t.numericLiteral(index), true ), [path.node.left, path.node.right] );
|
多个函数嵌套可读性很差,而且冗长,并且要从内往外读,不符合阅读习惯。于是,我们能否写一个工具,让所见即所得?如果我进行一次函数调用,就会套一层callExpression
,访问一个元素,就会套一层memberExpression
。
二、解决方案
我们不难想到用Proxy
拦截对一个对象的访问,然后在get
中返回生成好的抽象语法树节点memberExpression
。
1 2 3 4 5 6 7
| export const memberExpression = (obj, prop, computed = false) => { return new Proxy(t.memberExpression(obj, prop, computed), { get(target, prop) { return t.memberExpression(target, t.numericLiteral(Number(prop)), true) } }); }
|
使用实例:
1 2 3 4 5 6
| memberExpression(t.identifier("$operator"), t.identifier("plus"))[0]
t.memberExpression( t.memberExpression(t.identifier("$operator"), t.identifier("plus")), 0, true )
|
那我们不禁要问,能不能链式调用?按现在这样如想再访一次元素,就要再套一层memberExpression
,我们希望嵌套变成链式。此外,最好也不需要自己声明许多identifier
,只需要传入一些字符串。
在此基础上在增加函数调用的功能即可。如果用Proxy
代理一个函数function
,我们对代理对象进行函数调用,相当于调用被代理的函数。我们期望这个函数的参数就是callExpression
的参数。
1 2 3
| function f(...args) {} let p = new Proxy(f, {}) p(something) f(something)
|
我们最终可以得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
export const build = (_obj) => { let obj = _obj; if (typeof obj === "string" || obj instanceof String) { obj = t.identifier(String(obj)); } return new Proxy( (...args) => build(t.callExpression(obj, args.map(item => t.isExpression(item) ? item : fromLiteral(item) ))), { get(target, prop) { if (prop === "raw") { return obj; } else if (isAssignmentOperator(prop)) { return buildAssignment(obj, prop); } else if (typeof prop === "symbol") { return build(t.memberExpression(obj, t.identifier(prop.description), true)); } else { return build(t.memberExpression(obj, t.stringLiteral(prop), true)); } } }); }
|
我们就可以实现如下效果:
1 2 3 4 5 6
| import generator from "@babel/generator"
const node = build("obj")[Symbol("prop")][0].foo.bar(Symbol("id"), t.identifier("id"), "id", 1, null).baz console.log(generator.default(node.raw).code)
|
在构建抽象语法树时,所见即所得。
但我们在编写插件时还需要生成赋值语句,如果赋值语句也能所见即所得呢?我们引入buildAssignment
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export const buildAssignment = (obj, operator) => { return (_right) => { let right = _right; try { right = fromLiteral(right); } catch { right = right.raw ?? right; } return build(t.assignmentExpression( operator, obj, right )) }; }
|
当我们调用访问build
后之节点的属性,且该属性是赋值运算符,就会返回相应的buildAssignment
函数,我们调用这个函数并传入赋值运算符的右值,就可以得到相应的AssignmentExpression
。我们可以实现如下效果:
1 2 3 4 5 6
| import generator from "@babel/generator"
const node = build("obj")[Symbol("prop")]['='](build("p").k()).func() console.log(generator.default(node.raw).code)
|
虽然等号两边不对称,但大体还是所见即所得。
三、实施优化
我们提取出相同的逻辑,改造visitorFactory
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const visitorFactory = (replacement, tail = () => "") => (path) => { const operatorObjectParent = path.findParent((parentPath) => t.isVariableDeclaration(parentPath) && operatorObjName == parentPath.node.declarations?.[0].id.name ); if (operatorObjectParent) return; let key = path.node.operator; key += tail(path); const operator = outer.registeredOperators.get(key); if (operator) { const replacer = replacement(build(operatorObjName)[operator], path); path.replaceWith(replacer.raw ?? replacer); } }
|
visitor
可以改造为如下样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| path.traverse({ "BinaryExpression|LogicalExpression": visitorFactory((builded, { node: { left, right } }) => builded(left, right) ), AssignmentExpression: visitorFactory((builded, { node: { left, right } }) => t.parenthesizedExpression( build(left)['='](builded(left, right)).raw )), UpdateExpression: visitorFactory((builded, path) => { if (path.node.prefix) { return t.parenthesizedExpression( build(path.node.argument)['='](builded(path.node.argument)).raw ) } else { path.replaceWith(path.node.argument); path.insertAfter(build(path.node)['='](builded(path.node)).raw); return path.node; } }, (path) => path.node.prefix), UnaryExpression: visitorFactory( (builded, { node: { argument } }) => builded(argument), (path) => path.node.operator === '-' ? "negative" : "" ) });
|
可以看到这部分代码简洁了很多,整体逻辑也更清晰了。