babel這東西,須要配置時搞不清楚怎麼弄,弄清楚後配置好了就很長時間不會去觸碰。等再須要配置時又忘了當初怎麼弄的了。
回頭看以前寫的babel學習筆記,發現本身仍是有地方沒搞清楚,有必要系統性的對babel進行學習和整理。前端
babel主要做用就是將某些低版本容器(主要是瀏覽器,主要是IE...)不支持的js語法或api,用該容器支持的語法或api重寫,使開發者可使用更前沿的方式愉快的編寫代碼。node
但實際上更準確點說,是一堆插件在作代碼的轉換,babel自己是個容器,負責代碼解析、轉換抽象語法樹,而後經過各類插件作代碼轉換,最後根據轉換後的抽象語法樹生成最終的代碼。這個過程之後再細說,這裏想說的就是插件對於babel的做用,而咱們使用者可能比較關心的,也就是在作代碼轉換時,會用到哪些插件。react
(截止寫這篇時,babel官方最新版本爲7.1.0,如下簡稱babel7,demo均以7.1.0版本爲例)android
babel7主要就是兩個包,@babel/cli和@babel/core,cli用於執行命令行,core則是babel用於解析、轉換、代碼生成的核心包。在項目下執行如下命令便可完成安裝。webpack
npm i --save-dev @babel/cli @babel/core
有了這兩個包之後,就能夠對指定文件執行babel命令了。
好比項目目錄結構以下:ios
├── node_modules ├── src │ └── index.js // 源文件 ├── dist // 轉換後文件放置路徑 └── package.json
src/目錄文件index.js代碼以下:git
// src/index.js const fn = () => { Array.isArray([1, 2, 3]); };
如今在項目根目錄下執行程序員
npx babel src/index -d dist/
便可在dist目錄下生成同名文件index.js,而裏面代碼與src/index.js中的代碼徹底同樣。
之因此代碼徹底同樣,其實就是上面所說的,babel在沒有使用任何插件時,就是把代碼變成抽象語法樹,再把抽象語法樹原封不動的變成代碼,中間沒有作任何處理,固然代碼也就原樣還原回來了。下面,讓咱們來給babel加點兒料~es6
const和箭頭函數是es6的語法,相應的,@babel/plugin-transform-block-scoping插件用於轉換const和let,@babel/plugin-transform-arrow-functions插件用於轉換箭頭函數。安裝完這兩個插件以後,分別執行github
npx babel src/index -d dist/ --plugins=@babel/plugin-transform-block-scoping
和
npx babel src/index -d dist/ --plugins=@babel/plugin-transform-arrow-functions
分別查看dist/index.js確認下轉換後的結果,相信馬上就能明白每一個插件都幹了些啥了~(兩個插件一塊兒用的話,用「,」隔開:npx babel src/index -d dist/ --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions)
babel全部的語法轉換,就是靠一個個plugins完成的。完整的plugins列表能夠查看https://babeljs.io/docs/en/pl...
掃完一眼plugins列表,估計和我同樣一臉懵逼,這麼多插件誰能記得住用獲得哪些啊。babel能幫忙整理下打個包給我用麼?固然能夠,presets就是用來幹這事兒的。
一個特定的preset能夠簡單理解爲是一組特定的plugins的集合。不一樣的presets包含着不一樣的plugins,固然適用的場景也就各不相同了。好比@babel/preset-react包含了寫react須要用到的@babel/plugin-syntax-jsx,@babel/plugin-transform-react-jsx,@babel/plugin-transform-react-display-name等插件;@babel/preset-es2017包含了@babel/plugin-transform-async-to-generator插件。
而最爲經常使用,也是被官網推薦的,是@babel/preset-env。默認狀況下,全部已被歸入規範的語法(ES2015, ES2016, ES2017, ES2018, Modules)所須要使用的plugins都包含在env這個preset中。
仍是以上面例子來講
先安裝@babel/preset-env
npm i --save-dev @babel/preset-env
執行
npx babel src/index.js -d dist/ --presets=@babel/preset-env
生成的dist/index文件以下:
"use strict"; var fn = function fn() { Array.isArray([1, 2, 3]); };
主體部分與同時使用兩個plugins是徹底同樣的。實際上,presets能夠理解爲就是把其包含的plugins依次執行一遍。
固然env這個presets不是萬能的,其只包含了規範中的語法轉換,還沒有被歸入規範的處於各個階段的提案,好比目前處於stage-2(draft)階段的裝飾器語法,光是用presets是不會幫咱們轉好的,還得單獨再使用@babel/plugin-proposal-decorators這個專門用於轉換裝飾器代碼的插件。
值得一提的是,babel7明確指出用stage-x命名的presets已被棄用。具體緣由見https://babeljs.io/blog/2018/...。
若是但願和以前同樣使用處於各階段的提案功能,建議直接經過引入相應的plugins:
{ plugins: [ // Stage 0 "@babel/plugin-proposal-function-bind", // Stage 1 "@babel/plugin-proposal-export-default-from", "@babel/plugin-proposal-logical-assignment-operators", ["@babel/plugin-proposal-optional-chaining", { loose: false }], ["@babel/plugin-proposal-pipeline-operator", { proposal: "minimal" }], ["@babel/plugin-proposal-nullish-coalescing-operator", { loose: false }], "@babel/plugin-proposal-do-expressions", // Stage 2 ["@babel/plugin-proposal-decorators", { legacy: true }], "@babel/plugin-proposal-function-sent", "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-throw-expressions", // Stage 3 "@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-import-meta", ["@babel/plugin-proposal-class-properties", { loose: false }], "@babel/plugin-proposal-json-strings", ], }
關於stage-x各表明什麼含義,見The TC39 Process
另外一個問題,又是presets,又是plugins,--plugins和--presets後面要跟一堆東西,用命令執行babel未免也太費勁了。
babel官網提供了四種方式經過文件維護配置項,實際工做中,根據狀況選擇其一。
babel.config.js
適用場景:以編程方式建立配置;須要編譯編譯node_modules。
// Javascript module.exports = function () { const presets = [ ... ]; const plugins = [ ... ]; return { presets, plugins }; }
.babelrc
適用場景:適用於簡單的靜態配置
// JSON { "presets": [...], "plugins": [...] }
package.json
也能夠將.babelrc中的配置項移至package.json配置文件中
// JSON { "name": "my-package", "version": "1.0.0", "babel": { "presets": [ ... ], "plugins": [ ... ], } }
.babelrc.js
和babel.config.js相似,可使用編程方式建立配置
// Javascript const presets = [ ... ]; const plugins = [ ... ]; module.exports = { presets, plugins };
有了配置文件,上述既須要@babel/preset-env又須要@babel/plugin-proposal-decorators的狀況,babel.config.js文件配置以下:
// Javascript module.exports = function(api) { api.cache(true); const presets = ['@babel/env']; const plugins = [['@babel/proposal-decorators', { legacy: true }]]; return { presets, plugins }; };
這邊本身試了下,若是不寫api.cache(true),會報一個很奇怪的錯:
Error: Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured for various types of caching, using the first param of their handler functions
官網上暫時沒有找到爲何必定要執行下api的方法,甚至把
api.cache(true);
改爲
const env = api.env();
均可以免上述報錯,沒法理解。仍是換成.babelrc方式寫配置吧。。。
// JSON { "presets": ["@babel/env"], "plugins": [["@babel/proposal-decorators", { "legacy": true }]] }
驗證一下,在src/decorator.js寫一小段包含裝飾器的代碼:
// src/decorator.js const decro = (val) => (_class) => new _class(val); @decro("abc") class Test { constructor(val){ this.val = val } log() { console.log(this.val); } } Test.log(); // "abc"
完了在項目根目錄執行中執行一下
npx babel src/ -d dist/
在dist/下就能夠看到轉換後的decorator.js文件。
執行
node dist/decorator.js
便可看到結果"abc"啦~
注意到一件事情,plugins是個二維數組,而presets是個一位數組。這是babel配置文件中對指定preset或者plugin添加參數的方式。
babel提供的plugin和preset都容許傳入一些參數來達到不一樣的目的。
如下是引用presetA和pluginA的三種形式,結果徹底同樣。
{ "presets": [ "presetA", ["presetA"], ["presetA", {}] ], "plugins": [ "pluginA", ["pluginA"], ["pluginA", {}], ] }
意思就是說,單個preset或者plugin若是不須要添加參數,那麼直接用string就能夠了;若是須要添加參數,那麼須要將單個preset或者plugin放入數組中,第一項爲string表示preset或者plugin的名字,第二項爲object用於指定參數。
先舉個後面會用到的例子,具體在寫@babel/preset-env的時候細說
{ "presets": [ [ "@babel/env", { "targets": { "ie": "9" }, "useBuiltIns": "usage" } ] ] }
如今,回過頭來再看下src/index.js中的例子。最後轉換出來的代碼中,Array.isArray這個靜態方法在低版本IE瀏覽器中,依然是沒法執行的。
我理解爲,babel的插件專一於對語法作轉換,而API的調用並不是什麼新鮮的語法,這部分並不屬於babel插件的管轄範圍。正常來講,讓不識別Array.isArray的瀏覽器運行這個方法,最簡單的方法就是用瀏覽器能識別的方式爲Array寫一個靜態方法isArray。
Array.isArray = function(arg) { var toString = {}.toString; return toString.call(arg).slice(8, -1) == 'Array'; }
@babel/polyfill就是幹這活兒的。
首先安裝@babel/polyfill
npm i -S @babel/polyfill
而後在項目入口src/index.js開始時引入一下
// src/index.js import "@babel/polyfill"; const fn = () => { Array.isArray([1, 2, 3]); };
簡單粗暴,搞定收工。
等下等下,還沒完呢,不想知道@babel/polyfill這裏面都有些啥東西麼?若是你會使用webpack-bundle-analyzer作打包分析,會發現多出的core-js這個包有200多kb,這個就是@babel/polyfill的依賴包,(除了core-js外其實還有個regenerator-runtime用來處理async function的)。爲了實現Array.isArray,要增長這麼大致積的包,有沒有問題呢?
固然有問題,對於咱們這種有追求的程序員來講,能把包縮小一點是一點,關乎用戶體驗的事情能優化一點兒是一點兒~
好,怎麼辦?
找到@babel/polyfill中處理Array.isArray的包,單獨引用就行了唄~
import "core-js/modules/es6.array.is-array"; const fn = () => { Array.isArray([1, 2, 3]); };
搞定,包瞬間縮小到幾k。可問題又來了,這只是一個Array.isArray,那麼多新的String的API,Object的API等等,難道須要本身一個個把須要的包單獨引用進本身的項目裏去嗎?
No~ 見識一下@babel/preset-env的強大之處吧~
@babel/preset-env這個preset比較特殊,他不只僅是包含了衆多plugins,並且還提供了一些有用的配置項,動態引入依賴包,只爲跟小體積的包~ 來看下主要的兩個配置項 useBuiltIns 和 targets
useBuiltIns
該配置項有三個屬性: "usage", "entry", "false"。默認爲false。
須要事先說明一下,這個屬性若是是"usage"或"entry"時,必需要安裝@babel/polyfill,由於在轉換出來的代碼中,會引入core-js下面的包。
entry
要使用entry屬性,必須在項目入口處引入一次@babel/polyfill。而後,babel在作代碼轉換的時候,會把
import @babel/polyfill
轉成
require("core-js/modules/es6.array.copy-within"); require("core-js/modules/es6.array.every"); require("core-js/modules/es6.array.fill"); ...
問題來了,只是把一個大包拆成一個個小包,並不會減少體積啊。嗯,單獨使用"useBuiltIns": "entry"好像是沒什麼用,但結合後面要說的targets配置項就有用了,後面再說。
usage(experimental)
雖然官網標註了是個處於實驗階段的功能,但親測很強大。他能經過識別全部代碼中使用到的高級API,自動在該文件頭部注入相應的polyfill包。
使用這個屬性的時候,是不須要在項目中手動引入@babel/polyfill的。babel自動檢測哪一個文件要用到哪些包,就在那個文件頭部引入那些包。
搬個官網的例子:
// a.js var a = new Promise(); // b.js var b = new Map(); // .babelrc { "presets": [["@babel/env", {"useBuiltIns": "usage"}]] }
轉化後
// a.js import "core-js/modules/es6.promise"; var a = new Promise(); // b.js import "core-js/modules/es6.map"; var b = new Map();
一點點冗餘代碼都沒有了~
targets
targets配置項用於指定須要支持的環境,對於前端開發來講,主要指的就是瀏覽器版本。(targets還能夠指定node、android、ios等其餘環境)
常用的方式也很簡單,好比老闆說,咱們只須要支持chrome64,上例中所需的完整配置以下:
{ "presets": [ [ "@babel/env", { "useBuiltIns": "usage", "targets": { "chrome": "64" } } ] ], "plugins": [['@babel/proposal-decorators', { "legacy": true }]] }
直接看執行babel的運行結果,會發現轉換後的代碼基本和原來的同樣。也就是說,設置完targets後,babel會先判斷一下指定的環境已經支持了多少種新語法和API,對於已經支持的部分,就不會再轉換代碼或者引入相應的包了。
對於只要求在較高版本的瀏覽器運行的項目,targets + useBuiltIns兩個配置項就能將轉換後的代碼體積縮減到最小。不過若是要求支持IE9,那設不設置targets影響就不大了~
關於@babel/preset-env更爲詳細的文檔,固然是官網啦~https://babeljs.io/docs/en/ba...
補充一點,配置文件中,presets和plugins都是容許設置多個的,某些plugin對執行順序很敏感,這也就對配置中設置presets和plugins的順序有要求了。
babel執行presets和plugins的順序以下:
到這兒,babel7的基本使用方法就介紹完了。
不過在實際項目中,咱們如今通常不直接經過babel命令對代碼作轉換,webpack的babel-loader會在打包時幫助咱們作這件事情。
babel 7.x對應的babel-loader版本爲8.x。以前的babel 6.x對應的babel-loader版本爲7.x。
關於webpack的loader超出了本篇的範圍,這裏就很少加贅述了。有須要進一步瞭解的能夠看https://webpack.js.org/concep...。
這裏只簡單介紹一下babel-loader的配置。在webpack配置中可能常常會見到相似下面這段:
module: { rules: [ { test: /\.js|jsx$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }
若是你已經清楚.babelrc文件是如何配置的,那麼對上面的options必定不會陌生了。 webpack執行打包時,優先讀取options中的配置,若是沒有設置options屬性,再從package.json同級目錄中找babel配置文件。經過配置options,或者經過babel配置文件,兩種方式選其一就能夠了。