本文會對babel文檔文檔從一個推導角度來闡述每個babel模塊的作用,嘗試理清其中脈絡,方便快速理解。
本文不是官網的copyer或者中文翻譯
<a name="xVQ3u"></a>
核心
babel的核心功能在@babel/core
包中,核心api為transform
系列函數:
babel.transform(code, options, function(err, result) {
result; // => { code, map, ast }
});
該函數可以將es6+代碼轉譯成es5代碼,所以被廣泛集成在其他工具里面,完成代碼的轉譯工作,如babel-loader
內部就是調該api。
在babel中,還提供了@babel/cli
和@babel/register
兩個工具,前者提供命令行工具函數對文件進行轉譯;后者提供require
鉤子:對node的require函數改造,對后續require
函數在執行時自動對模塊進行源碼轉譯后在導入。
babel的目標是對代碼進行轉譯,這個過程可以拆解為:解析源碼,遍歷ast改造代碼,重新生成代碼這三個過程。為了提高使用范圍,在v7+
版本中,babel將功能拆解出來了多個工具,主要有:
- 解析源碼:
@babel/parser
; - 遍歷ast改造代碼:
@babel/traverse
和@babel/plugin-*
; - 重新生成代碼:
@babel/generator
;
<a name="Wh9AE"></a>
解析源碼
function square(n) {
return n * n;
}
解析成:
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}
ast可以簡單的理解為源碼字符串進行語法分析后的結構化數據,方便后續進行檢查或者改造。
ast中的節點一般還會包含坐標位置,如字符串下標,行數,列數等,更多詳細內容請參考官方文檔。
老版本babel中使用的是 acorn 和 acorn-jsx,在v7
以上時,進行了fork改造為@babel/parser
。
另外@babel/core
也集成了@babel/parser
功能,可以直接從@babel/core
中導出api直接使用:
babel.parse(code: string, options?: Object, callback: Function)
當前的解析器默認只支持最新的es6代碼,如果需要兼容一些新語法(非語法糖之類的新特性,新表達式和新操作服,如對象解構,可選表達式,類型等),需要擴展babel語法插件。
很多工具其實只需要解析代碼即可,如代碼檢驗,如語法高亮,源碼中數據收集。
<a name="rzqMT"></a>
遍歷ast改造代碼
講過解析器已經將源碼解析成更好處理的結構化數據ast,如果需求是對代碼進行調整,只需對ast數據進行調整,然后使用生成器生成新的代碼即可。但整個babel需要解決的是將所有最新的es6+特性轉譯成向后兼容的瀏覽器可執行代碼(es5),需要處理的情況眾多,如果直接對ast進行改造,那么代碼將非常臃腫。且es規范還在不停的迭代中,臃腫的代碼的對后續維護迭代也帶來巨大的挑戰。針對這種困境,必須需要進行架構上的調整,使用插件化架構。
babel即是處于這樣一個原因,采用了訪問者模式。可以簡單的理解為,在對ast進行一個遍歷時,每次進入一個新的節點或者退出一個節點時,都會拜訪每一個插件,咨詢它們是否需要對當前的情況進行處理。這鐘架構證了性能,也保證了擴展性。
另一種插件化架構,也就是流式架構,如gulp。也就是插件隊列依次對上一個插件處理后的ast對象進行更深一層次的改造。但這種架構,需要多次循環ast, 在實際使用中,一般一個生產項目,文件內容巨大,文件數量居多,會導致性能崩潰。
所以babel核心框架中,只包含了訪問節點和調用插件的邏輯。實際對ast的改造,全部轉交給了插件。這也是為什么babel自帶了那么多@babel/plugin-*
的插件。同樣社區也擁有非常多的插件,從能能夠支持flow, typescipt這些新語法。
插件化的架構,也允許使用者進行拔插式配置,根據當前使用場景進行高度定制。這也就是在配置文件中如babrlrc.js
可以配置插件的原因。
為了支持高度動態配置化來適配復雜的場景,babel會將每個插件負責的功能劃分足夠小,一般每個插件只會負責一個特性。這會導致使用時,需要去了解每一個插件的作用,然后在配置文件中配置超長的插件列表,帶來巨大的心智負擔和維護難度。為了解決這個問題,babel提供了預設的機制。簡單的理解就是一個babel配置可以繼承另一個配置,那么我們只需要繼承社區上或者官方專業人員配置的預設即可,如:
@babel/preset-env
@babel/preset-react
@babel/preset-typescript
@babel/preset-flow
為了方便插件中的復用,babel將遍歷ast的工具也開放出來為一個單獨的模塊@babel/traverse
。將節點類型的判斷和創建節點的工具庫,放在了@babel/types
。
另外對于一些babel中多個模塊公用的一些工具,都封裝成工具模塊,也就是@babel/helper-*
系列模塊,如:
@babel/helper-compilation-targets
@babel/helper-module-imports
由于babel自帶了那么多插件,所以很多helper其實是插件的輔助工具,如helper-module-imports
就是輔助生成一些導入節點。
在es6+轉成es5的過程中,很多語法糖語法(語法上的細微調整),如let
, const
等實現直接用插件調整代碼即可解決。但對于其他的需要大端代碼才能實現的特性,如Array#includes
,生成器,迭代器,async/await
, promise
等,如果每次都通過代碼展開,那么編譯后的代碼將會巨大。為了解決這個問題,會將includes
的實現放在補丁(polyfill)中,然后直接使用補丁中的實現。如生成器,迭代器,async/await
, promise
等都是通過這種機制支持。
這些的補丁(polyfill)的導入方式也有兩種,一種是全量導入,也就是導入@babel/polyfill
模塊。一種是按需導入,需要使用預設@babel/preset-env
,根據實際使用情況,在使用的模塊中按需導入@babel/runtime
中的補丁(polyfill)。如:
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var Circle = function Circle() {
_classCallCheck(this, Circle);
};
@babel/polyfill
和@babel/runtime
的底層實現都是core-js
。
實際情景下,還是存在插件無法解決的情況:一個無法用老代碼補丁實現,也無法使用語法糖替換代碼的特性,如Proxy
對象,這種特性一般需要js引擎從底層提供。在使用這些特性時,需要注意瀏覽器兼容性。
<a name="uaLlx"></a>
生成代碼
使用@babel/generator
即可對一個ast樹重新生成為代碼。
<a name="S8g5j"></a>
配置
我們通常見到的babel配置就是就是用于指導babel行為的配置文件,可以簡單的理解為@babel/core
中transform
函數的選項支持使用配置文件配置。
更多的配置詳細使用等請看官網。
<a name="sRLPS"></a>
其他官方工具
babel還提供了一些其他工具,用于擴展babel生態鏈:
-
@babel/standalone
: 支持瀏覽器上運行的babel版本,用于一些在線編輯網站,如JS Bin -
@babel/code-frame
: 代碼窗口,用于輸出類似這種:
1 | class Foo {
> 2 | constructor()
| ^
3 | }
-
@babel/template
: babel插件開發工具,支持根據代碼字符串創建ast節點。因為ast節點攜帶信息較多,且結構較深,在手動創建復雜的代碼節點時十分不便。使用官方提供的這個工具,可以快速創建一整段代碼節點,并且還支持占位符:
const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`);
const ast = buildRequire({
IMPORT_NAME: t.identifier("myModule"),
SOURCE: t.stringLiteral("my-module"),
});
// || || ||
// \\// \\// \\//
const myModule = require("my-module");