入門babel,咱們須要瞭解些什麼

說實話,我從工做開始就一直在接觸babel,然而對於babel並無一個清晰的認識,只知道babel是用於編譯javascript,讓開發者能使用超前的ES6+語法進行開發。本身配置babel的時候,老是遇到不少困惑,下面我就以babel@7爲例,從新簡單認識下babeljavascript

什麼是babel

Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的代碼轉換爲向後兼容的 JavaScript 語法,以便可以運行在當前和舊版本的瀏覽器或其餘環境中。

babel的配置文件通常是根目錄下的.babelrcbabel@7目前已經支持babel.config.js,不妨用babel.config.js試試。java

泰拳警告

泰拳警告

babel提供的基礎能力是語法轉換,或者叫語法糖轉換。好比把箭頭函數轉爲普通的function,而對於ES6新引入的全局對象是默認不作處理的,如Promise, Map, Set, Reflect, Proxy等。對於這些全局對象和新的API,須要用墊片polyfill處理,core-js有提供這些內容。node

因此babel作的事情主要是:react

  1. 根據你的配置作語法糖解析,轉換
  2. 根據你的配置塞入墊片polyfill

若是不搞清楚這點,babel的文檔看起來會很吃力!webpack

必須掌握的概念

plugins

babel默認不作任何處理,須要藉助插件來完成語法的解析,轉換,輸出。git

插件分爲語法插件Syntax Plugins和轉換插件Transform Pluginses6

語法插件

語法插件僅容許babel解析語法,不作轉換操做。咱們主要關注的是轉換插件。github

轉換插件

轉換插件,顧名思義,負責的是語法轉換。web

轉換插件將啓用相應的語法插件,若是啓用了某個語法的轉換插件,則沒必要再另行指定相應的語法插件了。

語法轉換插件有不少,從ES3ES2018,甚至是一些實驗性的語法和相關框架生態下的語法,都有相關的插件支持。算法

語法轉換插件主要作的事情有:

利用@babel/parser進行詞法分析和語法分析,轉換爲AST --> 利用babel-traverse進行AST轉換(涉及添加,更新及移除節點等操做) --> 利用babel-generator生成目標環境js代碼

插件簡寫

babel@7以前的縮寫形式是這樣的:

// 完整寫法
plugins: [
    "babel-plugin-transform-runtime"
]
// 簡寫形式
plugins: [
    "transform-runtime"
]

而在babel@7以後,因爲plugins都歸到了@babel目錄下,因此簡寫形式也有所改變:

// babel@7插件完整寫法
plugins: [
    "@babel/plugin-transform-runtime"
]
// 簡寫形式,須要保留目錄
plugins: [
    "@babel/transform-runtime"
]

插件開發

咱們本身也能夠開發插件,官網上的一個很是簡單的小例子:

export default function() {
  return {
    visitor: {
      Identifier(path) {
        const name = path.node.name;
        // reverse the name: JavaScript -> tpircSavaJ
        path.node.name = name
          .split("")
          .reverse()
          .join("");
      },
    },
  };
}

presets

preset,意爲「預設」,實際上是一組plugin的集合。個人理解是,根據這項配置,babel會爲你預設(或稱爲「內置」)好一些ECMA標準,草案,或提案下的語法或API,甚至是你本身寫的一些語法規則。固然,這都是基於plugin實現的。

官方presets

@babel/preset-env

@babel/preset-env提供了一種智能的預設,根據配置的options來決定支持哪些能力。

咱們看看關鍵的options有哪些。

  • targets

描述你的項目要支持的目標環境。寫法源於開源項目browserslist。這項配置應該根據你須要兼容的瀏覽器而設置,沒必要與其餘人如出一轍。示例以下:

"targets": {
  "browsers": ["> 1%", "last 2 versions", "not ie <= 9"]
}
  • loose

能夠直譯爲「鬆散模式」,默認爲false,即爲normal模式。簡單地說,就是normal模式轉換出來的代碼更貼合ES6風格,更嚴謹;而loose模式更像咱們平時的寫法。以class寫法舉例:

咱們先寫個簡單的class

class TestBabelLoose {
    constractor(name) {
        this.name = name
    }
    getName() {
        return this.name
    }
}

new TestBabelLoose('Tusi')

使用normal模式編譯獲得結果以下:

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var TestBabelLoose =
/*#__PURE__*/
function () {
  function TestBabelLoose() {
    _classCallCheck(this, TestBabelLoose);
  }

  _createClass(TestBabelLoose, [{
    key: "constractor",
    value: function constractor(name) {
      this.name = name;
    }
  }, {
    key: "getName",
    value: function getName() {
      return this.name;
    }
  }]);

  return TestBabelLoose;
}();

new TestBabelLoose('Tusi');

而使用loose模式編譯獲得結果是這樣的,是否是更符合咱們用prototype實現類的寫法?

"use strict";

var TestBabelLoose =
/*#__PURE__*/
function () {
  function TestBabelLoose() {}

  var _proto = TestBabelLoose.prototype;

  _proto.constractor = function constractor(name) {
    this.name = name;
  };

  _proto.getName = function getName() {
    return this.name;
  };

  return TestBabelLoose;
}();

new TestBabelLoose('Tusi');

我的推薦配置loose: false,固然也要結合項目實際去考量哪一種模式更合適。

  • modules

可選值有:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默認是auto

該配置將決定是否把ES6模塊語法轉換爲其餘模塊類型。注意,cjscommonjs的別名。

其實我一直有個疑惑,爲何我看到的開源組件中,基本都是設置的modules: false?後面終於明白了,原來這樣作的目的是把轉換模塊類型的處理權交給了webpack,由webpack去處理這項任務。因此,若是你也使用webpack,那麼設置modules: false就沒錯啦。

  • useBuiltIns

可選值有:"entry" | "usage" | false,默認是false

該配置將決定@babel/preset-env如何去處理polyfill

"entry"

若是useBuiltIns設置爲"entry",咱們須要安裝@babel/polyfill,而且在入口文件引入@babel/polyfill,最終會被轉換爲core-js模塊和regenerator-runtime/runtime。對了,@babel/polyfill也不會處理stage <=3的提案。

咱們用一段包含了Promise的代碼來作下測試:

import "@babel/polyfill";

class TestBabelLoose {
    constractor(name) {
        this.name = name
    }
    getName() {
        return this.name
    }
    testPromise() {
        return new Promise(resolve => {
            resolve()
        })
    }
}
new TestBabelLoose('Tusi')

可是編譯後,貌似引入了不少polyfill啊,一共149個,怎麼不是按需引入呢?嗯...你須要往下看了。

import "core-js/modules/es6.array.map";
import "core-js/modules/es6.map";
import "core-js/modules/es6.promise";
import "core-js/modules/es7.promise.finally";
import "regenerator-runtime/runtime";
// 此處省略了144個包。。。

"usage"

若是useBuiltIns設置爲"usage",咱們無需安裝@babel/polyfillbabel會根據你實際用到的語法特性導入相應的polyfill,有點按需加載的意思。

// 上個例子中,若是改用useBuiltIns: 'usage',最終轉換的結果,只有四個模塊
import "core-js/modules/es6.object.define-property";
import "core-js/modules/es6.promise";
import "core-js/modules/es6.object.to-string";
import "core-js/modules/es6.function.name";

配置"usage"時,常搭配corejs選項來指定core-js主版本號

useBuiltIns: "usage",
corejs: 3

false

若是useBuiltIns設置爲falsebabel不會自動爲每一個文件加上polyfill,也不會把import "@babel/polyfill"轉爲一個個獨立的core-js模塊。

  • @babel/preset-env還有一些配置,本身慢慢去折騰吧......

stage-x

stage-x描述的是ECMA標準相關的內容。根據TC39ECMA39號技術專家委員會)的提案劃分界限,stage-x大體分爲如下幾個階段:

  • stage-0:strawman,還只是一種設想,只能由TC39成員或者TC39貢獻者提出。
  • stage-1:proposal,提案階段,比較正式的提議,只能由TC39成員發起,這個提案要解決的問題須有正式的書面描述,通常會提出一些案例,以及API,語法,算法的雛形。
  • stage-2:draft,草案,有了初始規範,必須對功能的語法和語義進行正式描述,包括一些實驗性的實現,也能夠提出一些待辦事項。
  • stage-3:condidate,候選,該提議基本已經實現,須要等待實踐驗證,用戶反饋及驗收測試經過。
  • stage-4:finished,已完成,必須經過Test262驗收測試,下一步就是歸入到ECMA標準中。好比一些ES2016ES2017的語法就是經過這個階段被合入ECMA標準中了。

有興趣瞭解的能夠關注ecma262

須要注意的是,babel@7已經移除了stage-x的preset,stage-4部分的功能已經被@babel/preset-env集成了,而若是你須要stage <= 3部分的功能,則須要自行經過plugins組裝。
As of v7.0.0-beta.55, we've removed Babel's Stage presets.
Please consider reading our blog post on this decision at
https://babeljs.io/blog/2018/07/27/removing-babels-stage-presets
for more details. TL;DR is that it's more beneficial in the long run to explicitly add which proposals to use.
If you want the same configuration as before:
{
  "plugins": [
    // 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"
  ]
}

本身寫preset

如需建立一個本身的preset,只需導出一份配置便可,主要是經過寫plugins來實現preset。此外,咱們也能夠在本身的preset中包含第三方的preset

module.exports = function() {
  return {
    // 增長presets項去包含別人的preset
    presets: [
      require("@babel/preset-env")
    ],
    // 用插件來包裝成本身的preset
    plugins: [
      "pluginA",
      "pluginB",
      "pluginC"
    ]
  };
}

@babel/runtime

babel運行時,很重要的一個東西,它必定程度上決定了你產出的包的大小!通常適合於組件庫開發,而不是應用級的產品開發。

說明

這裏有兩個東西要注意,一個是@babel/runtime,它包含了大量的語法轉換包,會根據狀況被按需引入。另外一個是@babel/plugin-transform-runtime,它是插件,負責在babel轉換代碼時分析詞法語法,分析出你真正用到的ES6+語法,而後在transformed code中引入對應的@babel/runtime中的包,實現按需引入。

舉個例子,我用到了展開運算符...,那麼通過@babel/plugin-transform-runtime處理後的結果是這樣的:

/* 0 */
/***/ (function(module, exports, __webpack_require__) {

var arrayWithoutHoles = __webpack_require__(2);

var iterableToArray = __webpack_require__(3);

var nonIterableSpread = __webpack_require__(4);

function _toConsumableArray(arr) {
  return arrayWithoutHoles(arr) || iterableToArray(arr) || nonIterableSpread();
}

module.exports = _toConsumableArray;
    
// EXTERNAL MODULE: ../node_modules/@babel/runtime/helpers/toConsumableArray.js
var toConsumableArray = __webpack_require__(0);
var toConsumableArray_default = /*#__PURE__*/__webpack_require__.n(toConsumableArray);

安裝和簡單配置

@babel/runtime是須要按需引入到生產環境中的,而@babel/plugin-transform-runtimebabel輔助插件。所以安裝方式以下:

npm i --save @babel/runtime
npm i --save-dev @babel/plugin-transform-runtime

配置時也挺簡單:

const buildConfig = {
    presets: [
        // ......
    ],
    plugins: [
        "@babel/plugin-transform-runtime"
    ],
    // ......
}

@babel/runtime和useBuiltIns: 'usage'有什麼區別?

二者看起來都實現了按需加載的能力,可是實際上做用是不同的。@babel/runtime處理的是語法支持,把新的語法糖轉爲目標環境支持的語法;而useBuiltIns: 'usage'處理的是墊片polyfill,爲舊的環境提供新的全局對象,如Promise等,提供新的原型方法支持,如Array.prototype.includes等。若是你開發的是組件庫,通常不建議處理polyfill的,應該由調用者去作這些支持,防止重複的polyfill

  • 開發組件時,若是僅使用@babel/plugin-transform-runtime

@babel/runtime打包分析

  • 加上useBuiltIns: 'usage',多了不少沒必要要的包。

@babel/runtime + useBuiltIns: 'usage'打包分析

babel@7要注意的地方

最後簡單地提一下使用babel@7要注意的地方,固然更詳細的內容仍是要看babel官方

  • babel@7相關的包命名都改了,基本是@babel/plugin-xxx, @babel/preset-xxx這種形式。這是開發插件體系時一個比較標準的命名和目錄組織規範。
  • 建議用babel.config.js代替.babelrc,這在你要支持不一樣環境時特別有用。
  • babel@7已經移除了stage-xpresets,也不鼓勵再使用@babel/polyfill
  • 不要再使用babel-preset-es2015, babel-preset-es2016preset了,應該用@babel/preset-env代替。
  • ......

結語

本人只是對babel有個粗略的認識,因此這是一篇babel入門的簡單介紹,並無提到深刻的內容,可能也存在錯誤之處。本身翻來覆去也看過好幾遍babel的文檔了,一直以爲收穫不大,也沒理解到什麼東西,在與webpack配合使用的過程當中,仍是有不少疑惑沒搞懂的。其實錯在本身不應在複雜的項目中直接去實踐。在最近從新學習webpackbabel的過程當中,我以爲,對於不是很懂的東西,咱們不妨從寫一個hello world開始,由於不是每一個人都是理解能力超羣的天才......


首發於掘金社區

歡迎關注

相關文章
相關標籤/搜索