前端利器躬行記(2)——Babel
Babel是一個JavaScript編譯器,不僅能將當前執行環境不支援的JavaScript語法(例如ES6、ES7等)編譯成向下相容的可用語法(例如ES3或ES5),這其中會涉及新語法的轉換和缺失特性的修補;還支援語法擴充套件,從而能隨時隨地的使用JSX、TypeScript等語法。目前最新版本是7.4,自從6.0以來,Babel被分解的更加模組化,各種轉譯功能都以外掛的形式分離出來,可按自己的需求,靈活配置。
在7.0版本中,對Babel的包做了一次大調整,統一改成域級包,將原先以“babel-”為字首的包遷移到@babel的名稱空間,例如@babel/core、@babel/cli等。這種模組化的設計,既能區分是否是官方釋出的,也能避免命名衝突。
一、@babel/core
如果要以程式設計的方式使用Babel,那麼可以通過@babel/core實現,安裝命令如下所示。
npm install --save-dev @babel/core
在引入該包後,就能在JavaScript檔案中直接編譯程式碼、檔案或AST。以編譯程式碼為例,可選擇的方法有三個,如下所示。
var babel = require("@babel/core"); babel.transform(code, options, function(err, result) { console.log(result); // => { code, map, ast } }); babel.transformSync(code, options) // => { code, map, ast } babel.transformAsync(code, options) // => Promise<{ code, map, ast }>
雖然transform()用於非同步編譯,transformSync()用於同步編譯,但是它們得到的結果相同,都是一個由轉換後的程式碼(code)、原始碼對映資訊(map)和抽象語法樹(ast)組成的物件。而transformAsync()得到的結果與前面兩個方法不同,它返回一個包含code、map和ast的Promise物件。這些方法的第二個options引數,可用於傳遞配置資訊,具體可參考官方文件。
如果要編譯檔案或AST,那麼也有類似的三個方法可供選擇。除了能編譯之外,利用這個包還能把程式碼解析成AST(即傳入一段程式碼,得到一個AST)以便其它外掛對其進行語法分析,可使用的方法有parse()、parseSync()和parseAsync()。
二、@babel/cli
@babel/cli是一個Babel內建的命令列工具,可通過命令來編譯檔案,其安裝如下所示。
npm install --save-dev @babel/cli
假設有一個src.js檔案,其內容如下程式碼所示,用到了ES6中的箭頭函式。
let fn = () => true;
在執行下面的命令後,可以看到輸出和輸入的程式碼相同,這是因為此時還未指定任何轉譯外掛。
./node_modules/.bin/babel src.js
如果當前的npm版本是5.2以上,那麼可將./node_modules/.bin縮短為npx,如下所示。
npx babel src.js
在babel中,有多個可用的引數,下面只列舉其中的四個,其餘引數的用法可參考官方文件。
npx babel src --out-dir dist npx babel src.js --out-file dist.js npx babel src.js --out-file dist.js --plugins=@babel/plugin-transform-classes,@babel/transform-modules-amd npx babel src.js --out-file dist.js --presets=@babel/preset-env,@babel/flow
(1)“--out-dir”引數可編譯整個src目錄下的檔案並輸出到dist目錄中。
(2)“--out-file”引數可編譯src.js檔案並輸出到dist.js檔案中。
(3)“--plugins”引數可指定編譯過程中要使用的外掛,多個外掛用逗號隔開。
(4)“--presets”引數可指定編譯過程中要使用的預設,多個預設用逗號隔開。
在package.json檔案中,可以通過scripts欄位宣告指令碼命令。如果想要簡化babel命令,那麼可以將它們遷移到scripts欄位中,如下所示。
{ "scripts": { "compile": "npx babel src.js --out-file dist.js" } }
現在要編譯目錄的話,只要執行下面的這條npm命令即可。
npm run compile
三、配置
在Babel中,可以將各種命令的引數集中到一個配置檔案中,而可配置的檔案包括babel.config.js、.babelrc和package.json。
1)babel.config.js
這是Babel 7最新引入的配置檔案,存在於根目錄中(即package.json檔案所在的目錄)。它不僅能以程式設計的方式宣告全域性生效的配置引數,還能利用overrides欄位對不同的子目錄進行鍼對性的配置,從而就能避免為相關目錄建立一個.babelrc檔案了。在下面的示例中,為test目錄單獨配置了預設(presets)。
module.exports = { presets: [...], overrides: [{ test: ["./test"], presets: [...] }] };
當執行下面兩條命令進行編譯時,第一條讀取的是最外層的預設,第二條讀取的是overrides中的預設。
npx babel src.js npx babel ./test/src.js
2).babelrc
babel.config.js並不是.babelrc的替代品,.babelrc檔案用於區域性配置(如下程式碼所示),可放置於所有目錄中。如果當前目錄沒有.babelrc檔案,那麼就會往上查詢直至找到為止。
{ "presets": [...] }
注意,如果.babelrc的字尾是“.js”(即.babelrc.js),那麼在檔案中可以通過JavaScript配置引數。
3)package.json
在package.json檔案中,可以宣告一個babel欄位,其值就是.babelrc檔案中的配置引數,如下所示。
{ "babel": { "presets": [...] } }
注意,不能讓.babelrc和宣告過babel欄位的package.json處在相同的目錄中。
4)配置函式或方法
前面三種都是用單獨的檔案來配置Babel的引數,其實還可以通過相關的函式或方法來達到相同地目的。在下面的示例中,引入gulp-babel包後就能通過得到的babel()函式來配置Babel的引數。
let babel = require("gulp-babel"); babel({ presets: [...] });
四、外掛
Babel的編譯可分為三個階段:解析、轉換和生成,而外掛(Plugin)在轉換過程中起到了至關重要的作用。藉助Babel的外掛可將解析完成的AST按照特定的要求進行處理,然後再輸出生成的程式碼。
1)外掛型別
Babel中的外掛分為兩種型別:語法和轉換。語法外掛只允許Babel解析成特定型別的語法,例如JSX、Flow等。轉換外掛常用來編譯ES6、ES7、React和Modules等,由於能自動開啟相應的語法外掛,因此不用顯式的指定兩種外掛。以之前的箭頭函式為例,如果要將其編譯成所有瀏覽器都能識別的形式,那麼就得安裝相應的外掛,如下所示。
npm install --save-dev @babel/plugin-transform-arrow-functions
在.babelrc檔案中的配置如下所示。
{ "plugins": ["@babel/plugin-transform-arrow-functions"] }
在配置檔案中,不僅能指定外掛的相對或絕對路徑,還可省略外掛名稱中的“babel-plugin-”字首。假設有個外掛名叫@org/babel-plugin-name,那麼它的相對路徑和簡寫名稱如下所示。
{ "plugins": [ "../@org/babel-plugin-name", "@org/name" ] }
2)執行順序
外掛的執行順序會受配置時所處的位置的影響,具體規則如下所列,其中預設是指官方預先設計的一組外掛集,本質上仍然是外掛。
(1)外掛執行在預設之前。
(2)外掛會按順序從前往後執行。
(3)預設與外掛相反,從後往前執行。
以下面的配置資訊為例,在外掛中,先執行@babel/plugin-transform-arrow-functions,後執行@babel/plugin-transform-classes。而在預設中,先執行@babel/preset-react,後執行@babel/preset-env。
{ "plugins": ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-classes"], "presets": ["@babel/preset-env", "@babel/preset-react"] }
3)外掛引數
如果要配置外掛的引數,那麼得在外掛名稱後增加一個引數物件,並且寫成陣列的形式,如下所示。
{ "plugins": [ ["@babel/plugin-transform-arrow-functions", { "spec": true }], ["@babel/plugin-transform-classes", { "loose": true }] ] }
預設也能接收引數,其寫法與之類似。
五、預設
如果在配置檔案中一個一個的宣告外掛,那麼不僅會讓該檔案變得巨大,而且還難免會有所遺漏。官方為了避免此類問題,引入了預設(Preset)的概念。預設類似於生活中的套餐,每個套餐會集合不同的外掛,從而能夠一次性安裝各類外掛,並且還可共享配置的引數。
1)@babel/preset-env
此預設不僅集合了最新的ES語法(即編譯指定的ES標準),還能配置所要支援的平臺(例如Node、Chrome等)和版本,以及按需載入外掛,其安裝命令如下所示。
npm install --save-dev @babel/preset-env
下面是一個預設的配置示例,支援Chrome 58和IE 11,以及超過市場份額5%的瀏覽器。還有許多其它可供選擇的配置引數,具體可翻閱官方文件。
{ presets: [ [ "@babel/preset-env", { targets: { chrome: "58", ie: "11", browsers: "> 5%" } } ] ] }
除了@babel/preset-env之外,官方還給出了其它實用的預設,例如@babel/preset-flow、@babel/preset-react和@babel/preset-typescript等。
與外掛類似,預設也能指定相對或絕對路徑,只不過它省略的字首是“babel-preset-”。假設有個預設名叫@org/babel-preset-name,那麼它的相對路徑和簡寫名稱如下所示。
{ "presets": [ "../@org/babel-preset-name", "@org/name" ] }
2)stage-x
這是一組實驗性質的預設,囊括了處於提案階段的標準,TC39委員會將提案分為5個階段,如表2所示。
表2 提案的五個階段
階段 | 描述 | 預設 |
Stage 0 | 設想(Strawman),由TC39的成員或已經註冊成TC39貢獻者的會員提出的想法 | @babel/preset-stage-0 |
Stage 1 | 建議(Proposal),產生一個正式的提案,確定提案負責人 | @babel/preset-stage-1 |
Stage 2 | 草案(Draft),規範的第一個版本,包含新特性的語法和語義,接下來只接受增量修改 | @babel/preset-stage-2 |
Stage 3 | 候選(Candidate),完成規範,獲得使用者反饋,接下來只有出現重大問題才做修改 | @babel/preset-stage-3 |
Stage 4 | 完成(Finished),將新規範納入到標準中,準備釋出 | 不存在,因為它就是已釋出的標準預設,例如ES6、ES7等 |
以上4個階段的預設存在著依賴關係,階段靠前的依賴階段靠後的(即數字小的包含數字大的),例如@babel/preset-stage-0依賴@babel/preset-stage-1。
由於這些提案階段的預設不太穩定,很有可能會被TC39委員會除名或變更,並且混合使用在配置上容易出錯,因此從Babel 7開始,它們都將被廢棄。
六、@babel/polyfill
雖然env預設(@babel/preset-env)能統一JavaScript的新語法(即高版本編譯成低版本),但是無法支援內建的新方法或新物件,例如Promise、Array.of()等。為此,Babel引入了的Polyfill技術(全部打包在@babel/polyfill中),將所缺的特性新增到全域性物件中或內建物件的原型上,彌補env預設的不足,從而模擬出完整的ES6+語法和特性。
@babel/polyfill包含兩個模組:regenerator-runtime和core-js,前者用於編譯生成器與非同步函式(async和await),後者用於處理其它相容性問題。@babel/polyfill的安裝命令如下所示,注意,使用的引數是--save而不是--save-dev,因為需要在原始碼之前先執行Polyfill。
npm install --save @babel/polyfill
1)useBuiltIns
在env預設中,存在一個與@babel/polyfill有緊密聯絡的useBuiltIns引數,它有三個關鍵字可供選擇,分別是false、entry和usage,具體說明如下所列。
(1)false:預設值,不開啟Polyfill,顯式的配置如下所示。
{ presets: [ ["@babel/preset-env", { useBuiltIns: false }] ] }
(2)entry:載入執行環境(可在targets引數中宣告)所需的Polyfill,下面是一個使用Promise的例子。
require("@babel/polyfill"); new Promise();
當useBuiltIns引數的值為entry時,這段程式碼在編譯後,就會引入許多不相干的JavaScript檔案,造成資源的浪費,如下所示。
require("core-js/modules/es6.promise"); require("core-js/modules/es6.array.fill"); require("core-js/modules/es6.math.trunc"); require("core-js/modules/es6.string.fixed"); ...... new Promise();
(3)usage:自動載入原始碼所需的Polyfill,仍然以Promise為例,如下程式碼所示,不用再顯式的引入@babel/polyfill。
new Promise();
當useBuiltIns引數的值為usage時,這段程式碼在編譯後,就會只引入需要的JavaScript檔案,如下所示。
require("core-js/modules/es6.promise"); require("core-js/modules/es6.object.to-string"); new Promise();
七、@babel/runtime
@babel/runtime與@babel/polyfill類似,也是用來增強env預設的,只是它以非侵入式的輔助函式來填補平臺所沒有的特性,其安裝命令如下所示。
npm install --save @babel/runtime
在Babel 7中還引入了一個@babel/runtime-corejs2,它比@babel/runtime多包含一個core-js模組,即能夠編譯Promise、Symbol等。接下來以ES6的類為例,如下所示。
class People { name() {} }
在將People類編譯後,得到的結果如下所示,省略了三個函式中的邏輯程式碼。
"use strict"; function _classCallCheck(instance, Constructor) { } function _defineProperties(target, props) { } function _createClass(Constructor, protoProps, staticProps) { } var People = /*#__PURE__*/ function () { function People() { _classCallCheck(this, People); } _createClass(People, [{ key: "name", value: function name() {} }]); return People; }();
1)@babel/plugin-transform-runtime
由於編譯生成的輔助函式會滯留在所使用的檔案中,因此檔案越多冗餘的函式就越多。如果人工分離,那工作量將巨大,因此Babel提供了能自動將它們分離的@babel/plugin-transform-runtime外掛,其安裝命令如下所示。
npm install --save-dev @babel/plugin-transform-runtime
仍然以ES6的People類為例,先在配置檔案中將其宣告,如下所示。
{ plugins: ["@babel/plugin-transform-runtime"] }
然後再將類編譯,三個輔助函式就能通過引入的方式得到,如下所示。
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
八、動態編譯
在Babel中有兩種方式實現動態編譯,分別是@babel/register和@babel/node,它們的安裝命令如下所示。
npm install --save-dev @babel/register npm install --save-dev @babel/node
雖然動態編譯省去了手動操作,但是會損耗程式的速度和效能,因此不適合生產環境。
1)@babel/register
在將其安裝後,就會為Node.js的require()函式增加一個鉤子。每當通過require()載入字尾為.es6、.es、.jsx、.mjs或.js的檔案時,就能自動對其進行Babel編譯。接下來用一個例子來演示@babel/register的用法,首先建立一個src.js的檔案,其程式碼如下所示。
module.exports = () => true;
然後建立一個default.js檔案,引入@babel/register和之前的src.js,如下所示。
require("@babel/register"); var code = require("./src.js"); console.log(code.toString());
最後在命令列工具中輸入“node default.js”,打印出編譯後的匿名函式,如下所示。
function () { return true; }
在@babel/register中,如果需要使用Polyfill,那麼得逐個引入。還要注意,它預設不會編譯node_modules目錄中的模組,但是可以通過ignore引數修改忽略規則來覆蓋其行為。
ignore引數是一個由正則表示式和函式組成的陣列(如下程式碼所示),如果要讓檔案不被編譯,那麼得將其路徑與正則表示式匹配或函式返回true,其中函式的引數就是檔案路徑。
require("babel-register")({ ignore: [ /regex/, function(filepath) { return true; } ] });
另外還有一個only引數,其值也是一個數組,同樣也用來設定檔案不被編譯的條件。只是其規則與ignore引數正好相反,即路徑與正則表示式不匹配或函式返回false時,才不編譯。
@babel/register還可以接收其它引數,例如extensions、cache等,並且會合並配置檔案中的資訊,作為其引數傳遞進來。
2)@babel/node
在Babel 7之前,@babel/cli會自帶babel-node命令,但之後,官方將其拆成一個單獨的包:@babel/node。與@babel/register不同,@babel/node不需要調整原始碼,直接一個命令就能實現動態編譯,如下程式碼所示,其中src就是之前的src.js檔案。
npx babel-node src
&n