babel到底該如何配置?

Shenfq發表於2019-03-03

背景

說起ES6,webpack,打包,模組化總是離不開babel,babel作為一個js的編譯器已經被廣泛使用。在babel的官網是這樣介紹它的:

Babel is a JavaScript compiler.

Use next generation JavaScript, today.

大家都知道js作為宿主語言,很依賴執行的環境(瀏覽器、node等),不同環境對js語法的支援不盡相同,特別是ES6之後,ECMAScrip對版本的更新已經到了一年一次的節奏,雖然每年更新的幅度不大,但是每年的提案可不少。babel的出現就是為了解決這個問題,把那些使用新標準編寫的程式碼轉譯為當前環境可執行的程式碼,簡單點說就是把ES6程式碼轉譯(轉碼+編譯)到ES5。

經常有人在使用babel的時候並沒有弄懂babel是幹嘛的,只知道要寫ES6就要在webpack中引入一個babel-loader,然後胡亂在網上copy一個.babelrc到專案目錄就開始了(ps: 其實我說的是我自己)。理解babel的配置很重要,可以避免一些不必要的坑,比如:程式碼中使用Object.assign在一些低版本瀏覽器會報錯,以為是webpack打包時出現了什麼問題,其實是babel的配置問題。


ES6

正文之前先談談ES6,ES即ECMAScript,6表示第六個版本(也被稱為是ES2015,因為是2015年釋出的),它是javascript的實現標準。

被納入到ES標準的語法必須要經過如下五個階段:

  1. Stage 0: strawman
  2. Stage 1: proposal
  3. Stage 2: draft - 必須包含2個實驗性的具體實現,其中一個可以是用轉譯器實現的,例如Babel。
  4. Stage 3: candidate - 至少要有2個符合規範的具體實現
  5. Stage 4: finished

可以看到提案在進入stage3階段時就已經在一些環境被實現,在stage2階段有babel的實現。所以被納入到ES標準的語法其實在大部分環境都已經是有了實現的,那麼為什麼還要用babel來進行轉譯,因為不能確保每個執行程式碼的環境都是最新版本並已經實現了規範。

更多關於ES6的內容可以參考hax的live:Hax:如何學習和實踐ES201X?


Babel的版本變更

寫這篇文章時babel版本已經到了v7.0.0-beta.3,也就是說7.0的正式版就要釋出了,可喜可賀。但是今天不談7.0,只談babel6,在我知道並開始使用的babel的時候babel已經到了版本6,沒有經歷過5的時代。

在babel5的時代,babel屬於全家桶型,只要安裝babel就會安裝babel相關的所有工具, 即裝即用。

但是到了babel6,具體有以下幾點變更:

  • 移除babel全家桶安裝,拆分為單獨模組,例如:babel-core、babel-cli、babel-node、babel-polyfill等; 可以在babel的github倉庫看到babel現在有哪些模組。
    babel-package
  • 新增 .babelrc 配置檔案,基本上所有的babel轉譯都會來讀取這個配置;
  • 新增 plugin 配置,所有的東西都外掛化,什麼程式碼要轉譯都能在外掛中自由配置;
  • 新增 preset 配置,babel5會預設轉譯ES6和jsx語法,babel6轉譯的語法都要在perset中配置,preset簡單說就是一系列plugin包的使用。

babel各個模組介紹

babel6將babel全家桶拆分成了許多不同的模組,只有知道這些模組怎麼用才能更好的理解babel。

下面的一些示例程式碼已經上傳到了github,歡迎訪問,歡迎star。

安裝方式:

#通過npm安裝
npm install babel-core babel-cli babel-node

#通過yarn安裝
yarn add babel-core babel-cli babel-node
複製程式碼
1、babel-core

看名字就知道,babel-core是作為babel的核心存在,babel的核心api都在這個模組裡面,比如:transform。

下面介紹幾個babel-core中的api

  • babel.transform:用於字串轉碼得到AST
/*
 * @param {string} code 要轉譯的程式碼字串
 * @param {object} options 可選,配置項
 * @return {object} 
*/
babel.transform(code: string, options?: Object)
    
//返回一個物件(主要包括三個部分):
{
    generated code, //生成碼
    sources map, //源對映
    AST  //即abstract syntax tree,抽象語法樹
}
複製程式碼

更多關於AST知識點請看這裡

一些使用babel外掛的打包或構建工具都有使用到這個方法,下面是一些引入babel外掛中的原始碼:

//gulp-babel
const babel = require('babel-core');
/*
some codes...
*/
module.exports = function (opts) {
    opts = opts || {};
	return through.obj(function (file, enc, cb) {
        try {
            const fileOpts = Object.assign({}, opts, {
            	filename: file.path,
            	filenameRelative: file.relative,
            	sourceMap: Boolean(file.sourceMap),
            	sourceFileName: file.relative,
            	sourceMapTarget: file.relative
            });
            const res = babel.transform(file.contents.toString(), fileOpts);
            if (res !== null) {
            	//some codes
            }
        } catch (err) {
            //some codes
        }
    }
}

//babel-loader
var babel = require("babel-core");
/*
some codes...
*/
var transpile = function transpile(source, options) {
    //some code
    try {
        result = babel.transform(source, options);
    } catch (error) {
        //some codes
    }
    //some codes
}

//rollup-pugin-babel
import { buildExternalHelpers, transform } from 'babel-core';
/*
some codes...
*/
export default function babel ( options ) {
    //some codes
    return {
        // some methods
        transform ( code, id ) {
            const transformed = transform( code, localOpts );
            //some codes
            return {
            	code: transformed.code,
            	map: transformed.map
            };
        }
    }
}
複製程式碼

上面是一些打包工具引入babel外掛時的一些原始碼,可以看到基本都是先通過呼叫transform方法進行程式碼轉碼。

  • babel.transformFile
//非同步的檔案轉碼方式,回撥函式中的result與transform返回的物件一至。
babel.transformFile("filename.js", options, function (err, result) {
  result; // => { code, map, ast }
});
複製程式碼
  • babel.transformFileSync
//同步的檔案轉碼方式,返回結果與transform返回的物件一至。
babel.transformFileSync(filename, options) // => { code, map, ast }
複製程式碼
  • babel.transformFromAst
//將ast進行轉譯
const { code, map, ast } = babel.transformFromAst(ast, code, options);
複製程式碼
2、babel-cli

babel-cli是一個通過命令列對js檔案進行換碼的工具。

使用方法:

  • 直接在命令列輸出轉譯後的程式碼
    babel script.js
    複製程式碼
  • 指定輸出檔案
    babel script.js --out-file build.js
    或者是
    babel script.js -o build.js
    複製程式碼

讓我們來編寫了一個具有箭頭函式的程式碼:

//script.js
const array = [1,2,3].map((item, index) => item * 2);
複製程式碼

然後在命令列執行 babel script.js,發現輸出的程式碼好像沒有轉譯。

babel轉譯

因為我們沒有告訴babel要轉譯哪些型別,現在看看怎麼指定轉譯程式碼中的箭頭函式。

babel --plugins transform-es2015-arrow-functions script.js
複製程式碼

轉譯箭頭函式

或者在目錄裡新增一個.babelrc檔案,內容如下:

{
    "plugins": [
        "transform-es2015-arrow-functions"
    ]
}
複製程式碼

.babelrc是babel的全域性配置檔案,所有的babel操作(包括babel-core、babel-node)基本都會來讀取這個配置,後面會詳細介紹。

3、babel-node

babel-node是隨babel-cli一起安裝的,只要安裝了babel-cli就會自帶babel-node。 在命令列輸入babel-node會啟動一個REPL(Read-Eval-Print-Loop),這是一個支援ES6的js執行環境。

測試babel-node

其實不用babel-node,直接在node下,只要node版本大於6大部分ES6語法已經支援,況且現在node的版本已經到了8.7.0。

node環境箭頭函式測試

babel-node還能直接用來執行js指令碼,與直接使用node命令類似,只是會在執行過程中進行babel的轉譯,並且babel官方不建議在生產環境直接這樣使用,因為babel實時編譯產生的程式碼會快取在記憶體中,導致記憶體佔用過高,所以我們瞭解瞭解就好。

babel-node script.js
複製程式碼
4、babel-register

babel-register字面意思能看出來,這是babel的一個註冊器,它在底層改寫了node的require方法,引入babel-register之後所有require並以.es6, .es, .jsx 和 .js為字尾的模組都會經過babel的轉譯。

同樣通過箭頭函式做個實驗:

//test.js
const name = 'shenfq';
module.exports = () => {
    const json = {name};
    return json;
};
//main.js
require('babel-register');
var test = require('./test.js');  //test.js中的es6語法將被轉譯成es5

console.log(test.toString()); //通過toString方法,看看控制檯輸出的函式是否被轉譯
複製程式碼

register轉譯

預設babel-register會忽略對node_modules目錄下模組的轉譯,如果要開啟可以進行如下配置。

require("babel-register")({
  ignore: false
});
複製程式碼

babel-register與babel-core會同時安裝,在babel-core中會有一個register.js檔案,所以引入babel-register有兩種方法:

require('babel-core/register');
require('babel-register');
複製程式碼

但是官方不推薦第一種方法,因為babel-register已經獨立成了一個模組,在babel-core的register.js檔案中有如下注釋。

TODO: eventually deprecate this console.trace("use the babel-register package instead of babel-core/register");

5、babel-polyfill

polyfill這個單詞翻譯成中文是墊片的意思,詳細點解釋就是桌子的桌腳有一邊矮一點,拿一個東西把桌子墊平。polyfill在程式碼中的作用主要是用已經存在的語法和api實現一些瀏覽器還沒有實現的api,對瀏覽器的一些缺陷做一些修補。例如Array新增了includes方法,我想使用,但是低版本的瀏覽器上沒有,我就得做相容處理:

if (!Array.prototype.includes) {
  Object.defineProperty(Array.prototype, 'includes', {
    value: function(searchElement, fromIndex) {
      if (this == null) {
        throw new TypeError('"this" is null or not defined');
      }
      var o = Object(this);
      var len = o.length >>> 0;
      if (len === 0) {
        return false;
      }
      var n = fromIndex | 0;
      var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
      while (k < len) {
        if (o[k] === searchElement) {
          return true;
        }
        k++;
      }
      return false;
    }
  });
} 
複製程式碼

上面簡單的提供了一個includes方法的polyfill,程式碼來自MDN

理解polyfill的意思之後,再來說說babel為什麼存在polyfill。因為babel的轉譯只是語法層次的轉譯,例如箭頭函式、解構賦值、class,對一些新增api以及全域性函式(例如:Promise)無法進行轉譯,這個時候就需要在程式碼中引入babel-polyfill,讓程式碼完美支援ES6+環境。前面介紹的babel-node就會自動在程式碼中引入babel-polyfill包。

引入方法:

//在程式碼的最頂部進行require或者import

require("babel-polyfill");

import "babel-polyfill";

//如果使用webpack,也可以在檔案入口陣列引入
module.exports = {
  entry: ["babel-polyfill", "./app/js"]
};
複製程式碼

但很多時候我們並不會使用所有ES6+語法,全域性新增所有墊片肯定會讓我們的程式碼量上升,之後會介紹其他新增墊片的方式。


.babelrc

前面已經介紹了babel常用的一些模組,接下來看看babel的配置檔案 .babelrc

後面的字尾rc來自linux中,使用過linux就知道linux中很多rc結尾的檔案,比如.bashrc,rc是run command的縮寫,翻譯成中文就是執行時的命令,表示程式執行時就會來呼叫這個檔案。

babel所有的操作基本都會來讀取這個配置檔案,除了一些在回撥函式中設定options引數的,如果沒有這個配置檔案,會從package.json檔案的babel屬性中讀取配置。

plugins

先簡單介紹下 plugins ,babel中的外掛,通過配置不同的外掛才能告訴babel,我們的程式碼中有哪些是需要轉譯的。

這裡有一個babel官網的外掛列表,裡面有目前babel支援的全部外掛。

舉個例子:

{
    "plugins": [
        "transform-es2015-arrow-functions", //轉譯箭頭函式
        "transform-es2015-classes", //轉譯class語法
        "transform-es2015-spread", //轉譯陣列解構
        "transform-es2015-for-of" //轉譯for-of
    ]
}
//如果要為某個外掛新增配置項,按如下寫法:
{
    "plugins":[
        //改為陣列,第二個元素為配置項
        ["transform-es2015-arrow-functions", { "spec": true }]
    ]
}
複製程式碼

上面這些都只是語法層次的轉譯,前面說過有些api層次的東西需要引入polyfill,同樣babel也有一系列外掛來支援這些。

{
    "plugins":[
        //如果我們在程式碼中使用Object.assign方法,就用如下外掛
        "transform-object-assign"
    ]
}

//寫了一個使用Object.assign的程式碼如下:
const people = Object.assign({}, {
    name: 'shenfq'
});
//經過babel轉譯後如下:
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

const people = _extends({}, {
    name: 'shenfq'
});
複製程式碼

這種通過transform新增的polyfill只會引入到當前模組中,試想實際開發中存在多個模組使用同一個api,每個模組都引入相同的polyfill,大量重複的程式碼出現在專案中,這肯定是一種災難。另外一個個的引入需要polyfill的transform挺麻煩的,而且不能保證手動引入的transform一定正確,等會會提供一個解決方案:transform-runtime

除了新增polyfill,babel還有一個工具包helpers,如果你有安裝babel-cli,你可以直接通過下面的命令把這個工具包輸出:

./node_modules/.bin/babel-external-helpers > helpers.js
複製程式碼

這個工具包類似於babel的utils模組,就像我們專案中的utils一樣,很多地方都會用到,例如babel實現Object.assign就是使用的helpers中的_extend方法。為了避免同一個檔案多次引用babel的助手函式,通過external-helpers外掛,能夠把這些助手函式抽出放到檔案頂部,避免多次引用。

//安裝: cnpm install --save-dev babel-plugin-external-helpers

//配置
{
  "plugins": ["external-helpers"]
}
複製程式碼

雖然這個外掛能避免一個檔案多次引用助手函式,但是並不能直接避免多個檔案內重複引用,這與前面說到的通過transform新增polyfill是一樣的問題,這些引用都只是module級別的,在打包工具盛行的今天,需要考慮如何減少多個模組重複引用相同程式碼造成程式碼冗餘。

當然也可以在每個需要使用helpers的js檔案頂部直接引入之前生成的helpers檔案既可,通過打包工具將這個公共模組進行抽離。

require('helpers');
複製程式碼

在說完babel的helpers之後就到了外掛系統的最後的一個外掛:transform-runtime。前面在transform-polyfill的時候也有提到這個外掛,之所以把它放到helpers後面是因為這個外掛能自動為專案引入polyfill和helpers。

cnpm install -D babel-plugin-transform-runtime babel-runtime
複製程式碼

transform-runtime這個外掛依賴於babel-runtime,所以安裝transform-runtime的同時最好也安裝babel-runtime,為了防止一些不必要的錯誤。babel-runtime由三個部分組成:

  1. core-js

    core-js極其強悍,通過ES3實現了大部分的ES5、6、7的墊片,作者zloirock是來自戰鬥名族的程式設計師,一個人維護著core-js,聽說他最近還在找工作,上面是core-js的github地址,感興趣可以去看看。

  2. regenerator

    regenerator來自facebook的一個庫,用於實現 generator functions。

  3. helpers

    babel的一些工具函式,沒錯,這個helpers和前面使用babel-external-helpers生成的helpers是同一個東西

從babel-runtime的package.json檔案中也能看出,runtime依賴了哪些東西。

babel-runtime的package.json

安裝有babel-runtime之後要引入helpers可以使用如下方式:

require('babel-runtime/helpers');
複製程式碼

使用runtime的時候還有一些配置項:

{
    "plugins": [
        ["transform-runtime", {
            "helpers": false, //自動引入helpers
            "polyfill": false, //自動引入polyfill(core-js提供的polyfill)
            "regenerator": true, //自動引入regenerator
        }]
    ]
}
複製程式碼

比較transform-runtime與babel-polyfill引入墊片的差異:

  1. 使用runtime是按需引入,需要用到哪些polyfill,runtime就自動幫你引入哪些,不需要再手動一個個的去配置plugins,只是引入的polyfill不是全域性性的,有些侷限性。而且runtime引入的polyfill不會改寫一些例項方法,比如Object和Array原型鏈上的方法,像前面提到的Array.protype.includes
  2. babel-polyfill就能解決runtime的那些問題,它的墊片是全域性的,而且全能,基本上ES6中要用到的polyfill在babel-polyfill中都有,它提供了一個完整的ES6+的環境。babel官方建議只要不在意babel-polyfill的體積,最好進行全域性引入,因為這是最穩妥的方式。
  3. 一般的建議是開發一些框架或者庫的時候使用不會汙染全域性作用域的babel-runtime,而開發web應用的時候可以全域性引入babel-polyfill避免一些不必要的錯誤,而且大型web應用中全域性引入babel-polyfill可能還會減少你打包後的檔案體積(相比起各個模組引入重複的polyfill來說)。

presets

顯然這樣一個一個配置外掛會非常的麻煩,為了方便,babel為我們提供了一個配置項叫做persets(預設)。

預設就是一系列外掛的集合,就好像修圖一樣,把上次修圖的一些引數儲存為一個預設,下次就能直接使用。

如果要轉譯ES6語法,只要按如下方式配置即可:

//先安裝ES6相關preset: cnpm install -D babel-preset-es2015
{
    "presets": ["es2015"]
}

//如果要轉譯的語法不止ES6,還有各個提案階段的語法也想體驗,可以按如下方式。
//安裝需要的preset: cnpm install -D babel-preset-stage-0 babel-preset-stage-1 babel-preset-stage-2 babel-preset-stage-3
{
    "presets": [
        "es2015",
        "stage-0",
        "stage-1",
        "stage-2",
        "stage-3",
    ]
}

//同樣babel也能直接轉譯jsx語法,通過引入react的預設
//cnpm install -D babel-preset-react
{
    "presets": [
        "es2015",
        "react"
    ]
}
複製程式碼

不過上面這些preset官方現在都已經不推薦了,官方唯一推薦preset:babel-preset-env

這款preset能靈活決定載入哪些外掛和polyfill,不過還是得開發者手動進行一些配置。

// cnpm install -D babel-preset -env
{
    "presets": [
        ["env", {
            "targets": { //指定要轉譯到哪個環境
                //瀏覽器環境
                "browsers": ["last 2 versions", "safari >= 7"],
                //node環境
                "node": "6.10", //"current"  使用當前版本的node
                
            },
             //是否將ES6的模組化語法轉譯成其他型別
             //引數:"amd" | "umd" | "systemjs" | "commonjs" | false,預設為'commonjs'
            "modules": 'commonjs',
            //是否進行debug操作,會在控制檯列印出所有外掛中的log,已經外掛的版本
            "debug": false,
            //強制開啟某些模組,預設為[]
            "include": ["transform-es2015-arrow-functions"],
            //禁用某些模組,預設為[]
            "exclude": ["transform-es2015-for-of"],
            //是否自動引入polyfill,開啟此選項必須保證已經安裝了babel-polyfill
            //引數:Boolean,預設為false.
            "useBuiltIns": false
        }]
    ]
}
複製程式碼

關於最後一個引數useBuiltIns,有兩點必須要注意:

  1. 如果useBuiltIns為true,專案中必須引入babel-polyfill。
  2. babel-polyfill只能被引入一次,如果多次引入會造成全域性作用域的衝突。

做了個實驗,同樣的程式碼,只是.babelrc配置中一個開啟了useBuiltIns,一個沒有,兩個js檔案體積相差70K,戳我看看

檔案 大小
useBuiltIns.js 189kb
notUseBuiltIns.js 259kb

最後囉嗦一句

關於polyfill還有個叫做polyfill.io的神器,只要在瀏覽器引入

https://cdn.polyfill.io/v2/polyfill.js

伺服器會根據瀏覽器的UserAgent返回對應的polyfill檔案,很神奇,可以說這是目前最優雅的解決polyfill過大的方案。


前前後後寫完這個差不多寫了一個星期,查了很多資料(babel的官網和github都看了好幾遍),總算憋出來了。

原文連結


參考

  1. ECMAScript 6 會重蹈 ECMAScript 4 的覆轍嗎?
  2. Babel手冊
  3. Babel官網
  4. babel-preset-env: a preset that configures Babel for you

相關文章