史上最清晰易懂的babel配置解析

标题党了哈哈哈~~~react

原文地址git

相信不少人和笔者从前同样,babel的配置都是从网上复制黏贴或者使用现成的脚手架,虽然这可以工做但仍是但愿你们可以知其因此然,所以本文将对babel(babel@7)的配置作一次较为完整的梳理。es6

语法和api

es6增长的内容能够分为语法和api两部分,搞清楚这点很重要,新语法好比箭头函数、解构等:github

const fn = () => {}

const arr2 = [...arr1]

新的api好比Map、Promise等:web

const m = new Map()

const p = new Promise(() => {})

@babel/core

@babel/core,看名字就知道这是babel的核心,没他不行,因此首先安装这个包shell

npm install @babel/core

它的做用就是根据咱们的配置文件转换代码,配置文件一般为.babelrc(静态文件)或者babel.config.js(可编程),这里以.babelrc为例,在项目的根目录下建立一个空文件命名为.babelrc,而后建立一个js文件(test.js)测试用:npm

/* test.js */
const fn = () => {}

这里咱们安装下@babel/cli以便可以在命令行使用babel编程

npm install @babel/cli

安装完成后执行babel编译,命令行输入json

npx babel test.js --watch --out-file test-compiled.js

结果发现test-compiled.js的内容依然是es6的箭头函数,不用着急,咱们的.babelrc尚未写配置呢api

Plugins和Presets

Now, out of the box Babel doesn't do anything. It basically acts like const babel = code => code; by parsing the code and then generating the same code back out again. You will need to add plugins for Babel to do anything.

上面是babel官网的一段话,能够理解为babel是基于插件架构的,假如你什么插件也不提供,那么babel什么也不会作,即你输入什么输出的依然是什么。那么咱们如今想要把剪头函数转换为es5函数只须要提供一个箭头函数插件就能够了:

/* .babelrc */
{
  "plugins": ["@babel/plugin-transform-arrow-functions"]    
}

转换后的test-compiled.js为:

/* test.js */
const fn = () => {}

/* test-compiled.js */
const fn = function () {}

那我想使用es6的解构语法怎么办?很简单,添加解构插件就好了:

/* .babelrc */
{
  "plugins": [
    "@babel/plugin-transform-arrow-functions",
    "@babel/plugin-transform-destructuring"
  ]    
}

问题是有那么多的语法须要转换,一个个的添加插件也太麻烦了,幸亏babel提供了presets,他能够理解为插件的集合,省去了咱们一个个引入插件的麻烦,官方提供了不少presets,好比preset-env(处理es6+规范语法的插件集合)、preset-stage(处理尚处在提案语法的插件集合)、preset-react(处理react语法的插件集合)等,这里咱们主要介绍下preset-env

/* .babelrc */
{
  "presets": ["@babel/preset-env"]    
}

preset-env

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s).

以上是babel官网对preset-env的介绍,大体意思是说preset-env可让你使用es6的语法去写代码,而且只转换须要转换的代码。默认状况下preset-env什么都不须要配置,此时他转换全部es6+的代码,然而咱们能够提供一个targets配置项指定运行环境:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "targets": "ie >= 8"
    }]
  ]    
}

此时只有ie8以上版本浏览器不支持的语法才会被转换,查看咱们的test-compiled.js文件发现一切都很好:

/* test.js */
const fn = () => {}
const arr1 = [1, 2, 3]
const arr2 = [...arr1]


/* test-compiled.js */
var fn = function fn() {};
var arr1 = [1, 2, 3];
var arr2 = [].concat(arr1);

@babel/polyfill

如今咱们稍微改一下test.js:

/* test.js */
const fn = () => {}
new Promise(() => {})


/* test-compiled.js */
var fn = function fn() {};
new Promise(function () {});

咱们发现Promise并无被转换,什么!ie8还支持Promise?那是不可能的...。还记得本文开头提到es6+规范增长的内容包括新的语法和新的api,新增的语法是能够用babel来transform的,可是新的api只能被polyfill,所以须要咱们安装@babel/polyfill,再简单的修改下test.js以下:

/* test.js */
import '@babel/polyfill'

const fn = () => {}
new Promise(() => {})


/* test-compiled.js */
import '@babel/polyfill';

var fn = function fn() {};
new Promise(function () {});

如今代码能够完美的运行在ie8的环境了,可是还存在一个问题:@babel/polyfill这个包的体积太大了,咱们只须要Promise就够了,假如可以按需polyfill就行了。真巧,preset-env恰好提供了这个功能:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "useBuiltIns": "entry",
      "targets": "ie >= 8"
    }]
  ]    
}

咱们只需给preset-env添加一个useBuiltIns配置项便可,值能够是entryusage,假如是entry,会在入口处把全部ie8以上浏览器不支持api的polyfill引入进来,以下:

/* test.js */
import '@babel/polyfill'

const fn = () => {}
new Promise(() => {})


/* test-compiled.js */
import "core-js/modules/es6.array.copy-within";
import "core-js/modules/es6.array.every";
import "core-js/modules/es6.array.fill";
...   //省略若干引入
import "core-js/modules/web.immediate";
import "core-js/modules/web.dom.iterable";
import "regenerator-runtime/runtime";

var fn = function fn() {};
new Promise(function () {});

细心的你会发现transform后,import '@babel/polyfill'消失了,反却是多了一堆import 'core-js/...'的内容,事实上,@babel/polyfill这个包自己是没有内容的,它依赖于core-jsregenerator-runtime这两个包,这两个包提供了es6+规范的运行时环境。所以当咱们不须要按需polyfill时直接引入@babel-polyfill就好了,它会把core-jsregenerator-runtime所有导入,当咱们须要按需polyfill时只需配置下useBuiltIns就好了,它会根据目标环境自动按需引入core-jsregenerator-runtime

前面还提到useBuiltIns的值还能够是usage,其功能更为强大,它会扫描你的代码,只有你的代码用到了哪一个新的api,它才会引入相应的polyfill:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "useBuiltIns": "usage",
      "targets": "ie >= 8"
    }]
  ]    
}

transform后的test-compiled.js相应的会简化不少:

/* test.js */
const fn = () => {}
new Promise(() => {})

/* test-compiled.js */
import "core-js/modules/es6.promise";
import "core-js/modules/es6.object.to-string";

var fn = function fn() {};
new Promise(function () {});

遗憾的是这个功能还处于试验状态,谨慎使用。

事实上假如你是在写一个app的话,以上关于babel的配置差很少已经够了,你可能须要添加一些特定用途的PluginPreset,好比react项目你须要在presets添加@babel/preset-react,假如你想使用动态导入功能你须要在plugins添加@babel/plugin-syntax-dynamic-import等等,这些不在赘述。假如你是在写一个公共的库或者框架,下面提到的点可能还须要你注意下。

@babel/runtime

有时候语法的转换相对复杂,可能须要一些helper函数,如转换es6的class:

/* test.js */
class Test {}


/* test-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);
};

示例中es6的class须要一个_classCallCheck辅助函数,试想假如咱们多个文件中都用到了es6的class,那么每一个文件都须要定义一遍_classCallCheck函数,这也是一笔不小的浪费,假如将这些helper函数抽离到一个包中,由全部的文件共同引用则能够减小可观的代码量。而@babel/runtime作的正好是这件事,它提供了各类各样的helper函数,可是咱们如何知道该引入哪个helper函数呢?总不能本身手动引入吧,事实上babel提供了一个@babel/plugin-transform-runtime插件帮咱们自动引入helper。咱们首先安装@babel/runtime@babel/plugin-transform-runtime

npm install @babel/runtime @babel/plugin-transform-runtime

而后修改babel配置以下:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "useBuiltIns": "usage",
      "targets": "ie >= 8"
    }]
  ],
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]  
}

如今咱们再来看test-compiled.js文件,里面的_classCallCheck辅助函数已是从@babel/runtime引入的了:

/* test.js */
class Test {}


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

var Test = function Test() {
  _classCallCheck(this, Test);
};

看到这里你可能会说,这不扯淡嘛!几个helper函数能为我减小多少体积,我才懒得安装插件。事实上@babel/plugin-transform-runtime还有一个更重要的功能,它能够为你的代码建立一个sandboxed environment(沙箱环境),这在你编写一些类库等公共代码的时候尤为重要。

上文咱们提到,对于Promise、Map等这些es6+规范的api咱们是经过提供polyfill兼容低版本浏览器的,这样作会有一个反作用就是污染了全局变量,假如你是在写一个app还好,但若是你是在写一个公共的类库可能会致使一些问题,你的类库可能会把一些全局的api覆盖掉。幸亏@babel/plugin-transform-runtime给咱们提供了一个配置项corejs,它能够将这些变量隔离在局部做用域中:

/* .babelrc */

{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "targets": "ie >= 8"
    }]
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs": 2
    }]
  ]  
}

注意:这里必定要配置corejs,同时安装@babel/runtime-corejs2,不配置的状况下@babel/plugin-transform-runtime默认是不引入这些polyfill的helper的。corejs的值现阶段通常指定为2,能够近似理解为是@babel/runtime的版本。咱们如今再来看下test-compiled.js被转换成了什么:

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


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

var Test = function Test() {
  _classCallCheck(this, Test);
};

new _Promise(function () {});

如咱们所愿,已经为Promise的polyfill建立了一个沙箱环境。

最后咱们再为test.js稍微添加点内容:

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

const b = [1, 2, 3].includes(1)


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

var Test = function Test() {
  _classCallCheck(this, Test);
};

new _Promise(function () {});
var b = [1, 2, 3].includes(1);

能够发现,includes方法并无引入辅助函数,可这明明也是es6里面的api啊。这是由于includes是数组的实例方法,要想polyfill必须修改Array的原型,这样一来就污染了全局环境,所以@babel/plugin-transform-runtime是处理不了这些es6+规范的实例方法的。

tips

以上基本是本文的所有内容了,最后再来个总结和须要注意的地方:

  1. 本文没有提到preset-stage,事实上babel@7已经不推荐使用它了,假如你须要使用尚在提案的语法,请直接添加相应的plugin。
  2. 对于普通项目,能够直接使用preset-env配置polyfill
  3. 对于类库项目,推荐使用@babel/runtime,须要注意一些实例方法的使用
  4. 本文内容是基于babel@7,项目中遇到问题能够尝试更新下babel-loader的版本

...待补充


全文完

相关文章
相关标签/搜索