babel是一個編譯器,用於將ECMA2015+代碼轉換為向后兼容的javascript語法,其原因在於目前瀏覽器並不能及時的兼容js的新語法,而開發過程中我們往往會選擇es6、jsx、typescript進行開發,而瀏覽器並不能識別並執行這些代碼,因此就必須將這些代碼編譯並轉換成瀏覽器識別的代碼,所以我們才會發現所有的項目構建工具都是使用babel,這就顯示出來babel的重要性。雖然經常使用,但是每次使用都是使用固定的配置代碼,卻沒有了解其執行原理。知其然就需要知其所以然,所以記下自己對於babel的理解。
一、babel的執行過程
想要了解一個東西就需要先從宏觀上分析它,babel也不例外。我們知道對於一個計算機語言,我們敲出來的代碼都是字符串,想要執行就必須經過編譯。比如react、vue等框架也需要對其類html代碼進行編譯才能生成viertual dom。既然babel需要將高級語法轉換,那么babel也勢必需要進行編譯,而babel的執行過程就是一個編譯轉換的工程。如下圖是babel的執行過程:
由此我們可以看到babel的核心就是parse、transform和generator三個部分。
接下來我們采用babel中的插件演示一個最簡單的例子。
var {parse} = require('@babel/parser'); var {default: generate} = require('@babel/generator'); var code = `const name = "jyy";`; // 原始代碼 var ast = parse(code); // 源代碼生成的ast var targetCode = generate(ast); // 將ast轉成目標代碼 console.log(targetCode); //{ code: 'const name = "jyy";', map: null, rawMappings: undefined }
parse和generate顧名思義就是編譯器和生成器,你可能會發現缺少轉換過程,而且生成的目標代碼和原始代碼都是一樣的,只是多了屬性而已,這相當於什么都沒有做啊。那是因為轉換過程的具體操作需要插件來實現,如果沒有使用插件,最后生成的目標代碼是和原始代碼一樣的。
1.parse
在babel中編譯器插件是@babel/parser,其作用就是將源碼轉換為AST,使用前需要npm install @babel/parser,使用方法如下:
const babelParser = require("@babel/parser"); const code = "const name= 'jyy';"; const ast = babelParser.parse(code); console.log(ast);
執行后輸出的結果如下:
1 Node { 2 type: 'File', 3 start: 0, 4 end: 12, 5 loc: 6 SourceLocation { 7 start: Position { line: 1, column: 0 }, 8 end: Position { line: 1, column: 12 } }, 9 errors: [], 10 program: 11 Node { 12 type: 'Program', 13 start: 0, 14 end: 12, 15 loc: SourceLocation { start: [Position], end: [Position] }, 16 sourceType: 'script', 17 interpreter: null, 18 body: [ [Node] ], 19 directives: [] }, 20 comments: [] }
這就是babel生成的ast結構,你可能會發現這個ast和平常我們看到的ast好像結構不同,因為沒有發現“name"、“=”等詞法單元。是的,我一開始看到的時候也在懷疑這個輸出到底是不是ast。后來發現babel的parser是根據babel的AST結構生成的,他基於ESTree規范。於是深入輸出發現了code的詞法單元的位置,就在上面代碼的標紅處,可以使用ast.program.body[0],得到如下的輸出:
從結果中我們看到了declarations(聲明)關鍵字和我們源碼中的const,接着繼續輸出declarations,結果如下:
可以看到這個結果確實將我們的源碼構造成了一個AST。
2.transform
這個過程雖然在本文中名字叫transfom,但是事實上babel官網中並沒有這個詞,更沒有稱為轉換器的結構。想要知道為什么沒有,我們需要知道bable是一個工具鏈,所謂工具鏈就是babel是依賴於它的插件的,只有有了插件babel才能發揮出真正的作用,沒有插件的babel只是會將源碼生成AST,然后在通過生成器生成和原來的源碼一摸一樣的代碼,這樣的過程是沒有任何作用的。插件發揮作用的地方基本都是在tranfrom這個過程,當源碼通過parse生成了ast后,我們可以通過轉換插件,對ast進行操作。比如@babel/plugin-transform-react-jsx是將react中的jsx轉換為react的節點對象。這樣這些插件都涉及到對ast的操作,babel提供了一些工具插件,讓我們可以方便的操作ast節點,也就更方便我們開發適合自己項目的插件。比如在babel官網中設計到的插件,點這里。下面介紹兩個比較重要的插件,同時用這兩個實現一個比較簡單的操作ast過程。
@babel/types
這個插件的api非常多,見這里,我也沒有實際用過,在這里只是簡單的介紹下了。它的作用是創建、修改、刪除、查找ast節點,因為ast也是一個樹狀結構,我們可以像js操作dom節點一樣,使用types對ast進行操作。
另外我們知道ast的節點也是分為多種類型,比如ExpressionStatement是表達式、ClassDeclaration是類聲明、VariableDeclaration是變量聲明等等,同樣的這些類型都對應了其創建方法:t.expressionStatement、t.classDeclaration、t.variableDeclaration,也對應了判斷方法:t.isExpressionStatement、t.isClassDeclaration、t.isVariableDeclaration。這個插件往往和traverse遍歷插件一起使用,因為types只能對單一節點進行操作,一般是在對節點的迭代中使用,所以這個插件的例子會放在traverse的實例中。
@babel/traverse
這個插件的作用是對ast進行遍歷parse,在迭代的過程中可以定義回調函數,回調函數的參數提供了豐富的增、刪、改、查以及類型斷言的方法,比如replaceWith/remove/find/isMemberExpression。
下面以一個例子結合types和traverse進行演示。
假設我們在開發過程中使用一個函數findEleById來代替document.getElementById,如果我們直接使用findEle而不對其進行處理,js代碼執行過程中是會報錯,因為window下是沒有這個函數的。但是我們可以使用babel修改其ast,將findEleById改為document.getElementById,這樣babel的生成器生成的最新代碼就是document.getElementById,然后js引擎就可以編譯通過了。當然這個過程對於開發者是隱藏的,開發者只需要關注於使用findEleById便捷的開發就可以了,后續的操作交給babel。見如下代碼:
var t = require('@babel/types'); var {parse} = require('@babel/parser'); var {default: traverse} = require('@babel/traverse'); var {default: generate} = require('@babel/generator'); var orginCode = `findEleById("jyy")`; // 原始代碼 // 生成原始AST var originAST = parse(orginCode, { sourceType: "module" }); // 對AST進行遍歷並操作 traverse(originAST,{ Identifier(path){ var {node} = path; // 找到findEleById,將其替換成為目標節點 if(node && node.name === "findEleById"){ var newNode = t.memberExpression(t.identifier("document"), t.identifier("getElementById")); // 創建目標節點 path.replaceWith(newNode); // 替換原始節點 path.stop(); } } }); const targetCode = generate(originAST, { /* options */ }, orginCode); // 將轉換后的AST生成目標代碼 console.log(targetCode); // { code: 'document.getElementById("jyy");',map: null,rawMappings: undefined }
從上面代碼可以看到基本的轉換過程,生成的最終代碼可以直接交付給瀏覽器引擎編譯執行了。
在babel的工具插件還有一些,因為本文不是為了講解如何開發babel插件,所以這里僅介紹以上兩個插件只為介紹babel在transform階段的基本的工作原理。如果你真的需要開發自己的babel插件,那么需要了解babel提供的插件們,並了解其api的使用。
3.generator
這個過程已經在上面的實例中有所展現,使用的插件是@babel/generator,其作用就是將轉換好的ast重新生成代碼。這樣的代碼就就可以安全的在瀏覽器運行。
4.babel-core——整合基本插件
我們發現基本的babel插件如@babel/parse、@babel/generator都是提供了代碼轉換的基本功能,而另外的一些工具類型的插件比如@babel/types、@babel/traverser起作用是提供操作ast節點的功能。然而在開發插件的過程中如果每個都需要去引入實在太麻煩,所以就有了@babel/core插件,顧名思義就是核心插件,他將底層的插件進行封裝,並另外加入了其他功能,比如讀取、分析配置文件,這個后面會在配置中講到。而這個插件將復雜的過程進行簡化,如下代碼所示:
1 var babel = require("@babel/core"); 2 var code = "<div class='c'>jyy</div>"; // 代碼 3 babel.transform(code,{plugins: ["@babel/plugin-transform-react-jsx"],},function(err, result){ 4 console.log(result.code); 5 // React.createElement("div", { 6 // class: "c" 7 // }, "jyy"); 8 });
可以發現,我們可以使用transform就可以完成整個步驟。另外我們查看core的依賴可以發現,它依賴於底層的插件並基於次進行進行封裝:
core的api很多,可以查看babel官網,另外我們看transform函數的參數第一個為原始代碼,第二個為用於在轉換過程中對ast進行操作的插件,例子中我們使用的是轉換jsx的插件,第三個參數是一個回調函數。
二、插件
我們一般不會自己開發babel的轉換插件,實際項目中往往都是直接使用現成的插件。而配置插件卻時常讓人很煩悶,因為有各種插件,什么stage-1、env、es2015等等,各種插件的各種配合設置給人摸不到頭腦的感覺,會想問為什么不能出一個統一的插件,里面包含所有的轉換功能,這樣在配置的時候只需要在plugins里面放一個插件名就好了呢?這個問題主要是有以下幾點原因:
一是因為js發展太快了,我們知道這幾年js新的語法和函數不斷地出現,如果把所有對最新語法的轉換放在一個插件中,那么每次出現新的語法就需要不斷的修改代碼
二是babel不僅僅包含對ecma2015+的轉換,還包括ts、flow和上面我們提到的jsx的轉換,全都揉在一起的話實在是太大太亂了,不易於維護
三是這樣做可以讓用戶更自由地去選擇,就像菜市場去買菜,我只會去買我想要買的菜。這無疑減少了項目打包時的負擔,進而影響到所占用的網絡的帶寬。事實上這種方式廣泛的存在於產品中。比如echarts,我們可以選擇其中的某些圖表,然后下載對應的代碼,而這樣做的前提就是降低模塊之間的耦合。
以上的分析都是我在瞎扯淡,僅供參考,誰知道babel的開發者是怎么想的。但至少是本人的一個見解。
一般情況下,項目不需要我們去開發babel插件,因為這些插件都寫好了,看官網。里面的插件非常多,而且有些插件貌似很小,僅僅具體到一個語法,比如arrow-function,這是ES2015中的箭頭函數語法,下面使用這個插件進行演示:
var babel = require("@babel/core"); var code = `num => { return num ** 2; }`; // 代碼 babel.transform(code,{plugins: ["@babel/plugin-transform-arrow-functions"]},function(err, result){ console.log(result.code); // (function (num) { // return num ** 2; // }); });
結果如我們預期,使用arrow-function插件后,確實將箭頭函數轉換為了es5的語法。但是我們還會發現代碼中的**,冪等運算符是ES2016的新特性。這樣的代碼還是不能被es5識別,於是我們加入轉換冪等運算符的插件:
var babel = require("@babel/core"); var code = `num => { return num ** 2; }`; // 代碼 babel.transform(code,{plugins: ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-exponentiation-operator"]},function(err, result){ console.log(result.code); // (function (num) { // return Math.pow(num, 2); // }); });
發現結果已經將冪等運算符進行了轉換。對於插件的配置需要記住以下幾點:
1.plugin的段名稱
配置plugin的時候,可以設置插件的短名稱,可以將省略babel-plugin,例如:
["@babel/babel-plugin-name"]和["@babel/name"]是等價的
2.排列順序
多個插件的執行順序是按照從前到后的順序執行,例如["@babel/name1","@babel/name2"]兩個插件的執行順序是先執行name1,然后執行name2。
三、預設(preset)——babel的插件套裝
那么問題來了新語法新特性那么多,難道我們要挨個去加嗎?當然不是,babel已經預設了幾套插件,將最新的語法進行轉換,可以使用在不同的環境中,如下:
@babel/preset-env
@babel/preset-flow
@babel/preset-react
@babel/preset-typescript
從名字上就能看出他們使用的環境了,需要注意的是env,他的作用是將最新js轉換為es6代碼。預設是babel插件的組合,我們可以看下package.json(截取一部分):
由此看到他組合了很多的插件,是一個官方提供的,這樣我們只需要使用一個插件就可以了。那么有了這個插件,我們使用上一個例子,來測試一下:
var babel = require("@babel/core"); var code = `num => { const offset = 23; return num ** 2 + offset; }`; // 代碼 babel.transform(code,{presets: ["@babel/preset-env"]},function(err, result){ console.log(result.code); // "use strict"; // (function (num) { // var offset = 23; // return Math.pow(num, 2) + offset; // }); });
可以看到,代碼中額外將const轉為var,還加上了use strict
需要注意的是因為@babel/preset-env是預設的包含多個插件,所以不同於單一的插件,需要使用presets參數,如代碼紅色標記所示。
對於env插件,我們還需要知道他是以前es2015、es2016和es2017的集合,另外他默認不支持stage-x插件。
stage-x(babel7已廢棄)
那么什么是stage-x呢?state-x里面包含了當年最新規范的草案,每年更新。因為有可能項目所使用的是最新的語法,那么官方的預設插件還沒有將其納入,這時候就需要使用state-x。如下是state-x的階段:
- Stage 0 - 稻草人: 只是一個想法,經過 TC39 成員提出即可。
- Stage 1 - 提案: 初步嘗試。
- Stage 2 - 初稿: 完成初步規范。
- Stage 3 - 候選: 完成規范和瀏覽器初步實現。
- Stage 4 - 完成: 將被添加到下一年度發布。
所以我們經常會在代碼中看到這樣的preset配置:[es2015, react, stage-0]。好在在babel7,stage-x以被廢棄,詳情點這里。
額外注意的
1.preset可以設置短名稱
和插件一樣preset也可以設置段名稱。可以省略preset,例如:
{ presets: ["@babel/preset-env", "@babel/preset-react"]
}
可以省略為:
{ presets: ["@babel/env", "@babel/react"]
}
2.排列順序
預設的執行順序也同樣重要,preset在plugin之前執行,而且和plugin不同的是,preset是從后往前執行,比如我們使用react,那么應該這么寫:
{ presets: ["@babel/env", "@babel/react"] }
因為我們需要將react中的jsx轉為js,然后將js在轉換為es5,所以需要將react的插件放在后面,讓他先執行。
四、配置
實際項目中我們不會親自動手去調用babel的api去轉換代碼,而且如果我們整個項目很可能都是用es6編寫,不可能手動調用babel的api去一個一個轉換,我們希望使用命令行,通過傳遞文件夾的名稱去交給babel轉換。這個時候babel-cli就出現了,比如,我們想要轉換某個文件夾下的文件,那么我們可以在控制台輸入這樣的命令:
babel src --out-dir lib --presets=@babel/preset-env,@babel/react
這段命令的意思是,將src文件夾下的所有文件使用env和react預設進行轉換,並且將轉換后的文件存放在lib文件夾下。這樣就節省了我們很多的時間。需要注意的是:babel-cli只是一套命令,想要執行babel的轉換工作,仍然需要引入babel-core。
此時仍然有個問題預設也是有參數的,另外還有plugins等等,當然可以將這些參數加在命令中,但是這樣還是會很復雜。解決這個問題的通用方法就是在項目中創建一個配置文件,在里面配置相應的插件和預設。
1. .babelrc
只是項目中經常用到的方式,在項目根目錄創建名為.babelrc文件,內部包括兩個方面:plugins和p'resets
{ "presets": [...], "plugins": [...] }
2. babel.config.js
這種方式是使用js代碼編寫,並導出一個和上面方式相同的對象
1 module.exports = function () { 2 const presets = [ ... ]; 3 const plugins = [ ... ]; 4 return { 5 presets, 6 plugins 7 }; 8 }
3. package.json
這種該方式是在項目配置文件package.json中進行配置,如下:
1 { 2 "name": "my-package-babel", 3 "version": "1.0.0", 4 "babel": { 5 "presets": [ ... ], 6 "plugins": [ ... ], 7 } 8 }
這三種都是等效的,當配置完成后,可以在babel-cli的命令行中配置,比如:
babel --config-file /path/to/my/babel.config.json --out-dir dist ./src
事實上,真正收取並處理分析配置文件的還是@babel/core,在core源碼里面對於支持的配置文件名如下:
五、具體使用實例
下面以真實項目的搭建為例,簡單介紹babel的具體使用方式。
項目的框架使用react,構建工具是webpack。webpack有babel-loader的插件,加入這個loader之后表示所有打包的文件都會由babel-loader來處理,同時使用到的babel插件有@babel/core、@babel/preset-env、@babel/preset-react。此時webpack中的配置文件關於babel的配置如下:
webpack.config.js:
1 { 2 test: "\.js$", 3 loader: "babel-loader", 4 exclude: "/node_modules" 5 }
.babelrc
{ "presets": ["@babel/env", "@babel/react"], "plugins": ["transform-runtime"] }