Babel介绍及配置用法解析

你好,babel

写在前面

其实学babel是本人2019年Q3的一个计划,因为当时自己做的一个项目需要自己去配babel,也遇到了一些困难,发现自己对babel的了解还是很少的,所以决定好好看下babel;可是后来解决了当时的问题后,由于业务需求很多再加上自己懒了(这才是最终的原因),学习babel的事也就不了了之了。。。拖到了现在0.0

已经进入2020年了,还没有来得及总结自己的19年,但是2020年我要立下一个早该做到的flag,今年的Q1本人要彻底搞懂babel,也不说彻底搞懂啦,最起码本人要做到团队所有的babel问题我都可以直接解决。

其实我也不是为了搞懂babel而想着去学,我最近也看了一些关于babel的博客,慢慢的发现babel其实很有意思,去研究一个东西对自己的技术成长也会有提升,话不多说学就完了,奥利给!

对babel的基本理解

其实在学babel之前我也做了一些功课,对babel也有一些基本的理解,我先基本的谈谈我对最基本的理解

babel是什么?

babel就是一个转换器,它可以把高阶ES2015+版本的代码转换成浏览器可以解析的代码(向后兼容)。

为什么要用babel?

本人是做前端的,主要就是在用js,js版本也是一年比一年在更新,每次更新都会出一些更方便好用的方法,到现在ECMAScript已经到了ES10了,已经迭代了10个版本;不过现在多数浏览器支持的主流js语言是ES2014语法,有很多对ES2015以及ES2015+的一些语法是不支持的,也就是你用这些语法有部分浏览器是不支持的,就会报错。

但是高阶语法就是很方便啊,我就是想用,怎么办?这个时候为什么要用babel就很明显了,就是用它来把ES2015+的语法编译成浏览器能识别的通用语言,举个最简单的例子:

// babel编译前,ES2015箭头函数
(n) => { return n + 1 }
// babel编译后,ES2014函数
function (n) { return n + 1 }
复制代码

babel如何工作的?

看了一些关于babel的博客,我觉得babel就是一个插件集合,里面有一个个的插件。

babel将代码解析成AST(抽象语法树),然后用一个个对应的插件修改AST,将修改好的AST输出。
复制代码

babel的一些其他的特性

babel默认是只会去转义js语法的,不会去转换新的API,比如像Promise、Generator、Symbol这种全局API对象,babel是不会去编译的;

这个时候就需要使用babel的babel-polyfill或者babel-runtime库 和 babel-plugins-transform-runtime插件结合使用按需添加polyfill。
复制代码

列下我自己学习babel的目录脑图,我将开启我的babel学习之旅。

babel.png

babel版本变化 - 简单理解

babel版本变化

现在大部分使用babel的项目都是用的babel7.x了,但是还是需要去了解一下babel每个版本的一个重大变化,本篇主要是简单来记录下我了解的babel几个版本的重大变化

babel5.x

我看官网babel5已经没有了,,,但是在我了解的过程中,有好多博客都说有babel5,,,差不多就是说babel5就是一个全家桶,类似于vue-cli那样的东西,包括各种包和插件,babel5大概就是让你通过一次安装,尽可能的可以达到所有你想要的东西。

babel6.x

babel5升级到babel6最主要的原因可能就是之前都让大家一次npm就把所有的东西都安装了,但是有的东西是不需要的,导致包的体积很大,所有babel6就把一些包和插件都拆了出来可以去独立安装,我的猜测是这样的。。。

babel6的改变:

  • 把之前的一次性安装拆成了一些核心安装包:babel-core、babel-cli、babel-polyfill等等。。。
  • 插件化,根据自己的需求去手动的添加plugin,这点我觉得可以,一切都讲究个按需加载嘛;
  • 可以使用.babelrc 文件来配置babel,方便自定义的配置,难道说babel5只能在package.json中配置?知道的大佬可以给我评论下0.0
  • 添加了预设preset,这个预设在我学到的时候再细说,听说很牛逼;
  • babel5可以直接把require的转成import,babel6就不行,如果用了require需要加上.default才可以,不过我觉得直接改成import就好了,这一条我是有的说的,因为我,,,梦到过(卡姆说的),有次我也遇到了这个问题,引入的一个第三方包里面有用require,我的解决方案是引入了一个叫@babel/plugin-transform-modules-commonjs插件就把require编译成import了;
  • 差不多了,babel6先了解这么多,还想了解的可以去babel官网看看。

babel7.x

来了来了,最牛逼的babel7

  • 安装包都使用npm的scope包@balbel/xxx,不会再出现和别的包名冲突;
  • 所有阶段预设state-x均已弃用,使用plugin代替,官网也有一个升级方案,可以安装babel-upgrade包,它会把你之前有用过的stage-x自动换成对应的plugin,我没用过,有需求的可以去看看
Copy
https://github.com/babel/babel/blob/master/packages/babel-preset-stage-0/README.md
复制代码
  • 更强大的env
  • 之前有的插件是带着类似于es2015这样的(等等),比如@babel/plugin-transform-es2015-classes,现在直接写@babel/plugin-transform-classes就可以
  • 有些包被删了,比如babel-core/register.js,应直接用@babel/register
  • 差不多先这些

Babel 配置用法解析

写前面:babel默认是只会去转义js语法的,不会去转换新的API,比如像Promise、Generator、Symbol这种全局API对象,babel是不会去编译的。在我学会了babe配置l大法之后,看我一会儿怎么把这些新的API给它编译出来就完事儿了。
本文基于babel7.8.0。我主要记录下babel配置需要的一些重要的模块儿包,来一步步进行babel的一个配置解析(以babel.config.js方式配置为例)。
本文主要涉及到的一些babel包:@babel/core@babel/cli@babel/plugin*@babel/preset-env@babel/polyfill@babel/runtime@babel/plugin-transform-runtime
那,话不多说,发车?

@babel/core

@babel/core 这个包里主要都是一些去对代码进行转换的核心方法,具体里面的一些api方法我就不做介绍了;引用官网的一句话: “所有转换将使用本地配置文件” ,所以待会儿我们的babel.config.js配置文件就很重要了;再一个core就是核心的意思嘛,所以我们话不多说先把它装起来,gogogo

npm install --save-dev @babel/core
复制代码

@babel/cli

这个  @babel/cli就是babel带有的内置cli,可以用来让我们从命令行来编译我们的文件,有了他我们就很方便的对babel进行学习了,那话不多说,先装起来?

npm install --save-dev @babel/cli
复制代码

装完之后你就可以这样来编译你的文件:

npx babel study.js --watch --out-file study-compiled.js
复制代码

简单介绍下上面命令用到的几个参数:--out-file用来把编译后的目标文件输出到对应的文件;如果希望在每次更改目标文件时都进行编译,可以加上 --watch选项;当然还有一些别的选项,不过在我学习babel以及配置的话,这两个选项已经够我用了。

在操作的过程中如果改了babel配置发现编译出来的文件并没有实时编译的情况,需要注意下,如果改了配置文件那就需要重新执行这段命令,要不然babel读不到新的配置。

如果你已经创建了study.js文件并且执行了这段命令,你会发现,对应的study-compiled.js还没发生变化,因为我们还没开始写babel的配置文件,莫慌,马上开始。

@babel/plugin*和@babel/preset-env

babel插件和babel预设是babel配置的两个主要模块,所以我就放在一起说了。

@babel/plugin*

首先我们先来说下babelPluginsbabel官网也说了,人babel是基于插件化的,大概就是说全是插件,所以说我们配置文件里如果什么插件也不配的话,那输入和输出就是一样的,插件插件,你得插上我才让你用。我来编译一个最简单的箭头函数来看下这个babel的插件怎么用,来了,这波我们就需要配置文件了,以下所有的配置都是说的在babel.config.js文件里,相应的插件记得install

/* babel.config.js */

module.exports = {
  presets: [
  ],
  plugins: [
    "@babel/plugin-transform-arrow-functions"
  ]
}
复制代码

然后执行我们上面那段cli的命令,就会得到这种效果:

/* study.js */
const study = () => {}

/* study-compiled.js */
const study = function () {};
复制代码

当然,如果我们想要使用es6给数值新增的指数运算符怎么办,只需要添加相应的  @babel/plugin-transform-exponentiation-operator 插件即可:

/* babel.config.js */

module.exports = {
  presets: [
  ],
  plugins: [
    "@babel/plugin-transform-arrow-functions",
    "@babel/plugin-transform-exponentiation-operator"
  ]
}
复制代码

对应的es6语法就会变成:

/* study.js */
const exponentiation = 2 ** 2

/* study-compiled.js */
const exponentiation = Math.pow(2, 2);
复制代码

\

@babel/preset-env

从上面的插件化我们就知道需要哪个插件就去引入就完事儿,那么问题来了,es6新增的语法那么多,我总不能一个插件一个插件去添加吧,这样也太麻烦了,这个时候就要用到babel提供的presets了。

presets也就是预设的意思,大概意思就是可以预先设定好一些东西,就省得我们一个个的去引入插件了。官方提供了很多presets,比如preset-env(处理es6+规范语法的插件集合)、preset-stage(一些处理尚在提案阶段的语法的插件集合,当然这种预设的方式在 babel 7+  版本已经被废弃了)、preset-react(处理react语法的插件集合)等等。

我们主要来介绍下preset-envpreset-env是一个智能预设,配置了它就可以让你用es6+去书写你的代码,而且他会按需去加载所需要的插件,让你的生活更加美好。。。接下来我们记得先install这个  @babel/preset-env一波,不配任何插件,然后我们再来看看效果如何:

/* babel.config.js */

module.exports = {
  presets: [
    "@babel/preset-env"
  ],
  plugins: [
  ]
}
复制代码

对应的es6语法就会变成:

/* study.js */
const study = () => {}

const arr1 = [1, 2, 33]
const arr2 = [...arr1]

const exponentiation = 2 ** 2

// 新增API
new Promise(() => {})
new Map()


/* study-compiled.js */
var study = function study() {};

var arr1 = [1, 2, 33];
var arr2 = [].concat(arr1);
var exponentiation = Math.pow(2, 2);

// 新增API
new Promise(function () {});
new Map();
复制代码

你会发现es6+的语法都被编译了,我们并没有设置任何插件哦,应该也看到了新增的API方法并没有被编译,在这里我们埋下伏笔,等下文讲到polyfill的时候再治他。

Browserslist集成

关于preset-env,我们还可以提供一个targets配置项指定运行环境,就是我们可以配置对应目标浏览器环境,那么babel就会编译出对应目标浏览器环境可以运行的代码。相信有同学遇到过在低版本系统ios手机里自己的项目会白屏,其实是某些语法在ios低版本系统里不支持,这个时候我们可以直接配置ios 7浏览器环境都可以支持的代码:

/* babel.config.js */

module.exports = {
  presets: [
    [
      "@babel/preset-env", {
        'targets': {
          'browsers': ['ie >= 8', 'iOS 7'] // 支持ie8,直接使用iOS浏览器版本7
        }
      }
    ]
  ],
  plugins: [
  ]
}
复制代码

当然babelBrowserslist集成还支持在package.json文件里或者新建一个  .browserslistrc 文件来指定对应目标环境。browserslist配置源

@babel/polyfill(由core-js2和regenerator-runtime组成的一个集成包)

上文也提到了像Promise这种API咱们的babel并没有给转义,那是因为babel默认是只会去转义js语法的,不会去转换新的API,比如像Promise、Generator、Symbol这种全局API对象,babel是不会去编译的,这个时候就要掏出  @babel/polyfill 了。用法很简单,先安装一波,然后我们只需要在入口文件顶部引入  @babel/polyfill 就可以使用新增的API了。

/* study.js */
import '@babel/polyfill'
// 新增API
new Promise(function () {});

/* study-compiled.js */
require("@babel/polyfill");
// 新增API
new Promise(function () {});
复制代码

小细节:import被编译成了require,如果想要编译出来的模块引入规范还是import,则可以在preset-env的配置项中添加"modules": false即可。
modules的options:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为"auto"

但是问题又来了,有时候我们项目里并没有用到那么多的新增API,但是  @babel/polyfill 会把所有浏览器环境的的polyfill都引入,整个包的体积就会很大,我们想要对目标环境按需引入相应的polyfill应该怎么办呢,这个时候我们就可以使用 preset-env 的配置项中的useBuiltIns属性来按需引入polyfill。

/* babel.config.js */

module.exports = {
  presets: [
    [
      "@babel/preset-env", {
        "modules": false,
        "useBuiltIns": "entry",
        'targets': {
          'browsers': ['ie >= 8', 'iOS 7'] // 支持ie8,直接使用iOS浏览器版本7
        }
      }
    ]
  ],
  plugins: [
  ]
}
复制代码

这个时候就会在入口处只把所有ie8以上以及iOS 7浏览器不支持api的polyfill引入进来。

最终效果:

/* study.js */
import '@babel/polyfill'
// 新增API
new Promise(function () {});

/* study-compiled.js */
import "core-js/modules/es6.array.copy-within";
import "core-js/modules/es6.array.every";
...//省略若干
import "core-js/modules/web.immediate";
import "core-js/modules/web.dom.iterable";
import "regenerator-runtime/runtime";
// 新增API
new Promise(function () {});
复制代码

此时你会发现,import '@babel/polyfill'没有了,引入的是我们目标环境相应的polyfill。但是有没有发现引入的都是import 'core-js/...'的内容,标题已经说啦, @babel/polyfil是由core-js2和regenerator-runtime组成的一个集成包。

这个时候你又会想,假如我的项目里面只用到了Promise这个API,能不能只给我引入Promise相应的API呢?答案是必可以!,让我们先来好好了解下preset-env的配置项中的useBuiltIns属性。

useBuiltIns

选项:"usage"| "entry"| false,默认为false。

entry我们已经用过了,意义就是在入口处将根据我们配置的浏览器兼容,将目标浏览器环境所有不支持的API都引入。

usage就很nb了,当配置成usage的时候,babel会扫描你的每个文件,然后检查你都用到了哪些新的API,跟进我们配置的浏览器兼容,只引入相应API的polyfill,我们把useBuiltIns属性设置为usage再来看下编译效果:

/* study.js */
import '@babel/polyfill'
// 新增API
new Promise(function () {});

/* study-compiled.js */
import "core-js/modules/es.object.to-string";
import "core-js/modules/es.promise";
// 新增API
new Promise(function () {});
复制代码

我就问你帅不帅!完全的按需引入,牛逼了!

相信你也看到了一个东西,当我们使用useBuiltIns选项的时候,你的命令行里面是不是显示了一坨这样的警告,大概是在配置文件中未指定core-js版本时,默认会使用core-js2:

WARNING: We noticed you're using the useBuiltIns option without declaring a core-js version. Currently, we assume version 2.x when no version is passed. Since this default version will likely change in future versions of Babel, we recommend explicitly setting the core-js version you are using via the corejs option.

前面也说到了  @babel/polyfil 是由core-js2和regenerator-runtime组成的一个集成包,现在core-js3已经发布了,而且很稳定。但是core-js2在18年的时候已经不再维护了; @babel/polyfil引入的是2不是3,并且  @babel/polyfill 在babel7.4.0已经不再推荐使用了,要废掉(好像是因为@babel/polyfill不支持core-js2平滑的过渡到core-js3)。所以core-js官方现在推荐我们使用polyfill的时候直接引入core-js和regenerator-runtime/runtime这两个包完全取代  @babel/polyfil 来为了防止重大更改。

当然,我们需要在preset-env配置项中指定core-js版本,这样就不会再有警告⚠️了:

/* babel.config.js */

module.exports = {
  presets: [
    [
      "@babel/preset-env", {
        "modules": false,
        "useBuiltIns": "entry",
        "corejs": "3",
        'targets': {
          'browsers': ['not ie >= 8', 'iOS 7'] // 支持ie8,直接使用iOS浏览器版本7
        }
      }
    ]
  ],
  plugins: [
  ]
}
复制代码

@babel/runtime(依赖@babel/helpers和regenerator-runtime)

有的时候一些语法的转换会比较复杂,babel会引入一些helper函数,比如说对es6的class进行转换:

/* study.js */
class Test {}

/* study-compiled.js */
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Test = function Test() {
  _classCallCheck(this, Test);
};
复制代码

可以看到上面引入了helper函数来处理class的转换。但是问题又来了,如果好多文件都使用到了复杂语法的转换,这个还是简单点的,有些helper函数是很复杂代码量很多的,那岂不是每个文件都会定义一遍这些个函数,每个文件的代码会很多?如果说可以把这些helper函数都抽离到一个公共的包里,用到的地方只需要引入对应的函数即可,我们的编译出来的代码量会大大滴减少,这个时候就需要用到  @babel/plugin-transform-runtime 插件来配合@babel/runtime进行使用。记得先安装一波,然后在插件选项中加入  @babel/plugin-transform-runtime 这个插件,然后我们来看看编译后的效果:

/* study.js */
class Test {}

/* study-compiled.js */
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";

var Test = function Test() {
  _classCallCheck(this, Test);
};
复制代码

当然如果我们只是为了减少编译出来的文件中代码量而使用这个插件的话就太小看他了,而且也没有必要。

@babel/plugin-transform-runtime还有一个最重要的作用:比如说像上面我们说的Promise就需要提供相应的polyfill去解决,这样做会有一个副作用,就是会污染全局变量。如果我们只是在一个业务项目这样搞还好,也没别人要用到。但是如果我们是在维护一个公共的东西,比如公共组件库,我们这样搞,你的一些polyfill可能会把一些全局的api给改掉,副作用就会很明显,别人用你的组件库的时候就可能会出问题。 @babel/plugin-transform-runtime插件为我们提供了一个配置项corejs,他可以给这些polyfill提供一个沙箱环境,这样就不会污染到全局变量,无副作用你说美不美。

记得安装  @babel/runtime-corejs2 这个包(稳定版用2就可以),注意如果不配置的话,是不会提供沙箱环境的。然后在  @babel/plugin-transform-runtime 插件配置corejs:

/* babel.config.js */

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "useBuiltIns": "usage",
        "corejs": "3",
        'targets': {
          'browsers': ["ie >= 8", "iOS 7"] // 支持ie8,直接使用iOS浏览器版本7
        }
      }
    ]
  ],
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 2
      }
    ]
  ]
}
复制代码

我们来看下编译后的效果:

/* study.js */
new Promise(() => {})
class Test {}

/* study-compiled.js */
import _classCallCheck from "@babel/runtime-corejs2/helpers/classCallCheck";
import _Promise from "@babel/runtime-corejs2/core-js/promise";

new _Promise(function () {});
var Test = function Test() {
  _classCallCheck(this, Test);
};
复制代码

小节

  • 在你修改了babel配置项之后一定要记得重启编译命令,否则不会生效
  • 维护公共组件库或者一些别的公共库推荐要使用@babel/runtime配合@babel/plugin-transform-runtime来建立沙箱环境

转载:渴望成为大牛的男人

おすすめ

転載: juejin.im/post/7031848448831946760