<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          保姆級(jí)教學(xué)!這次一定學(xué)會(huì)babel插件開發(fā)!

          共 15136字,需瀏覽 31分鐘

           ·

          2022-01-10 07:29

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

          前置知識(shí)

          什么是AST

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

          那什么是AST呢?

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

          在計(jì)算機(jī)科學(xué)中,抽象語法樹(Abstract Syntax Tree,AST),或簡(jiǎn)稱語法樹(Syntax tree),是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)

          "源代碼語法結(jié)構(gòu)的一種抽象表示" 這幾個(gè)字要?jiǎng)澲攸c(diǎn),是我們理解AST的關(guān)鍵,說人話就是按照某種約定好的規(guī)范,以樹形的數(shù)據(jù)結(jié)構(gòu)把我們的代碼描述出來,讓js引擎和轉(zhuǎn)譯器能夠理解。

          舉個(gè)例子:就好比現(xiàn)在框架會(huì)利用虛擬dom這種方式把真實(shí)dom結(jié)構(gòu)描述出來再進(jìn)行操作一樣,而對(duì)于更底層的代碼來說,AST就是用來描述代碼的好工具。

          當(dāng)然AST不是JS特有的,每個(gè)語言的代碼都能轉(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,用來學(xué)習(xí)一下結(jié)構(gòu)

          babel的處理過程

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

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

          babel也是如此,babel利用AST的方式對(duì)代碼進(jìn)行編譯,首先自然是需要將代碼變?yōu)锳ST,再對(duì)AST進(jìn)行處理,處理完以后呢再將AST 轉(zhuǎn)換回來

          也就是如下的流程

          code轉(zhuǎn)換為AST -> 處理AST -> AST轉(zhuǎn)換為code

          然后我們?cè)俳o它們一個(gè)專業(yè)一點(diǎn)的名字

          解析 -> 轉(zhuǎn)換 -> 生成

          解析(parse)

          通過 parser 把源碼轉(zhuǎn)成抽象語法樹(AST)

          這個(gè)階段的主要任務(wù)就是將code轉(zhuǎn)為AST, 其中會(huì)經(jīng)過兩個(gè)階段,分別是詞法分析和語法分析。當(dāng)parse階段開始時(shí),首先會(huì)進(jìn)行文檔掃描,并在此期間進(jìn)行詞法分析。那怎么理解此法分析呢 如果把我們所寫的一段code比喻成句子,詞法分析所做的事情就是在拆分這個(gè)句子。如同?“我正在吃飯”?這句話,可以被拆解為“我”“正在”“吃飯”一樣, code也是如此。比如: const a = '1' 會(huì)被拆解為一個(gè)個(gè)最細(xì)粒度的單詞(tokon): 'const', 'a', '=', '1' 這就是詞法分析階段所做的事情。

          詞法分析結(jié)束后,將分析所得到的 tokens 交給語法分析, 語法分析階段的任務(wù)就是根據(jù) tokens 生成 AST。它會(huì)對(duì) tokens 進(jìn)行遍歷,最終按照特定的結(jié)構(gòu)生成一個(gè) tree 這個(gè) tree 就是 AST。

          如下圖, 可以看到上面語句的到的結(jié)構(gòu),我們找到了幾個(gè)重要信息, 最外層是一個(gè)VariableDeclaration意思是變量聲明,所使用的類型是?const, 字段declarations內(nèi)還有一個(gè)?VariableDeclarator[變量聲明符] 對(duì)象,找到了?a,?1?兩個(gè)關(guān)鍵字。

          除了這些關(guān)鍵字以為,還可以找到例如行號(hào)等等的重要信息,這里就不一一展開闡述。總之,這就是我們最終得到的 AST 模樣。

          那問題來了,babel里該如何將code 轉(zhuǎn)為 AST 呢?在這個(gè)階段我們會(huì)用到 babel 提供的解析器 @babel/parser,之前叫 Babylon,它并非由babel團(tuán)隊(duì)自己開發(fā)的,而是基于fork的 acorn 項(xiàng)目。

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

          file

          更多信息可以訪問官方文檔查看@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ù)的形式訪問每種不同類型的AST節(jié)點(diǎn)。以上面代碼為例, 我們可以編寫?VariableDeclaration?函數(shù)對(duì)?VariableDeclaration節(jié)點(diǎn)進(jìn)行訪問,每當(dāng)遇到該類型節(jié)點(diǎn)時(shí)都會(huì)觸發(fā)該方法。如下:

          file

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

          path

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

          state

          state包含了當(dāng)前plugin的信息和參數(shù)信息等等,并且也可以用來自定義在節(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)典案例嘗試

          俗話說,最好的學(xué)習(xí)就是動(dòng)手,我們來一起嘗試一個(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è)階段我們通過分析所生成的 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?訪問實(shí)際的?AST?節(jié)點(diǎn)
          ??????path.node.kind?=?'var'
          ????}
          });
          復(fù)制代碼

          好,此時(shí)我們憑借著猜想修改了?kind?,將其改寫為了?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è)賮砜纯葱Ч?/p>

          file

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

          如何開發(fā)插件

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

          實(shí)際上插件的開發(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ù)制代碼

          插件編寫

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

          • 可自動(dòng)插入某個(gè)函數(shù)并調(diào)用。
          • 自動(dòng)導(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ù)制代碼

          思路整理

          了解了大概的需求,先不著急動(dòng)手,我們要先想想要怎么開始做,已經(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í)我們還要需要處理各種情況下的函數(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 為依賴的包名

          比如,我需要插入?log?方法,它需要從?log4js?這個(gè)包里導(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è)計(jì)好了參數(shù)的規(guī)則, 我們就可以開始動(dòng)手了。

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

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

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

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

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

          好,知道了可以通過?leadingComments?來獲知函數(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)

          file

          代碼如下:

          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)?{
          ??????????//?過濾出所有匹配?@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?,??我們將它取出來
          ??????????????const?injectType?=?injectTypeMatchRes[1]
          ??????????????//?獲取插件參數(shù)的?key,?看xxx?是否在插件的參數(shù)中聲明過
          ??????????????const?sourceModuleList?=?Object.keys(options)
          ??????????????if(?sourceModuleList.includes(injectType)?)?{
          ????????????????//?搜索body?內(nèi)部是否有?@code:xxx?注釋
          ????????????????//?因?yàn)闊o法直接訪問到?comment,所以需要訪問?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ù)制代碼

          編寫完后我們看看結(jié)果,?log成功被插入了, 因?yàn)槲覀儧]有使用?@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ì)問了, 那箭頭函數(shù)這種沒有函數(shù)體的你怎么辦, 比如:

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

          這有問題嗎?沒有問題!

          file

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

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

          file

          分析完了結(jié)構(gòu),那怎么判斷是否有函數(shù)體呢?還記得上面處理函數(shù)聲明時(shí),我們?cè)?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é)果的第一個(gè)為?@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?注釋
          ??????????????????//?因?yàn)闊o法直接訪問到?comment,所以需要訪問?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?{
          ??????????????????//?無函數(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ù)聲明的處理過程沒有區(qū)別。

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

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

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

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

          自動(dòng)引入

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

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

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

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

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

          有了大概的思路,但是我們?nèi)绾翁崆巴瓿勺詣?dòng)引入邏輯呢。抱著疑問,我們?cè)賮砜纯?AST 的結(jié)構(gòu)。可以看到 AST 最外層是?File?節(jié)點(diǎn), 他有一個(gè)?comments?屬性,它包含了當(dāng)前文件里所有的注釋,有了這個(gè)我們就可以解析出文件里需要插入的函數(shù),并提前進(jìn)行引入。我們?cè)偻驴矗?內(nèi)部是一個(gè)?Program, 我們將首先訪問它, 因?yàn)樗鼤?huì)在其他類型的節(jié)點(diǎn)之前被調(diào)用,所以我們要在此階段實(shí)現(xiàn)自動(dòng)引入邏輯。

          小知識(shí):babel 提供了 path.traverse 方法,可以用來同步訪問處理當(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({
          ??????????//?首先訪問原有的?import?節(jié)點(diǎn),?檢測(cè)?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)入
          ??????????????//?生成一個(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
          ????????????}
          ??????????}

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

          Program?節(jié)點(diǎn)內(nèi)我們先將接收到的插件配置?options?拷貝了一份,掛到了?state?上, 之前有說過?state?可以用作 AST 節(jié)點(diǎn)之間的數(shù)據(jù)傳遞,然后我們首先訪問?Program?下的?ImportDeclaration?也就是?import?語句, 看看?log4js?是否有被導(dǎo)入過, 若引入過便會(huì)記錄到?identifierName?字段上,完成對(duì)?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?注釋
          ????//?因?yàn)闊o法直接訪問到?comment,所以需要訪問?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?{
          ????//?無函數(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

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

          結(jié)尾

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

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

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

          瀏覽 28
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  靠逼小视频 | 中国免费黄色视频 | 五月婷婷性爱 | 多人p| 性欧美在线观看 |