babel7學習筆記

babel這東西,須要配置時搞不清楚怎麼弄,弄清楚後配置好了就很長時間不會去觸碰。等再須要配置時又忘了當初怎麼弄的了。
回頭看以前寫的babel學習筆記,發現本身仍是有地方沒搞清楚,有必要系統性的對babel進行學習和整理。前端

babel主要做用就是將某些低版本容器(主要是瀏覽器,主要是IE...)不支持的js語法或api,用該容器支持的語法或api重寫,使開發者可使用更前沿的方式愉快的編寫代碼。node

但實際上更準確點說,是一堆插件在作代碼的轉換,babel自己是個容器,負責代碼解析、轉換抽象語法樹,而後經過各類插件作代碼轉換,最後根據轉換後的抽象語法樹生成最終的代碼。這個過程之後再細說,這裏想說的就是插件對於babel的做用,而咱們使用者可能比較關心的,也就是在作代碼轉換時,會用到哪些插件。react

(截止寫這篇時,babel官方最新版本爲7.1.0,如下簡稱babel7,demo均以7.1.0版本爲例)android

Babel的安裝

babel7主要就是兩個包,@babel/cli和@babel/core,cli用於執行命令行,core則是babel用於解析、轉換、代碼生成的核心包。在項目下執行如下命令便可完成安裝。webpack

npm i --save-dev @babel/cli @babel/core

有了這兩個包之後,就能夠對指定文件執行babel命令了。
好比項目目錄結構以下:ios

├── node_modules                  
├── src     
│   └── index.js       // 源文件
├── dist               // 轉換後文件放置路徑
└── package.json

src/目錄文件index.js代碼以下:git

// src/index.js

const fn = () => {
  Array.isArray([1, 2, 3]);
};

如今在項目根目錄下執行程序員

npx babel src/index -d dist/

便可在dist目錄下生成同名文件index.js,而裏面代碼與src/index.js中的代碼徹底同樣。
之因此代碼徹底同樣,其實就是上面所說的,babel在沒有使用任何插件時,就是把代碼變成抽象語法樹,再把抽象語法樹原封不動的變成代碼,中間沒有作任何處理,固然代碼也就原樣還原回來了。下面,讓咱們來給babel加點兒料~es6

Plugins

const和箭頭函數是es6的語法,相應的,@babel/plugin-transform-block-scoping插件用於轉換const和let,@babel/plugin-transform-arrow-functions插件用於轉換箭頭函數。安裝完這兩個插件以後,分別執行github

npx babel src/index -d dist/ --plugins=@babel/plugin-transform-block-scoping

npx babel src/index -d dist/ --plugins=@babel/plugin-transform-arrow-functions

分別查看dist/index.js確認下轉換後的結果,相信馬上就能明白每一個插件都幹了些啥了~(兩個插件一塊兒用的話,用「,」隔開:npx babel src/index -d dist/ --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions)
babel全部的語法轉換,就是靠一個個plugins完成的。完整的plugins列表能夠查看https://babeljs.io/docs/en/pl...

掃完一眼plugins列表,估計和我同樣一臉懵逼,這麼多插件誰能記得住用獲得哪些啊。babel能幫忙整理下打個包給我用麼?固然能夠,presets就是用來幹這事兒的。

Presets

一個特定的preset能夠簡單理解爲是一組特定的plugins的集合。不一樣的presets包含着不一樣的plugins,固然適用的場景也就各不相同了。好比@babel/preset-react包含了寫react須要用到的@babel/plugin-syntax-jsx,@babel/plugin-transform-react-jsx,@babel/plugin-transform-react-display-name等插件;@babel/preset-es2017包含了@babel/plugin-transform-async-to-generator插件。
而最爲經常使用,也是被官網推薦的,是@babel/preset-env。默認狀況下,全部已被歸入規範的語法(ES2015, ES2016, ES2017, ES2018, Modules)所須要使用的plugins都包含在env這個preset中。

仍是以上面例子來講
先安裝@babel/preset-env

npm i --save-dev @babel/preset-env

執行

npx babel src/index.js -d dist/ --presets=@babel/preset-env

生成的dist/index文件以下:

"use strict";

var fn = function fn() {
  Array.isArray([1, 2, 3]);
};

主體部分與同時使用兩個plugins是徹底同樣的。實際上,presets能夠理解爲就是把其包含的plugins依次執行一遍。
固然env這個presets不是萬能的,其只包含了規範中的語法轉換,還沒有被歸入規範的處於各個階段的提案,好比目前處於stage-2(draft)階段的裝飾器語法,光是用presets是不會幫咱們轉好的,還得單獨再使用@babel/plugin-proposal-decorators這個專門用於轉換裝飾器代碼的插件。

值得一提的是,babel7明確指出用stage-x命名的presets已被棄用。具體緣由見https://babeljs.io/blog/2018/...
若是但願和以前同樣使用處於各階段的提案功能,建議直接經過引入相應的plugins:

{
  plugins: [
    // Stage 0
    "@babel/plugin-proposal-function-bind",

    // Stage 1
    "@babel/plugin-proposal-export-default-from",
    "@babel/plugin-proposal-logical-assignment-operators",
    ["@babel/plugin-proposal-optional-chaining", { loose: false }],
    ["@babel/plugin-proposal-pipeline-operator", { proposal: "minimal" }],
    ["@babel/plugin-proposal-nullish-coalescing-operator", { loose: false }],
    "@babel/plugin-proposal-do-expressions",

    // Stage 2
    ["@babel/plugin-proposal-decorators", { legacy: true }],
    "@babel/plugin-proposal-function-sent",
    "@babel/plugin-proposal-export-namespace-from",
    "@babel/plugin-proposal-numeric-separator",
    "@babel/plugin-proposal-throw-expressions",

    // Stage 3
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-syntax-import-meta",
    ["@babel/plugin-proposal-class-properties", { loose: false }],
    "@babel/plugin-proposal-json-strings",
  ],
}

關於stage-x各表明什麼含義,見The TC39 Process

Configure Babel

另外一個問題,又是presets,又是plugins,--plugins和--presets後面要跟一堆東西,用命令執行babel未免也太費勁了。
babel官網提供了四種方式經過文件維護配置項,實際工做中,根據狀況選擇其一。

  1. babel.config.js
    適用場景:以編程方式建立配置;須要編譯編譯node_modules。

    // Javascript
    module.exports = function () {
      const presets = [ ... ];
      const plugins = [ ... ];
    
      return {
        presets,
        plugins
      };
    }
  2. .babelrc
    適用場景:適用於簡單的靜態配置

    // JSON
    {
      "presets": [...],
      "plugins": [...]
    }
  3. package.json
    也能夠將.babelrc中的配置項移至package.json配置文件中

    // JSON
    {
      "name": "my-package",
      "version": "1.0.0",
      "babel": {
        "presets": [ ... ],
        "plugins": [ ... ],
      }
    }
  4. .babelrc.js
    和babel.config.js相似,可使用編程方式建立配置

    // Javascript
    const presets = [ ... ];
    const plugins = [ ... ];
    
    module.exports = { presets, plugins };

有了配置文件,上述既須要@babel/preset-env又須要@babel/plugin-proposal-decorators的狀況,babel.config.js文件配置以下:

// Javascript
    module.exports = function(api) {
      api.cache(true);
      const presets = ['@babel/env'];
      const plugins = [['@babel/proposal-decorators', { legacy: true }]];
      return {
        presets,
        plugins
      };
    };

這邊本身試了下,若是不寫api.cache(true),會報一個很奇怪的錯:

Error: Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured for various types of caching, using the first param of their handler functions

官網上暫時沒有找到爲何必定要執行下api的方法,甚至把

api.cache(true);

改爲

const env = api.env();

均可以免上述報錯,沒法理解。仍是換成.babelrc方式寫配置吧。。。

// JSON
{
  "presets": ["@babel/env"],
  "plugins": [["@babel/proposal-decorators", { "legacy": true }]]
}

驗證一下,在src/decorator.js寫一小段包含裝飾器的代碼:

// src/decorator.js
const decro = (val) => (_class) => new _class(val);

@decro("abc")
class Test {
  constructor(val){
    this.val = val
  }
  log() {
    console.log(this.val);
  }
}

Test.log(); // "abc"

完了在項目根目錄執行中執行一下

npx babel src/ -d dist/

在dist/下就能夠看到轉換後的decorator.js文件。
執行

node dist/decorator.js

便可看到結果"abc"啦~

注意到一件事情,plugins是個二維數組,而presets是個一位數組。這是babel配置文件中對指定preset或者plugin添加參數的方式。

Plugin & Preset Options

babel提供的plugin和preset都容許傳入一些參數來達到不一樣的目的。
如下是引用presetA和pluginA的三種形式,結果徹底同樣。

{
  "presets": [
    "presetA",
    ["presetA"],
    ["presetA", {}]
  ],
  "plugins": [
    "pluginA",
    ["pluginA"],
    ["pluginA", {}],
  ]
}

意思就是說,單個preset或者plugin若是不須要添加參數,那麼直接用string就能夠了;若是須要添加參數,那麼須要將單個preset或者plugin放入數組中,第一項爲string表示preset或者plugin的名字,第二項爲object用於指定參數。

先舉個後面會用到的例子,具體在寫@babel/preset-env的時候細說

{
  "presets": [
    [
      "@babel/env", {
        "targets": {
          "ie": "9"
        },
        "useBuiltIns": "usage"
      }
    ]
  ]
}

Polyfill

如今,回過頭來再看下src/index.js中的例子。最後轉換出來的代碼中,Array.isArray這個靜態方法在低版本IE瀏覽器中,依然是沒法執行的。
我理解爲,babel的插件專一於對語法作轉換,而API的調用並不是什麼新鮮的語法,這部分並不屬於babel插件的管轄範圍。正常來講,讓不識別Array.isArray的瀏覽器運行這個方法,最簡單的方法就是用瀏覽器能識別的方式爲Array寫一個靜態方法isArray。

Array.isArray = function(arg) {
  var toString = {}.toString;
  return toString.call(arg).slice(8, -1) == 'Array';
}

@babel/polyfill就是幹這活兒的。

首先安裝@babel/polyfill

npm i -S @babel/polyfill

而後在項目入口src/index.js開始時引入一下

// src/index.js

import "@babel/polyfill";
const fn = () => {
  Array.isArray([1, 2, 3]);
};

簡單粗暴,搞定收工。
等下等下,還沒完呢,不想知道@babel/polyfill這裏面都有些啥東西麼?若是你會使用webpack-bundle-analyzer作打包分析,會發現多出的core-js這個包有200多kb,這個就是@babel/polyfill的依賴包,(除了core-js外其實還有個regenerator-runtime用來處理async function的)。爲了實現Array.isArray,要增長這麼大致積的包,有沒有問題呢?
固然有問題,對於咱們這種有追求的程序員來講,能把包縮小一點是一點,關乎用戶體驗的事情能優化一點兒是一點兒~

好,怎麼辦?
找到@babel/polyfill中處理Array.isArray的包,單獨引用就行了唄~

import "core-js/modules/es6.array.is-array";
const fn = () => {
  Array.isArray([1, 2, 3]);
};

搞定,包瞬間縮小到幾k。可問題又來了,這只是一個Array.isArray,那麼多新的String的API,Object的API等等,難道須要本身一個個把須要的包單獨引用進本身的項目裏去嗎?
No~ 見識一下@babel/preset-env的強大之處吧~

@babel/preset-env

@babel/preset-env這個preset比較特殊,他不只僅是包含了衆多plugins,並且還提供了一些有用的配置項,動態引入依賴包,只爲跟小體積的包~ 來看下主要的兩個配置項 useBuiltIns 和 targets

  • useBuiltIns

    該配置項有三個屬性: "usage", "entry", "false"。默認爲false。
    須要事先說明一下,這個屬性若是是"usage"或"entry"時,必需要安裝@babel/polyfill,由於在轉換出來的代碼中,會引入core-js下面的包。

    • entry

      要使用entry屬性,必須在項目入口處引入一次@babel/polyfill。而後,babel在作代碼轉換的時候,會把

      import @babel/polyfill

      轉成

      require("core-js/modules/es6.array.copy-within");
      require("core-js/modules/es6.array.every");
      require("core-js/modules/es6.array.fill");
      ...

      問題來了,只是把一個大包拆成一個個小包,並不會減少體積啊。嗯,單獨使用"useBuiltIns": "entry"好像是沒什麼用,但結合後面要說的targets配置項就有用了,後面再說。

    • usage(experimental)

      雖然官網標註了是個處於實驗階段的功能,但親測很強大。他能經過識別全部代碼中使用到的高級API,自動在該文件頭部注入相應的polyfill包。
      使用這個屬性的時候,是不須要在項目中手動引入@babel/polyfill的。babel自動檢測哪一個文件要用到哪些包,就在那個文件頭部引入那些包。

      搬個官網的例子:

      // a.js
      var a = new Promise();
      
      // b.js
      var b = new Map();
      
      // .babelrc
      {
        "presets": [["@babel/env", {"useBuiltIns": "usage"}]]
      }

      轉化後

      // a.js
      import "core-js/modules/es6.promise";
      var a = new Promise();
      
      // b.js
      import "core-js/modules/es6.map";
      var b = new Map();

      一點點冗餘代碼都沒有了~

  • targets

    targets配置項用於指定須要支持的環境,對於前端開發來講,主要指的就是瀏覽器版本。(targets還能夠指定node、android、ios等其餘環境)
    常用的方式也很簡單,好比老闆說,咱們只須要支持chrome64,上例中所需的完整配置以下:

    {
      "presets": [
        [
          "@babel/env",
          {
            "useBuiltIns": "usage",
            "targets": {
              "chrome": "64"
            }
          }
        ]
      ],
      "plugins": [['@babel/proposal-decorators', { "legacy": true }]]
    }

    直接看執行babel的運行結果,會發現轉換後的代碼基本和原來的同樣。也就是說,設置完targets後,babel會先判斷一下指定的環境已經支持了多少種新語法和API,對於已經支持的部分,就不會再轉換代碼或者引入相應的包了。
    對於只要求在較高版本的瀏覽器運行的項目,targets + useBuiltIns兩個配置項就能將轉換後的代碼體積縮減到最小。不過若是要求支持IE9,那設不設置targets影響就不大了~
    關於@babel/preset-env更爲詳細的文檔,固然是官網啦~https://babeljs.io/docs/en/ba...

Plugin ordering

補充一點,配置文件中,presets和plugins都是容許設置多個的,某些plugin對執行順序很敏感,這也就對配置中設置presets和plugins的順序有要求了。

babel執行presets和plugins的順序以下:

  • Plugins先於Presets執行。
  • Plugins由數組中的第一個plugin開始依次執行。
  • Presets與Plugins執行順序相反,由數組中最後一個preset開始執行。

到這兒,babel7的基本使用方法就介紹完了。
不過在實際項目中,咱們如今通常不直接經過babel命令對代碼作轉換,webpack的babel-loader會在打包時幫助咱們作這件事情。

Babel-loader

babel 7.x對應的babel-loader版本爲8.x。以前的babel 6.x對應的babel-loader版本爲7.x。
關於webpack的loader超出了本篇的範圍,這裏就很少加贅述了。有須要進一步瞭解的能夠看https://webpack.js.org/concep...
這裏只簡單介紹一下babel-loader的配置。在webpack配置中可能常常會見到相似下面這段:

module: {
  rules: [
    {
      test: /\.js|jsx$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

若是你已經清楚.babelrc文件是如何配置的,那麼對上面的options必定不會陌生了。 webpack執行打包時,優先讀取options中的配置,若是沒有設置options屬性,再從package.json同級目錄中找babel配置文件。經過配置options,或者經過babel配置文件,兩種方式選其一就能夠了。

相關文章
相關標籤/搜索