babel 7 的使用的个人理解

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命令,就可以看到结果了


QQ截图20181108223713.png

再介绍完以上的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了

推荐阅读更多精彩内容