babel 7 的使用的個人理解
最近看了很多關于babel的使用方法,大部分在一些點上都沒有說明白,同時給出的代碼也很難再現,為了能夠理解和防止以后自己遺忘,特寫此文。
首先我們要明白babel存在的意義,babel的目的就是為了解決瀏覽器的自身對于es語言的差異性而帶來的一款工具,有了babel就首先不用擔心瀏覽器不支持es語言這件事(當然現在的瀏覽器尤其是chrome對es6的支持越來越好),其實最重要的不是支持,而是解決差異性,這種差異性不僅介于瀏覽器之間,對于node這樣的環境也會存在這樣的問題,各個node版本對于es的支持,或者對于es的一些尚未提交的草案的支持都是不同的,所以不論是瀏覽器下還是node下都需要到使用babel的場景。
我這里就以node下的使用為例,來介紹babel的具體使用
首先在node項目里創建的index.js文件:
console.log([1,2,3].findIndex(x => x == 4))
console.log('abc'.padStart(10))
const alertMe = (msg) => {
console.log(msg)
}
class Robot {
constructor (msg) {
this.message = msg
}
say () {
alertMe(this.message)
}
}
const marvin = new Robot('hello babel')
marvin.say()
這段代碼很簡單,沒有特別含義,主要是測試babel對es6下的轉義能力
在具體使用babel之前,必須介紹一下babel的各個組成部分,注意這里我們使用babel7
- @babel/cli
- @babel/core
- @babel/preset-env
- @babel/polyfill
- @babel/runtime
- @babel/plugin-transform-runtime
- @babel/plugin-transform-xxx
以上這些就是我們以后常常會使用的babel的各個重要部分了
這里要注意一下這個@這個符號,這個是只有babel7才特有的,babel6都木有,市面上大量代碼都是基于6的所以要特別注意,安裝的時候要使用 npm install i -S @babel\cli 而不能是npm install i -S babel-cli了
這是 babel 7 的一大調整,原來的 babel-xx 包統一遷移到babel域 下 - 域由 @ 符號來標識
搞懂了這個之后,我們來一個一個看他們的使用:
@babel/cli
@babel/cli是babel提供的內建的命令行工具,主要是提供babel這個命令來對js文件進行編譯,這里要注意它與另一個命令行工具@babel/node的區別,首先要知道他們二者都是命令行工具,但是官方文檔明確對他們定義了他們各自的使用范圍:
@babel/cli 是一個適合安裝在本地項目里,而不是全局安裝
While you can install Babel CLI globally on your machine, it's much better to install it locally project by project.
@babel/node 跟node cli類似,不適用在產品中,意味著適合全局安裝
babel-node is a CLI that works exactly the same as the Node.js CLI
You should not be using babel-node in production
除了這個不同以外,他們還有一個共同點就是,直接使用它們編譯,上來就會報個錯(找不到@babel/core),原因是@babel/cli是依賴一個叫@babel/core的包文件的,沒有這個包文件是絕對不能執行任何編譯操作的(因為執行編譯的transform方法在這個包里),好吧,那我們就趕緊npm install --save @babel/core,接著再來執行babel編譯,結果!結果是編譯了之后都不會產生任何的編譯效果,例如下面這個箭頭函數所在的文件:test.js
let fun = () => console.log('hello babel')
我們在安裝了@babel/cli或者@babel/node之后
使用@babel/cli編譯
$ babel test.js
使用@babel/node編譯
$ babel-node test.js
兩個的編譯結果都是該文件無任何變化
這個問題的發生來自 babel 6 。Babel 6 做了大量模塊化的工作,將原來集成一體的各種編譯功能分離出去,獨立成插件。這意味著,默認情況下,當下版本的 babel 不會編譯代碼。
這里就扯淡了。。。你不能將箭頭函數編譯成es5,那搞個毛呀。。。
好吧,既然安裝了@babel/core,安裝了@babelb/cli這兩個還是不行,那就說明它還需要別人配合,這也就是所謂的光有刀(@babel/core,@babelb/cli)不行,還得有料(@babel/plugin-transform-xxx)才行,一堆配合前兩者,真正發揮作用的插件(@babel/plugin-transform-xxx)就登場了。
例如我們要將上面的箭頭函數編譯,我們需要這個插件
@babel/plugin-transform-arrow-functions
npm install --save-dev @babel/plugin-transform-arrow-functions
在安裝這個插件之后,再次使用babel命令行工具@babel/cli
babel test.js --plugins @babel/plugin-transform-arrow-functions
結果是箭頭函數成功的被成功編譯成了:
let fun = function () {
return console.log('hello babel');
};
這說明要使用各種真正的編譯功能是需要配合各種插件的,要將箭頭函數編譯成普通函數需要使用@babel/plugin-transform-arrow-functions,要將const或者let變量編譯成var變量需要@babel/plugin-transform-block-scoping,要將class關鍵字轉化成傳統基于原型的類需要@babel/plugin-transform-classes,所以為了真正的編譯我們是可能需要大量各種的插件的,具體插件有哪些請點這里
使用插件是可以編譯了,但是大量的使用插件會產生兩個問題:
- 第一個問題是在使用babel 調用插件采用的是命令行參數的形式
如上面的:--plugins @babel/plugin-transform-arrow-functions
這種寫法在單個插件的情況下還好,多個插件的使用就是噩夢了,所以為了解決第一種問題,出現了一個叫.babelrc的配置文件,我們可以將多個插件的信息寫入到配置文件中,因為@babel/cli在調用之時都會去調用.babelrc文件,這樣就不用寫老長的命令行參數了。
例如我們同樣是編譯這段代碼:let fun = () => console.log('hello babel')
我們在.babelrc文件中寫上
{
"plugins": [
"@babel/plugin-transform-arrow-functions"
"@babel/plugin-transform-block-scoping"
]
}
這樣我們就可以將代碼編譯成下面的樣子:
var fun = function () {
return console.log('hello babel');
};
這樣第一個問題就解決了,好了第二個問題是什么了?
- 第二個問題是同樣是關于多個插件的使用的,如果我的代碼中大量使用插件,那我依然避免不了在配置文件中大量填寫插件信息的工作,但是偉大的babel為了讓程序員們有更多的時間做自己喜愛的事情,而不是浪費生命在一個一個的挑選插件,然后把它們寫在.babelrc上,它提供了一個叫做preset的概念,說好聽點叫預設,直白點就是插件包的意思,意味著babel會預先替我們做好了一系列的插件包,例如下面這些babel認為程序員會用到的常用的插件包:
注意呀,除了這些插件包外,還有很多別的哦,這些只是常用的而已
我們這里主要介紹這個叫@babel/preset-env的插件包
我們還是通過示例來研究研究:
代碼是什么了,是我們在文章之初就創建的那個index.js文件,里面可有大量es6的語法代碼,那么我們在安裝了@babel/preset-env,并且在.babelrc中配置了@babel/preset-env之后
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "4"
}
}
]
]
}
這時候運行babel index.js之后,我們就可以帶到index.js被編譯之后的結果:
"use strict";
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; }
console.log([1, 2, 3].findIndex(function (x) {
return x == 4;
}));
console.log('abc'.padStart(10));
var alertMe = function alertMe(msg) {
console.log(msg);
};
var Robot =
/*#__PURE__*/
function () {
function Robot(msg) {
_classCallCheck(this, Robot);
this.message = msg;
}
_createClass(Robot, [{
key: "say",
value: function say() {
alertMe(this.message);
}
}]);
return Robot;
}();
var marvin = new Robot('hello babel');
marvin.say();
媽呀,嚇死哥哥了,居然產生了這么多鬼東西,我不要看這些,我要看es6
這說明在node下我們成功將es6的代碼轉化成了編譯后的js代碼。
這時候我們再回過頭來看@babel/preset-env這個插件包
Without any configuration options, babel-preset-env behaves exactly the same as babel-preset-latest (or babel-preset-es2015, babel-preset-es2016, and babel-preset-es2017 together).
默認情況下,babel-preset-env 等效于三個插件包,而不巧我們前面提到的單個的插件已經囊括在 babel-preset-es2015 中。
當然現在編譯結果已經出來了,但是馬上就有眼尖的同學發現新的問題,
- 第一個問題是這個:
{
"targets": {
"node": "4"
}
}
這個是什么鬼?這個targets實際上是針對上面的@babel/preset-env這個插件包的一個配置參數,它所代表的是你編譯代碼所針對的目標平臺,我們這里的目標是版本號為4的node(友情提示:node -v 命令可以檢查node的版本),也就是我編譯之后的代碼能夠在node版本號為4的環境下運行,同樣大家可以做個試驗,如果將node這個4改為6,再次編譯,你會發現編譯之后的代碼和編譯之前的代碼沒有任何變化,這表明原始的代碼實際上已經可以直接在版本為6的node上直接運行,不需要babel的編譯了。
當然這里的targets參數配置除了可以設置node環境外,還可以設置針對各個瀏覽器環境的配置,例如:
{
"targets": {
"chrome": "58",
"ie": "11"
}
}
當然針對瀏覽器差異的設置比node更多更靈活和復雜,感興趣的同學可以點這里看所有的配置參數信息。
好了,第一個問題解決了,我們可以引出第二個問題。
- 第二個問題眼尖的同學可以在代碼編譯之后的結果中找到,那就是代碼中的
findIndex方法和padStart方法,這兩個方法作為es6提出的新方法,居然沒有被babel編譯解析,這樣如果我直接使用node命令執行編譯后的index.js文件,那么必然是會報錯的,因為我版本為4的node環境哪里認識什么findIndex和padStart,那這樣就很尷尬了,所以光是使用@babel/preset-env是不夠的,我們還需要一個叫@babel/polyfill的包來解決。
引用別人的一段理解:解釋的很好
babel 編譯過程處理第一種情況 - 統一語法的形態,通常是高版本語法編譯成低版本的,比如 ES6 語法編譯成 ES5 或 ES3。而 babel-polyfill 處理第二種情況 - 讓目標瀏覽器支持所有特性,不管它是全局的,還是原型的,或是其它。這樣,通過 babel-polyfill,不同瀏覽器在特性支持上就站到同一起跑線。
我對polyfill的理解:polyfill我們又稱墊片,見名知意,所謂墊片也就是墊平不同瀏覽器或者不同環境下的差異,因為有的環境支持這個函數,有的環境不支持這種函數,解決的是有與沒有的問題,這個是靠單純的@babel/preset-env不能解決的,因為@babel/preset-env解決的是將高版本寫法轉化成低版本寫法的問題,因為不同環境下低版本的寫法有可能不同而已。
所以我們要使用node命令正常運行編譯后的index.js,那么要在編譯后的代碼里加上require('@babel/polyfill')這句
"use strict";
require('@babel/polyfill')
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; }
console.log([1, 2, 3].findIndex(function (x) {
return x == 4;
}));
console.log('abc'.padStart(10));
var alertMe = function alertMe(msg) {
console.log(msg);
};
var Robot =
/*#__PURE__*/
function () {
function Robot(msg) {
_classCallCheck(this, Robot);
this.message = msg;
}
_createClass(Robot, [{
key: "say",
value: function say() {
alertMe(this.message);
}
}]);
return Robot;
}();
var marvin = new Robot('hello babel');
marvin.say();
再次執行node命令,就可以看到結果了
再介紹完以上的babel包之后我們還剩最后兩個包給大家介紹一下,分別是:
- @babel/runtime
- @babel/plugin-transform-runtime
@babel/runtime的作用是提供統一的模塊化的helper,那什么是helper,我們舉個例子:
我們編譯之后的index.js代碼里面有不少新增加的函數,如_classCallCheck,_defineProperties,_createClass,這種函數就是helper。那這種helper跟我們的@babel/runtime有什么關系了,我們接著看,比如像這個_createClass就是我們將es6的class關鍵字轉化成傳統js時生成的一個函數,那么如果我有很多個js文件中都定義了class類,那么在編譯轉化時就會產生大量相同的_createClass方法,那這些_createClass這樣的helper方法是不是冗余太多,因為它們基本都是一樣的,所以我們能不能采用一個統一的方式提供這種helper,也就是利用es或者node的模塊化的方式提供helper,將這些helper做成一個模塊來引入到代碼中,豈不是可以減少這些helper函數的重復書寫。
那我們現在就
npm install --save @babel/runtime @babel/plugin-transform-runtime
然后就只需要在.babelrc中寫上:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "4"
}
}
]
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
這次再次使用babel命令編譯index.js就可以得到以下結果:
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
console.log([1, 2, 3].findIndex(function (x) {
return x == 4;
}));
console.log('abc'.padStart(10));
var alertMe = function alertMe(msg) {
console.log(msg);
};
var Robot =
/*#__PURE__*/
function () {
function Robot(msg) {
(0, _classCallCheck2.default)(this, Robot);
this.message = msg;
}
(0, _createClass2.default)(Robot, [{
key: "say",
value: function say() {
alertMe(this.message);
}
}]);
return Robot;
}();
var marvin = new Robot('hello babel');
marvin.say();
注意:我們這里要在編譯結果后人為添加require('@babel/polyfill')之后才可以用node命令去正常執行index.js
重點在:
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
這一類helper已經是被從@babel/runtime包require進來了,這都是@babel/runtime的功勞,但是事情還沒完,我們還有個包@babel/plugin-transform-runtime沒提到就用了,這個包的作用其實就是輔助@babel/runtime的,因為有了@babel/plugin-transform-runtime它會幫我自動動態require @babel/runtime中的內容,如果沒有這個@babel/plugin-transform-runtime,那么我們要使用@babel/runtime中的內容,就只有像require('@babel/polyfill')那樣人工去手動添加了,所以@babel/plugin-transform-runtime非常方便,由于@babel/plugin-transform-runtime是一個插件,所以它是需要配置到.babelrc中的,這一點要記住。
好了自此關于babel的知識就暫時告一段落,關于babel的內容肯定還有很多很多,但常用的這些相信也夠理解它的主要意義和基本使用了。
最后放上本人在gitee上的代碼例子:點這里哦
友情提示:使用npm run build來編譯哦,使用node命令執行lib/index.js就ok了