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

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

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

const?parser?=?require('@babel/parser');
const?traverse?=?require('@babel/traverse').default
const?ast?=?parser.parse('const?a?=?1');
traverse(ast,?{
????VariableDeclaration(path,?state)?{
???//?通過?path.node?訪問實際的?AST?節(jié)點
??????path.node.kind?=?'var'
????}
});
復制代碼
好,此時我們憑借著猜想修改了 kind ,將其改寫為了 var, 但是我們還不能知道實際是否有效,所以我們需要將其再轉(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)
復制代碼
我們再來看看效果:

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

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

代碼如下:
module.exports?=?(api,?options,?dirname)?=>?{
??return?{
????visitor:?{
???//?匹配函數(shù)聲明節(jié)點
??????FunctionDeclaration(path,?state)?{
????????//?path.get('body')?相當于?path.node.body
????????const?pathBody?=?path.get('body')
????????if(path.node.leadingComments)?{
??????????//?過濾出所有匹配?@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é)果的第一個為?@inject:xxx?中的?xxx?,??我們將它取出來
??????????????const?injectType?=?injectTypeMatchRes[1]
??????????????//?獲取插件參數(shù)的?key,?看xxx?是否在插件的參數(shù)中聲明過
??????????????const?sourceModuleList?=?Object.keys(options)
??????????????if(?sourceModuleList.includes(injectType)?)?{
????????????????//?搜索body?內(nèi)部是否有?@code:xxx?注釋
????????????????//?因為無法直接訪問到?comment,所以需要訪問?body內(nèi)每個?AST?節(jié)點的?leadingComments?屬性
????????????????const?codeIndex?=?pathBody.node.body.findIndex(block?=>?block.leadingComments?&&?block.leadingComments.some(comment?=>?new?RegExp(`@code:\s?${injectType}`).test(comment.value)?))
????????????????//?未聲明則默認插入位置為第一行
????????????????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}()`)());
????????????????}
??????????????}
????????????}
??????????})
????????}
??????}
??}
})
復制代碼
編寫完后我們看看結(jié)果, log成功被插入了, 因為我們沒有使用 @code:log所以就默認插入在了第一行
然后我們試試使用 @code:log 標識符, 我們將 demo 的代碼改為如下
//?@inject:log
function?fn()?{
?console.log('fn')
?//?@code:log
}
復制代碼
再次運行代碼查看結(jié)果, 確實是在 @code:log 位置成功插入了
處理完了我們第一個案例函數(shù)聲明,這時候可能有人會問了, 那箭頭函數(shù)這種沒有函數(shù)體的你怎么辦, 比如:
//?@inject:log
()?=>?true
復制代碼
這有問題嗎?沒有問題!

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

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

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

關(guān)于本文
作者:_布加拉提
https://juejin.cn/post/7012424646247055390
點贊和在看就是最大的支持??
