前言
在之前的一遍文章中,我简单地介绍了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
语句下的解析等等地扩展,欢迎大家讨论哈。
最后附上源码
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。