babel进阶用法之处理json文件

前言

社区里面关于babel的介绍非常多了,这里不想重复这些常见内容。很多人认为babel只是一个语法转译工具,将浏览器无法识别的高级语法进行转换(polyfill),提升开发体验。其实babel是一个强大的工具链,它基于acornacorn-jsx,将js转化为抽象语法树(AST),抽象语法树可以理解为一个大的对象,精细化定义了代码的所有细节。对这个大对象进行处理后,再将其转化为代码,这其实就是各种各样的babel插件代码转换的核心原理。本文也是从这里入手,以一个小例子,展示babel在处理json文件中的应用。

应用场景

假设有一个跟配置相关的大json文件,里面有各种各样的key,我们需要能够使用代码来动态修改这个大json中的内容,比如替换某个key的内容,删除某个key,新增内容等等。这里有人可能会说了,直接使用node中的fs读取文件并修改不可以吗?比较简单的文件的确可以这样操作,但是如果json结构比较复杂,比如有很多层级或者不同层级有相同的key,直接使用字符串进行匹配和搜索将变得非常繁琐,并且非常容易出错。这里我们借用babel的能力将json解析成抽象语法树,再进行对应的调整。

核心原理

读者可能会有疑问,babel只能处理js模块,如何处理json文件呢?json文件本身可以看做一个json对象,而babel内部显然是具有处理对象的能力的,为此我们只需要通过代码将json改写成js文件,将其传入babel转化成抽象语法树,对特定的部分进行修改之后,再转换回js代码。把js代码中的json对象导出,生成目标json文件。

代码实现

这里举一个简单的例子,假设有如下的json内容(origin.json):

{
    "name": "wang",
    "ppp": 123
}

我们的目标是删除到ppp这条属性,并增加一个新的属性。直接上代码:

const babel = require('@babel/core');
const fs = require('fs');
const path = require('path');
//  原始json
const UPLOAD_DIR = path.resolve(__dirname, "origin.json"); // 大文件存储目录

//  通过babel插件,写入新的文件
const oldContent = fs.readFileSync(UPLOAD_DIR);
//  强行转换成js文件
const addContent = 'let b=' + oldContent + ';exports.b = b;';
//  通过babel处理替换,替换内容
const newContent = babel.transformSync(addContent, {
    plugins: ['./progressJson/plugin']
}).code;
//  生成中介文件
fs.writeFileSync('progressJson/relayFile.js', newContent, 'utf8');

function writeFinalFile() {
    const trueInfo = require('./relayFile.js').b;
    //  生成最终的结果
    fs.writeFileSync('progressJson/result.json', JSON.stringify(trueInfo), 'utf8');
}

writeFinalFile();

这里讲解下,首先用fs模块读取原始json的内容(以字符串的形式),然后开启黑科技,在头尾拼接特殊字符串,使其转变为js文件的字符串形式,传入babel.transformSync,引入我们的插件,进行处理之后,生成代码字符串。然后使用fs.writeFileSync将生成的字符串写成文件。最后再引用该文件,读取json变量,对其stringfy获得最终的内容,在将内容写入最后的文件result.json。核心逻辑都在我们的babel插件中。

替换属性的babel插件

progressJson/plugin,直接上代码:

//  要小心循环引用,超过迭代次数还没有出来就会自动停止,而且不会报错
module.exports = function (babel) {
    const { types: t } = babel;
    return {
        name: 'write in new content', // not required
        visitor: {
            //  捕捉对象属性
            //  t表示的是type,也就是各种属性,要对节点做操作,需要对path做处理,相关api在@babel/traverse里面,市面上几乎没有文档
            ObjectProperty(path) {
                //  遍历所有的对象属性
                const node = path.node;
                //  定位到key为ppp的对象属性
                if (node.key.value === 'ppp') {
                    //  插入节点
                    path.insertAfter(t.objectProperty(t.identifier('load'), t.nullLiteral()));
                    //  删除节点
                    path.remove();
                }
            }
        }
    };
};

关于babel插件的具体写法,笔者之前写过一篇文章,里面有babel原理的详细分析,可以参考。这里再简单讲解下原理,。babel插件本质是一个函数,入参是babel的实例,返回值是一个对象,里面的visitor用来匹配目标内容,这里的
ObjectProperty表示遇到对象属性时进入执行逻辑,类似地,FunctionDeclaration表示遇到函数定义时进入操作逻辑,此外还有BinaryExpression(二元表达式)、Identifier(标识符)等等的visitor入口函数。babel将代码解析成抽象语法树之后,我们可以用上面提到的这些类型匹配函数来找到目标代码的位置,具体可以参考@babel/types。这里顺便吐槽下,官方的文档给的十分粗糙,只有非常含混的类型定义,也没有使用例子,全看个人领悟力,不参考其他babel插件的写法来配合理解根本不知道什么意思(参考一些官方插件的写法),体验极差。ObjectProperty的入参是path,表示当前正在检查的节点的路径,可以配合AST生成工具来配合理解,在笔者的那篇博文中有详解。t.objectProperty表示构造一个对象属性节点,其接受两个参数,第一个参数t.identifier(load)表示key为load,第二个参数t.nullLiteral()表示生成一个null。

babel插件常用api

行文至此,顺便梳理下babel中的一些api,市面上很少有这方面的内容,一般都是讲解插件配置和一些demo插件的编写。以下的内容都是参考源码和注释得来的。

path相关api

有关path相关的源码都在@babel/traverse这个目录下。想要查找到符合条件的节点并进行各种各样的操作,都要依赖这部分的api,官方文档基本等于没有,相关api的用法只有自己扒源码,源码中的api功能大致通过名字可以猜出来,大家可以先有个印象,以后有对应的需求再去查找具体用法。

ancestry.js相关api

这一部分的api主要是查找当前节点的祖先节点和有关判断,具体使用规则只有看源码自行体会。

//  从当前节点上溯,传入回调函数,通过函数来判断返回什么节点,从自己开始
exports.findParent = findParent;
//  从当前节点上溯,传入回调函数,通过函数来判断返回什么节点,从自己的父节点开始找
exports.find = find;
//  查找第一个函数式父组件
exports.getFunctionParent = getFunctionParent;
//  查找其声明的父组件(感觉指的是react中继承的父组件)
exports.getStatementParent = getStatementParent;
//  传入一个path,获取最上层的常规祖先节点
exports.getEarliestCommonAncestorFrom = getEarliestCommonAncestorFrom;
//  传入一个path,获取最底层的常规祖先节点
exports.getDeepestCommonAncestorFrom = getDeepestCommonAncestorFrom;
//  返回一个包含所有祖先的数组
exports.getAncestry = getAncestry;
//  传入一个节点,判断当前的节点是否是传入节点的祖先
exports.isAncestor = isAncestor;
//  传入一个节点,判断当前节点是否是出入节点的子节点
exports.isDescendant = isDescendant;
//  上溯,传入一个类型数组,判断所有节点中是否是数组中的类型
exports.inType = inType;

comments.js相关api

这一部分的api主要是查找跟注释相关的节点

//  和兄弟元素共享注释
exports.shareCommentsWithSiblings = shareCommentsWithSiblings;
//  添加单条注释
exports.addComment = addComment;
//  添加多行注释
exports.addComments = addComments;

context.js相关api

主要是跟当前访问上下文相关的api

//  调用一系列函数,返回布尔值
exports.call = call;
//  内部方法,配合call使用
exports._call = _call;
//  当前节点的类型是否在黑名单中
exports.isBlacklisted = isBlacklisted;
//  访问一个节点,返回布尔值,是否应该停止访问
exports.visit = visit;
//  标记跳过
exports.skip = skip;
//  置skipkey
exports.skipKey = skipKey;
//  停止
exports.stop = stop;
//  设置scope
exports.setScope = setScope;
//  设置上下文
exports.setContext = setContext;
//  再同步相关
exports.resync = resync;
exports._resyncParent = _resyncParent;
exports._resyncKey = _resyncKey;
exports._resyncList = _resyncList;
exports._resyncRemoved = _resyncRemoved;、
//  上下文出栈
exports.popContext = popContext;
//  上下文入栈
exports.pushContext = pushContext;
//  设置
exports.setup = setup;
exports.setKey = setKey;
//  重新入队
exports.requeue = requeue;
//  获取队列上下文
exports._getQueueContexts = _getQueueContexts;

conversion.js相关api

//  获取节点的key
exports.toComputedKey = toComputedKey;
//  讲一个节点变成语句块
exports.ensureBlock = ensureBlock;
//  将箭头函数表达式变成普通函数
exports.arrowFunctionToShadowed = arrowFunctionToShadowed;
//  去掉函数环境的封装?
exports.unwrapFunctionEnvironment = unwrapFunctionEnvironment;
//  类似arrowFunctionToShadowed
exports.arrowFunctionToExpression = arrowFunctionToExpression;

evaluation.js相关api

这里是进入输入节点并且做静态分析,看返回的值是true或者false,如果不能确定,返回undefined

//  返回true、false或者undefined
exports.evaluateTruthy = evaluateTruthy;
//  返回一个对象,里面有详细信息
exports.evaluate = evaluate;

family.js相关api

这个文件主要处理子元素和兄弟元素

//  获得对位的兄弟元素
exports.getOpposite = getOpposite;
//  获得完整路径记录
exports.getCompletionRecords = getCompletionRecords;
//  传入key,获得兄弟节点
exports.getSibling = getSibling;
//  获得上一个兄弟节点
exports.getPrevSibling = getPrevSibling;
//  获得下一个兄弟节点
exports.getNextSibling = getNextSibling;
//  获得所有的下方的兄弟节点
exports.getAllNextSiblings = getAllNextSiblings;
//  获得所有上方的兄弟节点
exports.getAllPrevSiblings = getAllPrevSiblings;
//  根据key和上下文,传入节点
exports.get = get;
//  配合get使用
exports._getKey = _getKey;
//  配合get使用
exports._getPattern = _getPattern;
//  获得绑定的标识符
exports.getBindingIdentifiers = getBindingIdentifiers;
//  获得外部绑定的标识符
exports.getOuterBindingIdentifiers = getOuterBindingIdentifiers;
//  获得绑定的标识符路径
exports.getBindingIdentifierPaths = getBindingIdentifierPaths;
//  获得外部绑定的标识符路径
exports.getOuterBindingIdentifierPaths = getOuterBindingIdentifierPaths;

introspection.js相关api

此文件包含负责为某些值内省当前路径的方法。

//  输入一个pattern,返回符合条件的节点
exports.matchesPattern = matchesPattern;
//  输入一个key,判断当前节点是否含有这个属性
exports.has = has;
//  判断是否是静态节点
exports.isStatic = isStatic;
//  节点是否不含有某个输入的key,与has相反
exports.isnt = isnt;
//  传入key和value,判断当前节点上key对应的值是否等于value
exports.equals = equals;
//  输入类型字符串,判断当前节点的类型是否和出入的类型相等
exports.isNodeType = isNodeType;
//  判断当前路径是合处在for循环中。因为for循环中允许变量声明和普通的表达式,我们需要告诉path的replactment相关方法
//  在这里替换掉表达式是ok的
exports.canHaveVariableDeclarationOrExpression = canHaveVariableDeclarationOrExpression;
//  这个方法减产我们是否在将箭头函数转换为表达式或者代码块(反之亦然),这是因为
//  箭头函数会隐式地返回表达式,这和块语句类似
exports.canSwapBetweenExpressionAndStatement = canSwapBetweenExpressionAndStatement;
//  判断当前路径是否指向一个完成的记录(是否是一个容器的最后的节点)
exports.isCompletionRecord = isCompletionRecord;
//  判断当前的节点是否允许单独的声明或者块声明,以便我们在必要的时候展开
exports.isStatementOrBlock = isStatementOrBlock;
//  判断当前的指定路径引用了moduleSource的importName
exports.referencesImport = referencesImport;
//  获取当前节点对应的源码
exports.getSource = getSource;
//  有可能会提前执行
exports.willIMaybeExecuteBefore = willIMaybeExecuteBefore;
//  传入一个节点,判断其执行状态是否和当前的节点相关
exports._guessExecutionStatusRelativeTo = _guessExecutionStatusRelativeTo;
exports._guessExecutionStatusRelativeToDifferentFunctions = _guessExecutionStatusRelativeToDifferentFunctions;
//  将一个指针node节点指向绝对路径
exports.resolve = resolve;
exports._resolve = _resolve;
//  是否是固定表达式
exports.isConstantExpression = isConstantExpression;
//  是否处于严格模式
exports.isInStrictMode = isInStrictMode;
//  has的别名
exports.is = void 0;

modification.js相关api

//  在当前节点之前插入目标节点
exports.insertBefore = insertBefore;
//  传入位置和节点,在对应的位置插入节点
exports._containerInsert = _containerInsert;
//  在目标节点之前插入
exports._containerInsertBefore = _containerInsertBefore;
//  在目标节点之后插入
exports._containerInsertAfter = _containerInsertAfter;
//  在当前的节点之后插入,但在一个表达式之后插入的时候,确保完成记录是正确的
exports.insertAfter = insertAfter;
//  传入两个参数(起点,终点),更新期间所有兄弟节点的路径
exports.updateSiblingKeys = updateSiblingKeys;
//  校验节点列表
exports._verifyNodeList = _verifyNodeList;
//  容器列表头部新增一个元素
exports.unshiftContainer = unshiftContainer;
//  容器列表尾部新增一个元素
exports.pushContainer = pushContainer;
//  尽可能提升当前节点的作用域,并且返回一个可以引用的uid
exports.hoist = hoist;

removal.js相关api

移除节点相关的api

//  移除当前节点
exports.remove = remove;
//  从作用域中移除
exports._removeFromScope = _removeFromScope;
//  是否调用移除钩子
exports._callRemovalHooks = _callRemovalHooks;
//  内部的remove实现
exports._remove = _remove;
//  标记移除
exports._markRemoved = _markRemoved;
//  声明某个节点不可以出
exports._assertUnremoved = _assertUnremoved;

replacement.js相关api

跟节点替换相关api

//  将当前节点替换为一系列节点(出入的是一个节点数组),该方法按照以下步骤执行
//  1、继承传入的第一个节点的注释 2、在当前的节点后插入传入的节点 3、删除当前节点
exports.replaceWithMultiple = replaceWithMultiple;
//  将传入字符串作为表达式解析,并且将当前的节点替换为解析的结果
//  这个方法很方便,但是是反模式的,不建议使用,这会使你的应用很脆弱
exports.replaceWithSourceString = replaceWithSourceString;
//  将当前节点替换为另一个
exports.replaceWith = replaceWith;
//  内部实现
exports._replaceWith = _replaceWith;
//  输入一个声明数组并且将他们在表达式中展开。这个方法将会保持完整的记录,这对于维护原始的语义非常重要
exports.replaceExpressionWithStatements = replaceExpressionWithStatements;
//  替换行内内容
exports.replaceInline = replaceInline;

types相关api

@babel/traverse提供的海量方法能够使我们对节点进行查找和替换各种操作,搭配@babel/types,让开发者能够自行拼装AST,从而"创造"出新的代码,types相关的api非常多,建议浏览一下官网,在使用的过程中再查找对应的api。

总结

babel是前端的大杀器,是前端能力进阶的试金石,掌握之后,开启无限可能。

参考资料

文中例子代码
babel插件分析-编写你的第一个插件
babel源码仓库

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343