1. 保姆級教學(xué)!這次一定學(xué)會babel插件開發(fā)!

        共 15210字,需瀏覽 31分鐘

         ·

        2022-02-14 23:57



        如果你有babel相關(guān)知識基礎(chǔ)建議直接跳過 前置知識 部分,直接前往 "插件編寫" 部分。

        前置知識

        什么是AST

        學(xué)習(xí)babel, 必備知識就是理解AST。

        那什么是AST呢?

        先來看下維基百科的解釋:

        在計算機(jī)科學(xué)中,抽象語法樹(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)描述出來再進(jìn)行操作一樣,而對于更底層的代碼來說,AST就是用來描述代碼的好工具。

        當(dāng)然AST不是JS特有的,每個語言的代碼都能轉(zhuǎn)換成對應(yīng)的AST, 并且AST結(jié)構(gòu)的規(guī)范也有很多, js里所使用的規(guī)范大部分是 estree[1] ,當(dāng)然這個只做簡單了解即可。

        AST到底長啥樣

        了解了AST的基本概念, 那AST到底長啥樣呢?

        astexplorer.net[2]這個網(wǎng)站可以在線生成AST, 我們可以在里面進(jìn)行嘗試生成AST,用來學(xué)習(xí)一下結(jié)構(gòu)

        babel的處理過程

        問:把冰箱塞進(jìn)大象有幾個階段?

        打開冰箱 -> 塞進(jìn)大象 -> 關(guān)上冰箱

        babel也是如此,babel利用AST的方式對代碼進(jìn)行編譯,首先自然是需要將代碼變?yōu)锳ST,再對AST進(jìn)行處理,處理完以后呢再將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)過兩個階段,分別是詞法分析和語法分析。當(dāng)parse階段開始時,首先會進(jìn)行文檔掃描,并在此期間進(jìn)行詞法分析。那怎么理解此法分析呢 如果把我們所寫的一段code比喻成句子,詞法分析所做的事情就是在拆分這個句子。如同 “我正在吃飯” 這句話,可以被拆解為“我”、“正在”、“吃飯”一樣, code也是如此。比如: const a = '1' 會被拆解為一個個最細(xì)粒度的單詞(tokon): 'const', 'a', '=', '1' 這就是詞法分析階段所做的事情。

        詞法分析結(jié)束后,將分析所得到的 tokens 交給語法分析, 語法分析階段的任務(wù)就是根據(jù) tokens 生成 AST。它會對 tokens 進(jìn)行遍歷,最終按照特定的結(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團(tuán)隊自己開發(fā)的,而是基于fork的 acorn 項目。

        它為我們提供了將code轉(zhuǎn)換為AST的方法,基本用法如下:

        file

        更多信息可以訪問官方文檔查看@babel/parser[3]

        轉(zhuǎn)換(transform)

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

        file

        該方法接受兩個參數(shù),

        path

        path為當(dāng)前訪問的路徑, 并且包含了節(jié)點的信息、父節(jié)點信息以及對節(jié)點操作許多方法。可以利用這些方法對 ATS 進(jìn)行添加、更新、移動和刪除等等。

        state

        state包含了當(dāng)前plugin的信息和參數(shù)信息等等,并且也可以用來自定義在節(jié)點之間傳遞數(shù)據(jù)。

        生成(generate)

        generate:把轉(zhuǎn)換后的 AST 打印成目標(biāo)代碼,并生成 sourcemap

        這個階段就比較簡單了, 在 transform 階段處理 AST 結(jié)束后,該階段的任務(wù)就是將 AST 轉(zhuǎn)換回 code, 在此期間會對 AST 進(jìn)行深度優(yōu)先遍歷,根據(jù)節(jié)點所包含的信息生成對應(yīng)的代碼,并且會生成對應(yīng)的sourcemap。

        經(jīng)典案例嘗試

        俗話說,最好的學(xué)習(xí)就是動手,我們來一起嘗試一個簡單的經(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');
        復(fù)制代碼

        第二步:處理 AST

        使用 @babel/traverse 處理 AST

        在這個階段我們通過分析所生成的 AST 結(jié)構(gòu),確定了在 VariableDeclaration 中由 kind 字段控制 const,所以我們是不是可以嘗試著把 kind 改寫成我們想要的 var ?既然如此,我們來嘗試一下

        file
        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'
        ????}
        });
        復(fù)制代碼

        好,此時我們憑借著猜想修改了 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)
        復(fù)制代碼

        我們再來看看效果:

        file

        執(zhí)行完成,成功了,是我們想要的效果~

        如何開發(fā)插件

        通過上面這個經(jīng)典案例, 大概了解了 babel 的使用,但我們平時的插件該如何去寫呢?

        實際上插件的開發(fā)和上面的基本思路是一樣的, 只是作為插件我們只需要關(guān)注這其中的 轉(zhuǎn)換 階段

        我們的插件需要導(dǎo)出一個函數(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'
        ??????????}
        ??}
        ?}
        }
        復(fù)制代碼

        插件編寫

        在有前置知識的基礎(chǔ)上,我們來一步步的講解開發(fā)一個 babel 插件。首先我們明確接下來要開發(fā)的插件的核心需求:

        • 可自動插入某個函數(shù)并調(diào)用。
        • 自動導(dǎo)入插入函數(shù)的相關(guān)依賴。
        • 可以通過注釋指定需要插入的函數(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ù)制代碼

        思路整理

        了解了大概的需求,先不著急動手,我們要先想想要怎么開始做,已經(jīng)設(shè)想一下過程中需要處理的問題。

        1. 找到帶有 @inject 標(biāo)記的函數(shù),再查看其內(nèi)部是否有 @inject:code 的位置標(biāo)記。
        2. 導(dǎo)入所有插入函數(shù)的相應(yīng)包。
        3. 匹配到了標(biāo)記,要做的就是插入函數(shù),同時我們還要需要處理各種情況下的函數(shù),如:對象方法、iife、箭頭函數(shù)等等情況。

        設(shè)計插件參數(shù)

        為了提升插件的靈活度,我們需要設(shè)計一個較為合適的參數(shù)規(guī)則。插件參數(shù)接受一個對象。

        • key 作為插入函數(shù)的函數(shù)名。

        • kind 表示導(dǎo)入形式。有三種導(dǎo)入方式 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 這個包里導(dǎo)入,并且是以 named 形式, 參數(shù)便為如下形式。

        //?babel.config.js
        module.exports?=?{
        ??plugins:?[
        ?//?填寫我們的plugin的js?文件地址
        ????['./babel-plugin-myplugin.js',?{
        ??????log:?{
        ????????//?導(dǎo)入方式為?named
        ????????kind:?'named',
        ????????require:?'log4js'
        ??????}
        ????}]
        ??]
        }
        復(fù)制代碼

        起步

        好,知道了具體要做什么事情并且設(shè)計好了參數(shù)的規(guī)則, 我們就可以開始動手了。

        首先我們進(jìn)入 astexplorer.net/[5] 將待處理的 code 生成 AST 方便我們梳理結(jié)構(gòu), 然后我們在進(jìn)行具體編碼

        首先是函數(shù)聲明語句,我們分析一下其 AST 結(jié)構(gòu)以及該如何處理, 來看一下demo

        //?@inject:log
        function?fn()?{
        ?console.log('fn')
        }
        復(fù)制代碼

        其生成的 AST 結(jié)構(gòu)如下,可以看到有比較關(guān)鍵的兩個屬性:

        • leadingComments 表示前方注釋,可以看到內(nèi)部有一個元素,就是我們demo里所寫的 @inject:log
        • body 是函數(shù)體的具體內(nèi)容, demo 所寫的 console.log('fn') 此時就在里面,我們等會代碼的插入操作就是需要操作它
        file

        好,知道了可以通過 leadingComments 來獲知函數(shù)是否需要被插入, 對 body 操作可以實現(xiàn)我們的代碼插入需求。。

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

        file

        代碼如下:

        module.exports?=?(api,?options,?dirname)?=>?{

        ??return?{
        ????visitor:?{
        ???//?匹配函數(shù)聲明節(jié)點
        ??????FunctionDeclaration(path,?state)?{
        ????????//?path.get('body')?相當(dāng)于?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)?))
        ????????????????//?未聲明則默認(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ù)制代碼

        編寫完后我們看看結(jié)果, log成功被插入了, 因為我們沒有使用 @code:log所以就默認(rèn)插入在了第一行

        然后我們試試使用 @code:log 標(biāo)識符, 我們將 demo 的代碼改為如下

        //?@inject:log
        function?fn()?{
        ?console.log('fn')
        ?//?@code:log
        }
        復(fù)制代碼

        再次運(yùn)行代碼查看結(jié)果, 確實是在 @code:log 位置成功插入了

        處理完了我們第一個案例函數(shù)聲明,這時候可能有人會問了, 那箭頭函數(shù)這種沒有函數(shù)體的你怎么辦, 比如:

        //?@inject:log
        ()?=>?true
        復(fù)制代碼

        這有問題嗎?沒有問題!

        file

        沒有函數(shù)體我們給它一個函數(shù)體就是了,怎么做呢?

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

        file

        分析完了結(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)?))
        ??????????????????//?未聲明則默認(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?{
        ??????????????????//?無函數(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é)點,除掉這些之外,大體上的邏輯跟之前的函數(shù)聲明的處理過程沒有區(qū)別。

        生成 AST 所使用的 @babel/template 的 API 相關(guān)用法可以查看文檔 @babel/template[6]

        針對不同情況的下的函數(shù)大體上相同,總結(jié)就是:

        分析 AST 找到 leadingComments 所在節(jié)點 -> 找到可插入的 body 所在節(jié)點 -> 編寫插入邏輯

        實際處理的情況還有很多,如:對象屬性、iife、函數(shù)表達(dá)式等很多, 處理思路都是一樣的,這里就不過重復(fù)闡述。我會將插件完整代碼發(fā)在文章底部。

        自動引入

        第一條完成了,那需求的第二條,我們使用的包如何自動引入呢, 如上面案例使用的 log4js, 那么我們處理后的代碼就應(yīng)該自動加上:

        import?{?log?}?from?'log4js'
        復(fù)制代碼

        此時,我們可以思考一下,我們需要處理以下兩種情況

        1. log 已經(jīng)被導(dǎo)入過了
        2. log 變量名已經(jīng)被占用

        針對 問題1 我們需要先檢索一下是否有導(dǎo)入過 log4js ,并且以 named 的形式導(dǎo)入了 log 針對 問題2 我們需要給 log 一個唯一的別名, 并且要保證在后續(xù)的代碼插入中也使用這個別名。所以這就要求了我們要在文件的一開始就處理完成自動引入的邏輯。

        有了大概的思路,但是我們?nèi)绾翁崆巴瓿勺詣右脒壿嬆?。抱著疑問,我們再來看?AST 的結(jié)構(gòu)??梢钥吹?AST 最外層是 File 節(jié)點, 他有一個 comments 屬性,它包含了當(dāng)前文件里所有的注釋,有了這個我們就可以解析出文件里需要插入的函數(shù),并提前進(jìn)行引入。我們再往下看, 內(nèi)部是一個 Program, 我們將首先訪問它, 因為它會在其他類型的節(jié)點之前被調(diào)用,所以我們要在此階段實現(xiàn)自動引入邏輯。

        小知識:babel 提供了 path.traverse 方法,可以用來同步訪問處理當(dāng)前節(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)被導(dǎo)入過
        ??????????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)入類型
        ????????????????????if(?specifier.isImportDefaultSpecifier()?)?{
        ??????????????????????//?找到已有?default?類型的引入
        ??????????????????????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?類型的引入
        ??????????????????????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)入
        ??????????????//?生成一個隨機(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
        ????????????}
        ??????????}

        ??????????//?如果沒有傳遞?require?會認(rèn)為是全局方法,不做導(dǎo)入處理
        ??????????if(?!option.require?)?{
        ????????????option.identifierName?=?key
        ??????????}
        ????????})
        ????}
        ??}
        }
        復(fù)制代碼

        Program 節(jié)點內(nèi)我們先將接收到的插件配置 options 拷貝了一份,掛到了 state 上, 之前有說過 state 可以用作 AST 節(jié)點之間的數(shù)據(jù)傳遞,然后我們首先訪問 Program 下的 ImportDeclaration 也就是 import 語句, 看看 log4js 是否有被導(dǎo)入過, 若引入過便會記錄到 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)?))
        ????//?未聲明則默認(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?{
        ????//?無函數(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ù)制代碼

        最終效果如下:

        file

        我們實現(xiàn)了函數(shù)自動插入并自動引入依賴包。

        結(jié)尾

        本篇文章是對自己學(xué)習(xí) “Babel 插件通關(guān)秘籍” 小冊子后的一個記錄總結(jié),我開始和大部分想寫babel插件卻無從下手的同學(xué)一樣,所以這篇文章主要也是按自己寫插件時摸索的思路去寫。希望也是能給大家提供一個思路。

        完整版已支持 自定義代碼片段 的插入,完整代碼已上傳至 githubhttps://github.com/nxl3477/babel-plugin-code-inject,同時也發(fā)布至了 npmhttps://www.npmjs.com/package/babel-plugin-code-inject 歡迎大家 star 和 issue。

        給 star 是人情,不給是事故,哈哈。

        file

        關(guān)于本文

        作者:_布加拉提

        https://juejin.cn/post/7012424646247055390


        最后

        “在看和轉(zhuǎn)發(fā)”就是最大的支持


        瀏覽 61
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 国产精品21一区二区 | 一级大片一级一大片 | 国产精品美女久久久免费 | 一区二区三区无码专区 | 成人免费视频网址 |