大前端的自動化工廠(3)—— babel



一. 關於babel

babel是ES6+語法的編譯器,官方網址:www.babeljs.io,用於將舊版本瀏覽器無法識別的語法和特性轉換成為ES5語法,使代碼能夠適用更多環境。

最初的babel使用起來是非常方便的,幾乎僅使用少量的配置就可以使用,但隨着工具的快速升級和代碼架構的轉變,babel已經裂變成非常多的部分,每個部分各司其職,這樣做的好處是可以縮小生產環境的正式包的代碼體積(因為可以按需引用)而加重了開發環境(開發階段需要引入更多碎片化的插件),但劣勢就是將其使用門檻提得非常高,對軟件架構不熟悉的開發者難以使用。

比如babel官方網站在webpack配置的章節,提及了babe-loader,babel-corebabel-preset-env三個插件,而當開發者在webpack中實際進行配置時除了上述三個基本插件外,又會遇到babel-polyfill,babel-runtime,babel-plugin-transform-runtime等等一系列插件,或許通過查看插件說明能夠理解插件的功能,但開發者卻很難判斷自己是否該使用這個功能或者什么時候使用。

二. 基本需求推演

我們從工具設計的角度,通過問題推演的方式來看看babel的變化。

ES6標准推出時,瀏覽器還不能很好地支持,但ES6的許多特性和語法又很誘人,所以大家想了個辦法,那就是用ES6編寫代碼,然后出包的時候拿個工具轉換一下,變成能被更多瀏覽器識別的ES5語法不就行了么,於是,Babel基本模型就出現了:

babel的功能被定義為編譯工具,那么理論上來說它就可以使用編譯器的通用代碼框架,通過ASTparser --> traverse --> stringify 的步驟實現編譯功能,在關鍵的traverse環節,是需要一個規則集合的,可是轉碼所參考的ES6的標准並不是一個定案的標准,其中每一個特性都需要經過從stage0stage4這樣5個階段才能正式定稿,只有stage-2草案(draft)階段以上的特性才會在未來被支持,而處於這個階段以下的標准是有可能被廢的,如果一味地全部轉換,不僅會降低工具效率,也會為代碼未來的維護造成隱患。

那如果我們有一個工廠函數,接受數字0-4作為參數,然后返回所有經歷了stage-x的規則集(是ES6規則的子集)作為規則集合,那么就可以在最終生成生產環境的代碼時減小代碼體積,假如在項目中通過babel_get_es6_by_stage(2)這樣一個函數返回了規則集,那么正式代碼中就不需要stage-0stage-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就需要將其轉碼成為使用functionprototype的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-module

四. 真正的babel

如果你能夠理解上述的需求推演和模塊划分的章節,那么恭喜你已經掌握了babel的基本結構,我們將原本模塊圖中的信息更換成實際的名稱或是插件,並進行一些組件划分,就可以看到真正的babel工具集的基本架構:

當然真正的babel功能遠不止這樣,它為各種環境,編輯器和自動化工具提供了接口,也開放了插件開發的API給開發者,感興趣的讀者可以繼續深入了解。

五. 使用babel

babel8.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。但如果有些對象、方法,瀏覽器本身不支持,比如:

  1. 全局對象:Promise、WeakMap 等。
  2. 全局靜態函數:Array.from、Object.assign 等。
  3. 實例方法:比如 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-jsregenerator構建的,只需要在引用時指明即可,例如:

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-polyfillbabel-plugin-transform-runtime都有各自的使用場景,也是可以結合使用的,需要根據實際項目需求進行篩選和引入

六. 資料推薦

  • 《webpack+babel項目在IE下報Promise未定義錯誤引出的思考》

    博文里詳細解說了babel-runtimebabel-plugin-transform-runtime的相關問題。

  • 《如何寫好.babelrc?》

    博文里詳細解說了各個配置項和可選參數的意思,非常實用。

  • 入門指南:babel-handbook

    非常棒的入門指南,對babel中的概念和用法都做了一定解釋,建議優先閱讀,可以幫助開發者了解本篇中未涉及的babel模塊。

  • 官方網站:www.babeljs.io

    很多開發者喜歡看教程卻容易忽略官網,這是非常奇怪的。官方網站會鏈接到非常多優秀的github倉庫,不僅包括babel中封裝的底層模塊,還包括能夠幫助我們理解的指引倉庫,甚至ES2015主要特性的解釋的網站,是學習babel的主要資源。


免責聲明!

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



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