聽說提升技術最快的方法是輸入,一直覺得自己文筆爛,技術菜,再加上惰性大,所以都是隻看不寫,但是一直這樣下去能進步就見鬼了,所以做了個決定,給自己定義一個小目標,準備每週寫一遍技術文章,管它寫的好還是亂,膚淺還是深入,有人看還是沒人看,開始寫就是進步,堅持寫就是一直進步。
這是我第一次在掘金上釋出文章,如果有錯誤歡迎大家指正,在此也鼓勵下大家學會輸出,知道是一回事,能寫出來是另外一回事,能深入淺出的寫出來那啥都不是事兒了。
用vue開發專案一年多了,但是一直沒因為專案中使用了babel, 這篇文章簡單整理下自己所知道的關於babel的知識,文章和程式碼gihub上也有,歡迎大家可以去github克隆專案an-article-to-know-bable。 最後有錯誤的地方歡迎大家指出來,共同學習和進步,謝謝!
目錄
- 一 babel是什麼
- 二 Babel的工作原理
- 三 使用方法
- .babelrc
- presets
- env
- 與構建工具整合
- 四 babel包介紹
- 六 bable7.x
一 babel是什麼
Babel is a JavaScript compiler
簡單來說就是把 JavaScript 中 es2015+ 的新語法轉化為 es5,讓低端執行環境 (如瀏覽器和 node ) 能夠認識並執行
二 Babel的工作原理
babel是一個編譯器compiler,但是叫轉譯器transpiler更準確,因為它只是把同種語言的高版本規則翻譯成低版本規則,而不像編譯器那樣,輸出的是另一種更低階的語言程式碼。
Babel的編譯過程跟絕大多數其他語言的編譯器大致同理,分為三個階段:
- 解析(parse):使用babylon將程式碼字串解析成抽象語法樹ast
- 變換(transform):使用babel-traverse, 利用配置好的 plugins/presets遍歷ast,更新節點,轉變為新的ast
- 再建(generate):使用babel-generator根據變換後的抽象語法樹再生成程式碼字串
babel編譯流程圖
既然要講編譯,就必須要了解一下抽象語法樹了 AST Explorer是一個線上實時編譯工具,可以選擇不同的語言和對應的編譯器,將原始碼解析成抽象語法樹, 幫助我們更直觀的認識AST的結構,編譯器選擇babylon6,
在左側輸入下面的的程式碼function abs(number) {
if (number >= 0) {
return number;
} else {number;
}
}
複製程式碼
在右側生成了對應的Ast, 下面的結構圖可以更加清楚地看清楚AST樹的節點資訊
可以看到AST就是由一個個節點構成,每個節點都是原始碼語法的一個標籤。解析這一步是如何生成抽象語法樹的, 給大家甩一個知乎上的一篇文章Babel是如何讀懂JS程式碼的裡面有講解析器解析的過程, 對編譯器有興趣的小夥伴可以去研究一下github上一個輕量級的編譯器實現the-super-tiny-compiler
此外,以下兩點請注意:
- babel本身不具備轉碼能力,轉碼都是通過外掛完成的,如果不新增外掛,那麼經過babel後的程式碼和原始碼是一樣的
let x = (a) => a*2;
複製程式碼
- babel只是轉譯新標準引入的語法,比如ES6的箭頭函式轉譯成ES5的函式;而新標準引入的新的原生物件(Symbol),部分原生物件新增的原型方法(陣列的include的方法),新增的API等(如Proxy、Set等),這些babel是不會轉譯的。需要使用者自行引入polyfill(後面會介紹)來解決。
let obj = Object.assign({},{a:1});
複製程式碼
三 使用方法
在專案中使用babel, 首先需要在根目錄下新建babel的配置檔案.babelrc,使用Babel的第一步,就是配置這個檔案。
{
"presets": [],
"plugins": []
}
複製程式碼
簡略情況下,plugins和 presets只要列出字串格式的名字即可。但如果某個 presets 或者plugins需要一些配置項(或者說引數),就需要把自己先變成陣列。第一個元素依然是字串,表示自己的名字;第二個元素是一個物件,即配置物件
{"presets": [
// 帶了配置項,自己變成陣列
[
// 第一個元素是preset的名稱
"env",
// 第二個元素是物件,列出該preset的配置項
{
"module": false
}
],
// 不帶配置項,直接列出名字
"stage-2"
]}
複製程式碼
presets和plugins的執行順序 執行順序
- plugins 會執行在 Presets 之前。
- plugins 會從前到後順序執行。
- presets 的順序則 剛好相反(從後向前)。
- presets 的逆向順序主要是為了保證向後相容,因為大多數使用者的編寫順序是 ['es2015', 'stage-0']。這樣必須先執行 + stage-0 才能確保 babel 不報錯。
presets
babel 本身不具有任何轉化功能,它把轉化的功能都分解到一個個外掛裡面, 外掛是在轉換這一階段起作用的。當我們不配置任何外掛時,經過 babel 的程式碼和輸入是相同的。 那麼如何配置外掛呢,比如 es2015 是一套規範,包含如下二十多個轉譯外掛,大家可以簡單瞭解一下
- check-es2015-constants
- transform-es2015-arrow-functions
- transform-es2015-block-scoped-functions
- transform-es2015-block-scoping
- transform-es2015-classes
- transform-es2015-computed-properties
- transform-es2015-destructuring
- transform-es2015-duplicate-keys
- transform-es2015-for-of
- transform-es2015-function-name
- transform-es2015-literals
- transform-es2015-modules-commonjs
- transform-es2015-object-super
- transform-es2015-parameters
- transform-es2015-shorthand-properties
- transform-es2015-spread
- transform-es2015-sticky-regex
- transform-es2015-template-literals
- transform-es2015-typeof-symbol
- transform-es2015-unicode-regex
- transform-regenerator
如果一個個新增然後安裝,就非常麻煩, babel官方就提供了外掛合集, 省去了開發者配置的麻煩,這就叫presets, 分為以下幾種
- 官方
- env
- react
- flow
- minify
- stage-x
- Stage 0 - 稻草人(strawman): 只是一個想法,經過 TC39 成員提出即可。
- Stage 1 - 提案(proposal): 初步嘗試。
- Stage 2 - 初稿(draft): 完成初步規範。
- Stage 3 - 候選(candidate): 完成規範和瀏覽器初步實現。
- es201x, latest 但因為 env 的出現,使得 es2016 和 es2017 都已經廢棄。latest 是 env 的雛形,它是一個每年更新的 preset,目的是包含所有 es201x。但也是因為更加靈活的 env 的出現,已經廢棄。
env
env 的核心目的是通過配置得知目標環境的特點,然後只做必要的轉換。例如目標瀏覽器支援 es2015,那麼 es2015 這個preset 其實是不需要的,於是程式碼就可以小一點(一般轉化後的程式碼總是更長),構建時間也可以縮短一些。 如果不寫任何配置項,env 等價於 latest,也等價於 es2015 + es2016 + es2017 三個相加(不包含 stage-x 中的外掛)。env包含的外掛列表維護在這裡
與構建工具整合
babel一般是整合到構建工具裡面使用的,這時需要安裝構建工具的外掛 (webpack 的 babel-loader, rollup 的 rollup-plugin-babel),以目前使用最頻繁的打包工具的webpack為例 配置檔案.babelrc如下,存放在專案的根目錄下
{
"presets": [
["env", { "modules": false }],
"stage-2"
],
"plugins": ["transform-runtime"]
}
複製程式碼
然後webpack對應的配置
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/,
}
]
}
複製程式碼
四 Babel包介紹
可以去github上看看,babel到底包含了多少包, babel包 按照功能分個類
(一) 核心包
- babel-core:babel轉譯器本身,提供了babel的轉譯API,如babel.transform等,用於對程式碼進行轉譯。像webpack 的babel-loader就是呼叫這些API來完成轉譯過程的。
- babylon:js的詞法解析器
- babel-traverse:用於對AST的遍歷,主要給plugin用
- babel-generator:根據AST生成最終的程式碼
babel-core的使用
var babel = require('babel-core');
// 字串轉碼, transform方法的第一個引數是一個字串,表示需要轉換的程式碼,第二個引數是轉換的配置物件
babel.transform('code', options);
// => { code, map, ast }
// 檔案轉碼(非同步)
babel.transformFile('filename.js', options, function(err, result) {
result; // => { code, map, ast }
});
// 檔案轉碼(同步)
babel.transformFileSync('filename.js', options);
// => { code, map, ast }
// Babel AST轉碼
babel.transformFromAst(ast, code, options);
// => { code, map, ast }
複製程式碼
(二) 功能包
- babel-types:用於檢驗、構建和改變AST樹的節點
- babel-template:輔助函式,用於從字串形式的程式碼來構建AST樹節點
- babel-helpers:一系列預製的babel-template函式,用於提供給一些plugins使用
- babel-code-frames:用於生成錯誤資訊,列印出錯誤點原始碼幀以及指出出錯位置
- babel-plugin-xxx:babel轉譯過程中使用到的外掛,其中babel-plugin-transform-xxx是transform步驟使用的
- babel-preset-xxx:transform階段使用到的一系列的plugin
- babel-polyfill:JS標準新增的原生物件和API的shim,實現上僅僅是core-js和regenerator-runtime兩個包的封裝
- babel-runtime:功能類似babel-polyfill,一般用於library或plugin中,因為它不會汙染全域性作用域
babel-polyfill
Babel includes a polyfill that includes a custom regenerator runtime and core-js.
前面說過,Babel預設只轉換新的JavaScript句法(syntax),而不轉換新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全域性物件,以及一些定義在全域性物件上的靜態方法(比如Object.assign,Array.from, Array.isArray,Array.of), 還有例項方法如Array.prototye.inclueds等,Babel都不會轉碼。 Babel預設不轉碼的API清單可以檢視babel-plugin-transform-runtime模組的definitions.js檔案。 如果想讓這些API和方法執行,必須使用babel-polyfill,為當前環境提供一個墊片。 拿inclueds來說,IE11仍不支援該方法(caniuse檢視相容性),在IE11瀏覽器的控制檯裡面執行下面的程式碼會報如下錯誤
var arr = [1,2,3]
arr.includeds(1)
// 物件不支援“includes”屬性或方法
複製程式碼
我們先來了解一下polyfill, 下面實現了一個includes方法的pollyfill
if(!Array.prototype.includeds) {
Object.defineProperty(Array.prototype, 'includeds', {
enumarable: false,
writable: false,
configurable: true,
value: function(arg) {
if(this == null) {
throw new TypeError('"this" is null or not defined');
}
var len = this.length;
if(!len) {
return false
} else {
return this.indexOf(arg) > -1;
}
}
})
}
複製程式碼
這段程式碼的意思是,如果目標環境中已經存在includeds, 什麼都不做,如果沒有就在 Array 的原型中定義一個,這便是polyfill 的意義。babel-polyfill 同理 雖說瀏覽器的特性對新javascript的語法規範支援狀況千差萬別,但其實可以提煉出兩類:
- 瀏覽器都有,只是不同語法的區別;
- 有的瀏覽器有,有的瀏覽器沒有。
babel 編譯過程處理第一種情況 - 統一語法的形態,通常是高版本語法編譯成低版本的,比如 ES6 語法編譯成 ES5 或 ES3。而 babel-polyfill 處理第二種情況 - 讓目標瀏覽器支援所有特性,不管它是全域性的,還是原型的,或是其它。這樣,通過 babel-polyfill,不同瀏覽器在特性支援上就站到同一起跑線上。
其實babel-polyfill就是一個針對ES2015+環境的shim,實現上來說babel-polyfill包只是簡單的把core-js和regenerator runtime包裝了下,這兩個包才是真正的實現程式碼所在
- babel-polyfill整合到webpack中的方法
- 先安裝包: npm install --save babel-polyfill, 因為polyfill會在原始碼之前執行,所以需要安裝成dependencies而不是devDependencies
- 在所有程式碼執行之前增加 require('babel-polyfill'),有以下兩種方式
- 在程式入口檔案app.js的頂部引用(因為polyfill程式碼需要在所有其他程式碼前先被呼叫): import "babel-polyfill"
- 或者在webpack配置的entry裡面第一個引入: module.exports = { entry: ["babel-polyfill", "./app/js"] };
但是,從上面很容易看出, babel-polyfill有兩個主要缺點:
- 使用 babel-polyfill 會導致打出來的包非常大,因為 babel-polyfill 是一個整體,把所有方法都加到原型鏈上。我們並不會用到所有的方法,這就造成了浪費
- babel-polyfill 修改原型鏈,會汙染全域性變數,如果我們開發的也是一個類庫供其他開發者使用,這種情況就會變得非常不可控。 因此在實際使用中, 更好的選擇是babel-runtime
babel-runtime 和 babel-plugin-transform-runtime
babel-runtime is a library that contain's Babel modular runtime helpers and a version of regenerator-runtime.
上面是官方定義,儘快我看得懂每一個單詞,但是連起來
我們去看看babel-runtime的包吧,package.json 裡沒有 main 欄位,用法肯定不是 require('babel-runtime')和import那樣, 我們時常在專案中看到 .babelrc 中使用 babel-plugin-transform-runtime,而 package.json 中的 dependencies (注意不是 devDependencies) 又包含了 babel-runtime,那這兩個是不是成套使用的呢?他們又起什麼作用呢?
babel 會轉換 js 語法,之前已經提過了。以 Object.assign舉例,IE不支援 Object.assign,此時,要想相容的話,我們有兩個方案
- 引入babel-polyfill, 這個當然能解決問題,但是弊端很大,汙染全域性變數,程式碼龐大
- 引入該語法外掛plugin-transfrom-object-assign來現實特定的轉換
進去克隆的專案,執行demo5(
Object.assign({}, {a:1})
)的npm run build1
(babel index.js --out-file compile1.js --plugins=transform-object-assign)生成compile1.js如下程式碼code1
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source){
if (Object.prototype.hasOwnProperty.call(source, key)){
target[key] = source[key]; }
}
}
return target;
};
var object = _extends({}, { a: 1 });
複製程式碼
從結果可以看出,這種方式的確解決了相容性的問題,但是新的問題來了,如果你的專案裡有多少個檔案用了 Object.assign,_extends 輔助函式會出現多少次,為了避免程式碼重複,我們必選要把這個方法分離出去, 改成import
引用的形式,babel-plugin-transform-runtime就是來做這些工作的,執行demo5的npm run build2
(babel index.js --out-file compile2.js --plugins=transform-runtime)生成下面檔案程式碼code2
import _extends from "babel-runtime/helpers/extends";
_extends({}, {a:1});
複製程式碼
從結果可以看出,定義方法改成了引用,那重複定義就變成了重複引用,就不存在程式碼重複的問題了。
上面的babel-runtime就是這些方法的集合處,也因此,在使用 babel-plugin-transform-runtime 的時候必須把 babel-runtime 當做依賴。 babel-runtime,它內部整合了
- core-js: 轉換一些內建類 (Promise, Symbols等等) 和靜態方法 (Array.from 等)。絕大部分轉換是這裡做的。在程式碼中使用這些內建類和靜態方法時自動引入。
- regenerator: 作為 core-js 的拾遺補漏,主要是 generator/yield 和 async/await 兩組的支援。當程式碼中有使用 generators/async 時自動引入。
- helpers,如上面的 _extends 就是其中之一,其他還有如 jsx, classCallCheck 等等。在程式碼中有內建的 helpers 使用時(如上面的程式碼code1)移除定義,並插入引用(於是就變成了code2)。
總結:
- babel-polyfill 與 babel-runtime 的區別,前者改造目標瀏覽器,讓你的瀏覽器擁有本來不支援的特性;後者改造你的程式碼,讓你的程式碼能在所有目標瀏覽器上執行,但不改造瀏覽器, babel-polyfill比babel-runtime多了對包含高版本 js 中型別的例項方法 (例如 [1,2,3].includes(1))的支援。
- babel-plugin-transform-runtime外掛依賴babel-runtime,babel-runtime是真正提供runtime環境的包;也就是說transform-runtime外掛是把js程式碼中使用到的新原生物件和靜態方法轉換成對runtime實現包的引用。
(三) 工具包
babel-cli
1 . Babel提供babel-cli工具,用於命令列轉碼, 基本命令如下
// 轉碼結果輸出到標準輸出
$ npx babel example.js
// 轉碼結果寫入一個檔案
// --out-file 或 -o 引數指定輸出檔案
$ npx babel example.js --out-file compiled.js
// 或者
$ npx babel example.js -o compiled.js
// 整個目錄轉碼
// --out-dir 或 -d 引數指定輸出目錄
$ npx babel src --out-dir lib
// 或者
$ npx babel src -d lib
// -s 引數生成source map檔案
$npx babel src -d lib -s
複製程式碼
babel-cli使用演示
babel-node
babel-cli工具自帶一個babel-node命令,提供一個支援ES6的REPL環境。它支援Node的REPL環境的所有功能,而且可以直接執行ES6程式碼。
它不用單獨安裝,而是隨babel-cli一起安裝。然後,執行babel-node就進入REPL環境。
開啟cmd終端,輸入babel-node
進入PEPL環境,直接輸入es6程式碼執行
babel-register
babel-register模組改寫require命令,為它加上一個鉤子。此後,每當使用require載入.js、.jsx、.es和.es6字尾名的檔案,就會先用Babel進行轉碼。
babel 7.x
生命不息,升級不止,babel7已經出了,各位小夥伴們可以移步官網去看看,但不是萬變不離其宗,核心原理沒有變化, 只是語法做了變化,我就用一張圖來結束這篇文章吧,要想知道的更多,大家老老實實地去啃官方文件吧。