编程多年,你知道实现自己喜欢的语法多简单吗。
实现的一个方便自己的语法,必须先了解ast(抽象语法树),当然这个并不是很难的东西,只是一个树型结构,因为纯粹的文本不方便去分析代码结构,转成一颗树就方便我们去进行分析啦。
ast树就长这样
有了ast树可以做什么呢,可以去通过分析,实现代码压缩,代码转换,代码分析,等等啦,这就开看看我们要实现的一个简单例子。
//我们希望可以执行的代码
//应该看起来还是比较好理解把
function log(fn,args){
console.log(fn.name,'开始执行');
fn(args);
console.log(fn.name,'开始结束');
}
@wrap(log)
function so(args){
console.log(args);
}
//转换之后可以运行代码
function log(fn, args) {
console.log(fn.name, 开始执行);
fn(args);
console.log(fn.name, 开始结束);
}
let so = args => log(function (args) {
console.log(args);
}, args);
js目前装饰器是用在类上,上面的例子是作用在函数上,那么就需要我们去实现一个, 那么解析ast这个工作我们就需要请出我们的主角acorn.js来帮我们进行翻译(这是一个小巧强大的解释器,作用在了很多开源库上)。
acorn使用起来也是非常的简单
//这样就可以获取需要的ast了
const acorn = require("acorn");
const ast = acorn.parse(`console.log('good')`,{})
console.log(ast);
但是acorn只理解标准的js语法,我们自定义的语法并不理解,所以我们需要去对其进行扩展,
const acorn = require("acorn");
//继承acorn的parse实现然后我们只需要对特定函数进行重写就要可以达到扩展的目的
const parse = acorn.Parser.extend(Parser => {
return class extends Parser {
//在这里重写对应的方法
}
})
const ast = parse.parse(`console.log('good')`,{})
console.log(ast);
在重写解析函数之前我们需要先创建类似关键字也就是对应的分词,有了分词主要让acorn知道这个符号不是乱写的,是有意义的。
//比如这样就创建的一个分词的类型
const atToken = new acorn.TokenType("@");
接下来附上基本的代码
const parse = acorn.Parser.extend((Parser) => {
const skipWhiteSpace = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g;
const whiteSpace = (acorn) => {
skipWhiteSpace.lastIndex = acorn.pos;
const skip = skipWhiteSpace.exec(acorn.input);
return skip[0].length;
};
//判断是否at开头的函数
const isAtFunction = (acorn) => {
const next = acorn.pos + whiteSpace(acorn);
return (
acorn.type.label === "@" && acorn.input.slice(next, next + 4) === "wrap"
);
};
const atToken = new acorn.TokenType("@");
return class extends Parser {
//acorn解析token时需要执行函数在这里让acorn知道我们的符号是有意义的
readToken(code) {
if (code === 64) {
++this.pos;
return this.finishToken(atToken);
}
return super.readToken(code);
}
parseStatement(context, topLevel, exports) {
if (isAtFunction(this)) {
//消耗掉当前的token符号
this.eat(atToken);
const wrap = [];
let isAsync = false;
//一直获取到时function类型的时候停止
while (this.type !== acorn.tokTypes._function) {
if (this.isContextual("async")) {
isAsync = true;
//迭代到下一个token
this.next();
break;
}
wrap.push(this.parseExpression(null, null));
this.eat(atToken);
}
//解析当前函数
const functionNode = this.parseFunctionStatement(
this.startNode,
isAsync,
!context
);
//把at的信息添加到一个我们自己定义的对象中保存
functionNode.wrap = wrap;
return functionNode;
}
//如果不是at开头的函数按正常去解析
return super.parseStatement(context, topLevel, exports);
}
};
});
有了这些数据之后我们可以通过各种支持转换ast的库来帮助进行ast转换转换为标准可以执行的ast,因为我们现在的ast属性wrap是自定义的,标准化之后就可以用来生成可以执行的代码。
//这里是使用babel因为babel用起来简单嘛
const buildHoc = (wrap, replaceAst, id) => {
const buildWrap = babel.template`let FUNCTIONNAME = (args)=> WRAPAST`;
while (wrap.length) {
const current = wrap.shift();
const args = [
replaceAst,
...current
.get("arguments")
.slice(1)
.map((v) => v.node),
];
replaceAst = babel.types.callExpression(current.get("arguments.0").node, [
...args,
babel.types.identifier("args"),
]);
}
return buildWrap({
FUNCTIONNAME: id,
WRAPAST: replaceAst,
});
};
const transitionCode = (code) => {
const ast = {
type: "File",
program: parse.parse(code, {}),
};
babel.traverse(ast, {
FunctionDeclaration(path) {
const wrap = path.get("wrap");
if (Array.isArray(wrap)) {
const node = path.node;
let id = node.id;
delete node.wrap;
node.id = null;
node.type = "FunctionExpression";
path.replaceWith(buildHoc(wrap, node, id));
}
},
});
return generate(ast);
};
好了,具体就是这样,完整代码点这里
我叫zr,新人一枚,请多多关照哈
转载自:https://juejin.cn/post/7180970900643283003