babel到底該如何配置?

背景

提及ES6,webpack,打包,模塊化老是離不開babel,babel做爲一個js的編譯器已經被普遍使用。在babel的官網是這樣介紹它的:javascript

Babel is a JavaScript compiler.html

Use next generation JavaScript, today.java

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

常常有人在使用babel的時候並無弄懂babel是幹嗎的,只知道要寫ES6就要在webpack中引入一個babel-loader,而後胡亂在網上copy一個.babelrc到項目目錄就開始了(ps: 其實我說的是我本身)。理解babel的配置很重要,能夠避免一些沒必要要的坑,好比:代碼中使用Object.assign在一些低版本瀏覽器會報錯,覺得是webpack打包時出現了什麼問題,實際上是babel的配置問題。react


ES6

正文以前先談談ES6,ES即ECMAScript,6表示第六個版本(也被稱爲是ES2015,由於是2015年發佈的),它是javascript的實現標準。linux

被歸入到ES標準的語法必需要通過以下五個階段:webpack

  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來進行轉譯,由於不能確保每一個運行代碼的環境都是最新版本並已經實現了規範。git

更多關於ES6的內容能夠參考hax的live:Hax:如何學習和實踐ES201X?程序員


Babel的版本變動

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

在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
一、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);
二、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)基本都會來讀取這個配置,後面會詳細介紹。

三、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
四、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");

五、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實現了大部分的ES五、六、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/po...

服務器會更具瀏覽器的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
相關文章
相關標籤/搜索