從零搭建webpack前端類庫腳手架[3]-強悍的babel

上一節咱們提到了ES6語法轉換插件 babel-loader, 然而babel-loader只是webpack調用 babel的一個橋樑。 實際上,babel是一個具備強大語言轉換功能的獨立程序。它的主要功能是把ES6或者更新的語言語法轉換爲瀏覽器可識別的ES5語法。javascript

瞭解ES6

ES6甚至包括後來出現的ES7都是下一代的JavaScript的語法版本名稱。目前chrome已經支持了大部分的ES6語法,而其餘一些瀏覽器仍是大多數支持ES5爲主。若是要在純前端兼容低端瀏覽器,則須要 es6-shim 之類的前端js庫解決polyfill的問題,用babel-standalone.js 解決新的es語法轉換的問題。 html

在這裏能夠看到各個平臺對ES6,ES7等的支持狀況:http://kangax.github.io/compa...
這個網址很是全,其平臺涵蓋了全部瀏覽器、server端平臺(包括node),以及各類polyfill對ES語法的支持狀況(其實babel-preset-env 這個智能預設就是利用這個對照表進行自動化的插件加載的)。若是要詳細看Nodejs全部版本對ES特性的支持狀況,在這裏能夠看到: http://node.green/前端

至於ES6的語法學習,請參考中文的阮一峯的:http://es6.ruanyifeng.com/#do...
或者我寫的 es6語法精要vue

babel

爲了提早使用更新的JavaScript語法,牛人們就發明了babel。經過babel,能夠把咱們寫的ES6語法的代碼,轉換爲ES5語法。這樣咱們就能夠寫ES6最終卻能夠在ES5的瀏覽器上跑了,豈不快哉。java

Babel 把用最新標準編寫的 JavaScript 代碼向下編譯成能夠在今天隨處可用的版本。 這一過程叫作「源碼到源碼」編譯, 也被稱爲轉換編譯(transpiling,是一個自造合成詞,即轉換+編譯。如下也簡稱爲轉譯)node

不過 Babel 的用途並不止於此,它支持語法擴展,能支持像 React 所用的 JSX 語法,同時還支持用於靜態類型檢查的流式語法(Flow Syntax)。更重要的是,Babel 的一切都是簡單的插件,誰均可以建立本身的插件,利用 Babel 的所有威力去作任何事情。react

再進一步,Babel 自身被分解成了數個核心模塊(babel-core, babel-cli, babel-node),任何人均可以利用它們來建立下一代的 JavaScript 工具。webpack

我對shim、polyfill和transpile的理解

上面咱們講了,babel是一種語言轉換的技術。那麼其實要想在瀏覽器裏運行更新的語言語法,須要解決2個問題。git

  • 一個是新的語法如箭頭函數怎麼解析
  • 一個是之前沒有的類或方法該怎麼hack(僞造),如Promise。

對於這種新增的類、方法,咱們很容易想到能夠在JavaScript運行以前去hack一個本身實現的類,如本身造一個Promise. 這種方法在業界叫作shim或者polyfill技術。好比,若是你想讓你的ES6代碼支持低端瀏覽器,這裏是一個shim庫: https://github.com/paulmillr/...es6

shim、polyfill所謂的墊片技術,是經過提早加載一個腳本,給宿主環境添加一些額外的功能。從而讓宿主擁有更多的能力。例如能夠基於JavaScript的原型能力,給Array.prototype增長額外的方法,就能夠必定程度上讓宿主環境擁有ES6的能力。除了對ES6+以外,咱們還得根據項目狀況,添加一些額外的shim或者polyfill。好比fetch、requestAnimationFrame 這種瀏覽器API,若是咱們須要兼容IE8,還須要添加 ES5 shim來兼容更早的JS語法

然而,有些功能,是經過shim/polyfill技術難以實現的,好比箭頭函數 =>,由於JavaScript自身沒法提供這樣的能力進行擴展。因此這種能力要想實現,就必然須要進行 語言轉換transpile (我在本文也叫作transform,實際上不太嚴謹),即將代碼中的 => 箭頭預先轉換爲ES5的 function 函數。

若是要在瀏覽器裏進行transpile,babel爲瀏覽器提供了一個運行時的轉換器babel-standlone. 這個版本內置了大量的babel插件,因此能夠直接在瀏覽器中運行並編譯特定標籤內的代碼(而不須要安裝額外的預設或插件),用戶的ES6腳本放在script標籤之中,可是要註明type="text/babel" (具體使用方法可參考其文檔); 由於放在這種script標籤內的腳本不會被瀏覽器執行,因此standalone版本的babel能夠讀取script標籤內容並解析轉換和執行它。 這種standalone版本主要用在那些非webpack打包的場景,好比說在線的try-out網站,jsfiddler這樣的網站,或者一些APP上內嵌一個V8引擎讓你REPL執行ES6語法的場景。

獨立版本的babel使用方法相似下面這樣:

standalone

因爲babel要在瀏覽器的運行時對你的js代碼從新編譯一遍執行,性能必然有所下降,所以不適合線上運行的生產環境站點。不過咱們後面會講到如何使用babel對代碼進行預編譯,這樣最終運行在瀏覽器中的代碼就是ES5了,就不存在性能問題了。因此正由於有了前端編譯的過程,如今babel這種transform才流行起來。

babel編譯代碼的幾種方式

在前端項目,咱們的目的每每是利用babel提早把ES6代碼轉成ES5代碼而後放到瀏覽器執行,而不是爲了當即執行他,這裏就要對用babel對代碼進行編譯成es5的源碼,而後再交給瀏覽器或node平臺去執行。下面咱們看下幾種不一樣的babel使用方式。

直接執行es6代碼

在後端node項目中,可能你會須要直接執行ES6編寫的代碼來進行測試(通常也只用在mocha等測試場景,生產環節仍是建議預編譯後再執行)。 基於babel-cli,你是能夠實現的直接執行node代碼的,由於babel-cli自帶了一個babel-node的命令,能夠直接執行node.js腳本。

首先安裝babel-cli

npm i -g babel-cli

如今babel7以後,babel內置的模塊和插件都放在了一個babel的命名空間下。並且通常建議局部安裝:

npm install --save-dev @babel/core @babel/cli // babel7的cli安裝方式

而後直接執行node腳本:

babel-node es6.js

若是是局部安裝的,則可使用npm scripts

{
  "scripts": {
    "start": "babel-node script.js"
  }
}

或者使用這兩種方式:

./node_modules/.bin/babel-node ./index.js   // 全部node版本都支持
npx babel-node ./index.js // npm@5.2.0以後支持

注意,在執行 babel-node 時,你須要配置本身的 .babelrc 文件 (babel7 裏面採用 babel.config.js ),開啓babel相關的轉換插件。不然你代碼中的ES6特性等均可能沒法使用。好比你不能使用export和import來定義模塊。能夠說,凡是使用babel的地方,你都必須對babel進行配置,不然babel什麼都不會作。

題外話: 爲何學習 webpack 的時候,在沒有使用babel的時候,webpack就能轉譯 esmodule 的模塊化語法呢? 答: webpack從版本2以後 確實內置了對 esmodule 的默認轉譯支持。但不表明它能轉換其餘語法(如箭頭函數)。 因此在webpack中要使用 ES 新特性,仍是要安裝並配置babel.

如今咱們來嘗試開啓下 node 的ES6語法轉換,最簡單的辦法是使用官方的 env 預設(這已是babel7默認的建議預設)。先安裝這個預設 babel-preset-env:

npm i @babel/preset-env --save-dev // babel7
npm install --save-dev babel-preset-env // 老的babel版本

而後在babelrc 或 babel.config.js(babel7的配置文件) 裏配置babel:

// babel.config.js
const presets = [
  ["@babel/env"]
];
module.exports = { presets };
// .babelrc
{
  "presets": ["env"]
}

其實跟 babel.config.js 跟 babelrc 的原理是同樣的,只是JavaScript文件更靈活,因此babel7建議使用 babel.config.js。

另外,babel-node 默認是加載了 babel-polyfill 的,因此各類新的API 都能用。 (關於babel-polyfill 和 語法轉換之間的情感糾葛,咱們後文再講)

babel-register

Node中另外一種直接執行ES6代碼的方式是使用 babel-register,該庫引入後會重寫你的require加載器,讓你的Node代碼中require模塊時自動進行ES6轉碼。例如在你的 index.js 中使用 babel-register:

// index.js
require('babel-register')
...
require('./abc.js') // abc.js能夠用ES6語法編寫,require時會自動使用babel編譯

另外,須要注意的是,babel-register只會對require命令加載的文件轉碼,而不會對當前文件轉碼。因此最好你要設計一個什麼都不作的入口讓它只作一件事情:就是加載其餘模塊。另外,因爲它是實時轉碼,因此只適合在開發環境使用。

babel命令

通常外網要上線的代碼,都不會用 babel-node 或 babel-standalone 直接去運行時運行的。所以,上線前必須提早編譯爲目標平臺可支持的語法的代碼。

若是你已經安裝了 @babel/cli, 那麼就有了babel和babel-node命令能夠用。babel命令就是對源碼進行轉譯的命令。使用方法以下:

# 編譯 example.js 輸出到 compiled.js
babel example.js -o compiled.js

# 或 整個目錄轉碼
# --out-dir 或 -d 參數指定輸出目錄
$ babel src --out-dir lib
# 或者
$ babel src -d lib
# -s 參數生成source map文件
$ babel src -d lib -s

看一個例子:

// index.js
// Babel Input: ES2015 arrow function
[1, 2, 3].map((n) => n + 1);

// Babel Output: ES5 equivalent
[1, 2, 3].map(function(n) {
  return n + 1;
});

而後安裝babel相關模塊,並執行編譯:

npm i @babel/cli @babel/core --save-dev // 安裝babel
npx babel ./index.js -d dist // 編譯index.js 生成到dist目錄下

生成結果以下:

// Babel Input: ES2015 arrow function
[1, 2, 3].map(n => n + 1); // Babel Output: ES5 equivalent

[1, 2, 3].map(function (n) {
  return n + 1;
});

跟源碼同樣,爲何沒有編譯呢? 由於咱們並無對babel進行配置,在沒有任何配置的狀況下,babel什麼都不會作。咱們像上文中babel-node那個例子同樣簡單安裝並配置下env這個預設,就獲得了編譯結果:

"use strict";

// Babel Input: ES2015 arrow function
[1, 2, 3].map(function (n) {
  return n + 1;
}); // Babel Output: ES5 equivalent

[1, 2, 3].map(function (n) {
  return n + 1;
});

箭頭函數已經被編譯成了普通函數。

API調用babel實現源碼編譯

若是想在代碼中調用babel API進行轉碼。則依賴的是babel-core,這時就不用babel-cli了。(理論上,babel-cli也是調用的babel-core而已啦)。咱們先來安裝babel-core

npm install babel-core --save-dev // 老版本的babel
npm install @babel/core --save-dev // babel7

安裝後能夠調用babel這個模塊的函數進行編譯:

var babel = require("@babel/core");
import { transform } from "@babel/core";
import * as babel from "@babel/core";

babel.transform("code();", options, function(err, result) {
  result.code;
  result.map;
  result.ast;
});

通常狀況下,咱們並不會用API的方式調用babel。這裏就很少作講述了。總之 本質上跟咱們經過其餘方式調用babel都是同樣的,配置方式也是同樣的,只是API方式調用時咱們的babel配置是經過函數傳給babel。具體轉碼API的使用,能夠參考官方文檔: https://babeljs.io/docs/core-....

經過babel-loader調用babel

咱們大部分狀況下,作前端項目是有一套本身的構建、打包過程的,這個過程會對js進行壓縮等處理。而這種狀況下,咱們要用ES6,就能夠順便把babel加入到這個構建過程中(豈不是更加靈活咯)。而babel也爲webpack這類的工具提供了對應的loader。(loader是webpack裏的概念哦,有了babel-loader,webpack就能在打包過程當中加入babel的強大編譯功能了)

其實babel除了能支持webpack,也能支持JavaScript社區全部的主流構建工具,能夠訪問這裏尋找各類構建工具的集成幫助:
http://babeljs.io/docs/setup

babel-loader的使用方法實際上跟你使用命令CLI或者API的方式都是如出一轍的。只是這個調用者變成了webpack,webpack執行時其實相似於你經過babel API來轉譯你的源碼。因此他們之間的關係是: webpack依賴babel-loader, babel-loader依賴babel編譯相關的包(如babel-core), 而babel編譯又依賴自身或社區一些插件(如preset-env等)。

babel-loader 是沒法獨立存在運行的。在babel-loader的package.json裏你會發現有個 peerDependencies,其中就定義了一個依賴項叫作webpack。peerDependencies依賴表示了一個模塊所依賴的宿主運行環境(通常各類插件的包內會使用 peerDependencies 來代表本身的宿主依賴)。

看下使用babel-loader時,webpack的配置文件:

{
  module: {
    loaders: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
                {
                    loader: 'babel-loader',
                    options: {
                        presets: ['es2015']
                    }
                }
            ]
          }
    ]
  }
}

因爲babel有本身的配置文件,因此上面代碼中babel-loader中的options配置能夠不寫,而是放到獨立配置文件當中。

babel配置詳解

OK,上面呢咱們已經學習了調用babel的N種方式。能夠說,不管哪一種方式調用嗎,都離不開babel的配置文件的配置(不然babel什麼都不作)。如今咱們學習如何配置babel。Babel的配置文件是.babelrc或者babel.config.js(babel7推薦的),存放在項目的根目錄下, rc結尾的文件一般表明運行時自動加載的文件、配置。使用Babel的第一步,就是配置這個文件。

由於全部babel命令的執行,都會去讀這個文件來做爲配置,若是沒有配置的話,至關於沒有預設轉碼規則,他是什麼都不會作的。

你能夠經過配置 插件(plugins)預設(presets,也就是一組插件) 來指示 Babel 去作什麼事情。

其格式以下:

{
  "presets": [],
  "plugins": []
}

除了放到 .babelrc 中,該配置還可放到package.json中也能夠生效, 如:

插件配置

babel6之後,babel自身只能完成基本的核心功能。並不去作轉換任何語法特性的事情。好比 transform-es2015-classes 這個插件就可讓babel轉譯你代碼中的class定義的語法。好比若是在babel6裏想用箭頭函數,得裝上插件:npm install babel-plugin-transform-es2015-arrow-functions。而後設置babelrc配置文件:

{
  "plugins": ["transform-es2015-arrow-functions"]
}

若是要編譯react jsx 語法,則能夠安裝react的插件:

npm install --save-dev @babel/preset-react

babel官方內置插件都在babel的官方倉庫package目錄下(babel-cli代碼也在這): https://github.com/babel/babe...

關於babel6的變化可查看http://jamesknelson.com/the-s...

preset預設

可是這麼多插件,寫起來很是麻煩。總不能讓開發者記住全部插件的功能而且去配置上項目所須要的插件吧。這顯然不行,因此有了preset預設。 一個預設就包含了不少插件咯。preset預設是一系列plugin插件的集合,配置了該預設,就不須要配置n個插件了,減小了配置的繁瑣。好比使用 preset-es2015 的預設爲何就能夠轉換class定義這種語法呢,其實就由於 es2015的預設中已經包含了 transform-es2015-classes 這個插件。官方的預設仍是在babel的這個倉庫裏.

babel內置的預設以下:

  • env
  • es2015
  • es2016
  • es2017
  • latest (已經廢棄,請用preset-env代替)
  • react
  • flow

還有其餘一些非官方的預設,能夠在npm上進行搜索: https://www.npmjs.com/search?...

其中,es2015, es2016, es2017分別表明不一樣ES標準。react、flow是另外一個領域的,暫且不表。另外還有 stage-0, stage-1 等預設表明最新標準的提案四個階段. (stage解釋)

$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3

例子:

{
    "presets": [
      "es2015",
      "react",
      "stage-2"
    ],
    "plugins": []
  }

若是要使用某個預設,就先安裝它。例如 npm i babel-preset-es2015。而後.babelrc中加入以下配置, 把包名的最後那個名字加進去便可:

{
    "presets": [
      "es2015"
    ],
    "plugins": []
  }

但安裝該預設時,須要使用完整的預設名稱。

着重介紹preset-env預設

有一個預設叫作 babel-preset-env, 他是一個高級的預設,能編譯 ES2015+ 到 ES5,但它是根據你提供的目標瀏覽器版本和運行時環境來決定使用哪些插件和polyfills。 這個預設是 babel7 裏面惟一推薦使用的預設, babel7建議廢棄掉其餘全部的預設。preset-env的目標是 make your bundles smaller and your life easier

對於preset-env預設來講,若是不作任何配置:

{
  "presets": ["env"]
}

那麼preset-env就至關於 babel-preset-latest 這個預設。它包含全部的 babel-preset-es2015, babel-preset-es2016, and babel-preset-es2017 預設。

若是你瞭解你的目標用戶所使用的平臺(好比大部分用戶都使用了較新的瀏覽器),那麼你大可沒必要轉譯全部的特性。你只須要告訴babel讓他轉譯你目標平臺如今不支持的語法便可。

此時你須要配置一個數組寫法, 且第二個元素是個對象用來配置preset-env的options:

{
  "presets": [
    ["env", {
      "targets": {
        "browsers": ["last 2 versions", "safari >= 7"]
      }
    }]
  ]
}

其中 targets字段能夠用來指明目標平臺和版本等信息。若是是面向node環境,能夠指明node環境版本:

"targets": {
  "node": "6.10"
}

能夠看到preset-env的options中,最重要的就是這個targets配置。targets中有2個選項,一個叫 node, 一個叫 browsers。node這個key後面能夠寫一個字符串類型的版本號或者"current", 若是想直接面向其babel運行環境的node版本,則能夠改寫爲這樣: "node": "current",此時babel會直接取 process.versions.node 中的版本號。browsers這個字段後面是一個Array類型的字符串數組或者是一個字符串。好比能夠是一個字符串:

"targets": {
  "browsers": "> 5%"
}

也能夠是個字符串數組:

"targets": {
  "browsers": ["last 2 versions", "ie >= 7"]
}

targets.browsers瀏覽器版本配置採用了browerslist寫法,所以具體寫法就去參考這個文檔吧。而browserslist的配置是能夠配置在多個地方的,其官方建議是配置在package.json中,這也是能夠被babel識別的。browserlist的源除了能夠配置在package.json中,還能夠單獨配置在一個叫作.browserslistrc文件中,甚至能夠配置在BROWSERSLIST的環境變量中。不過,在babel的 .babelrc 中配置了targets選項時,babel就會忽略其餘文件中的browserlist配置. 我我的以爲,在使用babel時就配置在babel的配置文件裏就行了。

preset-env還有其餘一些配置,如:

  • modules 設置爲true可讓babel把你的模塊編譯爲 "amd", 」"umd"或者"commonjs". 在配合webpack使用的時候,通常由webpack打包,所以通常將babel的這個配置設置爲false
  • include, exclude, 可讓babel加載或者去除指定名稱的插件。適用於咱們要自定義改動preset-env的狀況。
  • useBuiltIns. 這個配置用來給preset-env這個智能預設添加polyfill的。由於babel只轉換語法,不轉換API(下文會講),因此代碼中不少API須要根據你設置的targets環境進行polyfill處理,而在preset-env中能根據配置的環境進行智能添加polyfill的過程,就須要useBuiltIns的支持。 這也是在開發web應用(非類庫時)使用preset-env時的polyfill最佳實踐,下文會講。

問題:複雜語法轉換和babel-polyfill

babel只轉換語法,不轉換API。babel在語言轉換方面,只轉換各類ES新語法以及jsx等,但不轉換ES6提供的新的API功能,例如Promise、Array的新增的原型、靜態方法等。這時就須要polyfill墊片。

咱們能夠分析下,對於ES6轉換爲ES5這件事情來講。有幾種須要作不一樣實現的轉換類型呢?

大概是這樣的:

  1. 一種是僅僅是語法糖的區別,好比箭頭函數能直接轉爲ES5的function;
  2. 一種是API方法或類的。好比Array.from是ES6新加的API,ES5沒有這方法。babel要想提供只能提早給實現這個方法。
  3. 一種是既是新語法,但ES5也沒有能直接對應的語法. babel要想實現這個,就既要作語法變換,又要提早提供一些輔助函數。好比 class類聲明以及async這些,你不能簡單的轉換成一個 ES5 的映射,你須要一些輔助函數配合。

babel是怎麼處理這些狀況的呢?

  1. 對於第一種,babel是經過上文講到的插件直接進行代碼翻譯便可,很容易理解,也很簡單; 這是上文講到的babel+presetEnv預設所完成的。
  2. 對於第二種狀況,爲了解決這個問題,babel使用一種叫作 Polyfill(代碼填充,也可譯做兼容性補丁) 的技術。 簡單地說,polyfill 便是在當前運行環境中用來複制(意指模擬性的複製,而不是拷貝)尚不存在的原生 api 的代碼。 能讓你提早使用還不可用的 APIs,Array.from 就是一個例子。Babel 用了優秀的 core-js 用做 polyfill。
  3. 對於第三種狀況,babel採用的方法是:編譯你代碼的過程當中若是發現了這種語法,就會把你的語法包裝成另外一種ES5實現的語法,可是因爲實現比較複雜,因此除了對語法進行轉換以外,還須要輔助函數的配合,所以你會發現有運行時函數插入到代碼的最上方。

咱們來看一段代碼:

// 原型方法
[1, 2, 3].map((n) => n + 1);

// 新類型
var a = new Promise(function (resolve, reject) {
    resolve('123')
})
a.then(d => console.log(d))

// 新的class語法
class Foo {
    method() {}
}

// 新的async語法
async function testAsyncFn() {
    var a = await Promise.resolve('ok')
    return a
}
testAsyncFn().then(data=>{console.log(data)})

這段代碼中包含了上面我提到的3種情形: 新箭頭語法、原型/靜態方法/新類型、新的複雜語法class/async。 咱們使用 preset-env的默認設置對它進行編譯(preset-env預設的默認設置意味着對最新的全部ES特性都進行轉換)。 轉換結果以下:

"use strict";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

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; }

// 原型方法
[1, 2, 3].map(function (n) {
  return n + 1;
}); // 新類型

var a = new Promise(function (resolve, reject) {
  resolve('123');
});
a.then(function (d) {
  return console.log(d);
}); // 新的class語法

var Foo =
/*#__PURE__*/
function () {
  function Foo() {
    _classCallCheck(this, Foo);
  }

  _createClass(Foo, [{
    key: "method",
    value: function method() {}
  }]);

  return Foo;
}(); // 新的async語法


function testAsyncFn() {
  return _testAsyncFn.apply(this, arguments);
}

function _testAsyncFn() {
  _testAsyncFn = _asyncToGenerator(
  /*#__PURE__*/
  regeneratorRuntime.mark(function _callee() {
    var a;
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return Promise.resolve('ok');

          case 2:
            a = _context.sent;
            return _context.abrupt("return", a);

          case 4:
          case "end":
            return _context.stop();
        }
      }
    }, _callee, this);
  }));
  return _testAsyncFn.apply(this, arguments);
}

testAsyncFn().then(function (data) {
  console.log(data);
});

分析轉換結果,咱們能夠看到:

  1. 對於普通的ES語法如箭頭函數,babel會經過你preset-env指定的插件完成語法轉換。把箭頭函數轉爲ES5的函數寫法
  2. 對於ES5中沒有的原型方法和靜態方法,babel自身在進行語法轉換時,並不關注這一點。這個須要交給polyfill墊片js庫來完成。
  3. 對於複雜的語法如class/async,babel的preset-env裏面包含了對這類語法的轉換。可是這類語法因爲其比較複雜,因此會產生輔助函數,並且這些輔助函數的實現代碼會注入到轉譯後的文件裏。 如class的實現須要_createClass和classCallback函數,這兩個函數就注入到了編譯結果代碼裏。

因而可知,因爲默認的preset-env配置是轉換全部的ES6語法,因此咱們的箭頭函數、async、class都被啓用了相應的插件進行轉換,而且轉換成功了。 如今傷腦筋的問題有兩個:

  1. 原型方法、靜態方法等都沒法轉換,包括但不限於Array.from Object.assign Array.prototype.includes. 那麼,咱們上文也說了,這種活應該交給polyfill(好比在頁面裏引入一個shim.js),那麼babel有沒有提供相應的polyfill辦法呢? 答案是有的,它就是 babel-polyfill.
  2. 複雜的ES語法,通過轉換後會生成一坨函數實現代碼在文件裏。若是隻有一個文件/模塊還好,若是有 a.js, b.js, c.js 等多個模塊文件,babel編譯後每一個js文件裏都有一堆重複的 _createClass函數實現;若是再將來用webpack對他們打包上線,則會致使打包裏面每一個模塊裏也包含重複的_createClass函數實現(由於這個函數在每一個js文件裏至關因而個私有函數)

怎麼辦呢? 下面咱們來分別分析一下這倆傷腦筋的問題如何解決。

如何解決語法API沒法被轉換的問題

【備註:此小節是3級標題】

babel自身只轉換語法,不負責hack語法的API。這個通常用polyfill代碼實現。其實用一個polyfill墊片庫最簡單的方式就是全量引入了。若是你是但願在執行代碼的頁面裏進行墊片,則在頁面中引入babel-polyfill的頁面版本便可:

使用 babel-polyfill/dist/polyfill.js

若是但願在預編譯階段引入到業務代碼中,你能夠 require 到業務代碼的開頭;將來打包到bundle.js的時候就能加載polyfill的代碼了。步驟以下

  • 首先用 npm 安裝它:
$ npm install --save babel-polyfill // 要做爲運行依賴哦,由於polyfill要最終交給瀏覽器執行
$ npm install @babel/polyfill // babel7 版本的安裝方式
  • 而後只須要在入口文件最頂部導入 polyfill 就能夠了:import "babel-polyfill" . babel7須要使用 import @babel/polyfill. 若是是webpack能夠做爲entry數組的第一項。具體官方文檔

示例代碼:

// polyfill.js
import 'babel-polyfill'
console.log([1, 2, 3].includes(2))
console.log(Object.assign({}, {a: 1}))
console.log(Array.from([1,2,3]))

// babel.config.js
const presets = [
    ["@babel/env"]
];
module.exports = { presets };

用這個 preset-env 的默認配置進行 npx babel ./polyfill.js -d dist 編譯,獲得:

"use strict";

require("babel-polyfill");

console.log([1, 2, 3].includes(2));
console.log(Object.assign({}, {
  a: 1
}));
console.log(Array.from([1, 2, 3]));

能夠發現,babel編譯的過程,除了對js模塊代碼進行了上文講述的必要的語法轉譯外,並無作任何事情。對於此案例,僅僅就是把esmodule語法轉譯爲commonjs語法(由於你源碼中寫了import這樣的es模塊引用的代碼)。 但實際上,咱們這段代碼在通過webpack等工具打包放入頁面後,是能夠polyfill的,由於打包後 require('babel-polyfill') 這一句會把babel-polyfill的代碼打包進來。

因此,能夠看出來,墊片這個事情跟babel的轉譯其實無關。是由於咱們在頁面或代碼開頭引入了一些babel-polyfill的墊片代碼,因此才讓咱們的業務代碼可使用一些新的API特性。babel-polyfill 能夠墊片的API包括這些:

墊片能力

仔細研究babel-polyfill的話就會發現,這個包其實就是依靠 core-jsregenerator-runtime 實現了全部的shim/polyfill。因此在babel-polyfill這個npm包裏面,只有一個index.js文件,裏面直接引用了這兩個npm庫而已。

會看到babel-polyfill引用了core-js/shim.js, 其實shim.js這個文件就是把core-js包裏的全部polyfill的API暴漏出來。

雖然polyfill的使用很簡單,甚至跟babel都沒有多少關係。但是如今問題來了:

  1. 若是你的代碼是要支持chrome的某個較新版本便可,因爲chrome已經支持了大部分的ES6能力,可能你只需polyfill該版本chrome尚不支持的少許API便可;結果卻引入了一個龐然大物babel-polyfill。能不能根據目標平臺的支持狀況來精簡polyfill呢?
  2. 你的業務代碼中可能僅僅使用了一個Object.assign和Promise,結果卻要引入一個龐然大物 babel-polyfill。能不能根據代碼中用到的API來精簡polyfill引入呢?
  3. 儘管polyfill的目的就是能全局hack API,可是有些時候好比你開發的是一個類庫。你可能僅僅但願局部去hack一下你用到的這個API就行了,不要影響外部環境。能不能只在局部hack個人Array.from呢?

優化是無止境的,讓咱們看看怎麼解決上面問題呢?

1.根據目標平臺的支持狀況引入須要的polyfill

【備註,此小節已是4級標題】

恭喜,這個能力已經被 preset-env 這個預設所支持了。只要你打開preset-env預設的這個特性,那麼preset-env就能自動根據你配置的env targets,按照目標平臺的支持狀況引入對應平臺所需的polyfill模塊。來個例子:

// babel.config.js 配置
const presets = [
    ["@babel/env", {
        targets: {
            node: '0.10.42',
            // node: 'current'
        },
        useBuiltIns: 'usage' // 這裏是關鍵,要配置爲 usage
    }]
];

module.exports = { presets };

編譯以下源碼:

import 'babel-polyfill'
console.log([1, 2, 3].includes(2))
console.log(Object.assign({}, {a: 1}))
console.log(Array.from([1,2,3]))
console.log(new Promise())
console.log(Object.defineProperties())
console.log([1,2,3].flat())

因爲目標平臺是node的0.10版本,這個版本是不支持Object.assign, Array.from 這些API的。所以編譯結果中就引入了該平臺所須要的polyfill模塊:

"use strict";

require("core-js/modules/es6.promise");

require("core-js/modules/es6.array.from");

require("core-js/modules/es6.object.assign");

require("core-js/modules/es7.array.includes");

require("core-js/modules/es6.string.includes");

require("babel-polyfill");

console.log([1, 2, 3].includes(2));
console.log(Object.assign({}, {
  a: 1
}));
console.log(Array.from([1, 2, 3]));
console.log(new Promise());
console.log(Object.defineProperties());
console.log([1, 2, 3].flat());

注意到咱們上面除了preset-env幫咱們按需引入的polyfill以外,還有個 require('babel-polyfill') 的代碼。這一行是多餘的,所以,當咱們開啓了 preset-env 的useBuiltIns能力後,源碼中就不要再import babel-polyfill 了。

另外就是發現:這裏除了只加載了目標平臺支持的,還跟進一步只加載了我代碼中用到的。這是由於咱們把UserBuiltIns設置爲usage。若是設置爲 etnry,則只加載目標平臺不支持的那些特性的polyfill,而不會根據代碼使用狀況來加載(這在性能上要快一些)。不過,useBuiltIns: 'entry' 是替換import "@babel/polyfill" / require("@babel/polyfill") 語句爲獨立的(根據環境)須要引入的polyfill特性的import語句,所以你必須在源碼中顯式聲明 imoprt 'babel-polyfill'

2. 根據代碼中用到的API來加載polyfill

經過上面的 useBuiltIns 案例咱們已經發行,preset-env 開啓了useage的 useBuiltIns以後,它既可以根據目標平臺來選擇性的引入polyfill,並且它引入的polyfill是你業務代碼中用到的,並不會把全部平臺不支持的polyfill都引入。

這一點在 @babel/preset-env@7.0 版本我驗證是 OK 的, 在以前的版本中我曾經測試發現preset-env不能實現按需引入。 應該是在7.0版本修復了這個問題。

3.局部hack

babel-polyfill有個缺點,就是污染了宿主全局環境。此時有個babel-runtime的包能夠解決局部使用的問題,babel-runtime更像是分散的 polyfill 模塊,咱們能夠在本身的模塊裏單獨引入,好比 var innerPromise = require(‘babel-runtime/core-js/promise’) ,它們不會在全局環境添加未實現的方法. 這樣你在使用Promise的時候就要這樣了:

var innerPromise = require(‘babel-runtime/core-js/promise’)
var a = new innerPromise(...)

但是,本身去發現並改寫業務代碼裏的API調用未免有點麻煩了. 這裏就有個插件來幫忙作這個事情了: babel-transform-runtime 插件。 首先安裝它:

npm install --save-dev @babel/plugin-transform-runtime // babel7的安裝方式
npm install --save @babel/runtime // 這個要做爲運行依賴

而後咱們配置下transform-runtime插件:

// babel.config.js
const presets = [
    ["@babel/env", {
        targets: {
            node: '0.10.42',
            // node: 'current'
        },
        useBuiltIns: 'usage'
    }]
];

const plugins = [
    ["@babel/plugin-transform-runtime", {
        "corejs": 2, // 只能設置爲 undefined,false,2
        "helpers": true,
        "regenerator": true,
        "useESModules": false
    }]
]


module.exports = { presets, plugins };

咱們執行編譯看下結果:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");

var _defineProperties = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/define-properties"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

var _from = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/array/from"));

var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign"));

require("core-js/modules/es7.array.includes");

require("core-js/modules/es6.string.includes");

console.log([1, 2, 3].includes(2));
console.log((0, _assign.default)({}, {
  a: 1
}));
console.log((0, _from.default)([1, 2, 3]));
console.log(new _promise.default());
console.log((0, _defineProperties.default)());
console.log([1, 2, 3].flat());

彷彿很完美的樣子, 全部的ES6特性,都被 transform-runtime 編譯成了對 corejs2的函數調用,並且是按照實際的使用狀況按需引用和改寫的。 不過這裏有個疑惑點:就是 [1,2,3].includes 這種咱們在網上常常看到資料說 transform-runtime 沒法作到的這裏也作到了,這是爲何呢?

實際上之因此上面編譯後出現:

require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.string.includes");

是由於 preset-env 的 useBuiltIns 設置致使的。 咱們知道preset-env的useBuiltIns能夠按需在全局進行polyfill,因此纔出現了這個墊片。 所以能夠說,transform-runtime開啓corejs的方案和babel-runtime的方案是互斥的,最好不要同時polyfill。transform-runtime的確沒法解決實例的原型方法調用的hack問題。(固然因爲transform-runtime常建議用在類庫項目裏,因此這種實例寫法問題不大,只需類庫開發者本身文檔提醒開發者要在全局作includes的polyfill)

另外要注意的一點是:transform-runtime使用core-js:2的配置進行polyfill時,沒法感知你目標平臺環境(即不能像preset-env同樣感知目標平臺)。所以局部polyfill時務必要知道這一點,也就說只要你局部polyfill,你設置的preset-env環境跟你polyfill的效果無關(事實上,preset-env跟transform-runtime原本就是兩個東西)

如何解決複雜語法轉換後重復問題

實際上babel-runtime裏不止包含了全部ES6的API(即core-js),也包含了ES6語法轉換時須要的那些輔助函數helpers, 也包含了async和生成器的實現(即regenerator-runtime)。仔細觀察babel-runtime的包依賴也能夠證明這一點. 因此 transform-runtime 的方案也不止用來局部hack polyfill,也會用在上文中提到的另一個疑難問題: 「複雜語法編譯後多文件重複」 的問題。

上文的例子中,咱們看到,代碼中使用了ES7的async,babel會使用了定製化的 regenerator 來讓 generators(生成器)和 async functions(異步函數)正常工做。 但這個regenerator函數會插入到編譯後代碼的最上方。若是源碼中使用了ES6的class,也會出現相似的 _createClass 等函數的實現代碼放在代碼模塊文件的上方。

此時,若是有多個js模塊文件,每一個文件編譯後都會有本身文件內的輔助函數插入,很是影響未來的打包合併。(會致使打包後每一個js factory工廠函數模塊裏都有重複代碼)

要解決這個問題,咱們其實能夠想到辦法:

若是是用本身寫代碼的思路來看,根據DRY原則,若是每一個js文件裏都使用同一個函數如_createClass, 那麼咱們最好把他們放到一個單獨的文件/模塊裏,而後須要的時候require它。 這樣寫的話,最終webpack等工具打包的時候會以模塊爲粒度打包,你們都依賴的這個模塊只會存在一份,不會存在重複。

因此上文講到的 _classCallback 這些輔助函數其實能夠改成 var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); 這樣從babel-runtime包種引用。 是否是跟上面局部hack polyfill很像啊?

是的,跟局部polyfill的原理同樣,咱們可讓代碼中的複雜ES6語法如class、async,自動引入對應的babel-runtime輔助函數。解決辦法:同樣是藉助 transform-runtime 插件來自動化處理這一切。

步驟:插件安裝方式跟上文同樣

npm install --save-dev @babel/plugin-ransform-runtime
npm install --save @babel/runtime // runtime是運行時依賴

而後修改 babel.config.js 的配置爲:

// babel.config.js
const presets = [
    ["@babel/env", {
        targets: {
            node: '0.10.42',
            // node: 'current'
        },
        useBuiltIns: 'usage'
    }]
];
const plugins = [
    ["@babel/plugin-transform-runtime", {
        "corejs": 2,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
    }]
]
module.exports = { presets, plugins };

這樣再運行babel編譯時,這個插件會把這種generator或者class的運行時的定義移到單獨的文件裏。 咱們看下編譯示例:

// 源碼
console.log(Object.assign({}, {a: 1}))
console.log(new Promise())

// 新的class語法
class Foo {
    method() {}
}

編譯結果以下:

// 編譯結果
"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign"));

console.log((0, _assign.default)({}, {
  a: 1
}));
console.log(new _promise.default()); // 新的class語法

var Foo =
/*#__PURE__*/
function () {
  function Foo() {
    (0, _classCallCheck2.default)(this, Foo);
  }

  (0, _createClass2.default)(Foo, [{
    key: "method",
    value: function method() {}
  }]);
  return Foo;
}();

可是 假如咱們這是一個Web應用,咱們發現上面的編譯結果是有問題的。因爲transform-runtime的存在,致使咱們本該全局polyfill的靜態方法變成了局部polyfill。 這個緣由就是transform-runtime致使的。不過幸虧,tranform-runtime是可配置的,咱們能夠配置他是否局部hack polyfill, 是否局部hack helpers, 是否局部x修正regenertor。

對於web應用 咱們通常是但願:

  1. ES6複雜語法轉換能夠不重複打包。(用transform-runtime配合babel-runtime來實現)
  2. polyfill可以按需引入並全局polyfill(用preset-env配合開啓useBuiltIns實現)

所以這種場景下正確的babel配置應該是這樣的:

// babel.config.js
const presets = [
    ["@babel/env", {
        targets: {
            node: '0.10.42',
            // node: 'current'
        },
        useBuiltIns: 'usage'
    }]
];

const plugins = [
    ["@babel/plugin-transform-runtime", {
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
    }]
]
module.exports = { presets, plugins };

最佳實踐和取捨

小總結

首先,上面講了那麼多polyfill和語法轉換的使用和優化方式。咱們能夠看到要想正確配置babel須要看咱們的須要和場景。並且,做爲babel的使用者,咱們須要理解幾個個概念:helper, 墊片函數,一個是墊片庫,一個是regenerator-runtime。

  • helper是爲了幫助你構造ES6的ES5 class實現和generator實現的輔助函數
  • 而墊片是爲了實現ES6的ES5版本的API,如Array.from. core-js這個墊片函數庫是對某個API的具體實現。regenerator-runtime也是一些墊片函數,只是它特定地用來實現ES6裏面的generaotor語法。
  • 墊片庫是指的一個調用了各個墊片函數再對頁面全局進行污染的墊片庫,如babel-polyfill

如此,咱們就能明白babel-polyfill只是爲實現API墊片爲目的的一個庫,能夠全局污染來墊片。它包含了core-js和regeneraor-runtime兩個墊片庫的實現,core-js墊片用於普通的API墊片實現,regenerator-runtime墊片用於實現generator生成器。

babel-runtime是什麼?它不是一個能夠直接用的庫(它的package.json裏都沒有main),能夠認爲它是core-js、regenerator-runtime、helpers函數的集合。它的corejs和regeneratorRuntime能夠幫助你局部不污染全局的按需加載polyfill,它的helpers能夠幫助你改變babel編譯async等語法帶來的輔助函數重複問題。固然,在局部利用babel-runtime裏的使用某個墊片函數或helpers函數時,通常都不是手工操做,而是經過transform-runtime插件來完成。

類庫項目

對於類庫項目來講,你可使用最新的語法特性,而後用babel+presetEnv進行語法編譯後釋出一個ES5的dist.js。但你代碼中使用的API你不能直接全局給他polyfill掉,哪怕你按需polyfill也很差,由於這會污染全局環境。你在未知你的調用者環境的狀況下,你不能污染全局。因此,類庫中最好的polyfill方式是局部polyfill(利用transform-runtime或手工引入core-js的module)。在babel官方polyfill文檔裏有提到這個小細節

If you are looking for something that won't modify globals to be used in a tool/library, checkout the transform-runtime plugin. This means you won't be able to use the instance methods mentioned above like Array.prototype.includes.

Depending on what ES2015 methods you actually use, you may not need to use @babel/polyfill or the runtime plugin. You may want to only load the specific polyfills you are using (like Object.assign) or just document that the environment the library is being loaded in should include certain polyfills.

也就是說,若是你是開發一個類庫項目,那麼你通常是不要污染全局的。若是你不想污染全局,你能夠用transform-runtime配合 babel-runtime的方案,可是這個方案 沒法解決實例的原型方法的polyfill問題 這個缺點你必需要注意。 而若是你很明顯地知道這個類庫調用了哪些較新的API(你的客戶環境可能會不支持的API),那麼你就不要使用 @babel/polyfill 或 babel-runtime方案了,你能夠直接手工走core-js來加載它,或者你在你的類庫文檔裏告訴你的開發者說你這個類庫須要依賴什麼polyfill。

Web應用項目

這種項目因爲不怕全局polyfill污染,所以通常採用全局polyfill的方式。不過爲了提升頁面性能,通常也經過 preset-env 配合 useBuiltIns配置的方式實現按需加載polyfill。注意,如今版本的preset-env若是開啓了useBuildIns,你就不要本身在代碼的開頭出引用babel-polyfill了。

至於複雜語法轉換帶來的輔助函數問題,就靠 transform-runtime來解決了。注意不要開啓 core-js選項,從而避免局部polyfill(由於你已經preset-env+useBuiltIns使用了全局polyfill的方式)。

關於async語法

async generator轉換成新的輔助函數後,到底須要依賴哪些東西才能正常運行?

通過個人測試發現,它須要依賴兩個polyfill:

  • regenerator-runtime這個polyfill(由於helpers輔助函數是不夠的)。
  • promise的polyfill(由於轉譯後的代碼中用到了promise)

咱們進行preset-env+useBultIns的全局轉換,能夠看到結果裏面自動引入了須要的polyfill:

require("regenerator-runtime/runtime");

require("core-js/modules/es6.promise");


function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

參考issue:
5085
112

咱們再試試用transform-runtime來局部polyfill,能夠看到結論是同樣的:

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator"));

只是這個promise和regenerator-runtime的polyfill換成了babel-runtime(或runtime-corejs2)裏面的。

babel與mocha和lint結合使用

用ES6寫代碼以後,測試有時也但願使用ES6來編寫。並且eslint進行代碼檢查時也要利用babel進行轉換。關於結合mocha的使用將在後面的文章講解。eslint的使用請參看博文[[實踐]-使用ESLINT檢查代碼規範]()

總之,測試這些環節執行ES6的測試用例代碼時就不需走編譯步驟了。因爲不在意性能,所以能夠直接走實時編譯執行的模式。

mocha --compilers js:babel-core/register --require babel-polyfill

babel不止於ES

如今流行框架,都在使用babel進行框架特有的語法轉換。例如除了react,還有Vue2.0的jsx

咱們也能夠寫本身的babel插件,詳情可參考手冊: https://github.com/thejamesky...
官方腳手架:https://github.com/babel/gene...

下一節,就用這些知識點真正搭建一個類庫開發項目了。

Refer

babel-handbook中文
babel 7 教程
babel-preset-env
https://babeljs.io/docs/plugi...
https://github.com/brunoyang/...
你真的會用 Babel 嗎?
21 分鐘精通前端 Polyfill 方案
https://leanpub.com/setting-u...
babel筆記
測試external-helper
creeperyang的博客
Babel 入門教程(三):babel-plugin- 插件及與 babel-preset- 預設插件的關係
Babel 入門教程(六):babel-polyfill 與 相關插件和包

相關文章
相關標籤/搜索