前言
在之前的一遍文章中,我简单地介绍了Babel的一些概念,大家可以从我发表的文章记录中可以找到;但是大家有没有想过自己也能开发Babel插件呢,Babel官方提供很多插件进行新语法的解析,同时它也允许我们自己开发插件,去解析和转换代码。
或许大家在实际开发中也遇到过一下问题:
- 我们想要对
a.b.c中的c进行操作,但是我们可能会担心a.b中的a或b是不存在的,这个时候我们难免会做一下操作:
if (a && a.b && a.b.c) {
// 对c进行操作
}
- 当这个对象的层次很深的时候,例如
a.b.c.d,那么,这个判断将会更加的冗长,我们不得不写一段很长的判断,非常地不美观。 - 不过自从
optional chaining出现后,我们终于可以优雅地解决这个问题:
if (a.b?.c) {
// 对c进行操作
}
这里需要配置
@babel/plugin-proposal-optional-chaining
但是假设没有这个插件的时候,如果想要达到这种optional chaining的效果,你有没有想过怎么实现呢?
下文将自己实现一个简版的optional chaining,达到在if语句中实现optional chaining。
预期效果
由于在值后面直接添加?是属于不合法的,在编译生成AST的过程中,会提示Unexpected token。
因此,为了能间接实现optional chaining,我们需要实现一种特殊的标识?,在需要达到可选链效果的值后添加?。
- 转换前
const a = { b: { c: '123' } };
if (a.b?.c) {
// 其他操作
}
- 转换后
const a = { b: { c: '123' } };
if (a.b && a.b.c) {
// 其他操作
}
预备知识
对Babel的AST解析过程有一定的了解,如果不了解的话,可以来这里学习一下
插件的声明
在package.json声明你的入口文件
// package.json
{
"name": "@jg/babel-plugin-tiny-optional-chaining",
"main": "src/index.js",
...
}
入口文件导出方法
// src/index.js
export default function(babel) {
return {
name: 'babel-plugin-tiny--optional-chaining',
visitor: {
}
};
}
插件需要返回visitor,而内部将以babel作为入参,后面我们需要用到babel对象的types帮助创建新的节点。
处理AST节点
visitor通过访问器模式达到处理AST语法树上各种类型的节点的目的,我们只需要在visitor中声明需要处理的节点的类型,即可在里面进行进一步的分析:
visitor: {
MemberExpression: {
enter(path) {
// 处理节点
},
exit() {
}
}
}
节点的类型和属性繁多,如果需要更形象的分析节点,可以通过AST explorer来帮助我们。
分析节点
if (a.b?.c) {}
我们需要解析的表达式a.b?.c是一个MemberExpression节点,而且它必须是包裹在if语句中,从AST explorer中可以看出,它们的层次结构是这样的:
所以我们需要判断该MemberExpression是否在IfStatement中:
MemberExpression: {
enter(path) {
if (types.isIfStatement(path.parent)) {
// 其他操作
}
}
}
进一步分析发现,转换后的AST结构是这样的:
MemberExpression会被LogicalExpression包裹,而且LogicalExpression会存在两个MemberExpression,分别为left和right节点,这两个节点不是新增的,可以从转换前的MemberExpression的子节点中拿到它们的引用,所以在我们构造新的LogicalExpression节点时,可以复用MemberExpression节点。
不过在新增LogicalExpression节点前,我们首先要将所有的?符号去掉:
function transformOptionValue(value) {
return value.replace(/\$\?/, '');
}
... 其他代码
let currentNode = path.node;
const members = [];
const options = [];
while(types.isMemberExpression(currentNode)) {
if (types.isLiteral(currentNode.property)) {
const literalNode = currentNode.property;
if (literalNode.value.endsWith('?')) {
const value = transformOptionValue(literalNode.value);
literalNode.value = value;
literalNode.raw = value;
options.push(currentNode);
}
} else if (types.isIdentifier(currentNode.property)) {
const identifierNode = currentNode.property;
if (identifierNode.name.endsWith('?')) {
const name = transformOptionValue(identifierNode.name);
identifierNode.name = name;
options.push(currentNode);
}
}
members.push(currentNode);
currentNode = currentNode.object;
}
这里members和options两个数组是用来保存MemberExpression中的所有Member表达式以及被声明为optional chaining的节点。
在我们的例子中:
if (a.b?.c) {}
members数组有:a.b.c和a.b以及a,但是上面的代码并没有处理a,a是需要单独处理的,相关的代码这里不贴出来了,大家可以看文末的源码地址。options数组有:b。
构造节点
现在我们已经知道了整个optional chaining表达式中有多少members和options,现在就通过遍历options数组递归构造LogicalExpression。
if (options.length > 0) {
const root = recursiveLogicalExpression(types, options, members);
path.replaceWith(root);
}
// 递归构造新节点
function recursiveLogicalExpression(types, options, members) {
const node = members.pop();
if (members.length >= 1) {
if (options.indexOf(node) > -1) {
return types.logicalExpression('&&', node, recursiveLogicalExpression(types, options, members));
} else {
return recursiveLogicalExpression(types, options, members);
}
} else {
return node;
}
}
这里刚好member数组中节点的顺序和LogicalExpression中MemberExpression的顺序是反序的,所以需要从数组最后一位开始取节点。
到这里,核心功能基本已经完成了。
测试
这里我们运行了三个单元测试,基本都通过测试:
// Before:
const test = a.b.c.d;
// After:
const test = a.b.c.d;
// Before:
if (a?.b?.c.d?.e) {}
// After:
if (a && a.b && a.b.c.d && a.b.c.d.e) {}
// Before:
if (a.b.c.d.e) {}
// After:
if (a.b.c.d.e) {}
总结
到这里,插件基本已经完成,其实还有很多可以优化的地方,这里先抛砖引玉了;下一步的话,可以着手尝试一些非if语句下的解析等等地扩展,欢迎大家讨论哈。
最后附上源码