前言
社群裡面有很多關於Babel的文章,有些寫的很好,我自己也受這些文章啟發很大。但我發現一個問題就是,這類文章一進來就講了很多babel底層的概念,說實話對基礎不深的一些童鞋來說,看完之後理解起來還是有一定難度的,最重要的是看完了之後,自己並不知道如何去寫一個Babel外掛,因而這促使了如何從0到1完成一個babel外掛這篇文章的編寫,學習完本篇文章,期望是大家能對Babel有一個整體的認識,知道Babel是什麼?Babel是如何運作的?並且自己能實現一個簡單的Babel外掛。
什麼是Babel
Babel是一個JavaScript編譯器,意思就是說你為Babel提供一些程式碼,Babel做一些轉換,給你返回一些新的程式碼。比如,我們常見的將ES5+的程式碼轉換成ES5+之前的一些程式碼。
Babel的處理步驟
如圖,Babel經過3個處理步驟,分別為解析(parse),轉換(transform),生成(generate)。
解析
解析又經過詞法分析,語法分析兩個步驟,將輸入的程式碼生成抽象語法數(AST),AST可以理解為就是描述一段程式碼的節點樹,看如下這個例子:
我們輸入
const a = 1
複製程式碼
經過解析(parse),生成如下結構的節點樹(為了方便觀看,去掉了一些表明節點位置資訊的屬性),詳細的可以通過這個工具檢視
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"rawValue": 1,
"raw": "1"
}
}
],
"kind": "const"
}
複製程式碼
每一個{"type":""}包裹的內容都可以視為一個節點(Node)
轉換
得到了AST抽象語法樹,本質就是一個用來描述程式碼的節點樹(Node),我們就可以通過 樹形遍歷來遍歷它,從而進行程式碼轉換(對節點新增、更新及移除等操作),也就是Babel外掛真正處理的地方
生成
經過轉換之後的AST還是AST,所以我們還需要將AST生成字串形式的程式碼
實戰
Babel的基礎知識還有很多,我覺得一開始瞭解這麼多就夠了,我們現在開始開發一個簡單的Babel轉換。
如前面所說Babel的3個步驟,解析,轉換,生成,Babel都提供了對應的方法,分別如下:
- @babel/parser 提供解析
parse
- @babel/traverse 提供轉換
traverse
- @babel/generator 提供生成
generate
我們要實現一個外掛,將整個引入元件的程式碼
import { Select as MySelect, Pagination } from 'UI';
// import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
複製程式碼
處理為如下按需處理的形式
import MySelect from "/MySelect/MySelect.js";
import Pagination from "/Pagination/Pagination.js"; // import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
複製程式碼
第一:搭建一個開發環境
這裡我使用了codesandbox線上編寫的方式,訪問這裡,將需要的依賴包引進來。
const parse = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
複製程式碼
其中,@babel/types
是用於 AST 節點的 Lodash 式工具庫, 它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯非常有用。
第二:解析程式碼
const code = `import { Select as MySelect, Pagination } from '';
// import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
`;
const ast = parse(code);
複製程式碼
第三:轉換程式碼
這步很關鍵,我們的轉換處理都在這一步
traverse(ast, {
ImportDeclaration(path) {
// 獲取原本元件名
const source = path.node.source.value;
// 獲取Select as MySelect , Pagination兩個節點
const specifiers = path.node.specifiers;
// import specifiers有3種形式,ImportSpecifier ,ImportNamespaceSpecifier,ImportDefaultSpecifier
// 獲取specifiers型別是否是 名稱空間型別,類似 import * as UI from 'xxx-ui' 這種
const isImportNamespaceSpecifier = t.isImportNamespaceSpecifier(
specifiers[0]
);
// 獲取specifiers型別是否是 預設匯出型別,類似 import UI2 from 'xxx-ui' 這種
const isImportDefaultSpecifier = t.isImportDefaultSpecifier(specifiers[0]);
if (!isImportNamespaceSpecifier && !isImportDefaultSpecifier) {
const declarations = specifiers.map(specifier => {
// 快取單個元件名
let localName = specifier.local.name;
// 拼接引入路徑
let newSource = `${source}/${localName}/${localName}.js`;
// 構造新的ImportDeclaration節點
return t.importDeclaration(
[t.importDefaultSpecifier(specifier.local)],
t.stringLiteral(newSource)
);
});
// 將構造好的新AST替換原來的AST
path.replaceWithMultiple(declarations);
}
}
});
複製程式碼
traverse
方法第二個引數傳入的就是我們對具體節點遍歷的處理方法,這裡有個概念需要明確的是,當我們以訪問者身份遍歷節點的時候,我們其實訪問的是路徑path,而非具體某個節點,所以示例中我們我們有2個ImportDeclaration
節點,但我們只寫了一個處理方法,因為這裡這個方法會被執行2次。
第四:生成
最後,我們需要將轉換後的AST重新生成程式碼
let newCode = generate(ast).code;
console.log(newCode);
複製程式碼
第五:執行
終端輸入
node ./src/index.js
複製程式碼
可以看到最終我們生成的程式碼
import MySelect from "/MySelect/MySelect.js";
import Pagination from "/Pagination/Pagination.js"; // import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
複製程式碼
實際專案引用
我們完成了一個babel外掛,那在專案中如何引入呢?其實,上述所述的步驟程式碼只是從內部剖析了下Babel外掛的處理原理,真正我們在專案中只需要對外暴露一個方法,裡面返回一個包含visitor
屬性的物件。
visitor訪問者是一個物件,定義了一系列訪問樹形結構中節點的方法
// myPlugin.js
const babel = require(@babel/core');
const t = require('@babel/types');
export default function() {
return {
visitor: {
ImportDeclaration(path, state) {
//轉換邏輯
},
}
};
};
複製程式碼
然後在babel-loader
的plugin引入
options:{
plugins:[
["myPlugin"]
]
}
複製程式碼
原理是啥呢?是因為通過babel-loader
引入,Babel裡面core模組提供了transform
方法,具體APi可以檢視這裡,只需要傳入visitor物件,該方法據此預設會去做解析,轉換,生成工作,內部處理邏輯如下:
const visitor = require('visitor.js');
const babel = require('@babel/core');
const result = babel.transform(code, {
plugins: [visitor],
});
複製程式碼
進階例子
這裡提供一個簡化版的vue
轉react
的示例,有興趣的可以學習下,地址。
最後,如果你對多端開發有興趣,我們微店有個小組,從事多端統一開發研究,有興趣的童鞋也可以加入進來看看,裡面有很多Babel相關例項和文章,訪問地址