關(guān)于babel
Babel 是一個(gè) JavaScript 編譯器
Babel 是一個(gè)工具鏈嗤瞎,主要用于將 ECMAScript 2015+ 版本的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,以便能夠運(yùn)行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中。
// Babel 輸入: ES2015 箭頭函數(shù)
[1, 2, 3].map((n) => n + 1);
// Babel 輸出: ES5 語法實(shí)現(xiàn)的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});
babel做了什么
babel 的轉(zhuǎn)譯過程也分為三個(gè)階段伟件,這三步具體是:
解析 Parse
將代碼解析生成抽象語法樹( 即AST )。
轉(zhuǎn)換 Transform
對(duì)于 AST 進(jìn)行變換一系列的操作弛说,babel 接受得到 AST 并通過 babel-traverse 對(duì)其進(jìn)行遍歷容客,在此過程中進(jìn)行添加、更新及移除等操作二跋。
生成 Generate
將變換后的 AST 再轉(zhuǎn)換為 JS 代碼, 使用到的模塊是 babel-generator战惊。
我們編寫的 babel 插件則主要專注于第二步轉(zhuǎn)換過程的工作,專注于對(duì)于代碼的轉(zhuǎn)化和拓展扎即,解析與生成的偏底層相關(guān)操作則有對(duì)應(yīng)的模塊支持样傍,在此我們理解它主要做了什么即可。
準(zhǔn)備工作
正式開始之前铺遂,需要介紹兩個(gè)概念:
Visitors 訪問者
訪問者是一個(gè)用于 AST 遍歷的跨語言的模式衫哥。
簡單的說它們就是一個(gè)對(duì)象,定義了用于在一個(gè)樹狀結(jié)構(gòu)中獲取具體節(jié)點(diǎn)的方法襟锐。 這么說有些抽象撤逢,所以讓我們來看一個(gè)例子
const MyVisitor = {
Identifier(path) {
console.log("Im Identifier");
},
FunctionDeclaration(path){
console.log("Im FunctionDeclaration");
}
};
這是一個(gè)簡單的訪問者,把它用于AST遍歷中時(shí)粮坞,每當(dāng)在樹中遇見一個(gè) Identifier
的時(shí)候會(huì)調(diào)用 Identifier()
方法蚊荣,遇見一個(gè) FunctionDeclaration
的時(shí)候則會(huì)調(diào)用 FunctionDeclaration()
方法。
Paths 路徑
Visitors
在遍歷到每個(gè)節(jié)點(diǎn)的時(shí)候莫杈,
都會(huì)給我們傳入 path
參數(shù)互例,
它包含了節(jié)點(diǎn)的信息以及節(jié)點(diǎn)和所在的位置,
供我們對(duì)特定節(jié)點(diǎn)進(jìn)行修改筝闹。
之所以稱之為 path
是其表示的是兩個(gè)節(jié)點(diǎn)之間連接的對(duì)象媳叨,而非指當(dāng)前的節(jié)點(diǎn)對(duì)象。
更具體的API可以查看Babel插件手冊
插件格式
一個(gè)完整的插件格式如下
export default function({ types: t }) {
return {
pre(state) { // 遍歷之前
},
visitor: { // 訪問者
VariableDeclaration(path) {
// ... ...
}
},
post(state) { // 遍歷結(jié)束
},
};
}
注意
這里有一個(gè)值得注意的問題关顷,所有的babel插件會(huì)共享同一次遍歷過程糊秆。
也就是說,他們對(duì)節(jié)點(diǎn)的處理可能會(huì)相互影響议双。
比如我們需要對(duì)所有的方法添加 try-catch ,就需要定義一個(gè)FunctionDeclaration訪用來訪問所有的函數(shù)痘番。
但是在其他插件里,比如babel-preset,它會(huì)生成一些輔助函數(shù)汞舱,這些輔助函數(shù)也會(huì)被我們的訪問者訪問伍纫。但我們只需要對(duì)源碼進(jìn)行處理。
想要避免對(duì)這些不在原始代碼中的節(jié)點(diǎn)進(jìn)行訪問昂芜,筆者現(xiàn)在也沒找到一個(gè)最好的方法翻斟,有以下嘗試:
- 使用sourceMap,如果節(jié)點(diǎn)在sourceMap中找不到说铃,則判斷為生成的代碼访惜。
但sourceMap需要借助于webpack獲取(或許存在更好的方法腻扇,但筆者還沒找到债热,歡迎指正),這樣插件配置起來比較復(fù)雜幼苛,并且通用性不夠好窒篱。 - 借助
path.node.loc
。這個(gè)方法不準(zhǔn)確舶沿,有些生成的節(jié)點(diǎn)也會(huì)含有l(wèi)ocation屬性墙杯。 - 在節(jié)點(diǎn)開始遍歷之前手動(dòng)添加一次額外的遍歷,我們處理完成后再交由其他插件處理括荡。也是筆者目前在用的方法高镐。目前來看比較準(zhǔn)確,但需要一次額外的遍歷開銷畸冲。
開始
首先定義 Visitor 來訪問方法聲明
const funcVisitor = {
FunctionDeclaration(path) {
const functionBody = path.node.body; //獲取方法的 body
if (functionBody.type === 'BlockStatement') { // 含有block
const body = functionBody.body; // 獲取原來的block body
path.get('body').replaceWith(wrapFunction({
BODY: body,
HANDLER:t.identifier('console.log')
}))
}
}
}
借助 babel-template 快速生成AST節(jié)點(diǎn)
const wrapFunction = template(`{
try {
BODY
} catch(err) {
HANDLER(err)
}
}`);
接下來組裝
const t = require('@babel/types');
const wrapFunction = template(`{
try {
BODY
} catch(err) {
HANDLER(err)
}
}`);
const funcVisitor = {
FunctionDeclaration(path) {
const functionBody = path.node.body; //獲取方法的 body
if (functionBody.type === 'BlockStatement') { // 含有block
const body = functionBody.body; // 獲取原來的block body
path.get('body').replaceWith(wrapFunction({
BODY: body,
HANDLER:t.identifier('console.log')
}))
}
}
}
module.exports = function () {
return {
pre(file){ // 開始遍歷之前
file.path.traverse(funcVisitor); // 插入額外的遍歷
}
};
};
使用
webpack.config.js
const myPlugin = require('xxx')
module: {
rules: [
{
test: /\.js$/,
exclude:/node_modules/,//排除掉node_module目錄
loader:'babel-loader',
options:{
plugins:[myPlugin]
},
},
]
},
測試一下嫉髓,輸入代碼
function Foo(){
console.log('Im foo')
}
輸出為
function Foo(){
try{
console.log('Im foo')
}catch (err) {
console.error(err)
}
}
一個(gè)簡單的為方法聲明增加try-catch的babel插件就開發(fā)完成了。但它也就僅僅能夠應(yīng)付測試中的簡單情況邑闲。
筆者已經(jīng)寫好了一個(gè)相對(duì)完善的插件算行,它可以為Promise
添加.catch
,也可以對(duì) 方法聲明 | 類方法 注入捕獲語句苫耸。配置也相對(duì)靈活州邢,支持目錄以及文件篩選。
不完善的地方歡迎大家補(bǔ)充~