前端利器躬行記(2)——Babel

  Babel是一個JavaScript編譯器,不只能將當前運行環境不支持的JavaScript語法(例如ES六、ES7等)編譯成向下兼容的可用語法(例如ES3或ES5),這其中會涉及新語法的轉換和缺失特性的修補;還支持語法擴展,從而能隨時隨地的使用JSX、TypeScript等語法。目前最新版本是7.4,自從6.0以來,Babel被分解的更加模塊化,各類轉譯功能都以插件的形式分離出來,可按本身的需求,靈活配置。node

  在7.0版本中,對Babel的包作了一次大調整,統一改爲域級包,將原先以「babel-」爲前綴的包遷移到@babel的命名空間,例如@babel/core、@babel/cli等。這種模塊化的設計,既能區分是不是官方發佈的,也能避免命名衝突。react

1、@babel/core

  若是要以編程的方式使用Babel,那麼能夠經過@babel/core實現,安裝命令以下所示。git

npm install --save-dev @babel/core

  在引入該包後,就能在JavaScript文件中直接編譯代碼、文件或AST。以編譯代碼爲例,可選擇的方法有三個,以下所示。es6

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參數,可用於傳遞配置信息,具體可參考官方文檔github

  若是要編譯文件或AST,那麼也有相似的三個方法可供選擇。除了能編譯以外,利用這個包還能把代碼解析成AST(即傳入一段代碼,獲得一個AST)以便其它插件對其進行語法分析,可以使用的方法有parse()、parseSync()和parseAsync()。正則表達式

2、@babel/cli

  @babel/cli是一個Babel內置的命令行工具,可經過命令來編譯文件,其安裝以下所示。chrome

npm install --save-dev @babel/cli

  假設有一個src.js文件,其內容以下代碼所示,用到了ES6中的箭頭函數。typescript

let fn = () => true;

  在運行下面的命令後,能夠看到輸出和輸入的代碼相同,這是由於此時還未指定任何轉譯插件。npm

./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

3、配置

  在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: [...]
});

4、插件

  Babel的編譯可分爲三個階段:解析、轉換和生成,而插件(Plugin)在轉換過程當中起到了相當重要的做用。藉助Babel的插件可將解析完成的AST按照特定的要求進行處理,而後再輸出生成的代碼。

1)插件類型

  Babel中的插件分爲兩種類型:語法和轉換。語法插件只容許Babel解析成特定類型的語法,例如JSX、Flow等。轉換插件經常使用來編譯ES六、ES七、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 }]
  ]
}

  預設也能接收參數,其寫法與之相似。

5、預設

  若是在配置文件中一個一個的聲明插件,那麼不只會讓該文件變得巨大,並且還不免會有所遺漏。官方爲了不此類問題,引入了預設(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),將新規範歸入到標準中,準備發佈 不存在,由於它就是已發佈的標準預設,例如ES六、ES7等

  以上4個階段的預設存在着依賴關係,階段靠前的依賴階段靠後的(即數字小的包含數字大的),例如@babel/preset-stage-0依賴@babel/preset-stage-1。

  因爲這些提案階段的預設不太穩定,頗有可能會被TC39委員會除名或變動,而且混合使用在配置上容易出錯,所以從Babel 7開始,它們都將被廢棄。

6、@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();

7、@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"));

8、動態編譯

  在Babel中有兩種方式實現動態編譯,分別是@babel/register和@babel/node,它們的安裝命令以下所示。

npm install --save-dev @babel/register
npm install --save-dev @babel/node

  雖然動態編譯省去了手動操做,可是會損耗程序的速度和性能,所以不適合生產環境。

1)@babel/register

  在將其安裝後,就會爲Node.js的require()函數增長一個鉤子。每當經過require()加載後綴爲.es六、.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
相關文章
相關標籤/搜索