babel進階用法之處理json文件

前言

社區(qū)里面關于babel的介紹非常多了,這里不想重復這些常見內(nèi)容。很多人認為babel只是一個語法轉譯工具,將瀏覽器無法識別的高級語法進行轉換(polyfill),提升開發(fā)體驗。其實babel是一個強大的工具鏈,它基于acornacorn-jsx,將js轉化為抽象語法樹(AST),抽象語法樹可以理解為一個大的對象,精細化定義了代碼的所有細節(jié)。對這個大對象進行處理后,再將其轉化為代碼,這其實就是各種各樣的babel插件代碼轉換的核心原理。本文也是從這里入手,以一個小例子,展示babel在處理json文件中的應用。

應用場景

假設有一個跟配置相關的大json文件,里面有各種各樣的key,我們需要能夠使用代碼來動態(tài)修改這個大json中的內(nèi)容,比如替換某個key的內(nèi)容,刪除某個key,新增內(nèi)容等等。這里有人可能會說了,直接使用node中的fs讀取文件并修改不可以嗎?比較簡單的文件的確可以這樣操作,但是如果json結構比較復雜,比如有很多層級或者不同層級有相同的key,直接使用字符串進行匹配和搜索將變得非常繁瑣,并且非常容易出錯。這里我們借用babel的能力將json解析成抽象語法樹,再進行對應的調(diào)整。

核心原理

讀者可能會有疑問,babel只能處理js模塊,如何處理json文件呢?json文件本身可以看做一個json對象,而babel內(nèi)部顯然是具有處理對象的能力的,為此我們只需要通過代碼將json改寫成js文件,將其傳入babel轉化成抽象語法樹,對特定的部分進行修改之后,再轉換回js代碼。把js代碼中的json對象導出,生成目標json文件。

代碼實現(xiàn)

這里舉一個簡單的例子,假設有如下的json內(nèi)容(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處理替換,替換內(nèi)容
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的內(nèi)容(以字符串的形式),然后開啟黑科技,在頭尾拼接特殊字符串,使其轉變?yōu)閖s文件的字符串形式,傳入babel.transformSync,引入我們的插件,進行處理之后,生成代碼字符串。然后使用fs.writeFileSync將生成的字符串寫成文件。最后再引用該文件,讀取json變量,對其stringfy獲得最終的內(nèi)容,在將內(nèi)容寫入最后的文件result.json。核心邏輯都在我們的babel插件中。

替換屬性的babel插件

progressJson/plugin,直接上代碼:

//  要小心循環(huán)引用,超過迭代次數(shù)還沒有出來就會自動停止,而且不會報錯
module.exports = function (babel) {
    const { types: t } = babel;
    return {
        name: 'write in new content', // not required
        visitor: {
            //  捕捉對象屬性
            //  t表示的是type,也就是各種屬性,要對節(jié)點做操作,需要對path做處理,相關api在@babel/traverse里面,市面上幾乎沒有文檔
            ObjectProperty(path) {
                //  遍歷所有的對象屬性
                const node = path.node;
                //  定位到key為ppp的對象屬性
                if (node.key.value === 'ppp') {
                    //  插入節(jié)點
                    path.insertAfter(t.objectProperty(t.identifier('load'), t.nullLiteral()));
                    //  刪除節(jié)點
                    path.remove();
                }
            }
        }
    };
};

關于babel插件的具體寫法,筆者之前寫過一篇文章,里面有babel原理的詳細分析,可以參考。這里再簡單講解下原理,。babel插件本質(zhì)是一個函數(shù),入?yún)⑹莃abel的實例,返回值是一個對象,里面的visitor用來匹配目標內(nèi)容,這里的
ObjectProperty表示遇到對象屬性時進入執(zhí)行邏輯,類似地,FunctionDeclaration表示遇到函數(shù)定義時進入操作邏輯,此外還有BinaryExpression(二元表達式)、Identifier(標識符)等等的visitor入口函數(shù)。babel將代碼解析成抽象語法樹之后,我們可以用上面提到的這些類型匹配函數(shù)來找到目標代碼的位置,具體可以參考@babel/types。這里順便吐槽下,官方的文檔給的十分粗糙,只有非常含混的類型定義,也沒有使用例子,全看個人領悟力,不參考其他babel插件的寫法來配合理解根本不知道什么意思(參考一些官方插件的寫法),體驗極差。ObjectProperty的入?yún)⑹莗ath,表示當前正在檢查的節(jié)點的路徑,可以配合AST生成工具來配合理解,在筆者的那篇博文中有詳解。t.objectProperty表示構造一個對象屬性節(jié)點,其接受兩個參數(shù),第一個參數(shù)t.identifier(load)表示key為load,第二個參數(shù)t.nullLiteral()表示生成一個null。

babel插件常用api

行文至此,順便梳理下babel中的一些api,市面上很少有這方面的內(nèi)容,一般都是講解插件配置和一些demo插件的編寫。以下的內(nèi)容都是參考源碼和注釋得來的。

path相關api

有關path相關的源碼都在@babel/traverse這個目錄下。想要查找到符合條件的節(jié)點并進行各種各樣的操作,都要依賴這部分的api,官方文檔基本等于沒有,相關api的用法只有自己扒源碼,源碼中的api功能大致通過名字可以猜出來,大家可以先有個印象,以后有對應的需求再去查找具體用法。

ancestry.js相關api

這一部分的api主要是查找當前節(jié)點的祖先節(jié)點和有關判斷,具體使用規(guī)則只有看源碼自行體會。

//  從當前節(jié)點上溯,傳入回調(diào)函數(shù),通過函數(shù)來判斷返回什么節(jié)點,從自己開始
exports.findParent = findParent;
//  從當前節(jié)點上溯,傳入回調(diào)函數(shù),通過函數(shù)來判斷返回什么節(jié)點,從自己的父節(jié)點開始找
exports.find = find;
//  查找第一個函數(shù)式父組件
exports.getFunctionParent = getFunctionParent;
//  查找其聲明的父組件(感覺指的是react中繼承的父組件)
exports.getStatementParent = getStatementParent;
//  傳入一個path,獲取最上層的常規(guī)祖先節(jié)點
exports.getEarliestCommonAncestorFrom = getEarliestCommonAncestorFrom;
//  傳入一個path,獲取最底層的常規(guī)祖先節(jié)點
exports.getDeepestCommonAncestorFrom = getDeepestCommonAncestorFrom;
//  返回一個包含所有祖先的數(shù)組
exports.getAncestry = getAncestry;
//  傳入一個節(jié)點,判斷當前的節(jié)點是否是傳入節(jié)點的祖先
exports.isAncestor = isAncestor;
//  傳入一個節(jié)點,判斷當前節(jié)點是否是出入節(jié)點的子節(jié)點
exports.isDescendant = isDescendant;
//  上溯,傳入一個類型數(shù)組,判斷所有節(jié)點中是否是數(shù)組中的類型
exports.inType = inType;

comments.js相關api

這一部分的api主要是查找跟注釋相關的節(jié)點

//  和兄弟元素共享注釋
exports.shareCommentsWithSiblings = shareCommentsWithSiblings;
//  添加單條注釋
exports.addComment = addComment;
//  添加多行注釋
exports.addComments = addComments;

context.js相關api

主要是跟當前訪問上下文相關的api

//  調(diào)用一系列函數(shù),返回布爾值
exports.call = call;
//  內(nèi)部方法,配合call使用
exports._call = _call;
//  當前節(jié)點的類型是否在黑名單中
exports.isBlacklisted = isBlacklisted;
//  訪問一個節(jié)點,返回布爾值,是否應該停止訪問
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

//  獲取節(jié)點的key
exports.toComputedKey = toComputedKey;
//  講一個節(jié)點變成語句塊
exports.ensureBlock = ensureBlock;
//  將箭頭函數(shù)表達式變成普通函數(shù)
exports.arrowFunctionToShadowed = arrowFunctionToShadowed;
//  去掉函數(shù)環(huán)境的封裝?
exports.unwrapFunctionEnvironment = unwrapFunctionEnvironment;
//  類似arrowFunctionToShadowed
exports.arrowFunctionToExpression = arrowFunctionToExpression;

evaluation.js相關api

這里是進入輸入節(jié)點并且做靜態(tài)分析,看返回的值是true或者false,如果不能確定,返回undefined

//  返回true、false或者undefined
exports.evaluateTruthy = evaluateTruthy;
//  返回一個對象,里面有詳細信息
exports.evaluate = evaluate;

family.js相關api

這個文件主要處理子元素和兄弟元素

//  獲得對位的兄弟元素
exports.getOpposite = getOpposite;
//  獲得完整路徑記錄
exports.getCompletionRecords = getCompletionRecords;
//  傳入key,獲得兄弟節(jié)點
exports.getSibling = getSibling;
//  獲得上一個兄弟節(jié)點
exports.getPrevSibling = getPrevSibling;
//  獲得下一個兄弟節(jié)點
exports.getNextSibling = getNextSibling;
//  獲得所有的下方的兄弟節(jié)點
exports.getAllNextSiblings = getAllNextSiblings;
//  獲得所有上方的兄弟節(jié)點
exports.getAllPrevSiblings = getAllPrevSiblings;
//  根據(jù)key和上下文,傳入節(jié)點
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

此文件包含負責為某些值內(nèi)省當前路徑的方法。

//  輸入一個pattern,返回符合條件的節(jié)點
exports.matchesPattern = matchesPattern;
//  輸入一個key,判斷當前節(jié)點是否含有這個屬性
exports.has = has;
//  判斷是否是靜態(tài)節(jié)點
exports.isStatic = isStatic;
//  節(jié)點是否不含有某個輸入的key,與has相反
exports.isnt = isnt;
//  傳入key和value,判斷當前節(jié)點上key對應的值是否等于value
exports.equals = equals;
//  輸入類型字符串,判斷當前節(jié)點的類型是否和出入的類型相等
exports.isNodeType = isNodeType;
//  判斷當前路徑是合處在for循環(huán)中。因為for循環(huán)中允許變量聲明和普通的表達式,我們需要告訴path的replactment相關方法
//  在這里替換掉表達式是ok的
exports.canHaveVariableDeclarationOrExpression = canHaveVariableDeclarationOrExpression;
//  這個方法減產(chǎn)我們是否在將箭頭函數(shù)轉換為表達式或者代碼塊(反之亦然),這是因為
//  箭頭函數(shù)會隱式地返回表達式,這和塊語句類似
exports.canSwapBetweenExpressionAndStatement = canSwapBetweenExpressionAndStatement;
//  判斷當前路徑是否指向一個完成的記錄(是否是一個容器的最后的節(jié)點)
exports.isCompletionRecord = isCompletionRecord;
//  判斷當前的節(jié)點是否允許單獨的聲明或者塊聲明,以便我們在必要的時候展開
exports.isStatementOrBlock = isStatementOrBlock;
//  判斷當前的指定路徑引用了moduleSource的importName
exports.referencesImport = referencesImport;
//  獲取當前節(jié)點對應的源碼
exports.getSource = getSource;
//  有可能會提前執(zhí)行
exports.willIMaybeExecuteBefore = willIMaybeExecuteBefore;
//  傳入一個節(jié)點,判斷其執(zhí)行狀態(tài)是否和當前的節(jié)點相關
exports._guessExecutionStatusRelativeTo = _guessExecutionStatusRelativeTo;
exports._guessExecutionStatusRelativeToDifferentFunctions = _guessExecutionStatusRelativeToDifferentFunctions;
//  將一個指針node節(jié)點指向絕對路徑
exports.resolve = resolve;
exports._resolve = _resolve;
//  是否是固定表達式
exports.isConstantExpression = isConstantExpression;
//  是否處于嚴格模式
exports.isInStrictMode = isInStrictMode;
//  has的別名
exports.is = void 0;

modification.js相關api

//  在當前節(jié)點之前插入目標節(jié)點
exports.insertBefore = insertBefore;
//  傳入位置和節(jié)點,在對應的位置插入節(jié)點
exports._containerInsert = _containerInsert;
//  在目標節(jié)點之前插入
exports._containerInsertBefore = _containerInsertBefore;
//  在目標節(jié)點之后插入
exports._containerInsertAfter = _containerInsertAfter;
//  在當前的節(jié)點之后插入,但在一個表達式之后插入的時候,確保完成記錄是正確的
exports.insertAfter = insertAfter;
//  傳入兩個參數(shù)(起點,終點),更新期間所有兄弟節(jié)點的路徑
exports.updateSiblingKeys = updateSiblingKeys;
//  校驗節(jié)點列表
exports._verifyNodeList = _verifyNodeList;
//  容器列表頭部新增一個元素
exports.unshiftContainer = unshiftContainer;
//  容器列表尾部新增一個元素
exports.pushContainer = pushContainer;
//  盡可能提升當前節(jié)點的作用域,并且返回一個可以引用的uid
exports.hoist = hoist;

removal.js相關api

移除節(jié)點相關的api

//  移除當前節(jié)點
exports.remove = remove;
//  從作用域中移除
exports._removeFromScope = _removeFromScope;
//  是否調(diào)用移除鉤子
exports._callRemovalHooks = _callRemovalHooks;
//  內(nèi)部的remove實現(xiàn)
exports._remove = _remove;
//  標記移除
exports._markRemoved = _markRemoved;
//  聲明某個節(jié)點不可以出
exports._assertUnremoved = _assertUnremoved;

replacement.js相關api

跟節(jié)點替換相關api

//  將當前節(jié)點替換為一系列節(jié)點(出入的是一個節(jié)點數(shù)組),該方法按照以下步驟執(zhí)行
//  1、繼承傳入的第一個節(jié)點的注釋 2、在當前的節(jié)點后插入傳入的節(jié)點 3、刪除當前節(jié)點
exports.replaceWithMultiple = replaceWithMultiple;
//  將傳入字符串作為表達式解析,并且將當前的節(jié)點替換為解析的結果
//  這個方法很方便,但是是反模式的,不建議使用,這會使你的應用很脆弱
exports.replaceWithSourceString = replaceWithSourceString;
//  將當前節(jié)點替換為另一個
exports.replaceWith = replaceWith;
//  內(nèi)部實現(xiàn)
exports._replaceWith = _replaceWith;
//  輸入一個聲明數(shù)組并且將他們在表達式中展開。這個方法將會保持完整的記錄,這對于維護原始的語義非常重要
exports.replaceExpressionWithStatements = replaceExpressionWithStatements;
//  替換行內(nèi)內(nèi)容
exports.replaceInline = replaceInline;

types相關api

@babel/traverse提供的海量方法能夠使我們對節(jié)點進行查找和替換各種操作,搭配@babel/types,讓開發(fā)者能夠自行拼裝AST,從而"創(chuàng)造"出新的代碼,types相關的api非常多,建議瀏覽一下官網(wǎng),在使用的過程中再查找對應的api。

總結

babel是前端的大殺器,是前端能力進階的試金石,掌握之后,開啟無限可能。

參考資料

文中例子代碼
babel插件分析-編寫你的第一個插件
babel源碼倉庫

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容