近期,在波洞星球的PC官網項目中,咱們採用了新版的 babel7 做爲 ES 語法轉換器。而 babel7 中的一大變動就是對配置文件的加載邏輯進行了改進,然而實際上對於不熟悉 babel 配置邏輯的朋友每每會帶來更多問題。本文就是 babel7 配置文件的中文指南,它是英語渣渣的救星,是給懶人送到口邊的一道美味。若有錯誤 概不負責 歡迎指正。前端
babel7 從 2018年3月開始進入 alpha 階段,時隔5個月直到 2018年8月份 release 第一個版本,目前的最新版是2019年2月26號發佈的 7.3.4. 時光如梭,在這美好的 9012 年,ES2019 都快要發佈了的時刻,我想: 是時候用一用 babel7 了。vue
本文不是 babel7 的升級教程,而是對 babel7 的新變化和配置邏輯的一點心得。babel7 對monorepo 結構項目的優化剛好符合咱們目前項目架構的預期,這簡化了咱們配置的複雜度,但其難以理解的配置加載邏輯,卻讓我踩了很多坑,這也正是本文的來源。node
在開始講 babel7 的配置邏輯以前,咱們先從如下幾個方面來囉嗦幾句 babel7 所作的變動及其邏輯意義。react
在歷史上(babel6)的時代,人們一般使用 babel 提供的 preset-stage 預設來體驗 ES6 以後的處於建議階段的語法特性。例如作以下的 babel 配置:webpack
"presets": ["es2015", "react", "stage-0"]
其中,es2015 預設會包含 ES6 標準中全部語法特性;stage-0預設會包含當前(安裝該預設npm包的時刻) 的 ES 語法進展中的 stage 0到3的特性(數字小的包含數字大的)。但事實上 babel 官方這樣提供 stage 預設,會有很多問題git
例如:github
現在,babel 官方認爲,把不穩定的 stage0-3 做爲一種預設是不太合理的,所以廢棄了 stage 預設,轉而讓用戶本身選擇使用哪一個 proposal 特性的插件,這將帶來更多的明確性(用戶無須理解 stage,本身選的插件,本身便能明確的知道代碼中可使用哪一個特性)。全部建議特性的插件,都改變了命名規範,即相似 @babel/plugin-proposal-function-bind
這樣的命名方式來代表這是個 proposal 階段特性。web
對於正經的 ES 標準特性,babel從6開始就建議使用 babel-preset-env 這個能根據環境進行自動配置的預設。到了 babel7,咱們就能夠徹底告別這幾個歷史預設了: preset-es2015/es2016/es2017/latestnpm
爲何 preset-env 要更好呢?json
我認爲,對於開發者而言,關注目標用戶平臺(兼容哪些瀏覽器)要比關注 "編譯爲哪份ES標準" 要更易理解。把選擇編譯插件的事情交給 preset-env 就行了。它會根據 compat table 和你設置的目標用戶平臺選擇正確的插件。
跟 stage 預設的結局同樣,對於處於建議階段的特性,polyfill裏面也移除了對他們的支持。
之前的 babel-polyfill 是這麼實現的:
import "core-js/shim"; // included < Stage 4 proposals import "regenerator-runtime/runtime"
如今的 @babel/polyfill 就直接引入 core-js v2 的屬於ES正式標準的模塊。這意味着,若是你須要使用處於 proposal 階段的語法特性,你須要手工 import core-js 中的對應模塊。
從 babel7 開始,全部的官方插件和主要模塊,都放在了 @babel 的命名空間下。從而能夠避免在 npm 倉庫中 babel 相關名稱被搶注的問題。有必要說一下的,好比 @babel/node @babel/core @babel/clil @babel/preset-env
之前的 babel-transform-runtime
是包含了 helpers 和 polyfill。而如今的 @babel/runtime
只包含 helper,若是須要 polyfill,則需主動安裝 core-js 的 runtime版本 @babel/runtime-corejs2
。並在 @babel/plugin-transform-runtime
的插件中作配置。
這是本文的重點,先來看一段 babel7 對配置的變動說明
Babel has had issues previously with handling node_modules, symlinks, and monorepos. We've made some changes to account for this: Babel will stop lookup at the package.json boundary instead of looking up the chain. For monorepo's we have added a new babel.config.js file that centralizes our config across all the packages (alternatively you could make a config per package). In 7.1, we've introduced a rootMode option for further lookup if necessary.
段落的意思大概有這麼幾點:
除此以外,babel7 還有一個特性是:
然而,對上面的解釋,你可能: 每一個字都認識,連在一塊兒殊不知道在說什麼。下面咱們來剖析一下
爲了理解 babel7 的配置邏輯,咱們就以 babel7 真正所解決的痛點 [monorepo 類型的項目] 爲例來剖析。在此以前,咱們須要預先肯定幾個概念。
monorepo。這是個自造詞。按個人理解,它的含義是說 單個大項目可是包含多個子項目
的含義。若是仍是不能理解的話,就把 項目 二字 換成 npm模塊包
(以package.json文件做爲分界線)。即 單個npm包中又包含多個子npm包
的項目。
例如,波洞的 PC 版採用的是 Node.js 做爲前端接入層的方式,在咱們的項目結構組織上,是這樣的:
|- backend |-package.json |- frontend |-package.json |- node_modules |- config.js |- babel.config.js |- package.json
這就是典型的 monorepo 結構。
懂了幾種配置文件的概念和做用範圍以後,咱們就能夠來根據文檔和代碼測試結果來精確描述 babel7 的配置規則。這裏咱們直接以 monorepo 類型項目爲例來講,由於普通項目會更簡單。
下文中可能用到的名詞解釋:
咱們用 package 來代指一個具備獨立 package.json 的項目,如上面案例中的 frontend 能夠稱做一個 package,backend也能夠稱做一個package; 咱們用 相對配置 這個名詞來表達所謂的 .babelrc 和 .babelrc.js,用全局配置來代指 babel.config.js這份配置
對monorepo類型項目,babel7 的處理邏輯是:
【全局配置】全局配置 babel.config.js 裏的配置默認對整個項目生效,包括node_modules。除非經過 exclude 配置進行剔除。
【全局配置】全局配置中若是沒有配置 babelrcRoots 字段,那麼babel 默認狀況下不會加載任何子package中的相對配置(如.babelrc文件)。除非在全局配置中經過 babelrcRoots 字段進行配置。
【全局配置】babel 全局配置文件所在的位置就決定了你的項目根目錄在哪裏,默認就是執行babel的當前工做目錄,例如上面的例子,你在根目錄執行babel,babel才能找到babel.config.js,從而肯定該monorepo的根目錄,進而將配置對整個項目生效
【相對配置】相對配置可被加載的前提是在 babel.config.js 中配置了 babelrcRoots. 如 babelrcRoots: ['.', './frontend'],這表示要對當前根目錄和frontend這個子package開啓 .babelrc 的加載。(注意: 項目根目錄除了能夠擁有一個 babel.config.js,同時也能夠擁有一個 .babelrc 相對配置)
【相對配置】相對配置加載的邊界是當前package的最頂層。假設上文案例中要編譯 frontend/src/index.js 那麼,該文件編譯時能夠加載 frontend 下的 .babelrc 配置,但沒法向上檢索總項目根目錄下的 .babelrc
仍是以上面的代碼結構爲例。
|- backend |-package.json |- frontend |-package.json |- node_modules |- config.js |- babel.config.js |- package.json
該案例中,咱們思考發現,咱們須要利用 babel7 的全局配置能力
。緣由在於,monrepo 中存在多個 子 package。因爲 babel7 默認檢索 babelrc 的邊界是 當前package。所以每一個package中撰寫的babelrc只會對當前package生效,這會致使咱們的frontend中依賴根目錄的config.js時沒法獲得正確的編譯;另外一個問題是: frontend和backend中的相同的babel配置部分沒法共享 存在必定冗餘。爲此,咱們須要在項目根目錄設置一個 babel.config.js的配置,用它再配合babelrc來作babel配置的共享和融合。
可是,問題很快來了:當工做目錄不在根目錄時,沒法加載到全局配置
。咱們的前端編譯腳本一般放置在 frontend目錄下,(咱們執行編譯的工做目錄是在 frontend 中),此時 babel build 行爲的 工做目錄 即是 frontend. 因爲 babel 默認只在當前目錄尋找 babel.config.js 這個全局配置,所以會致使沒法找到根目錄的 babel.config.js,這樣咱們所設想的整個項目的全局配置就沒法生效。 幸虧,babel7 提供了 rootMode 選項,能夠將它指定爲 "upward", 這樣babel 會自動向上尋找全局配置,並肯定項目的根目錄位置。
設置方法:
CLI: babel --rootMode=upward
webpack: 在 babel-loader 的配置上設置 rootMode: 'upward'
如今,全局配置有了,咱們能夠在裏面配置 babel 轉譯規則,它能夠對全項目生效,frontend下的 vue.js 編譯天然沒有問題了。
不過,假設咱們 backend 項目中也要使用 babel 轉譯(目前咱們實際在 backend 中並無使用,由於咱們認爲只圖esmodule而多加一層編譯得不償失),那麼必然 backend 與 frontend 中的編譯配置是不一樣的
,frontend 須要加載 vue 的 jsx 插件和polyfill (useBuiltIns: usage,modules: false),而backend只須要轉譯基本模塊語法(modules: true, useBuiltIns: false)。該場景的解決方案即是,爲每一個子 package 提供獨立的 .babelrc 相對配置,在全局 babel.config.js 中設置共用的配置。此時項目組織結構以下:
|- backend |- .babelrc.js |-package.json |- frontend |- .babelrc.js |-package.json |- node_modules |- config.js |- .babelrc.js // 這份配置在本場景下不須要(若是根目錄下的代碼有區別於子package的babel配置,則須要使用) |- babel.config.js |- package.json
根目錄的 babel.conig.js 配置應該以下:
const presets = [ // 根、frontend、backend 的公共預設 ] const plugins = [ // 根、frontend、backend 的公共插件 ] module.exports = { presets, plugins, babelrcRoots: ['.', './frontend', './backend'] // 容許這兩個子 package 加載 babelrc 相對配置 }
覺得此時已經高枕無憂了?navie,因爲咱們前端 Vue.js 採用 webpack 打包。實際開發過程當中發現,這種配置會形成 webpack 打包模塊時出現故障,故障緣由在於:同一個模塊中錯誤混用 esmodule 和 commonjs 語法會形成 webpack故障
。
前文講到 全局配置 global.config.js 會做用到 整個項目
,甚至包括 node_modules。所以babel編譯時會同時編譯 node_modules 下的模塊,雖然模塊做者不可能在一個js文件中混用不一樣模塊語法,但他們做爲釋出包 一般是commonjs的模塊語法。 而preset-env預設在編譯時會經過 usage
方式 默認注入import語法的 polyfill
Since Babel defaults to treating files are ES modules, generally these plugins/presets will insert import statements
這即是蛋疼的來源:babel加載過的node_modules模塊會變成 同一個js文件裏既有commonjs語法又有esmodule語法。
解決方案:不要對 node_modules 下的模塊採用babel編譯。咱們須要在 babel.config.js 配置中增長選項:
exclude: /node_modules/
至此,咱們的 monorepo 項目就可使用一份 全局配置+兩份相對配置,實現分別對 前端和後端 進行合理的ES6+語法的編譯了。這是咱們配置工程師的一小步,可是前端走向將來語法的一大步。
總結 babel7 的配置加載邏輯以下:
待編譯文件
生效的配置,子package若想加載.babelrc是須要babel配置babelrcRoots才能夠(父package自身的babelrc是默承認用的)。