Babel簡介
Babel是Javascript編譯器,代碼到代碼的編譯器,通常也叫做『轉換編譯器』。
Babel的工作過程
Babel的處理過程主要爲3個:解析(parse)、轉換(transform)、生成(generate)。
- 代碼解析
詞法分析和語法分析構造AST。 - 代碼轉換
處理AST,處理工具、插件等就是在這個階段進行代碼轉換,返回新的AST。 - 代碼生成
遍歷AST,輸出代碼字符串。所以我們需要對AST有一定了解才能進行Babel插件開發。
AST
在這整個過程中,都是圍繞着抽象語法樹(AST)來進行的。在Javascritp中,AST,簡單來說,就是一個記錄着代碼語法結構的Object。感興趣的同學可到https://astexplorer.net/ 去深入體驗
比如下面的代碼:
import {Button} from 'antd';
import Card from 'antd/button/lib/index.js';
轉換成AST後如下,
{
"type": "Program",
"start": 0,
"end": 253,
"body": [
{
"type": "ImportDeclaration",
"start": 179,
"end": 207,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 187,
"end": 193,
"imported": {
"type": "Identifier",
"start": 187,
"end": 193,
"name": "Button"
},
"local": {
"type": "Identifier",
"start": 187,
"end": 193,
"name": "Button"
}
}
],
"source": {
"type": "Literal",
"start": 200,
"end": 206,
"value": "antd",
"raw": "'antd'"
}
},
{
"type": "ImportDeclaration",
"start": 209,
"end": 253,
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 216,
"end": 220,
"local": {
"type": "Identifier",
"start": 216,
"end": 220,
"name": "Card"
}
}
],
"source": {
"type": "Literal",
"start": 226,
"end": 252,
"value": "antd/button/lib/index.js",
"raw": "'antd/button/lib/index.js'"
}
}
],
"sourceType": "module"
}
插件開發思路
- 確定我們需要處理的節點類型
- 處理節點
- 返回新的節點
簡單插件結構
插件必須是一個函數,根據官方文檔要求,形式如下:
module.exports = function ({ types: t }) {
return {
visitor: {
ImportDeclaration(path, source){
//todo
},
FunctionDeclaration(path, source){
//todo
},
}
}
}
types來自@babel/types工具類,主要用途是在創建AST的過程中判斷各種語法的類型。
實現示例
很多同學用過 babel-plugin-import ,它幫助我們在使用一些JS類庫是達到按需加載。其實,該插件幫助我們做了如下代碼轉換:
//from
import {Button } from 'antd/es/button';
//to
import Button from 'antd/es/button';
import 'antd/es/button/style';
我們先看看兩者的AST有何差別,以幫助我們對轉換有個清晰的認識:
轉換前:
[{
"type": "ImportDeclaration",
"start": 6,
"end": 45,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 14,
"end": 20,
"imported": {
"type": "Identifier",
"start": 14,
"end": 20,
"name": "Button"
},
"local": {
"type": "Identifier",
"start": 14,
"end": 20,
"name": "Button"
}
}
],
"source": {
"type": "Literal",
"start": 28,
"end": 44,
"value": "antd/es/button",
"raw": "'antd/es/button'"
}
}]
轉換後:
[{
"type": "ImportDeclaration",
"start": 5,
"end": 41,
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 12,
"end": 18,
"local": {
"type": "Identifier",
"start": 12,
"end": 18,
"name": "Button"
}
}
],
"source": {
"type": "Literal",
"start": 24,
"end": 40,
"value": "antd/es/button",
"raw": "'antd/es/button'"
}
},
{
"type": "ImportDeclaration",
"start": 46,
"end": 76,
"specifiers": [],
"source": {
"type": "Literal",
"start": 53,
"end": 75,
"value": "antd/es/button/style",
"raw": "'antd/es/button/style'"
}
}]
對比兩棵樹,我們應該有個大致的思路。在轉換過程中,我們還需要一些參數,這些參數在配置文件(package.json或者.babelrc)中,提供了一些自定義配置,比如antd的按需加載:
["import",{libraryName:"antd",libraryDireactory:"es","style":"css"}]
現在我們開始嘗試實現這個插件吧:
module.exports = function ({ types: t }) {
return {
visitor: {
ImportDeclaration(path, source) {
//取出參數
const { opts: { libraryName, libraryDirectory, style="css" } } = source;
//拿到老的AST節點
let node = path.node
if(node.source.value !== libraryName){
return;
}
//創建一個數組存入新生成AST
let newImports = [];
path.node.specifiers.forEach(item => {
newImports.push(t.importDeclaration([t.importDefaultSpecifier(item.local)], t.stringLiteral(`${libraryName}/${libraryDirectory}/${item.local.name}`)));
newImports.push(t.importDeclaration([t.importDefaultSpecifier(item.local)], t.stringLiteral(`${libraryName}/${libraryDirectory}/${style}/index.css`)))
});
path.replaceWithMultiple(newImports);
return newImports;
}
}
}
}
現在,簡單的babel產檢實現我們已經體驗了。若感興趣瞭解更多內容,babel官網提供了很多詳細資料。