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編譯器。被拆分三個模塊:
- @babel/parser: 接受源碼,進(jìn)行詞法分析车猬、語法分析霉猛,生成AST。
- @babel/traverse:接受一個AST珠闰,并對其遍歷惜浅,根據(jù)preset、plugin進(jìn)行邏輯處理伏嗜,進(jìn)行替換坛悉、刪除、添加節(jié)點承绸。
-
@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
在.babelrc
或babel.config.js
里面配置,如果沒有配置荡澎,則target code
和source 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é)點杨蛋。
一個傻瓜例子
- 安裝
@babel/core
@babel/cli
yarn add -D @babel/core @babel/cli
- 創(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: {
}
}
}
- 在
.babelrc
配置自定義的文件
{
"plugins": [
["./src/babelPlugin/self_babel_plugin.js"]
]
}
我們的插件里并沒做任何操作凡壤,所以我們編譯出來的代碼幾乎和源碼一樣署尤,除了可能會刪除一些空格,如
- 現(xiàn)在我們有一段代碼:
// src/babelPlugin/test.js
function Test() {
}
- 運(yùn)行:將編譯結(jié)果放入dist目錄亚侠。
npx babel src/babelPlugin/test.js --out-dir dist // 這里編譯你們自己的文件曹体,下面的命令也是同樣的道理
- 得到的結(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
- 首先在
types
里面注冊這個token
,任意命名狮杨,這里我命名為addAndMinus
addAndMinus: new TokenType("$", {
beforeExpr,
binop: 9,
prefix,
startsExpr
}),
- 在
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)址, 參考
可見我們要我們的插件要訪問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é)果的AST比吭,
可見原來的BinaryExpression
節(jié)點的left
和right
都變成了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é)果:
編譯成功啦q锰佟!
我們改寫
src/babelPlugin/test.js
測試一下涛漂,
// src/babelPlugin/test.js
function Test(a, b) {
return a $ b;
}
console.log(10 $ 5)
重新編譯得到:
變量名使用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