day07: 自定義babel plugin

babel是什么?

用官方的解釋就是Babel is a JavaScript compiler.Use next generation JavaScript, today. 什么意思呢赋朦?簡單來說止剖,就是新JavaScript特性已經(jīng)出來了,但是瀏覽器并沒有完全支持决瞳,為了能使用新特性,又能夠在瀏覽器里使用沪斟,我們就需要借助babel了士飒,babel會把新特性編譯成瀏覽器能夠識別的代碼悔耘。這里可以測試查看通過babel編譯前后的代碼; 這里可以查看解析的AST語法樹翩蘸。

babel在編譯時候,會把源代碼分為兩部分來處理:語法syntax淮逊、api。語法syntax比如const扶踊、let泄鹏、模版字符串、擴(kuò)展運(yùn)算符等秧耗。 api比如Array.includes()等新函數(shù)备籽。

@babel/core:

babel編譯器。被拆分三個模塊:

  1. @babel/parser: 接受源碼,進(jìn)行詞法分析车猬、語法分析霉猛,生成AST。
  2. @babel/traverse:接受一個AST珠闰,并對其遍歷惜浅,根據(jù)preset、plugin進(jìn)行邏輯處理伏嗜,進(jìn)行替換坛悉、刪除、添加節(jié)點承绸。
  3. @babel/generator:接受最終生成的AST裸影,并將其轉(zhuǎn)換為代碼字符串,同時此過程也可以創(chuàng)建source map军熏。
    babel轉(zhuǎn)碼流程:input string -> @babel/parser parser -> AST -> transformer[s] -> AST -> @babel/generator -> output string轩猩。


    編譯過程

上圖中的plugins.babelrcbabel.config.js里面配置,如果沒有配置荡澎,則target codesource code是一樣的均践。至于我們常見的配置:

module.exports = {
  presets: [
   [
    "@babel/env",
    {
      targets: {
        edge: "17",
        firefox: "60",
        chrome: "67",
        safari: "11.1",
      },
      useBuiltIns: "usage",
      corejs: "3.6.4",
    },
   ]
  ], 
}

presets里面配置的@babel/env可以理解為一份“配置文件”,里面預(yù)設(shè)了哪些需要的plugins衔瓮,因為我們開發(fā)一個項目可能經(jīng)常導(dǎo)入這些插件浊猾,所以干脆把它們寫成一個模板。如果presets里面缺少了某些plugin热鞍,如@babel/env里面沒有可以識別裝飾器的插件葫慎,就需要通過配置plugins屬性將缺少的插件@babel/plugin-proposal-decorators導(dǎo)進(jìn)來,無論是配置presets還是plugins薇宠,本質(zhì)上還是都是導(dǎo)入需要的plugin偷办,plugins只是提供了語法轉(zhuǎn)換的規(guī)則,要轉(zhuǎn)換API需要借助polyfill。

定義babel plugin

接下來我們就來了解一下如何開發(fā)一個babel插件澄港,參考 開發(fā)插件

babel使用一種 訪問者模式 來遍歷整棵語法樹椒涯,即遍歷進(jìn)入到每一個Node節(jié)點時,可以說我們在「訪問」這個節(jié)點回梧。訪問者就是一個對象废岂,定義了在一個樹狀結(jié)構(gòu)中獲取具體節(jié)點的方法。簡單來說狱意,我們可以在訪問者中湖苞,使用Node的type來定義一個hook函數(shù),Node的type有ClassDeclaration详囤,F(xiàn)unctionDeclaration等财骨,每一次遍歷到對應(yīng)type的Node時,hook函數(shù)就會被觸發(fā),我們可以在這個hook函數(shù)中隆箩,修改该贾、查看、替換捌臊、刪除這個節(jié)點杨蛋。

一個傻瓜例子

目錄結(jié)構(gòu)
  1. 安裝@babel/core @babel/cli
yarn add -D @babel/core @babel/cli
  1. 創(chuàng)建自定義plugins文件。
    本質(zhì)上一個plugin就是一個函數(shù), 函數(shù)接受一個babel對象(包含babel所有的api)娃属,最后返回一個包含visitor屬性的對象六荒。visitor里面是什么?visitor屬性中每個key都是一個ast節(jié)點的類型矾端,值就是訪問這個節(jié)點的函數(shù)掏击。在遍歷AST時,如果節(jié)點的type和visitor里面的key一樣秩铆,則會觸發(fā)對應(yīng)的函數(shù)砚亭,對該節(jié)點進(jìn)行操作。每個訪問者函數(shù)都會接受兩個參數(shù):path和state殴玛。path對象表示兩個節(jié)點之間連接的對象, state對象包含一些額外的狀態(tài)信息捅膘,例如可以從state.opts中取出為插件配置的特定選項,甚至可以取出path對象滚粟,具體內(nèi)容可以自己打印看看寻仗。以下這個插件什么也沒做。
// src/babelPlugin/self_babel_plugin.js
module.exports = function (babel) {
    return {
        visitor: {
            
        }
    }
}
  1. .babelrc配置自定義的文件
{
    "plugins": [
        ["./src/babelPlugin/self_babel_plugin.js"]
    ]
}

我們的插件里并沒做任何操作凡壤,所以我們編譯出來的代碼幾乎和源碼一樣署尤,除了可能會刪除一些空格,如

  1. 現(xiàn)在我們有一段代碼:
// src/babelPlugin/test.js
function Test() {
}
  1. 運(yùn)行:將編譯結(jié)果放入dist目錄亚侠。
npx babel src/babelPlugin/test.js --out-dir dist // 這里編譯你們自己的文件曹体,下面的命令也是同樣的道理
  1. 得到的結(jié)果
// dist/test.js
function Test() {}

開發(fā)一個plugin

現(xiàn)在我們重寫src/babelPlugin/test.js代碼:

// src/babelPlugin/test.js
function Test(a, b) {
 return a $ b;
}

我們希望通過編譯變成以下的樣子:

function Test(a, b) {
 return (a + b) * (a - b);
}

運(yùn)行:npx babel src/babelPlugin/test.js --out-dir dist

報錯

結(jié)合前面的知識,我們之所以能把一種代碼編譯成另一種樣子硝烂,說白了就是babel plugin對AST做了操作箕别,所以第一步是我們要拿到AST,但是上面我們直接編譯報錯了滞谢,看來我們要讓babel識別$呀串稀,那么怎么做?

正確編譯成AST

  1. 首先在types里面注冊這個token,任意命名狮杨,這里我命名為addAndMinus
addAndMinus: new TokenType("$", {
    beforeExpr,
    binop: 9,
    prefix,
    startsExpr
  }),
  1. getTokenFromCode解析這個token, 因為$的unicode為36母截,且我們是把它當(dāng)成操作符,所以調(diào)用finishOp方法
case 36:
        this.finishOp(types$1.addAndMinus, 1);
        return;

重新編譯npx babel src/babelPlugin/test.js --out-dir dist禾酱,看下現(xiàn)在的結(jié)果:

編譯成功

到這里已經(jīng)能把$正確解析為AST, 接下來就可以在遍歷這棵樹的時候操作節(jié)點了,即到了插件開發(fā)環(huán)節(jié)。

開發(fā)

前面講了一大段都是在建立你的Babel知識颤陶,現(xiàn)在我們進(jìn)入重頭戲, babel plugin怎么寫颗管? 我們希望$能夠發(fā)揮它的作用,于是打開網(wǎng)址, 參考

image.png

可見我們要我們的插件要訪問BinaryExpression類型的節(jié)點滓走, 結(jié)合前面我們關(guān)于插件的知識垦江,我們改寫插件打印一下節(jié)點

// src/babelPlugin/self_babel_plugin.js
module.exports = function (babel) {
    return {
        visitor: {
            BinaryExpression (path, statas) {
                console.log(path.node)
            }
        }
    }
}

運(yùn)行npx babel src/babelPlugin/test.js,可見獲取成功

節(jié)點信息

接下來我們就可以操作節(jié)點愉快地玩耍了搅方。先看一下我們要的結(jié)果的AST比吭,


image.png

可見原來的BinaryExpression節(jié)點的leftright都變成了BinaryExpression節(jié)點,所以我們借助@babel/types創(chuàng)建BinaryExpression節(jié)點姨涡,實現(xiàn)如下:

// src/babelPlugin/self_babel_plugin.js
module.exports = function (babel) {
    return {
        visitor: {
            BinaryExpression (path, statas) {
                const { node } = path
                const { types: t } = babel
                if (node.operator && node.operator.codePointAt() === 36) {
                    const {left: {name: leftName, value: leftValue}} = node
                    const {right: {name: rightName, value: rightValue}} = node 
                    const leftNode = t.binaryExpression("+", leftName ? t.identifier(leftName) : t.numericLiteral(leftValue), rightName ? t.identifier(rightName) : t.numericLiteral(rightValue));
                    const rightNode = t.binaryExpression("-", leftName ? t.identifier(leftName) : t.numericLiteral(leftValue), rightName ? t.identifier(rightName) : t.numericLiteral(rightValue));
                    node.operator = '*'
                    node.left = leftNode
                    node.right = rightNode
                }
            }
        }
    }
}

運(yùn)行npx babel src/babelPlugin/test.js --out-dir dist得到結(jié)果:

image.png

編譯成功啦q锰佟!
我們改寫src/babelPlugin/test.js測試一下涛漂,

// src/babelPlugin/test.js
function Test(a, b) {
 return a $ b;
}

console.log(10 $ 5)

重新編譯得到:


image.png

變量名使用identifier 創(chuàng)建節(jié)點赏表,數(shù)值使用numericLiteral創(chuàng)建節(jié)點。

上面的例子的編譯過程匈仗,不使用babel命令瓢剿,用代碼實現(xiàn)如下

// src/babelPlugin/compile.js
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const path = require('path')
const fs = require('fs')
const content = fs.readFileSync(path.join(__dirname, 'test.js'), 'utf-8')
const ast = parser.parse(content, { sourceType: 'module'})
traverse(ast, { // 這一步其實就是我們要寫的插件
    BinaryExpression (path, statas) {
                const { node } = path
                const { types: t } = babel
                if (node.operator && node.operator.codePointAt() === 36) {
                    const {left: {name: leftName, value: leftValue}} = node
                    const {right: {name: rightName, value: rightValue}} = node 
                    const leftNode = t.binaryExpression("+", leftName ? t.identifier(leftName) : t.numericLiteral(leftValue), rightName ? t.identifier(rightName) : t.numericLiteral(rightValue));
                    const rightNode = t.binaryExpression("-", leftName ? t.identifier(leftName) : t.numericLiteral(leftValue), rightName ? t.identifier(rightName) : t.numericLiteral(rightValue));
                    node.operator = '*'
                    node.left = leftNode
                    node.right = rightNode
                }
            }
})
// console.log(ast)

const { code } = babel.transformFromAstSync(ast, null, {
    // presets:["@babel/preset-env"]
})
console.log(code) 
/**function Test(a, b) {
  return (a + b) * (a - b);
}
*/

到這里,我們就知道了怎么去創(chuàng)建一個babel插件了悠轩。查看源碼
參考:https://www.zhihu.com/question/277409645

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末间狂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子火架,更是在濱河造成了極大的恐慌鉴象,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件距潘,死亡現(xiàn)場離奇詭異炼列,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)音比,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門俭尖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洞翩,你說我怎么就攤上這事稽犁。” “怎么了骚亿?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵已亥,是天一觀的道長。 經(jīng)常有香客問我来屠,道長虑椎,這世上最難降的妖魔是什么震鹉? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮捆姜,結(jié)果婚禮上传趾,老公的妹妹穿的比我還像新娘。我一直安慰自己泥技,他們只是感情好浆兰,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著珊豹,像睡著了一般簸呈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上店茶,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天蜕便,我揣著相機(jī)與錄音,去河邊找鬼忽妒。 笑死玩裙,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的段直。 我是一名探鬼主播吃溅,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鸯檬!你這毒婦竟也來了决侈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤喧务,失蹤者是張志新(化名)和其女友劉穎赖歌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體功茴,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡庐冯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了坎穿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片展父。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖玲昧,靈堂內(nèi)的尸體忽然破棺而出栖茉,到底是詐尸還是另有隱情,我是刑警寧澤孵延,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布吕漂,位于F島的核電站,受9級特大地震影響尘应,放射性物質(zhì)發(fā)生泄漏惶凝。R本人自食惡果不足惜吼虎,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望苍鲜。 院中可真熱鬧鲸睛,春花似錦、人聲如沸坡贺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遍坟。三九已至,卻和暖如春晴股,著一層夾襖步出監(jiān)牢的瞬間愿伴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工电湘, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留隔节,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓寂呛,卻偏偏與公主長得像怎诫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子贷痪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容