babel那些事兒

夏天來嘍發表於2019-03-14

聽說提升技術最快的方法是輸入,一直覺得自己文筆爛,技術菜,再加上惰性大,所以都是隻看不寫,但是一直這樣下去能進步就見鬼了,所以做了個決定,給自己定義一個小目標,準備每週寫一遍技術文章,管它寫的好還是亂,膚淺還是深入,有人看還是沒人看,開始寫就是進步,堅持寫就是一直進步。

這是我第一次在掘金上釋出文章,如果有錯誤歡迎大家指正,在此也鼓勵下大家學會輸出,知道是一回事,能寫出來是另外一回事,能深入淺出的寫出來那啥都不是事兒了。

用vue開發專案一年多了,但是一直沒因為專案中使用了babel, 這篇文章簡單整理下自己所知道的關於babel的知識,文章和程式碼gihub上也有,歡迎大家可以去github克隆專案an-article-to-know-bable。 最後有錯誤的地方歡迎大家指出來,共同學習和進步,謝謝!

目錄


一 babel是什麼

Babel is a JavaScript compiler

簡單來說就是把 JavaScript 中 es2015+ 的新語法轉化為 es5,讓低端執行環境 (如瀏覽器和 node ) 能夠認識並執行

二 Babel的工作原理

babel是一個編譯器compiler,但是叫轉譯器transpiler更準確,因為它只是把同種語言的高版本規則翻譯成低版本規則,而不像編譯器那樣,輸出的是另一種更低階的語言程式碼。

Babel的編譯過程跟絕大多數其他語言的編譯器大致同理,分為三個階段:

  1. 解析(parse):使用babylon將程式碼字串解析成抽象語法樹ast
  2. 變換(transform):使用babel-traverse, 利用配置好的 plugins/presets遍歷ast,更新節點,轉變為新的ast
  3. 再建(generate):使用babel-generator根據變換後的抽象語法樹再生成程式碼字串

babel編譯流程圖

編譯流程圖
編譯流程圖

既然要講編譯,就必須要了解一下抽象語法樹了 AST Explorer是一個線上實時編譯工具,可以選擇不同的語言和對應的編譯器,將原始碼解析成抽象語法樹, 幫助我們更直觀的認識AST的結構,編譯器選擇babylon6,

babel那些事兒
在左側輸入下面的的程式碼

function abs(number) {
    if (number >= 0) {  
        return number;  
    } else {number; 
    }
}
複製程式碼

在右側生成了對應的Ast, 下面的結構圖可以更加清楚地看清楚AST樹的節點資訊

!ast樹
可以看到AST就是由一個個節點構成,每個節點都是原始碼語法的一個標籤。

解析這一步是如何生成抽象語法樹的, 給大家甩一個知乎上的一篇文章Babel是如何讀懂JS程式碼的裡面有講解析器解析的過程, 對編譯器有興趣的小夥伴可以去研究一下github上一個輕量級的編譯器實現the-super-tiny-compiler

此外,以下兩點請注意

  1. babel本身不具備轉碼能力,轉碼都是通過外掛完成的,如果不新增外掛,那麼經過babel後的程式碼和原始碼是一樣的
let x = (a) => a*2;
複製程式碼
  1. 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中的方法
  1. 先安裝包: npm install --save babel-polyfill, 因為polyfill會在原始碼之前執行,所以需要安裝成dependencies而不是devDependencies
  2. 在所有程式碼執行之前增加 require('babel-polyfill'),有以下兩種方式
    • 在程式入口檔案app.js的頂部引用(因為polyfill程式碼需要在所有其他程式碼前先被呼叫): import "babel-polyfill"
    • 或者在webpack配置的entry裡面第一個引入: module.exports = { entry: ["babel-polyfill", "./app/js"] };

但是,從上面很容易看出, babel-polyfill有兩個主要缺點:

  1. 使用 babel-polyfill 會導致打出來的包非常大,因為 babel-polyfill 是一個整體,把所有方法都加到原型鏈上。我們並不會用到所有的方法,這就造成了浪費
  2. 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,此時,要想相容的話,我們有兩個方案

  1. 引入babel-polyfill, 這個當然能解決問題,但是弊端很大,汙染全域性變數,程式碼龐大
  2. 引入該語法外掛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,它內部整合了

  1. core-js: 轉換一些內建類 (Promise, Symbols等等) 和靜態方法 (Array.from 等)。絕大部分轉換是這裡做的。在程式碼中使用這些內建類和靜態方法時自動引入。
  2. regenerator: 作為 core-js 的拾遺補漏,主要是 generator/yield 和 async/await 兩組的支援。當程式碼中有使用 generators/async 時自動引入。
  3. helpers,如上面的 _extends 就是其中之一,其他還有如 jsx, classCallCheck 等等。在程式碼中有內建的 helpers 使用時(如上面的程式碼code1)移除定義,並插入引用(於是就變成了code2)。

總結

  1. babel-polyfill 與 babel-runtime 的區別,前者改造目標瀏覽器,讓你的瀏覽器擁有本來不支援的特性;後者改造你的程式碼,讓你的程式碼能在所有目標瀏覽器上執行,但不改造瀏覽器, babel-polyfill比babel-runtime多了對包含高版本 js 中型別的例項方法 (例如 [1,2,3].includes(1))的支援。
  2. 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已經出了,各位小夥伴們可以移步官網去看看,但不是萬變不離其宗,核心原理沒有變化, 只是語法做了變化,我就用一張圖來結束這篇文章吧,要想知道的更多,大家老老實實地去啃官方文件吧。

babel7的變化

babel7
文章腦圖

相關文章