babel7入門 —— 超詳細學習筆記

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

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

(截止筆者轉載這篇文章時,babel-cli官方最新版本爲7.8.4,babel-core的最新版本爲7.9.0,以下簡稱babel7,demo均以最新版本爲例)

Babel的安裝

babel7主要就是兩個包,@babel/cli@babel/core,cli用於執行命令行,core則是babel用於解析、轉換、代碼生成的核心包。在項目下執行以下命令即可完成安裝,親測使用npm安裝特別慢,可以用cnpm安裝。

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

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

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

src目錄文件index.js代碼如下:

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

現在在項目根目錄下執行(不瞭解npx命令的,可以看這篇文章:npx 使用教程

npx babel src/index.js -d dist/

即可在dist目錄下生成同名文件index.js,而裏面代碼與src/index.js中的代碼完全一樣,生成的dist/index.js內容如下:

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

之所以代碼完全一樣,其實就是上面所說的,babel在沒有使用任何插件時,就是把代碼變成抽象語法樹,再把抽象語法樹原封不動的變成代碼,中間沒有做任何處理,當然代碼也就原樣還原回來了。下面,讓我們來給babel加點兒料~

Plugins

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

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

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

分別查看dist/index.js確認下轉換後的結果,相信立刻就能明白每個插件都幹了些啥了~兩個插件一起用的話,用" , "隔開:

npx babel src/index.js -d dist/ --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

babel所有的語法轉換,就是靠一個個plugins完成的。完整的plugins列表可以查看https://babel.docschina.org/docs/en/plugins

掃完一眼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.js文件如下:

"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
  };
}
  1. .babelrc
    適用場景:適用於簡單的靜態配置
// JSON
{
  "presets": [...],
  "plugins": [...]
}
  1. package.json
    也可以將.babelrc中的配置項移至package.json配置文件中
// JSON
{
  "name": "my-package",
  "version": "1.0.0",
  "babel": {
    "presets": [ ... ],
    "plugins": [ ... ],
  }
}
  1. .babelrc.js
    和babel.config.js類似,可以使用編程方式創建配置
// Javascript
const presets = [ ... ];
const plugins = [ ... ];

module.exports = { presets, plugins };

有了配置文件,上述既需要@babel/preset-env又需要@babel/plugin-proposal-decorators(測試之前,先通過npm安裝好這個插件)的情況,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
	};
};

根目錄有了這個配置文件,執行babel指令就不需要帶上preset或者plugin參數了:

npx babel src/index.js -d dist/

這邊自己試了下,如果不寫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寫一小段包含裝飾器的代碼:

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

(不瞭解polyfill含義的,可以看這篇文章:你不得不知道的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開始時引入一下

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

我們再執行下

npx babel src/ -d dist/

生成的dist/index.js如下:

"use strict";

require("@babel/polyfill");

var fn = function 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]);
};

生成的dist/index.js如下:

"use strict";

require("core-js/modules/es6.array.is-array");

var fn = function 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
"use strict";
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
var a = new Promise();

// b.js
"use strict";
require("core-js/modules/web.dom.iterable");
require("core-js/modules/es6.array.iterator");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es6.string.iterator");
require("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的運行結果,會發現轉換後的代碼比之前require的少了些東西。也就是說,設置完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配置文件,兩種方式選其一就可以了。

關於webpack4和babel7的配置,可以參考文章:webpack@4中babel7配置

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章