babel實戰--如何在項目中使用babel


概要

目前在使用的三大框架(vue.js, react.js, angular.js)都有相應的腳手架工具已經貼心的幫我們集成了babel的各種配置,因此我們少了很多配置的工作量,一條命令就可以開始開發業務代碼了,覺得自己又牛逼閃閃了。顯示那是我們的錯覺,與大佬之間的距離也許就差一個對babel深度的學習,接下來我們來詳細了解一下babel的配置,讓你見到這些配置時不再一臉懵逼。

餐前小菜

babel將es6+(指es6及以上版本)分為

  • 語法層: letconstclass箭頭函數等,這些需要在構建時進行轉譯,是指在語法層面上的轉譯,(比如class...將來會被轉譯成var function...)
  • api層:Promiseincludesmap等,這些是在全局或者Object、Array等的原型上新增的方法,它們可以由相應es5的方式重新定義babel對這兩個分類的轉譯的做法肯定是不一樣的,我們也需要給出相應的配置

准備工作

以windows系統為例:
在你的項目目錄下新建文件夾babel-demo, 執行cd babel-demo進入文件夾,執行npm init -y 初始化項目。
執行 npm install --save-dev @babel/core @babel/cli 安裝babel必要的依賴。

  • babel-core:Babel 的核心,包含各個核心的 API,供 Babel 插件和打包工具使用
  • babel-cli:命令行對 js 文件進行換碼的工具

新建目錄src, 在目錄下新建index.js, 並寫下如下測試代碼:

在package.json中配置一條命令--compiler,用於執行babel

"scripts": {
    "compiler": "babel src --out-dir lib --watch"
  },

然后執行 npm run compiler理論上babel會將let都轉化成var,但是你會發現這時候轉譯出來的代碼跟原來一樣,這是為啥呢?
Babel 本身不具有任何轉化功能,它把轉化的功能都分解到一個個 plugin 里面。因此當我們不配置任何插件時,經過 babel 處理的代碼和輸入是相同的。所以我們需要安裝插件。

插件(用於處理語法層)

兩種方式使用插件,一種是一個個的安裝(比較麻煩),另一種是以preset的方式安裝一組插件,我們當然要選省事的preset了。

  1. 首先安裝preset
    npm i @babel/preset-env -D@babel/preset-env 包含的插件支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 階段)
  2. 配置
    建立 .babelrc文件或者babelconfig.js文件,添加以下代碼,babel會自動尋找這個文件
{
  "presets": [
    [
      "@babel/preset-env"
    ]
  ]
}

補充說明:如果你用 Vue ,presets 一般是 @vue/app,這個是把 在@babel/preset-env 包含的 plugins 上又加了很多自己定義的 plugins。
所有的 Vue CLI 應用都使用 @vue/babel-preset-app,它包含了 babel-preset-envJSX 支持以及為最小化包體積優化過的配置。@vue/babel-preset-app已經默認配置了@babel/plugin-transform-runtime。通過它的文檔可以查閱到更多細節和 preset 選項。

執行npm run compiler命令,這時候就轉譯成功了,轉譯結果如下:

"use strict";

var a = 1;
var b = 2;
var c = a + b;

babel還可配置taget或者提供.browserlist文件,用於指定目標環境,這樣能使你的代碼體積保持更小。

//.browserslistrc
> 0.25%
not dead

polyfill(用於處理Api層)

上邊只驗證了語法層面的轉譯,接下來我們試試api層面的轉譯是怎樣的
我們修改index.js如下:

// Promise是api層面的,是一個es6中的全局對象
const p = new Promise((resolve, reject) => {    
  resolve(100);
});

執行npm run compiler命令,編譯結果如下:

"use strict";

var p = new Promise(function (resolve, reject) {
  resolve(100);
});

const-->var沒有問題,但是Promise並沒有任何變化,難道是我被詛咒了嗎,其實並不是,這時候就需要Polyfill這個東西了。
polyfill的中文意思是墊片,顧名思義就是墊平不同瀏覽器或者不同環境下的差異,讓新的內置函數、實例方法等在低版本瀏覽器中也可以使用。
使用方法如下:
安裝 @babel/polyfill 依賴。注意這是一個運行時依賴。所以不要加-dev,npm install --save @babel/polyfill@babel/polyfill 模塊包括 core-js 和一個自定義的 regenerator runtime 模塊,可以模擬完整的 ES2015+ 環境。然后在index.js中引入。

import '@babel/polyfill';
const p = new Promise((resolve, reject) => {    
  resolve(100);
});

轉譯結果如下:

"use strict";

require("@babel/polyfill");

var p = new Promise(function (resolve, reject) {
  resolve(100);
});

雖然看起來Promise還是沒有轉譯,但是我們引入的 polyfill 中已經包含了對Promise的定義,所以這時候代碼便可以在低版本瀏覽器中運行了。

useBuiltIns屬性

不知道大家又沒有想到這樣一個問題,我代碼里邊只用到了幾個es6,卻需要引入所有的墊片,這不合情理啊。要優化這一點,就需要用到useBuiltIns這個屬性了。useBuiltIns這一配置項,它的值有三種:

  • false: 不對polyfills做任何操作
  • entry: 根據target中瀏覽器版本的支持,將polyfills拆分引入,僅引入有瀏覽器不支持的polyfill
  • usage(新):檢測代碼中ES6/7/8等的使用情況,僅僅加載代碼中用到的polyfills

然后我們修改配置如下:

執行npm run compiler命令,這是命令行中會出現如下警告:

意思是需要我們指定corejs的版本。修改.babelrc如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

目前推薦使用的corejs版本是3版本,新的特性也只會添加在這個版本。此時,轉譯結果如下:

"use strict";

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

var p = new Promise(function (resolve, reject) {
  resolve(100);
});

@babel/plugin-transform-runtime解決代碼冗余

代碼冗余是出現在轉譯語法層時出現的問題。修改下我們的index.js,這次我們再寫一個語法層面的es6-->class(let,const這些對比不出問題,所以用class),看看babel會把class轉化成什么

//index.js
class Student {
    constructor(name,age){
        this.name = name;
        this.age = age
    }
}

轉換結果如下:

"use strict";

require("core-js/modules/es.function.name");

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

var Student = function Student(name, age) {
  _classCallCheck(this, Student);

  this.name = name;
  this.age = age;
};

從結果看似乎沒什么問題,但事實上,如果我們在其他文件中也使用了class,你會發現_classCallCheck在每個文件中都會出現,這就造成了代碼冗余(我們示例中只使用了class,可能冗余的不明顯,實際項目中這些函數可能是很長的)。
這時候需要用到另外一個插件了@babel/plugin-transform-runtime。該插件會開啟對 Babel 注入的輔助函數(比如上邊的_classCallCheck)的復用,以節省代碼體積。
這些輔助函數在@babel/runtime中,所以需要安裝@babel/runtime,當然@babel/runtime也是運行時依賴。(在對一些語法進行編譯的時候,babel需要借助一些輔助函數)
安裝@babel/plugin-transform-runtime和@babel/runtime npm i --save-dev @babel/plugin-transform-runtime @babel/runtime
然后修改配置:

{
    "presets": [
        ["@babel/preset-env",{
            "useBuiltIns":"usage",
            "corejs":3
        }]
    ],
    "plugins": [
        ["@babel/plugin-transform-runtime"]
    ]
}

轉換結果如下:

"use strict";

require("core-js/modules/es.function.name");

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var Student = function Student(name, age) {
  (0, _classCallCheck2["default"])(this, Student);
  this.name = name;
  this.age = age;
};

可以發現,相關的輔助函數是以require的方式引入而不是被直接插入進來的,這樣就不會冗余了。
除了解決代碼冗余,@babel/plugin-transform-runtime還有另外一個重要的能力——解決全局污染

解決全局污染

全局污染是出現在轉譯api層出現的問題
我們這次依然用Promise來做實驗
修改index.js

new Promise(function (resolve, reject) {
    resolve(100);
});

轉譯結果如下:

"use strict";

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

new Promise(function (resolve, reject) {
  resolve(100);
});

可以看出preset-env在處理例如Promise這種的api時,只是引入了core-js中的相關的js庫,這些庫重新定義了Promise,然后將其掛載到了全局。

這里的問題就是:必然會造成全局變量污染,同理其他的例如Array.from等會修改這些全局對象的原型prototype,這也會造成全局對象的污染。
解決方式就是:將core-js交給transform-runtime處理。添加一個配置即可,非常簡單。

{
  "presets": [
    ["@babel/preset-env"]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

可以看出,我們是將core-js這個屬性添加到@babel/plugin-transform-runtime這個插件的配置下,讓這個插件處理,同時也不需要配置useBuiltIns了,因為在babel7中已經將其設置為默認值
(transform-runtime是利用plugin自動識別並替換代碼中的新特性,檢測到需要哪個就用哪個)
這里注意下:有的博客上說無法轉譯includes等實例方法,其實說錯了,官方文檔是這樣說的:corejs: 2僅支持全局變量(例如Promise)和靜態屬性(例如Array.from),corejs: 3還支持實例屬性(例如[].includes)。轉譯結果:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

new _promise["default"](function (resolve, reject) {
  resolve(100);
});

可以看出,這時的代碼並沒有在全局直接添加一個Promise,而是定義了一個_promise["default"]方法,這樣便不會出現全局變量污染的情況所以綜上可得出@babel/plugin-transform-runtime這個插件的強大之處有以下幾點:

實現對輔助函數的復用,解決轉譯語法層時出現的代碼冗余
解決轉譯api層出現的全局變量污染

但是transform-runtime也有缺點:
每個特性都會經歷檢測和替換,隨着應用增大,可能會造成轉譯效率不高

webpack中使用babel

安裝babel-loader, npm install --save-dev babel-loader
webpack.config.js配置如下:

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
          "plugins": [
              ["@babel/plugin-transform-runtime",{
                  "corejs":3
              }]
          ]
        }
      }
    }
  ]
}

使用vue-cli的同學需要注意, 因為babel-loader很慢,所以webpack官方推薦轉譯盡可能少的文件(參考),所以vue-cli配置了該loader的exclude選項,將node_moduls中的文件排除了,但是這樣可能會造成某個依賴出現兼容性問題。所以,如果你的項目中某個依賴出現了兼容性問題,這可能就是原因。解決辦法就是在vue.config.js中配置transpileDependencies這個選項,Babel就會 顯式轉譯這個依賴。

文章引用

關於babel的詳細解讀(精華又通俗)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM