美文网首页
babel进阶用法之处理json文件

babel进阶用法之处理json文件

作者: 广兰路地铁 | 来源:发表于2020-10-20 10:20 被阅读0次

    前言

    社区里面关于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源码仓库

    相关文章

      网友评论

          本文标题:babel进阶用法之处理json文件

          本文链接:https://www.haomeiwen.com/subject/aogrmktx.html