一. 關於babel 是ES6+語法的編譯器,官方網址: "www.babeljs.io" ,用於將舊版本瀏覽器無法識別的語法和特性轉換成為ES5語法,使代碼能夠適用更多環境。 最初的 使用起來是非常方便的,幾乎僅使用少量的配置就可以使用,但隨著工具的快速升級和代碼架構的轉變, 已經裂變成非常多的部 ...
一. 關於babel
babel
是ES6+語法的編譯器,官方網址:www.babeljs.io,用於將舊版本瀏覽器無法識別的語法和特性轉換成為ES5語法,使代碼能夠適用更多環境。
最初的babel
使用起來是非常方便的,幾乎僅使用少量的配置就可以使用,但隨著工具的快速升級和代碼架構的轉變,babel
已經裂變成非常多的部分,每個部分各司其職,這樣做的好處是可以縮小生產環境的正式包的代碼體積(因為可以按需引用)而加重了開發環境(開發階段需要引入更多碎片化的插件),但劣勢就是將其使用門檻提得非常高,對軟體架構不熟悉的開發者難以使用。
比如babel
官方網站在webpack
配置的章節,提及了babe-loader
,babel-core
和babel-preset-env
三個插件,而當開發者在webpack
中實際進行配置時除了上述三個基本插件外,又會遇到babel-polyfill
,babel-runtime
,babel-plugin-transform-runtime
等等一系列插件,或許通過查看插件說明能夠理解插件的功能,但開發者卻很難判斷自己是否該使用這個功能或者什麼時候使用。
二. 基本需求推演
我們從工具設計的角度,通過問題推演的方式來看看babel
的變化。
在ES6
標準推出時,瀏覽器還不能很好地支持,但ES6
的許多特性和語法又很誘人,所以大家想了個辦法,那就是用ES6
編寫代碼,然後出包的時候拿個工具轉換一下,變成能被更多瀏覽器識別的ES5
語法不就行了麽,於是,Babel
基本模型就出現了:
babel
的功能被定義為編譯工具,那麼理論上來說它就可以使用編譯器的通用代碼框架,通過ASTparser --> traverse --> stringify 的步驟實現編譯功能,在關鍵的traverse環節,是需要一個規則集合的,可是轉碼所參考的ES6
的標準並不是一個定案的標準,其中每一個特性都需要經過從stage0到stage4這樣5個階段才能正式定稿,只有stage-2草案(draft)階段以上的特性才會在未來被支持,而處於這個階段以下的標準是有可能被廢的,如果一味地全部轉換,不僅會降低工具效率,也會為代碼未來的維護造成隱患。
那如果我們有一個工廠函數,接受數字0-4作為參數,然後返回所有經歷了stage-x的規則集(是ES6
規則的子集)作為規則集合,那麼就可以在最終生成生產環境的代碼時減小代碼體積,假如在項目中通過babel_get_es6_by_stage(2)
這樣一個函數返回了規則集,那麼正式代碼中就不需要stage-0和stage-1的實現代碼了。基於以上的考慮,我們對Babel
工具進行第一次功能剝離:
推演繼續,在對規則集進行了一次體積縮減後,我們得到了一個相對精簡的規則集,它包含了諸多新的語法和方法,如果直接使用那的確很爽,畢竟引入了一個工具後就可以毫無後顧之憂地使用新特性,但對於生產環境的代碼包來說,這種做法造成的代碼冗餘確是非常難以接受的。
用大家都熟悉的bootstrap
為例,bootstrap.min.css
的體積大約為120k
,可你會發現很多人引入它完全是出於心裡慣性,而在最後僅僅使用了非常基礎的btn
相關的樣式類,或者僅僅為了使用col-md-4
這種響應式佈局的樣式,所有使用到的樣式可能只占了20k-30k的空間,但是卻不得不為項目引進一個120k大的庫,當然並不是所有的項目都會在意20k和120k之間的差別的。
那麼我們就需要一個能夠按更小粒度組合的方法babel_get_es6_by_rules([rule , ...])
,讓使用者可以選擇自己所使用到的語法和方法,從而達到縮小引用庫體積的目的:
推演繼續進行。處理過相容性問題的開發者都知道,瀏覽器是存在版本區分的,許多特性在不同瀏覽器中的實現和表現都不一樣,對於ES6
也是這樣,較高版本的瀏覽器對於ES6
中的一些特性是已經逐步實現支持了的,如果我們的目標用戶所使用的運行環境對某些ES6
特性已經提供了原生支持,或者目標用戶的運行環境根本就是由開發者直接封裝好的,那麼原先“一鍋端”的轉碼方式里就會存在很多沒有必要的部分。
比如你在規則集中選擇了對Class
關鍵字來定義類這個特性進行轉碼,那麼babel就需要將其轉碼成為使用function
和prototype
的ES5的實現方式,但如果你的目標用戶全都是程式員,幾乎全都是使用高版本的chrome作為項目環境,那麼上面的轉碼可能就是畫蛇添足了。
綜上所述,我們就需要為babel提供一個判斷目標環境是否需要轉碼的方法babel_get_rule_as_need( rule_set , env_info)
,將經過第一次篩選後的規則集和目標用戶的環境信息傳入方法,對規則集進行再一次的精簡,那麼我們需要再次對babel進行優化:
至此,babel
便具備了針對不同的使用環境進行必要轉碼的能力,可這並不是問題的全部,ES6
的新特性除了語法的更新外,還增加了很多原生方法或類型,例如Map
,Set
,Promise
等這類新的全局對象,或是Array.from
這類靜態方法等等,語法轉義並不能完成對這些特性的識別,因為無論在ES5環境還是ES6環境你都是這麼寫的,只有運行的時候,瀏覽器才會報錯,告訴你某個對象或者某個方法不存在。
比如下麵的代碼:
function addAll() {
return Array.from(arguments).reduce((a, b) => a + b);
}
轉義後會變為:
function addAll() {
return Array.from(arguments).reduce(function(a, b) {
return a + b;
});
}
然而,它依然無法隨處可用因為不是所有的 JavaScript 環境都支持 Array.from
。對於這一類非語法層面的特性,我們希望在工具中能夠自動提供支持,這項工作有一個專有的稱謂,叫做【polyfill】(或稱為墊片)。
我們既可以主動提供一個polyfill列表指明需要添加的墊片插件數組,也可以採用被動的方式,在轉碼過程中遇到的這種API
類型的新特性放進一個數組,通過babel_add_polyfill ( polyfill_list )
為根據安裝相應的墊片,需要註意的是,polyfill相當於為瀏覽器進行功能擴展,需要優先於項目業務邏輯代碼運行,那麼babel的邏輯框架就變成了:
推演繼續。在上面的邏輯結構中,我們只是簡單地將polyfill庫添加至全局變數,而全局變數是很有可能被重寫而失效或是與其他第三方庫發生代碼衝突的。那麼如果不將polyfill添加至全局,就需要將其剝離為一個具有同等功能的獨立模塊,通過類似於lodash
或是underscore
那樣的方式調用,我們對邏輯結構進行再一次拆分:
至此,我們已經完成了babel工具集基本功能的*邏輯層劃分*,通過傳說中的多退少補(也就是語法超前了就回退,方法不夠了就打補丁)的方式來實現代碼編譯。
三. 模塊劃分
根據上述業務邏輯層的劃分結果,我們需要對Babel工具進行代碼層的模塊劃分:
四. 真正的babel
如果你能夠理解上述的需求推演和模塊劃分的章節,那麼恭喜你已經掌握了babel的基本結構,我們將原本模塊圖中的信息更換成實際的名稱或是插件,併進行一些組件劃分,就可以看到真正的babel
工具集的基本架構:
當然真正的babel
功能遠不止這樣,它為各種環境,編輯器和自動化工具提供了介面,也開放了插件開發的API
給開發者,感興趣的讀者可以繼續深入瞭解。
五. 使用babel
babel
8.0以上的版本將許多插件移入官方倉庫,安裝方式發生了改變,例如babel-preset-env
地址變為了@babel/preset-env
,使用時請參考babel
官網進行配置。
1.babel-cli
為了方便直接在命令行使用babel的功能,通過yarn global add babel-cli
在全局安裝命令行工具babel-cli,在package.json中加入如下腳本:
"scripts":{
"babel":"babel main.js -o maines5.js"
}
然後通過yarn run babel
即可在命令行使用babel進行編譯了,但查看編譯後的代碼就可以發現,編譯前後的文件是一樣的,因為我們沒有為其指定任何轉碼規則,運行babel只是把生成的AST遍歷了一下而已,想要babel能夠實現轉碼,請繼續向下看。
2.babel-preset-env
提供轉碼規則,它低版本babel
中使用的幾個插件的結合。babel-preset-env
實際上實現的,就是我們在問題推演中所描述的【All Rules規則集 + get_rules()方法集】,你會在node_modules
文件夾中找到許多babel-plugin-transform-***
這種命名的包,他們就是規則集,你既可以通過設置preset
屬性來使用,也可以通過在plugins
屬性中挑選需要的轉碼規則進行引用。
安裝babel-preset-env
後在項目文件夾新建.babelrc
文件並添加如下配置:
{
"presets":["env"],
"plugins": []
}
或自定義所需要支持的轉義規則:
{
"presets":[],
"plugins": [
"babel-plugin-transform-es2015-arrow-functions"//箭頭函數轉換規則
]
}
再次運行babel
,就可以看到所編寫的代碼已經進行了轉換。
轉換前:
//Arrow Function Array.from method
Array.from([1, 2, 3]).map((i) => {
return i * i;
});
轉換後:
"use strict";
//Arrow Function Array.from method
Array.from([1, 2, 3]).map(function (i) {
return i * i;
});
當然也可以指定目標瀏覽器,去除不必要的轉碼,例如在.babelrc
指定要匹配的瀏覽器為較高版本的chrome:
//.babelrc
{
"presets":[
["env", {
"targets": {
"browsers": "chrome 56"
}
}]
],
"plugins":[]
}
就可以發現編譯後的腳本文件中箭頭函數依然存在,說明這個版本的chrome
瀏覽器已經支持箭頭函數了,也就沒有必要進行轉義了。
新版本的babel已經計劃支持在package.json中設置
browserslist
參數來指定需要適配的使用環境,也就是說同一套針對使用環境的配置被剝離出來,而被postcss
,babel
,autoprefixer
等工具共用使用。
3.babel-polyfill
babel只負責語法轉換,比如將ES6的語法轉換成ES5。但如果有些對象、方法,瀏覽器本身不支持,比如:
- 全局對象:Promise、WeakMap 等。
- 全局靜態函數:Array.from、Object.assign 等。
- 實例方法:比如 Array.prototype.includes 等。
此時,需要引入babel-polyfill
來模擬實現這些對象、方法。
如果上面編譯後的代碼在IE10
瀏覽器中打開,就會看到瀏覽器出現不支持Array.from方法的報錯,如果生成的代碼需要在IE10
中運行,那我們就需要引入相容補丁庫,讓IE10
瀏覽器環境中能夠支持這個方法。
babel-polyfill
需要通過如下的方式引入,然後通過打包工具將其融入腳本:
//ES Module
import 'babel-polyfill'
//或 CommonJs
require ('babel-polyfill')
當你真的這樣去使用時,就會發現,它的確能夠解決報錯的問題,但是如此打包會引入整個babel-polyfill
,打包後的代碼增加了將近4000行(約400k體積增量),著實讓人難以接受。那這個插件能否像babel-preset-env
一樣按需引用呢?必須可以的。babel-polyfill
是基於core-js
和regenerator
構建的,只需要在引用時指明即可,例如:
import 'core-js/modules/es6.array.from';
//Arrow Function Array.from method
Array.from([1, 2, 3]).map((i) => {
return i * i;
});
再進行打包時就會發現bundle文件的體積減小了非常多。
babel-polyfill
的實現方式如問題推演中所提到的那樣,就是污染了全局環境,而且你可能已經意識到,這個工具,要麼簡單配置後代碼量激增,要麼按需引用配置繁瑣。除非是在中型以上項目中有相容低版本IE的需求,否則不建議使用。
4.babel-runtime/babel-plugin-transform-runtime
如果一個東西難用,那麼很快就會有替代品出現,軟體的世界也是這樣,babel-runtime
就是這樣一個替代品。摘錄下文資料推薦的博文中的解釋:
babel-polyfill
簡單粗暴,他會污染全局環境,比如在不支持Promise的瀏覽器會polyfill一個全局的Promise對象供調用;另外,不支持的實例方法也在對應的構造函數原型鏈上添加要polyfill的方法。
babel-runtime
不會污染全局環境,會在局部進行polyfill,另外不會轉換一些實例方法,如'abc'.includes('a'),其中的includes方法就不會翻譯。它一般結合
babel-plugin-transform-runtime
來使用。
簡單地說,除了實例方法以外,其他的特性babel-runtime
都會幫你打好補丁。使用時直接在plugins
配置項中添加babel-plugin-transform-runtime
即可。
總的來說,babel-polyfill
和babel-plugin-transform-runtime
都有各自的使用場景,也是可以結合使用的,需要根據實際項目需求進行篩選和引入。
六. 資料推薦
《webpack+babel項目在IE下報Promise未定義錯誤引出的思考》
博文里詳細解說了
babel-runtime
和babel-plugin-transform-runtime
的相關問題。-
博文里詳細解說了各個配置項和可選參數的意思,非常實用。
入門指南:babel-handbook
非常棒的入門指南,對babel中的概念和用法都做了一定解釋,建議優先閱讀,可以幫助開發者瞭解本篇中未涉及的babel模塊。
官方網站:www.babeljs.io
很多開發者喜歡看教程卻容易忽略官網,這是非常奇怪的。官方網站會鏈接到非常多優秀的github倉庫,不僅包括babel中封裝的底層模塊,還包括能夠幫助我們理解的指引倉庫,甚至ES2015主要特性的解釋的網站,是學習babel的主要資源。