保姆級(jí)教學(xué)!這次一定學(xué)會(huì)babel插件開(kāi)發(fā)!
如果你有babel相關(guān)知識(shí)基礎(chǔ)建議直接跳過(guò) 前置知識(shí) 部分,直接前往 "插件編寫(xiě)" 部分。
前置知識(shí)
什么是AST
學(xué)習(xí)babel, 必備知識(shí)就是理解AST。
那什么是AST呢?
先來(lái)看下維基百科的解釋:
在計(jì)算機(jī)科學(xué)中,抽象語(yǔ)法樹(shù)(Abstract Syntax Tree,AST),或簡(jiǎn)稱(chēng)語(yǔ)法樹(shù)(Syntax tree),是源代碼語(yǔ)法結(jié)構(gòu)的一種抽象表示。它以樹(shù)狀的形式表現(xiàn)編程語(yǔ)言的語(yǔ)法結(jié)構(gòu),樹(shù)上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)
"源代碼語(yǔ)法結(jié)構(gòu)的一種抽象表示" 這幾個(gè)字要?jiǎng)澲攸c(diǎn),是我們理解AST的關(guān)鍵,說(shuō)人話就是按照某種約定好的規(guī)范,以樹(shù)形的數(shù)據(jù)結(jié)構(gòu)把我們的代碼描述出來(lái),讓js引擎和轉(zhuǎn)譯器能夠理解。
舉個(gè)例子:就好比現(xiàn)在框架會(huì)利用虛擬dom這種方式把真實(shí)dom結(jié)構(gòu)描述出來(lái)再進(jìn)行操作一樣,而對(duì)于更底層的代碼來(lái)說(shuō),AST就是用來(lái)描述代碼的好工具。
當(dāng)然AST不是JS特有的,每個(gè)語(yǔ)言的代碼都能轉(zhuǎn)換成對(duì)應(yīng)的AST, 并且AST結(jié)構(gòu)的規(guī)范也有很多, js里所使用的規(guī)范大部分是 estree[1] ,當(dāng)然這個(gè)只做簡(jiǎn)單了解即可。
AST到底長(zhǎng)啥樣
了解了AST的基本概念, 那AST到底長(zhǎng)啥樣呢?
astexplorer.net[2]這個(gè)網(wǎng)站可以在線生成AST, 我們可以在里面進(jìn)行嘗試生成AST,用來(lái)學(xué)習(xí)一下結(jié)構(gòu)
babel的處理過(guò)程
問(wèn):把冰箱塞進(jìn)大象有幾個(gè)階段?
打開(kāi)冰箱 -> 塞進(jìn)大象 -> 關(guān)上冰箱
babel也是如此,babel利用AST的方式對(duì)代碼進(jìn)行編譯,首先自然是需要將代碼變?yōu)锳ST,再對(duì)AST進(jìn)行處理,處理完以后呢再將AST 轉(zhuǎn)換回來(lái)
也就是如下的流程
code轉(zhuǎn)換為AST -> 處理AST -> AST轉(zhuǎn)換為code
然后我們?cè)俳o它們一個(gè)專(zhuān)業(yè)一點(diǎn)的名字
解析 -> 轉(zhuǎn)換 -> 生成
解析(parse)
通過(guò) parser 把源碼轉(zhuǎn)成抽象語(yǔ)法樹(shù)(AST)
這個(gè)階段的主要任務(wù)就是將code轉(zhuǎn)為AST, 其中會(huì)經(jīng)過(guò)兩個(gè)階段,分別是詞法分析和語(yǔ)法分析。當(dāng)parse階段開(kāi)始時(shí),首先會(huì)進(jìn)行文檔掃描,并在此期間進(jìn)行詞法分析。那怎么理解此法分析呢 如果把我們所寫(xiě)的一段code比喻成句子,詞法分析所做的事情就是在拆分這個(gè)句子。如同 “我正在吃飯” 這句話,可以被拆解為“我”、“正在”、“吃飯”一樣, code也是如此。比如: const a = '1' 會(huì)被拆解為一個(gè)個(gè)最細(xì)粒度的單詞(tokon): 'const', 'a', '=', '1' 這就是詞法分析階段所做的事情。
詞法分析結(jié)束后,將分析所得到的 tokens 交給語(yǔ)法分析, 語(yǔ)法分析階段的任務(wù)就是根據(jù) tokens 生成 AST。它會(huì)對(duì) tokens 進(jìn)行遍歷,最終按照特定的結(jié)構(gòu)生成一個(gè) tree 這個(gè) tree 就是 AST。
如下圖, 可以看到上面語(yǔ)句的到的結(jié)構(gòu),我們找到了幾個(gè)重要信息, 最外層是一個(gè)VariableDeclaration意思是變量聲明,所使用的類(lèi)型是 const, 字段declarations內(nèi)還有一個(gè) VariableDeclarator[變量聲明符] 對(duì)象,找到了 a, 1 兩個(gè)關(guān)鍵字。
除了這些關(guān)鍵字以為,還可以找到例如行號(hào)等等的重要信息,這里就不一一展開(kāi)闡述。總之,這就是我們最終得到的 AST 模樣。
那問(wèn)題來(lái)了,babel里該如何將code 轉(zhuǎn)為 AST 呢?在這個(gè)階段我們會(huì)用到 babel 提供的解析器 @babel/parser,之前叫 Babylon,它并非由babel團(tuán)隊(duì)自己開(kāi)發(fā)的,而是基于fork的 acorn 項(xiàng)目。
它為我們提供了將code轉(zhuǎn)換為AST的方法,基本用法如下:

更多信息可以訪問(wèn)官方文檔查看@babel/parser[3]
轉(zhuǎn)換(transform)
在 parse 階段后,我們已經(jīng)成功得到了AST。babel接收到 AST后,會(huì)使用 @babel/traverse 對(duì)其進(jìn)行深度優(yōu)先遍歷,插件會(huì)在這個(gè)階段被觸發(fā),以vistor 函數(shù)的形式訪問(wèn)每種不同類(lèi)型的AST節(jié)點(diǎn)。以上面代碼為例, 我們可以編寫(xiě) VariableDeclaration 函數(shù)對(duì) VariableDeclaration節(jié)點(diǎn)進(jìn)行訪問(wèn),每當(dāng)遇到該類(lèi)型節(jié)點(diǎn)時(shí)都會(huì)觸發(fā)該方法。如下:

該方法接受兩個(gè)參數(shù),
path
path為當(dāng)前訪問(wèn)的路徑, 并且包含了節(jié)點(diǎn)的信息、父節(jié)點(diǎn)信息以及對(duì)節(jié)點(diǎn)操作許多方法。可以利用這些方法對(duì) ATS 進(jìn)行添加、更新、移動(dòng)和刪除等等。
state
state包含了當(dāng)前plugin的信息和參數(shù)信息等等,并且也可以用來(lái)自定義在節(jié)點(diǎn)之間傳遞數(shù)據(jù)。
生成(generate)
generate:把轉(zhuǎn)換后的 AST 打印成目標(biāo)代碼,并生成 sourcemap
這個(gè)階段就比較簡(jiǎn)單了, 在 transform 階段處理 AST 結(jié)束后,該階段的任務(wù)就是將 AST 轉(zhuǎn)換回 code, 在此期間會(huì)對(duì) AST 進(jìn)行深度優(yōu)先遍歷,根據(jù)節(jié)點(diǎn)所包含的信息生成對(duì)應(yīng)的代碼,并且會(huì)生成對(duì)應(yīng)的sourcemap。
經(jīng)典案例嘗試
俗話說(shuō),最好的學(xué)習(xí)就是動(dòng)手,我們來(lái)一起嘗試一個(gè)簡(jiǎn)單的經(jīng)典案例:將上面案例中的 es6 的 const 轉(zhuǎn)變?yōu)?es5 的 var
第一步: 轉(zhuǎn)換為 AST
使用 @babel/parser 生成AST
比較簡(jiǎn)單,跟上面的案例是一樣的, 此時(shí)我們ast變量中就是轉(zhuǎn)換后的 AST
const?parser?=?require('@babel/parser');
const?ast?=?parser.parse('const?a?=?1');
復(fù)制代碼
第二步:處理 AST
使用 @babel/traverse 處理 AST
在這個(gè)階段我們通過(guò)分析所生成的 AST 結(jié)構(gòu),確定了在 VariableDeclaration 中由 kind 字段控制 const,所以我們是不是可以嘗試著把 kind 改寫(xiě)成我們想要的 var ?既然如此,我們來(lái)嘗試一下

const?parser?=?require('@babel/parser');
const?traverse?=?require('@babel/traverse').default
const?ast?=?parser.parse('const?a?=?1');
traverse(ast,?{
????VariableDeclaration(path,?state)?{
???//?通過(guò)?path.node?訪問(wèn)實(shí)際的?AST?節(jié)點(diǎn)
??????path.node.kind?=?'var'
????}
});
復(fù)制代碼
好,此時(shí)我們憑借著猜想修改了 kind ,將其改寫(xiě)為了 var, 但是我們還不能知道實(shí)際是否有效,所以我們需要將其再轉(zhuǎn)換回 code 看看效果。
第三步:生成 code
使用 @babel/generator 處理 AST
const?parser?=?require('@babel/parser');
const?traverse?=?require('@babel/traverse').default
const?generate?=?require('@babel/generator').default
const?ast?=?parser.parse('const?a?=?1');
traverse(ast,?{
????VariableDeclaration(path,?state)?{
??????path.node.kind?=?'var'
????}
});
//?將處理好的?AST?放入?generate
const?transformedCode?=?generate(ast).code
console.log(transformedCode)
復(fù)制代碼
我們?cè)賮?lái)看看效果:

執(zhí)行完成,成功了,是我們想要的效果~
如何開(kāi)發(fā)插件
通過(guò)上面這個(gè)經(jīng)典案例, 大概了解了 babel 的使用,但我們平時(shí)的插件該如何去寫(xiě)呢?
實(shí)際上插件的開(kāi)發(fā)和上面的基本思路是一樣的, 只是作為插件我們只需要關(guān)注這其中的 轉(zhuǎn)換 階段
我們的插件需要導(dǎo)出一個(gè)函數(shù)/對(duì)象, 如果是函數(shù)則需要返回一個(gè)對(duì)象, 我們只需要在改對(duì)象的 visitor 內(nèi)做同樣的事情即可,并且函數(shù)會(huì)接受幾個(gè)參數(shù), api繼承了babel提供的一系列方法, options 是我們使用插件時(shí)所傳遞的參數(shù),dirname 為處理時(shí)期的文件路徑。
以上面的案例改造為如下:
module.exports?=?{
?visitor:?{
?????VariableDeclaration(path,?state)?{
??????????path.node.kind?=?'var'
????????}
?}
}
//?或是函數(shù)形式
module.exports?=?(api,?options,?dirname)?=>?{
?return?{
??visitor:?{
??????????VariableDeclaration(path,?state)?{
????????????path.node.kind?=?'var'
??????????}
??}
?}
}
復(fù)制代碼
插件編寫(xiě)
在有前置知識(shí)的基礎(chǔ)上,我們來(lái)一步步的講解開(kāi)發(fā)一個(gè) babel 插件。首先我們明確接下來(lái)要開(kāi)發(fā)的插件的核心需求:
可自動(dòng)插入某個(gè)函數(shù)并調(diào)用。 自動(dòng)導(dǎo)入插入函數(shù)的相關(guān)依賴(lài)。 可以通過(guò)注釋指定需要插入的函數(shù)和需要被插入的函數(shù),若未用注釋指定則默認(rèn)插入位置在第一列。
基本效果展示如下:
處理前
//?log?聲明需要被插入并被調(diào)用的方法
//?@inject:log
function?fn()?{
?console.log(1)
?//?用?@inject:code指定插入行
?//?@inject:code
?console.log(2)
}
復(fù)制代碼
處理后
//?導(dǎo)入包?xxx?之后要在插件參數(shù)內(nèi)提供配置
import?log?from?'xxx'
function?fn()?{
?console.log(1)
?log()
?console.log(2)
}
復(fù)制代碼
思路整理
了解了大概的需求,先不著急動(dòng)手,我們要先想想要怎么開(kāi)始做,已經(jīng)設(shè)想一下過(guò)程中需要處理的問(wèn)題。
找到帶有 @inject 標(biāo)記的函數(shù),再查看其內(nèi)部是否有 @inject:code 的位置標(biāo)記。 導(dǎo)入所有插入函數(shù)的相應(yīng)包。 匹配到了標(biāo)記,要做的就是插入函數(shù),同時(shí)我們還要需要處理各種情況下的函數(shù),如:對(duì)象方法、iife、箭頭函數(shù)等等情況。
設(shè)計(jì)插件參數(shù)
為了提升插件的靈活度,我們需要設(shè)計(jì)一個(gè)較為合適的參數(shù)規(guī)則。插件參數(shù)接受一個(gè)對(duì)象。
key 作為插入函數(shù)的函數(shù)名。
kind 表示導(dǎo)入形式。有三種導(dǎo)入方式 named 、 default、 namespaced, 此設(shè)計(jì)參考 babel-helper-module-imports[4]
named 對(duì)應(yīng) import { a } from "b"形式default 對(duì)應(yīng) import a from "b"形式namespaced 對(duì)應(yīng) import * as a from "b"形式require 為依賴(lài)的包名
比如,我需要插入 log 方法,它需要從 log4js 這個(gè)包里導(dǎo)入,并且是以 named 形式, 參數(shù)便為如下形式。
//?babel.config.js
module.exports?=?{
??plugins:?[
?//?填寫(xiě)我們的plugin的js?文件地址
????['./babel-plugin-myplugin.js',?{
??????log:?{
????????//?導(dǎo)入方式為?named
????????kind:?'named',
????????require:?'log4js'
??????}
????}]
??]
}
復(fù)制代碼
起步
好,知道了具體要做什么事情并且設(shè)計(jì)好了參數(shù)的規(guī)則, 我們就可以開(kāi)始動(dòng)手了。
首先我們進(jìn)入 astexplorer.net/[5] 將待處理的 code 生成 AST 方便我們梳理結(jié)構(gòu), 然后我們?cè)谶M(jìn)行具體編碼
首先是函數(shù)聲明語(yǔ)句,我們分析一下其 AST 結(jié)構(gòu)以及該如何處理, 來(lái)看一下demo
//?@inject:log
function?fn()?{
?console.log('fn')
}
復(fù)制代碼
其生成的 AST 結(jié)構(gòu)如下,可以看到有比較關(guān)鍵的兩個(gè)屬性:
leadingComments 表示前方注釋?zhuān)梢钥吹絻?nèi)部有一個(gè)元素,就是我們demo里所寫(xiě)的 @inject:logbody 是函數(shù)體的具體內(nèi)容, demo 所寫(xiě)的 console.log('fn')此時(shí)就在里面,我們等會(huì)代碼的插入操作就是需要操作它

好,知道了可以通過(guò) leadingComments 來(lái)獲知函數(shù)是否需要被插入, 對(duì) body 操作可以實(shí)現(xiàn)我們的代碼插入需求。。
首先我們得先找到 FunctionDeclaration 這一層,因?yàn)橹挥羞@一層才有 leadingComments 屬性, 然后我們需要遍歷它,匹配出需要插入的函數(shù)。再將匹配到的函數(shù)插入至 body 只中, 但我們這里需要注意可插入的body 所在層級(jí), FunctionDeclaration 內(nèi)的body 他不是一個(gè)數(shù)組而是 BlockStatement,這表示函數(shù)的函數(shù)體,并且它也有body , 所以我們實(shí)際操作位置就在這個(gè)BlockStatement 的 body 內(nèi)

代碼如下:
module.exports?=?(api,?options,?dirname)?=>?{
??return?{
????visitor:?{
???//?匹配函數(shù)聲明節(jié)點(diǎn)
??????FunctionDeclaration(path,?state)?{
????????//?path.get('body')?相當(dāng)于?path.node.body
????????const?pathBody?=?path.get('body')
????????if(path.node.leadingComments)?{
??????????//?過(guò)濾出所有匹配?@inject:xxx?字符?的注釋
??????????const?leadingComments?=?path.node.leadingComments.filter(comment?=>?/\@inject:(\w+)/.test(comment.value)?)
??????????leadingComments.forEach(comment?=>?{
????????????const?injectTypeMatchRes?=?comment.value.match(/\@inject:(\w+)/)
????????????//?匹配成功
????????????if(?injectTypeMatchRes?)?{
??????????????//?匹配結(jié)果的第一個(gè)為?@inject:xxx?中的?xxx?,??我們將它取出來(lái)
??????????????const?injectType?=?injectTypeMatchRes[1]
??????????????//?獲取插件參數(shù)的?key,?看xxx?是否在插件的參數(shù)中聲明過(guò)
??????????????const?sourceModuleList?=?Object.keys(options)
??????????????if(?sourceModuleList.includes(injectType)?)?{
????????????????//?搜索body?內(nèi)部是否有?@code:xxx?注釋
????????????????//?因?yàn)闊o(wú)法直接訪問(wèn)到?comment,所以需要訪問(wèn)?body內(nèi)每個(gè)?AST?節(jié)點(diǎn)的?leadingComments?屬性
????????????????const?codeIndex?=?pathBody.node.body.findIndex(block?=>?block.leadingComments?&&?block.leadingComments.some(comment?=>?new?RegExp(`@code:\s?${injectType}`).test(comment.value)?))
????????????????//?未聲明則默認(rèn)插入位置為第一行
????????????????if(?codeIndex?===?-1?)?{
??????????????????//?操作`BlockStatement`?的?body
?????????pathBody.node.body.unshift(api.template.statement(`${state.options[injectType].identifierName}()`)());
????????????????}else?{
??????????????????pathBody.node.body.splice(codeIndex,?0,?api.template.statement(`${state.options[injectType].identifierName}()`)());
????????????????}
??????????????}
????????????}
??????????})
????????}
??????}
??}
})
復(fù)制代碼
編寫(xiě)完后我們看看結(jié)果, log成功被插入了, 因?yàn)槲覀儧](méi)有使用 @code:log所以就默認(rèn)插入在了第一行
然后我們?cè)囋囀褂?@code:log 標(biāo)識(shí)符, 我們將 demo 的代碼改為如下
//?@inject:log
function?fn()?{
?console.log('fn')
?//?@code:log
}
復(fù)制代碼
再次運(yùn)行代碼查看結(jié)果, 確實(shí)是在 @code:log 位置成功插入了
處理完了我們第一個(gè)案例函數(shù)聲明,這時(shí)候可能有人會(huì)問(wèn)了, 那箭頭函數(shù)這種沒(méi)有函數(shù)體的你怎么辦, 比如:
//?@inject:log
()?=>?true
復(fù)制代碼
這有問(wèn)題嗎?沒(méi)有問(wèn)題!

沒(méi)有函數(shù)體我們給它一個(gè)函數(shù)體就是了,怎么做呢?
首先我們還是先學(xué)會(huì)來(lái)分析一下 AST 結(jié)構(gòu), 首先看到最外層其實(shí)是一個(gè)ExpressionStatement表達(dá)式聲明,然后其內(nèi)部才是 ArrowFunctionExpression箭頭函數(shù)表達(dá)式, 可見(jiàn)跟我們之前的函數(shù)聲明生成的結(jié)構(gòu)是大有不同, 其實(shí)我們不用被這么多層結(jié)構(gòu)迷了眼睛,我們只需要找對(duì)我們有用的信息就可以了,一句話:哪一層有 leadingComments 我們就找哪一層。這里的 leadingComments 在 ExpressionStatement 上,所以我們找它就行

分析完了結(jié)構(gòu),那怎么判斷是否有函數(shù)體呢?還記得上面處理函數(shù)聲明時(shí),我們?cè)?body 中看到的 BlockStatement 嗎,而你看到我們箭頭函數(shù)的 body 卻是 BooleanLiteral。所以,我們可以判斷其 body 類(lèi)型來(lái)得知是否有函數(shù)體 具體方法可以使用babel 提供的類(lèi)型判斷方法 path.isBlockStatement() 來(lái)區(qū)分是否有函數(shù)體。
module.exports?=?(api,?options,?dirname)?=>?{
??return?{
????visitor:?{
??????ExpressionStatement(path,?state)?{
????????//?訪問(wèn)到?ArrowFunctionExpression
????????const?expression?=?path.get('expression')
????????const?pathBody?=?expression.get('body')
????????if(path.node.leadingComments)?{
??????????//?正則匹配?comment?是否有?@inject:xxx?字符
??????????const?leadingComments?=?path.node.leadingComments.filter(comment?=>?/\@inject:(\w+)/.test(comment.value)?)
??????????
??????????leadingComments.forEach(comment?=>?{
????????????const?injectTypeMatchRes?=?comment.value.match(/\@inject:(\w+)/)
????????????//?匹配成功
????????????if(?injectTypeMatchRes?)?{
??????????????//?匹配結(jié)果的第一個(gè)為?@inject:xxx?中的?xxx?,??我們將它取出來(lái)
??????????????const?injectType?=?injectTypeMatchRes[1]
??????????????//?獲取插件參數(shù)的?key,?看xxx?是否在插件的參數(shù)中聲明過(guò)
??????????????const?sourceModuleList?=?Object.keys(options)
??????????????if(?sourceModuleList.includes(injectType)?)?{
????????????????//?判斷是否有函數(shù)體
????????????????if?(pathBody.isBlockStatement())?{
??????????????????//?搜索body?內(nèi)部是否有?@code:xxx?注釋
??????????????????//?因?yàn)闊o(wú)法直接訪問(wèn)到?comment,所以需要訪問(wèn)?body內(nèi)每個(gè)?AST?節(jié)點(diǎn)的?leadingComments?屬性
??????????????????const?codeIndex?=?pathBody.node.body.findIndex(block?=>?block.leadingComments?&&?block.leadingComments.some(comment?=>?new?RegExp(`@code:\s?${injectType}`).test(comment.value)?))
??????????????????//?未聲明則默認(rèn)插入位置為第一行
??????????????????if(?codeIndex?===?-1?)?{
????????????????????pathBody.node.body.unshift(api.template.statement(`${injectType}()`)());
??????????????????}else?{
????????????????????pathBody.node.body.splice(codeIndex,?0,?api.template.statement(`${injectType}()`)());
??????????????????}
????????????????}else?{
??????????????????//?無(wú)函數(shù)體情況
??????????????????//?使用?ast?提供的?`@babel/template`??api?,?用代碼段生成?ast
??????????????????const?ast?=?api.template.statement(`{${injectType}();return?BODY;}`)({BODY:?pathBody.node});
?????//?替換原本的body
??????????????????pathBody.replaceWith(ast);
????????????????}
??????????????}
????????????}
??????????})
????????}
??????}
??}
}
}
復(fù)制代碼
可以看到除了新增的函數(shù)體判斷,生成函數(shù)體插入代碼再用新的 AST 替換原本的節(jié)點(diǎn),除掉這些之外,大體上的邏輯跟之前的函數(shù)聲明的處理過(guò)程沒(méi)有區(qū)別。
生成 AST 所使用的
@babel/template的 API 相關(guān)用法可以查看文檔 @babel/template[6]
針對(duì)不同情況的下的函數(shù)大體上相同,總結(jié)就是:
分析 AST 找到 leadingComments 所在節(jié)點(diǎn) -> 找到可插入的 body 所在節(jié)點(diǎn) -> 編寫(xiě)插入邏輯
實(shí)際處理的情況還有很多,如:對(duì)象屬性、iife、函數(shù)表達(dá)式等很多, 處理思路都是一樣的,這里就不過(guò)重復(fù)闡述。我會(huì)將插件完整代碼發(fā)在文章底部。
自動(dòng)引入
第一條完成了,那需求的第二條,我們使用的包如何自動(dòng)引入呢, 如上面案例使用的 log4js, 那么我們處理后的代碼就應(yīng)該自動(dòng)加上:
import?{?log?}?from?'log4js'
復(fù)制代碼
此時(shí),我們可以思考一下,我們需要處理以下兩種情況
log 已經(jīng)被導(dǎo)入過(guò)了 log 變量名已經(jīng)被占用
針對(duì) 問(wèn)題1 我們需要先檢索一下是否有導(dǎo)入過(guò) log4js ,并且以 named 的形式導(dǎo)入了 log 針對(duì) 問(wèn)題2 我們需要給 log 一個(gè)唯一的別名, 并且要保證在后續(xù)的代碼插入中也使用這個(gè)別名。所以這就要求了我們要在文件的一開(kāi)始就處理完成自動(dòng)引入的邏輯。
有了大概的思路,但是我們?nèi)绾翁崆巴瓿勺詣?dòng)引入邏輯呢。抱著疑問(wèn),我們?cè)賮?lái)看看 AST 的結(jié)構(gòu)??梢钥吹?AST 最外層是 File 節(jié)點(diǎn), 他有一個(gè) comments 屬性,它包含了當(dāng)前文件里所有的注釋?zhuān)辛诉@個(gè)我們就可以解析出文件里需要插入的函數(shù),并提前進(jìn)行引入。我們?cè)偻驴矗?內(nèi)部是一個(gè) Program, 我們將首先訪問(wèn)它, 因?yàn)樗鼤?huì)在其他類(lèi)型的節(jié)點(diǎn)之前被調(diào)用,所以我們要在此階段實(shí)現(xiàn)自動(dòng)引入邏輯。
小知識(shí):babel 提供了 path.traverse 方法,可以用來(lái)同步訪問(wèn)處理當(dāng)前節(jié)點(diǎn)下的子節(jié)點(diǎn)。
如圖:
代碼如下:
const?importModule?=?require('@babel/helper-module-imports');
//?......
{
????visitor:?{
??????Program(path,?state)?{
????????//?拷貝一份options?掛在?state?上,??原本的?options?不能操作
????????state.options?=?JSON.parse(JSON.stringify(options))
????????path.traverse({
??????????//?首先訪問(wèn)原有的?import?節(jié)點(diǎn),?檢測(cè)?log?是否已經(jīng)被導(dǎo)入過(guò)
??????????ImportDeclaration?(curPath)?{
????????????const?requirePath?=?curPath.get('source').node.value;
????????????//?遍歷options
????????????Object.keys(state.options).forEach(key?=>?{
??????????????const?option?=?state.options[key]
??????????????//?判斷包相同
??????????????if(?option.require?===?requirePath?)?{
????????????????const?specifiers?=?curPath.get('specifiers')
????????????????specifiers.forEach(specifier?=>?{
??????????????????//?如果是默認(rèn)type導(dǎo)入
??????????????????if(?option.kind?===?'default'?)?{
????????????????????//?判斷導(dǎo)入類(lèi)型
????????????????????if(?specifier.isImportDefaultSpecifier()?)?{
??????????????????????//?找到已有?default?類(lèi)型的引入
??????????????????????if(?specifier.node.imported.name?===?key?)?{
????????????????????????//?掛到?identifierName?以供后續(xù)調(diào)用獲取
????????????????????????option.identifierName?=?specifier.get('local').toString()
??????????????????????}
????????????????????}
??????????????????}
????????????????????//?如果是?named?形式的導(dǎo)入
??????????????????if(?option.kind?===?'named'?)?{
????????????????????//?
????????????????????if(?specifier.isImportSpecifier()?)?{
??????????????????????//?找到已有?default?類(lèi)型的引入
??????????????????????if(?specifier.node.imported.name?===?key?)?{
????????????????????????option.identifierName?=?specifier.get('local').toString()
??????????????????????}
????????????????????}
??????????????????}
????????????????})
??????????????}
????????????})
??????????}
????????});
????????//?處理未被引入的包
????????Object.keys(state.options).forEach(key?=>?{
??????????const?option?=?state.options[key]
??????????//?需要require?并且未找到?identifierName?字段
??????????if(?option.require?&&?!option.identifierName?)??{
????????????
????????????//?default形式
????????????if(?option.kind?===?'default'?)?{
??????????????//?增加?default?導(dǎo)入
??????????????//?生成一個(gè)隨機(jī)變量名,?大致上是這樣?_log2
??????????????option.identifierName?=?importModule.addDefault(path,?option.require,?{
????????????????nameHint:?path.scope.generateUid(key)
??????????????}).name;
????????????}
????????????//?named形式
????????????if(?option.kind?===?'named'?)?{
??????????????option.identifierName?=?importModule.addNamed(path,?key,?option.require,?{
????????????????nameHint:?path.scope.generateUid(key)
??????????????}).name
????????????}
??????????}
??????????//?如果沒(méi)有傳遞?require?會(huì)認(rèn)為是全局方法,不做導(dǎo)入處理
??????????if(?!option.require?)?{
????????????option.identifierName?=?key
??????????}
????????})
????}
??}
}
復(fù)制代碼
Program 節(jié)點(diǎn)內(nèi)我們先將接收到的插件配置 options 拷貝了一份,掛到了 state 上, 之前有說(shuō)過(guò) state 可以用作 AST 節(jié)點(diǎn)之間的數(shù)據(jù)傳遞,然后我們首先訪問(wèn) Program 下的 ImportDeclaration 也就是 import 語(yǔ)句, 看看 log4js 是否有被導(dǎo)入過(guò), 若引入過(guò)便會(huì)記錄到 identifierName 字段上,完成對(duì) import 語(yǔ)句的訪問(wèn)后,我們就可根據(jù) identifierName 字段判斷是否已被引入,若未引入則使用 @babel/helper-module-imports[7] 創(chuàng)建 import ,并用 babel 提供的 generateUid 方法創(chuàng)建唯一的變量名。
這樣在之前的代碼我們也需要略微調(diào)整, 不能直接使用從注釋 @inject:xxx 提取出的方法名, 而是應(yīng)該使用 identifierName, 關(guān)鍵部分代碼修改如下:
if(?sourceModuleList.includes(injectType)?)?{
??//?判斷是否有函數(shù)體
??if?(pathBody.isBlockStatement())?{
????//?搜索body?內(nèi)部是否有?@code:xxx?注釋
????//?因?yàn)闊o(wú)法直接訪問(wèn)到?comment,所以需要訪問(wèn)?body內(nèi)每個(gè)?AST?節(jié)點(diǎn)的?leadingComments?屬性
????const?codeIndex?=?pathBody.node.body.findIndex(block?=>?block.leadingComments?&&?block.leadingComments.some(comment?=>?new?RegExp(`@code:\s?${injectType}`).test(comment.value)?))
????//?未聲明則默認(rèn)插入位置為第一行
????if(?codeIndex?===?-1?)?{
??????//?使用?identifierName?
??????pathBody.node.body.unshift(api.template.statement(`${state.options[injectType].identifierName}()`)());
????}else?{
??????//?使用?identifierName?
??????pathBody.node.body.splice(codeIndex,?0,?api.template.statement(`${state.options[injectType].identifierName}()`)());
????}
??}else?{
????//?無(wú)函數(shù)體情況
????//?使用?ast?提供的?`@babel/template`??api?,?用代碼段生成?ast
????//?使用?identifierName?
????const?ast?=?api.template.statement(`{${state.options[injectType].identifierName}();return?BODY;}`)({BODY:?pathBody.node});
????//?替換原本的body
????pathBody.replaceWith(ast);
??}
}
復(fù)制代碼
最終效果如下:

我們實(shí)現(xiàn)了函數(shù)自動(dòng)插入并自動(dòng)引入依賴(lài)包。
結(jié)尾
本篇文章是對(duì)自己學(xué)習(xí) “Babel 插件通關(guān)秘籍” 小冊(cè)子后的一個(gè)記錄總結(jié),我開(kāi)始和大部分想寫(xiě)babel插件卻無(wú)從下手的同學(xué)一樣,所以這篇文章主要也是按自己寫(xiě)插件時(shí)摸索的思路去寫(xiě)。希望也是能給大家提供一個(gè)思路。
完整版已支持 自定義代碼片段 的插入,完整代碼已上傳至 github:https://github.com/nxl3477/babel-plugin-code-inject,同時(shí)也發(fā)布至了 npm:https://www.npmjs.com/package/babel-plugin-code-inject。 歡迎大家 star 和 issue。
給 star 是人情,不給是事故,哈哈。

關(guān)于本文
作者:_布加拉提
https://juejin.cn/post/7012424646247055390
