🙏向這次肺炎疫情中逝世的同胞表示哀悼。前端
本文首發於政採雲前端團隊博客: 前端工程師的自我修養-關於 Babel 那些事兒
隨着 Nodejs 的崛起,編譯這個昔日在 Java、C++ 等語言中流行的詞,在前端也逐漸火了起來,如今一個前端項目在開發過程當中沒有編譯環節,總感受這個項目是沒有靈魂的。提及前端編譯就不得不提早端編譯界的扛把子 Babel ,大部分前端攻城獅對 Babel 並不陌生,可是在這個 Ctrl+C 和 Ctrl+V 的年代,大多數人對它也只是知道、瞭解或者聽過,少數可能配置過 Babel,但也僅此而已。做爲一個有想法和靈魂的前端攻城獅僅僅知道這些是不夠的,你須要對 Babel 有一個系統的瞭解,今天就來聊聊 Babel 那些事兒。node
官方的解釋 Babel 是一個 JavaScript 編譯器,用於將 ECMAScript 2015+ 版本的代碼轉換爲向後兼容的 JavaScript 語法,以便可以運行在當前版本和舊版本的瀏覽器或其餘環境中。簡單來講 Babel 的工做就是:react
原理很簡單,核心就是 AST (抽象語法樹)。首先將源碼轉成抽象語法樹,而後對語法樹進行處理生成新的語法樹,最後將新語法樹生成新的 JS 代碼,整個編譯過程能夠分爲 3 個階段 parsing (解析)、transforming (轉換)、generating (生成),都是在圍繞着 AST 去作文章,話很少說上圖:npm
整個過程很清晰,可是,好多東西都是看着簡單,可是實現起來賊複雜,好比這裏說到的 AST,要是你以爲你對 AST 已經信手拈來了,老哥麻煩在下面留下聯繫方式,我要來找你要簡歷。言歸正傳,這裏提一下,Babel 只負責編譯新標準引入的新語法,好比 Arrow function、Class、ES Modul 等,它不會編譯原生對象新引入的方法和 API,好比 Array.includes,Map,Set 等,這些須要經過 Polyfill 來解決,文章後面會提到。json
運行 babel 所需的基本環境segmentfault
npm install i -S @babel/cli
api
@babel/cli 是 Babel 提供的內建命令行工具。提到 @babel/cli 這裏就不得不提一下 @babel/node ,這哥倆雖然都是命令行工具,可是使用場景不一樣,babel/cli 是安裝在項目中,而 @babel/node 是全局安裝。數組
@babel/corepromise
npm install i -S @babel/core
瀏覽器
安裝完 @babel/cli 後就在項目目錄下執行babel test.js
會報找不到 @babel/core 的錯誤,由於 @babel/cli 在執行的時候會依賴 @babel/core 提供的生成 AST 相關的方法,因此安裝完 @babel/cli 後還須要安裝 @babel/core。
安裝完這兩個插件後,若是在 Mac 環境下執行會出現 command not found: babel
,這是由於 @babel/cli是安裝在項目下,而不是全局安裝,因此沒法直接使用 Babel 命令,須要在 package.json 文件中加上下面這個配置項:
"scripts": { "babel":"babel" }
而後執行 npm run babel ./test.js
,順利生成代碼,此時生成的代碼並無被編譯,由於 Babel 將原來集成一體的各類編譯功能分離出去,獨立成插件,要編譯文件須要安裝對應的插件或者預設,咱們常常看見的什麼 @babel/preset-stage-0、@babel/preset-stage-1,@babel/preset-env 等就是幹這些活的。那這些插件和預設怎麼用呢?下面就要說到 Babel 的配置文件了,這些插件須要在配置文件中交代清楚,否則 Babel 也不知道你要用哪些插件和預設。
安裝完基本的包後,就是配置 Babel 配置文件,Babel 的配置文件有四種形式:
babel.config.js
在項目的根目錄(package.json
文件所在目錄)下建立一個名爲 babel.config.js 的文件,並輸入以下內容。
module.exports = function (api) { api.cache(true); const presets = [ ... ]; const plugins = [ ... ]; return { presets, plugins }; }
.babelrc
在你的項目中建立名爲 .babelrc
的文件
{ "presets": [...], "plugins": [...] }
.babelrc.js
與 .babelrc 的配置相同,你可使用 JavaScript 語法編寫。
const presets = [ ... ]; const plugins = [ ... ]; module.exports = { presets, plugins };
package.json
還能夠選擇將 .babelrc 中的配置信息寫到 package.json
文件中
{ ... "babel": { "presets": [ ... ], "plugins": [ ... ], } }
四種配置方式做用都同樣,你就合着本身的口味來,那種看着順眼,你就翻它。
插件是用來定義如何轉換你的代碼的。在 Babel 的配置項中填寫須要使用的插件名稱,Babel 在編譯的時候就會去加載 node_modules 中對應的 npm 包,而後編譯插件對應的語法。
.babelrc
{ "plugins": ["transform-decorators-legacy", "transform-class-properties"] }
插件在預設(Presets) 前運行。
插件的執行順序是從左往右執行。也就是說在上面的示例中,Babel 在進行 AST 遍歷的時候會先調用 transform-decorators-legacy 插件中定義的轉換方法,而後再調用 transform-class-properties 中的方法。
參數是由插件名稱和參數對象組成的一個數組。
{ "plugins": [ [ "@babel/plugin-proposal-class-properties", { "loose": true } ] ] }
插件名稱若是爲 @babel/plugin-XX
,可使用短名稱@babel/XX
,若是爲 babel-plugin-xx
,能夠直接使用 xx
。
大部分時間咱們都是在用別人的寫的插件,可是有時候咱們老是想秀一下,本身寫一個 Babel 插件,那應該怎麼操做呢?
插件加載
要致富先修路,要用本身寫的插件首先得知道怎麼使用自定義的插件。一種方式是將本身寫的插件發佈到 npm 倉庫中去,而後本地安裝,而後在 Babel 配置文件中配置插件名稱就行了:
npm install @babel/plugin-myPlugin
.babelrc
{ "plugins": ["@babel/plugin-myPlugin"] }
另一種方式就是不發佈,直接將寫好的插件放在項目中,而後在 babel 配置文件中經過訪問相對路徑的方式來加載插件:
.babelrc
{ "plugins": ["./plugins/plugin-myPlugin"] }
第一種經過 npm 包的方式通常是插件功能已經完善和穩定後使用,第二種方式通常在開發階段,本地調試時使用。
編寫插件
插件實際上就是在處理 AST 抽象語法樹,因此編寫插件只須要作到下面三點:
好像少了生成 AST 對象和生成源碼的步驟,不急,後面會講。說一千道一萬不如一個例子來的實在,下面實現一個預計算(在編譯階段將表達式計算出來)的插件:
const result = 1 + 2;
轉換成:
const result = 3;
在寫插件前你須要明確轉換先後的 AST 長什麼樣子,就好像整容同樣,你總得選個參考吧。 AST explorer 你值得擁有。
轉換前:
轉換後:
找到差異,而後就到了用代碼來解決問題的時候了
let babel = require('@babel/core'); let t = require('babel-types'); let preCalculator={ visitor: { BinaryExpression(path) { let node = path.node; let left = node.left; let operator = node.operator; let right = node.right; if (!isNaN(left.value) && !isNaN(right.value)) { let result = eval(left.value + operator + right.value); //生成新節點,而後替換原先的節點 path.replaceWith(t.numericLiteral(result)); //遞歸處理 若是當前節點的父節點配型仍是表達式 if (path.parent && path.parent.type == 'BinaryExpression') { preCalculator.visitor.BinaryExpression.call(null,path.parentPath); } } } } } const result = babel.transform('const sum = 1+2+3',{ plugins:[ preCalculator ] });
上面這段代碼,Babel 在編譯的時候會深度遍歷 AST 對象的每個節點,採用訪問者的模式,每一個節點都會去訪問插件定義的方法,若是類型和方法中定義的類型匹配上了,就進入該方法修改節點中對應屬性。在節點遍歷完成後,新的 AST 對象也就生成了。babel-types 提供 AST 樹節點類型對象。
上面這樣寫只是爲了咱們開發測試方便,其實最終的完總體是下面這樣的:
const types = require('babel-types'); const visitor = { BinaryExpression(path) {//須要處理的節點路徑 let node=path.node; let left=node.left; let operator=node.operator; let right=node.right; if (!isNaN(left.value) && !isNaN(right.value)) { let result=eval(left.value+operator+right.value); path.replaceWith(t.numericLiteral(result)); if (path.parent&& path.parent.type == 'BinaryExpression') { preCalculator.visitor.BinaryExpression.call(null,path.parentPath); } } } } module.exports = function(babel){ return { visitor } }
咱們在插件中只須要修改匹配上的 AST 屬性,不須要關注源碼到 AST 以及新 AST 到源碼的過程,這些都是 Babel 去幹的事,咱們幹好本身的活就行了,其餘的交給 babel。這也就解釋了我上面的步驟中爲嘛沒有 AST 的生成和源碼的生成,那就不是咱們在插件中乾的事兒。
預設就是一堆插件(Plugin)的組合,從而達到某種轉譯的能力,就好比 react 中使用到的 @babel/preset-react ,它就是下面幾種插件的組合。
固然咱們也能夠手動的在 plugins 中配置一系列的 plugin 來達到目的,就像這樣:
{ "plugins":["@babel/plugin-syntax-jsx","@babel/plugin-transform-react-jsx","@babel/plugin-transform-react-display-name"] }
可是這樣一方面顯得不那麼優雅,另外一方面增長了使用者的使用難度。若是直接使用預設就清新脫俗多了~
{ "presets":["@babel/preset-react"] }
前面提到插件的執行順序是從左往右,而預設的執行順序剛好反其道行之,它是從右往左
{ "presets": [ "a", "b", "c" ] }
它的執行順序是 c、b、a,是否是有點奇怪,這主要是爲了確保向後兼容,由於大多數用戶將 "es2015" 放在 "stage-0" 以前。
這種場景通常不多,在這個拿來主義的時代,插件咱們都不多寫,就更別說自定義預設了。不過前面插件咱們都說了怎麼寫了,預設咱也不能冷落她呀。
前面提到預設就是已有插件的組合,主要就是爲了不使用者配置過多的插件,經過預設把插件收斂起來,其實寫起來特別簡單,前提是你已經肯定好要用哪些插件了。
import { declare } from "@babel/helper-plugin-utils"; import pluginA from "myPluginA"; import pluginB from "myPluginB" export default declare((api, opts) => { const pragma = opts.pragma; return { plugins: [ [ pluginA, {pragma}//插件傳參 ], pluginB ] }; });
其實就是把 Babel 配置中的 plugins 配置放到 presets 中了,實質上仍是在配置 Plugins,只是寫 Presets 的人幫咱們配置好了。
@babel/preset-stage-xxx 是 ES 在不一樣階段語法提案的轉碼規則而產生的預設,隨着被批准爲 ES 新版本的組成部分而進行相應的改變(例如 ES6/ES2015)。
提案分爲如下幾個階段:
preset-es2015 是僅包含 ES6 功能的 Babel 預設。
實際上在 Babel7 出來後上面提到的這些預設 stage-x,preset-es2015 均可以廢棄了,由於 @babel/preset-env 出來一統江湖了。
@babel/preset-env
前面兩個預設是從 ES 標準的維度來肯定轉碼規則的,而 @babel/preset-env 是根據瀏覽器的不一樣版本中缺失的功能肯定代碼轉換規則的,在配置的時候咱們只須要配置須要支持的瀏覽器版本就行了,@babel/preset-env 會根據目標瀏覽器生成對應的插件列表而後進行編譯:
{ "presets": [ ["env", { "targets": { "browsers": ["last 10 versions", "ie >= 9"] } }], ], ... }
在默認狀況下 @babel/preset-env 支持將 JS 目前最新的語法轉成 ES5,但須要注意的是,若是你代碼中用到了尚未成爲 JS 標準的語法,該語法暫時還處於 stage 階段,這個時候仍是須要安裝對應的 stage 預設,否則編譯會報錯。
{ "presets": [ ["env", { "targets": { "browsers": ["last 10 versions", "ie >= 9"] } }], ], "stage-0" }
雖然能夠採用默認配置,但若是不須要照顧全部的瀏覽器,仍是建議你配置目標瀏覽器和環境,這樣能夠保證編譯後的代碼體積足夠小,由於在有的版本瀏覽器中,新語法自己就能執行,不須要編譯。@babel/preset-env 在默認狀況下和 preset-stage-x 同樣只編譯語法,不會對新方法和新的原生對象進行轉譯,例如:
const arrFun = ()=>{} const arr = [1,2,3] console.log(arr.includes(1))
轉換後
"use strict"; var arrFun = function arrFun() {}; var arr = [1, 2, 3]; console.log(arr.includes(1));
箭頭函數被轉換了,可是 Array.includes 方法,並無被處理,這個時候要是程序跑在低版本的瀏覽器上,就會出現 includes is not function
的錯誤。這個時候就須要 polyfill 閃亮登場了。
polyfill
的翻譯過來就是墊片,墊片就是墊平不一樣瀏覽器環境的差別,讓你們都同樣。
@babel/polyfill
@babel/polyfill
模塊能夠模擬完整的 ES5 環境。
安裝:
npm install --save @babel/polyfill
注意 @babel/polyfill 不是在 Babel 配置文件中配置,而是在咱們的代碼中引入。
import '@babel/polyfill'; const arrFun = ()=>{} const arr = [1,2,3] console.log(arr.includes(1)) Promise.resolve(true)
編譯後:
require("@babel/polyfill"); var arrFun = function arrFun() {}; var arr = [1, 2, 3]; console.log(arr.includes(1)); Promise.resolve(true);
這樣在低版本的瀏覽器中也能正常運行了。
不知道你們有沒有發現一個問題,這裏是require("@babel/polyfill")
將整個 @babel/polyfill 加載進來了,可是在這裏咱們須要處理 Array.includes 和 Promise 就行了,若是這樣就會致使咱們最終打出來的包體積變大,顯然不是一個最優解。要是能按需加載就行了。其實 Babel 早就爲咱們想好了。
useBuiltIns
回過頭來再說 @babel/preset-env,他出現的目的就是實現民族大統一,連 stage-x 都幹掉了,又怎麼會漏掉 Polyfill 這一功能,在 @babel/preset-env 的配置項中提供了 useBuiltIns 這一參數,只要在使用 @babel/preset-env 的時候帶上他,Babel 在編譯的時候就會自動進行 Polyfill ,再也不須要手動的在代碼中引入@babel/polyfill 了,同時還能作到按需加載
{ "presets": [ "@babel/preset-flow", [ "@babel/preset-env", { "targets": { "node": "8.10" }, "corejs": "3", // 聲明 corejs 版本 "useBuiltIns": "usage" } ] ] }
注意,這裏須要配置一下 corejs 的版本號,不配置編譯的時候會報警告。講都講到這裏了就再順便提一嘴 useBuiltIns 的機構參數:
import ' @babel/polyfill'
編譯後:
"use strict"; require("core-js/modules/es.array.includes"); require("core-js/modules/es.object.to-string"); require("core-js/modules/es.promise"); var arrFun = function arrFun() {}; var arr = [1, 2, 3]; console.log(arr.includes(1)); Promise.resolve(true);
這個時候咱們再借助 Webpack 編譯後,產出的代碼體積會大大減少。
說完了上面這些你覺得我就說完了嗎?
其實 Babel 在編譯中會使用一些輔助函數,好比:
class Person { constructor(){} say(word){ console.log(":::",word) } }
編譯後:
"use strict"; require("core-js/modules/es.object.define-property"); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } var Person = /*#__PURE__*/ function () { function Person() { _classCallCheck(this, Person); } _createClass(Person, [{ key: "say", value: function say(word) { console.log(":::", word); } }]); return Person; }();
這些方法會被 inject
到每一個文件中,無法作到複用,這樣也會致使打包體積的增長。
沒事兒,逢山開路遇水搭橋,是時候讓@babel/plugin-transform-runtime
登場了。
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime 可讓 Babel 在編譯中複用輔助函數,從而減少打包文件體積,不信你看:
npm install --save-dev @babel/plugin-transform-runtime npm install --save @babel/runtime
順便說一下,這一對 CP 要同時出現,如影隨行,因此安裝的時候你就一塊兒裝上吧~
配置 Babel:
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 } ] ], "plugins": [ "@babel/plugin-transform-runtime" ] }
結果:
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var Person = /*#__PURE__*/ function () { function Person() { (0, _classCallCheck2["default"])(this, Person); } (0, _createClass2["default"])(Person, [{ key: "say", value: function say(word) { console.log(":::", word); } }]); return Person; }();
這些用到的輔助函數都從 @babel/runtime 中去加載,這樣就能夠作到代碼複用了。
<img src="https://zcy-cdn.oss-cn-shanghai.aliyuncs.com/f2e-assets/ca10147e-1fca-4431-8197-c942fac395e0.png?x-oss-process=image/quality,Q_75/format,jpg" />
在這個拿來主義的社會,有時候知其然的同時也須要知其因此然。但願這篇關於 Babel 知識的梳理對你有幫助。
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com