1. 程式人生 > >前端利器躬行記(2)——Babel

前端利器躬行記(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