添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

前言

Babel 是一款 JavaScript 的編譯器,你可能會有疑問,JavaScript 不是可以直接在 Browser 上運行嗎?為何還需要編譯?事實上 JavaScript 從發行到現在,經過了許多版本的更新,常見的 ES6、ES7 都屬於較新的版本,最為穩定的版本為 ES5,兼容性也是最高的, Babel 的用意就是將較新版本的 JavaScript 編譯成穩定版本,以提高兼容性。此篇將介紹如何透過 babel-loader 編譯我們的 ES6+ 代碼,後面也會補充介紹 @babel/runtime 與 @babel/polyfill 組件的使用。

  • babel-loader 安裝
  • babel-loader 基本使用
  • babel-loader 可傳遞選項
  • 補充:@babel/runtime 與 @babel/polyfill 組件使用的必要
  • 補充:@babel/runtime 使用方式
  • 補充:@babel/polyfill 使用方式
  • babel-loader 安裝

    套件連結: babel-loader

    主要的套件:

    1
    npm install babel-loader @babel/core @babel/preset-env -D

    package.json:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "devDependencies": {
    "@babel/core": "^7.9.0",
    "@babel/preset-env": "^7.9.0",
    "babel-loader": "^8.1.0",
    "webpack": "^4.42.1",
    "webpack-cli": "^3.3.11"
    }
    }

    Webpack 通過 babel-loader 調用 Babel,直接安裝即可,同時也必須安裝 @babel/core 與 @babel/preset-env,用作 Babel 核心與插件集。

    babel-loader 基本使用

    初始專案結構:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    webpack-demo/

    ├─── node_modules/
    ├─── src/
    │ │
    │ └─── js/
    │ │
    │ └─── all.js # JavaScript 主檔案
    │ │
    │ └─── main.js # entry 入口檔案

    ├─── index.html # 引入 bundle.js 測試用檔案
    ├─── webpack.config.js # Webpack 配置檔案
    ├─── package-lock.json
    └─── package.json

    撰寫 ES6+ 版本代碼:

    1
    2
    3
    4
    5
    const arr = ['Roya', 'Owen', 'Eric'];

    const index = arr.findIndex((item) => item === 'Owen');

    console.log(`Owen 排在第 ${index + 1} 順位`);

    配置 webpack.config.js 檔案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    const path = require('path');

    module.exports = {
    entry: './src/main.js',
    output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    },
    module: {
    rules: [
    // 配置 babel-loader (第一步)
    {
    test: /\.m?js$/,
    // 排除 node_modules 與 bower_components 底下資料 (第二步)
    exclude: /(node_modules|bower_components)/,
    use: {
    loader: 'babel-loader',
    options: {
    // 配置 Babel 解析器 (第三步)
    presets: ['@babel/preset-env'],
    },
    },
    },
    ],
    },
    };

    通常在配置 Babel 時,我們都是習慣把 options 的內容撰寫在獨立的 .babelrc 檔案內,如果 Babel 的配置較為複雜,相比於撰寫在 webpack.config.js 內,使用 .babelrc 更能提高其辨識度,在之後的 @babel/runtime 與 @babel/polyfill 章節會再做補充,讓我們先暫時以此方式進行配置。

    entry 入口處 ( src/main.js ) 引入 JavaScript 檔案:

    1
    import './js/all'; // JavaScript 預設不需要附檔名

    package.json 新增編譯指令:

    1
    2
    3
    4
    5
    {
    "scripts": {
    "build": "webpack --mode development"
    }
    }

    執行編譯指令:

    1
    npm run build

    讓我們打開編譯完成的 bundle.js 檔案,看看 Babel 究竟做了什麼處理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /*!********************!*\
    !*** ./src/all.js ***!
    \********************/
    /*! no static exports found */
    /***/ (function(module, exports) {

    eval("var arr = [\"Roya\", \"Owen\", \"Eric\"];\nvar index = arr.findIndex(function (item) {\n return item === \"Owen\";\n});\nconsole.log(\"Owen \\u6392\\u5728\\u7B2C \".concat(index + 1, \" \\u9806\\u4F4D\"));\n\n//# sourceURL=webpack:///./src/all.js?");

    /***/ }),

    看到編譯完成的代碼,你的第一個想法大概都是 WTF … 這是什麼鬼?不用擔心,Babel 只是將你的代碼優化為兼容性較高版本的代碼,你也不需要針對這一個檔案做任何修改,可以直接給 HTML 讀取,執行結果如同未編譯的 JavaScript 檔案,你只需要專注於目標的編程,不管你用多新版本的代碼來實現,Babel 都可以幫你改善兼容性等相關問題。

    ./index.html 引入打包而成的 bundle.js 檔案:

    1
    2
    3
    4
    5
    <!-- 其他省略 -->
    <body>
    <!-- 引入打包生成的 JavaScript -->
    <script src="dist/bundle.js"></script>
    </body>

    查看結果:

    如果你覺得 bundle.js 檔案閱讀起來很吃力,你也可以先將其引入至 index.html 內,之後再按 F12 切換至 Source 觀察編譯結果,可能會更好理解喔!

    從上面結果可以得知,我們的 Babel 是有成功運行的,但這邊要注意的是, Babel 默認只針對 Syntax 做轉換 ,像是上面範例的 findIndex 實例就沒有被轉換,因為他不屬於 Syntax,關於這一個問題,可以使用 @babel/runtime 或 @babel/polyfill 進行處理,這點在下面會有補充說明,讓我們先以此方式進行。

    babel-loader 可傳遞選項

    可參考 babel-loader Options 可傳遞參數列表,以下為常用的參數配置:

  • presets: Array
    Babel 插件集,默認為 none

  • cacheDirectory: Boolean
    用於利用緩存加載程序的結果,默認 false

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    module.exports = {
    module: {
    rules: [
    {
    test: /\.m?js$/,
    exclude: /(node_modules|bower_components)/,
    use: {
    loader: 'babel-loader',
    options: {
    presets: ['@babel/preset-env'],
    cacheDirectory: true,
    },
    },
    },
    ],
    },
    };

    補充:@babel/runtime 與 @babel/polyfill 組件使用的必要

    當前使用 Babel 版本: v7.9.0

    Babel 7 版本時,各種運行錯誤,官方 API 雖然完整,但各章節並沒有連貫性,操作下來也不知道問題在哪,在我們探討這兩個組件之前,我們先來解釋這兩個組件到底是要幫我們解決什麼問題。

    ./src/js/all.js 檔案,修改為如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* --- 箭頭函式、ES6 變數、ES6 陣列方法 --- */
    let color = [1, 2, 3, 4, 5];
    let result = color.filter((item) => item > 2);

    /* --- Class 語法糖 --- */
    class Circle {}

    /* --- Promise 物件 --- */
    const promise = Promise.resolve();

    針對上面這一個 JavaScript 檔案,我們使用之前配置好的 webpack.config.js 來編譯它,編譯結果如下:

    聰明的你應該發現問題了,Babel 不是會幫我們處理兼容性的問題嗎? Array.prototype.filter Promise 物件好像都沒有編譯到的感覺,不要懷疑!Babel 真的沒有幫我們編譯到;事實上,如果你採用預設的編譯環境, Babel 只會針對語法 (Syntax) 做編譯,底層的 API 與原型擴展都不會進行編譯 ,這也就代表兼容性的問題根本沒有解決,在 IE 11 等較舊瀏覽器上面,它還是不知道什麼是 Promise,運行時就會發生錯誤;在這邊還有一個問題, Babel 針對 Class 語法糖的處理,你會發現它新增了一個全域的 function 當作語法糖的呼叫,這樣子的處理會造成嚴重的全域汙染 ,如果你有多個 JavaScript 檔案,同時都進行編譯的動作,產生出來的 function 都會是一模一樣的,不僅造成檔案的肥大,也有可能發生全域汙染影響運行等問題;這時候就會需要 @babel/runtime 與 @babel/polyfill 的幫忙,在介紹這兩個組件時,我們先將 Babel 的設定移置專屬的設定檔,如下所示:

    路徑 ./webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    module.exports = {
    module: {
    rules: [
    {
    test: /\.m?js$/,
    exclude: /(node_modules|bower_components)/,
    use: {
    // 將可傳遞選項移至 .babelrc
    loader: 'babel-loader',
    },
    },
    ],
    },
    };

    將原有的可傳遞選項移除,並新增 ./.babelrc 專屬配置檔:

    1
    2
    3
    {
    "presets": ["@babel/preset-env"]
    }

    此時運行 npm rum build 指令,結果會是一模一樣的,在之後針對 Babel 所作的處理,我們都會使用 .babelrc 這一個檔案做修改,接下來讓我們開始正式介紹 @babel/runtime 與 @babel/polyfill。

    補充:@babel/runtime 使用方式

    @babel/runtime 是由 Babel 提供的 polyfill 套件,由 core-js 和 regenerator 組成,core-js 是用於 JavaScript 的組合式標準化庫,它包含各種版本的 polyfills 實現;而 regenerator 是來自 facebook 的一個函式庫,主要用於實現 generator/yeild,async/await 等特性,我們先從安裝開始講起。

    套件連結: @babel/runtime @babel/plugin-transform-runtime

    @babel/runtime:

    1
    npm install @babel/runtime

    @babel/plugin-transform-runtime:

    1
    npm install @babel/plugin-transform-runtime --save-dev

    在安裝 @babel/runtime 時,記得不要安裝錯誤,新版的是帶有 @ 開頭的;同時也必須安裝 @babel/plugin-transform-runtime 這個套件,babel 在運行時是依賴 plugin 去做取用,這兩個套件雖然不是相依套件,但實際使用時缺一不可,在後面會有相關說明,在這邊我們先把這兩個套件裝好就可以了。

    修改 ./.babelrc 內容為下面範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "presets": ["@babel/preset-env"],
    "plugins": [
    [
    "@babel/plugin-transform-runtime",
    {
    "corejs": false
    }
    ]
    ]
    }

    我們以之前的 JavaScript 檔案進行示範,執行 npm run build 指令進行編譯,結果如下:

    從編譯後的結果可以得知,之前提到的 Class 語法糖全域汙染問題已經解決了,透過 @babel/plugin-transform-runtime 這個套件,它會幫我們分析是否有 polyfill 的需求,並自動透過 require 的方式,向 @babel/runtime 拿取 polyfill,簡單來講, @babel/runtime 提供了豐富的 polyfill 供組件使用,開發者可以自行 require,但自行 require 太慢了,使用 @babel/plugin-transform-runtime 可以自動分析並拿取 @babel/runtime 的 polyfill ,這也是為什麼這兩個套件缺一不可的原因。

    可能有些人還是有疑問,透過 require 的方式為什麼就能避免全域污染的問題?事實上,當初我也很困惑,結果恍然大悟,終於理解了,簡單來講,當初是因為 babel 會在全域環境宣告 function,只要同時有 1 個檔案以上需要編譯時,這些 function 就會相遇干擾,實際運行就會發生錯誤,透過 @babel/runtime 直接 require 的方式進行取用,最後編譯出來的檔案就不會汙染到全域環境,而是生成許多的 require 指令, Node.js 默認是從緩存中載入模組,一個模組被加載一次之後,就會在緩存中維持一個副本,如果遇到重複取用問題,會直接向緩存拿取副本,這也就代表每個模組在緩存中止存在一個實例

    仔細觀察,Babel 還是沒有幫我們編譯 Promise 物件,那是因為我們還沒有解放 @babel/runtime 這一個套件全部力量,由上面範例,你會發現我在 plugin 中傳遞了一個 corejs 選項,預設是關閉的,可傳遞的選項為:

    corejs 選項 false npm install - -save @babel/runtime npm install - -save @babel/runtime-corejs2 npm install - -save @babel/runtime-corejs3

    事實上 @babel/runtime 有許多的擴展版本,在之前的範例中,我們都是將 corejs 給關閉,這也就導致它並沒有幫我們編譯底層的 API 與相關的方法,這次我們就來使用各版本進行編譯,記得要執行相對應的安裝指令喔!

    修改 ./.babelrc 內容為下面範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "presets": ["@babel/preset-env"],
    "plugins": [
    [
    "@babel/plugin-transform-runtime",
    {
    "corejs": 2
    }
    ]
    ]
    }

    corejs2 版本編譯結果:

    corejs3 版本編譯結果:

    從上面結果可以得知, corejs2 版本主要針對底層 API 做編譯,如 Promise、Fetch 等等;corejs3 版本主要針對底層 API 和相關實例方法,如 Array.pototype.filter,Array.pototype.map 等等 ,簡單來講,如果你要將兼容性的問題徹底解決,就得使用 corejs3 版本,到了這邊,我們之前所提到 Babel 的種種問題都已經獲得解決。

    使用 @babel/runtime 能夠在不汙染全域環境下提供相對應的 polyfill,擁有自動識別功能,在某些情況下,編譯出來的檔案大小可能比使用 @babel/polyfill 來的小,適合開發組件庫或對環境較為嚴格的專案

    補充:@babel/polyfill 使用方式

    @babel/polyfill 與 @babel/runtime 一直以來這兩者的差別都很模糊,網上的文章大多也都是複製官方的說明文檔,並沒有實際去使用,造成開發者一知半解的疑慮,這一次我們就來討論 @babel/polyfill 究竟要如何使用。先從安裝開始說起:

    Babel 版本 < v7.4.0

    1
    npm install @babel/polyfill

    Babel 版本 >= v7.4.0

    1
    npm install core-js regenerator-runtime/runtime

    從 Babel >= 7.4.0 後,@babel/polyfill 組件庫已被棄用,事實上 @babel/polyfill 本身就是由 stable 版本的 core-js 和 regenerator-runtime 組成,我們可以直接下載這兩個組件庫當作 @babel/polyfill 來使用,官方也推薦此做法,這邊要注意的是 regenerator-runtime 為 @babel/runtime 的相依套件,可以自行檢查是否有正確安裝。

    修改 ./.babelrc 內容為下面範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "presets": [
    [
    "@babel/preset-env",
    {
    "useBuiltIns": false,
    "corejs": 3 // 當前 core-js 版本
    }
    ]
    ]
    }

    我們使用之前的 JavaScript 檔案進行示範,執行 npm run build 指令進行編譯,結果如下:

    編譯結果就如同單純使用 Babel 一樣,只有針對語法 (Syntax) 做編譯,那是因為我們尚未開啟 polyfill 的功能,可通過更改 useBuiltIns 來變更模式,可選模式為 false usage entry ,以下為各模式的編譯結果:

    useBuiltIns: usage

    很明顯的將 useBuiltIns 更改為 usage ,就如同使用 @babel/runtime-corejs3 一樣,自動識別需要 require 的新語法,將兼容性問題徹底解決,不同的地方在於,@babel/runtime 在不汙染全域環境下提供 polyfill,而 @babel/polyfill 則是將需要兼容的新語法掛載到全局對象,這樣子的做法即會造成所謂的全局汙染,讓我們來看最後一個 useBuiltIns 選項。

    useBuiltIns: entry

    使用 entry 選項記得在前面 import core-js/stable 和 regenerator-runtime/runtime 組件庫

    代編譯檔案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import 'core-js/stable';
    import 'regenerator-runtime/runtime';

    /* --- 箭頭函式、ES6 變數、ES6 陣列方法 --- */
    let color = [1, 2, 3, 4, 5];
    let result = color.filter((item) => item > 2);

    /* --- class 語法糖 --- */
    class Circle {}

    /* --- Promise 物件 --- */
    const promise = Promise.resolve();

    完成編譯檔案

    entry 這一個選項就簡單多了,沒有做任何的識別,直接將整個 ES 環境掛載到全局對象,確保瀏覽器可以兼容所有的新特性,但這樣子做的缺點也顯而易見,整個專案環境會較為肥大,你可能會好奇 entry 選項的必要,事實上 Babel 默認不會檢測第三方依賴組件,所以使用 usage 選項時,可能會出現引入第三方的代碼包未載入模組而引發的 Bug,這時就有使用 entry 的必要。

    @babel/polyfill 提供一次性載入或自動識別載入 polyfill 的功能,使用掛載全局對象的方法,達到兼容新特性目的,適合開發在專案環境,較不適合開發組件庫或工具包,存在汙染全局對象疑慮。

    經過了一番對於 @babel/runtime 與 @babel/polyfill 的討論,相信各位已經了解兩者的差別,在這邊做一個總結:

  • Babel 版本 < 7.4.0

  • 開發組件庫、工具包,選擇 @babel/runtime
  • 開發本地專案,選擇 @babel/polyfill
  • Babel 版本 >= 7.4.0

  • 配置較簡單,會汙染全域環境,選擇 @babel/polyfill
  • 配置較繁瑣,不會汙染全域環境,選擇 @babel/runtime
  •