前言
社區(qū)里面關于babel的介紹非常多了,這里不想重復這些常見內(nèi)容。很多人認為babel只是一個語法轉譯工具,將瀏覽器無法識別的高級語法進行轉換(polyfill),提升開發(fā)體驗。其實babel是一個強大的工具鏈,它基于acorn和acorn-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是前端的大殺器,是前端能力進階的試金石,掌握之后,開啟無限可能。