背景
說起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標准的語法必須要經過如下五個階段:
- Stage 0: strawman
- Stage 1: proposal
- Stage 2: draft - 必須包含2個實驗性的具體實現,其中一個可以是用轉譯器實現的,例如Babel。
- Stage 3: candidate - 至少要有2個符合規范的具體實現。
- 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現在有哪些模塊。
- 新增 .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 = functiontranspile(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 --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,直接在node下,只要node版本大於6大部分ES6語法已經支持,況且現在node的版本已經到了8.7.0。

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方法,看看控制台輸出的函數是否被轉譯 -

默認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-registerpackage instead ofbabel-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由三個部分組成:
- core-js
core-js極其強悍,通過ES3實現了大部分的ES5、6、7的墊片,作者zloirock是來自戰斗名族的程序員,一個人維護着core-js,聽說他最近還在找工作,上面是core-js的github地址,感興趣可以去看看。
- regenerator
regenerator來自facebook的一個庫,用於實現 generator functions。
- helpers
babel的一些工具函數,沒錯,這個helpers和前面使用babel-external-helpers生成的helpers是同一個東西
從babel-runtime的package.json文件中也能看出,runtime依賴了哪些東西。

安裝有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引入墊片的差異:
- 使用runtime是按需引入,需要用到哪些polyfill,runtime就自動幫你引入哪些,不需要再手動一個個的去配置plugins,只是引入的polyfill不是全局性的,有些局限性。而且runtime引入的polyfill不會改寫一些實例方法,比如Object和Array原型鏈上的方法,像前面提到的
Array.protype.includes。 - babel-polyfill就能解決runtime的那些問題,它的墊片是全局的,而且全能,基本上ES6中要用到的polyfill在babel-polyfill中都有,它提供了一個完整的ES6+的環境。babel官方建議只要不在意babel-polyfill的體積,最好進行全局引入,因為這是最穩妥的方式。
- 一般的建議是開發一些框架或者庫的時候使用不會污染全局作用域的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,有兩點必須要注意:
- 如果useBuiltIns為true,項目中必須引入babel-polyfill。
- babel-polyfill只能被引入一次,如果多次引入會造成全局作用域的沖突。
做了個實驗,同樣的代碼,只是.babelrc配置中一個開啟了useBuiltIns,一個沒有,兩個js文件體積相差70K,戳我看看。
| useBuiltIns.js | 189kb |
| notUseBuiltIns.js | 259kb |
最后啰嗦一句
關於polyfill還有個叫做polyfill.io的神器,只要在瀏覽器引入
服務器會更具瀏覽器的UserAgent返回對應的polyfill文件,很神奇,可以說這是目前最優雅的解決polyfill過大的方案。
前前后后寫完這個差不多寫了一個星期,查了很多資料(babel的官網和github都看了好幾遍),總算憋出來了。
