小白都能听懂的最新版Babel配置探索——保姆级教学

1,445 阅读26分钟

前言

说起babel,很多前端小伙伴都会望而生畏,直接打起了退堂鼓,有的小伙伴会说,我知道我知道,就是把我们项目里ES6及以上的的高级语法转成ES5等向后兼容的JS代码,这样就能在低版本浏览器或者其他环境中跑通了,还有小伙伴说,不需要了解啊,脚手架生成项目的时候,不是都配好了么,或者说这些都是我领导他们去配的,我就是个无情的业务cv工具人🙃。我相信每个小伙伴都曾经探索过一些我们未知不熟悉的新知识,但是由于种种原因,最终都放弃并躺平了,我觉得有很大一部分原因在于,没有一个由浅入深的渠道和方式,来让我们对新知识有一个很好的入门;

大佬们都会说,哎呀去百度google一搜一大把文章,不是很简单么,直接去官网看文档不就好了?但是对于一个从未接触过这方面新知识的小白来说,看文档无异于是买一本新华字典,来学习写作一样吃力。网上关于Babel的文章可谓是众说纷纭,再加上版本的更新,一些本来是可以几句话说清楚的事情,却很少有人能够照顾到小白的想法,硬是把文章写成了文档配置项,阅读起来晦涩不堪,甚至连版本的差异性都不进行标注说明,看文章发布时间没几个月,但是里边的版本却是老的版本,看起来苦不堪言。 error

可能你觉得你智商超群,看看文档就能学懂新知识,新技术,但是据我的了解,包括我自己,大多数人是都没有这个能力的(除非文档写的是真的棒)。而我认为学习新技术新知识最佳的途径就是能够有人能带你入门,带你理解最基本的一些概念,之后再靠自己去研究文档,看其他大佬写的相关文章,参与讨论。
这便是我写这篇文章的初衷,也是在我从一个小白学到现在的心得,本文只是能教会你什么是babel,babel里有哪些配置,这些配置都是干嘛用的,并不会涉及到其原理(原理的话,欢迎看我写的从零开始学习webpack原理专栏,后续文章会有所提及)。文章会比较长,我深深理解作为小白的那种无助,所以都会以最简单的白话来结合实例一一讲解,语言可能有不准确的地方,任何以为可以在评论区一起交流,我们一起来完善整篇文章~

前言感慨太多了,如果屏幕前的你,在完整的看完这篇文章之后,一定会自信满满,把babel的常用配置理解的透透的,请务必耐心看完,我会用大白话来进行说明和演示,有些地方用词不准,请不要锱铢必较;如果你还是不懂,来张机票我去你家门口给你现场教学🐱

文章很长,如果每个步骤都跟着我进行了实际操作,没有技术的话那么需要2天的时间才能完全理解学透;时间不充裕的话,建议按标题分天看完。

Babel简介

什么是Babel呢?Babel其实就是一个工具集合,是围绕着@babel/core这个核心包构成的,每次@babel/core发新版本的时候,其他的工具集都要将版本更新到和@babel/core相同的版本号。在我们的前端项目中,充当着将ES6及之后的新语法,转译成ES5等向后兼容的JS代码,比如下边的代码,转化前使用了let声明和箭头函数:

let func = age => age + 3

经过babel转化后的结果为

var func = function (age) {
  return age + 3
}

Babel的版本

现在好多新项目都已经用上了最新的Babel 7版本,但是对于一些老的项目,依旧在使用Babel 6,其实他们的区别我们可以简单的从包名字上看出来,Babel 7采用了Monorepo单仓库的方式来管理其工具集,所以包的名字都是@babel/开头的比如@babel/core,@babel/cli,而Babel 6包的名字都是babel-core,babel-cli,他们的本质其实是一样的,core是核心包,cli是命令行工具,所以我们在开发前有必要了解这个区别,Babel6和7之间在配置上的细小差别,本文都是基于Babel 7版本进行讲解,那么在Babel 7版本中也有些细小的变化,后文也会一起提到。

Babel的配置文件

我们在使用Babel的时候,一定要有一个配置文件,所有的配置,都是在这个配置文件里边完成的,那么配置文件有几种写法,他们内部的配置项和作用是一模一样的,我们看哪种顺眼,就选哪种方式就好啦:

// .babelrc 文件配置项写法
{
  "presets": [],
  "plugins": []
}
// babelrc.js, babel.config.js(推荐)文件配置项写法
module.expors = {
  "presets": [],
  "plugins": []
}
// 还可以内嵌在package.json文件中,只需啊哟增加个babel属性即可
{
  "name": "babel_test",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "babel": {
    "presets": [],
    "plugins": []
  }
}

我们不难发现,不管各种写法,配置的参数就是两个数组,presets我们称之为预设数组,plugins顾名思义,称之为插件数组,还有一些其他的配置项并不重要,我们在这里也不多提。关于他们之间的作用和区别,我们后续会一一演示介绍。有了这些前置的知识了解,我们便可以进行实练了。

Babel的插件和预设应用

我们需要新建一些文件来进行实例演示:

创建babel_test文件夹,在文件夹中创建before.js作为我们转换前的示例代码,然后创建babel.config.js文件
作为babel的配置文件,使用npm init -y命令初始化package.json文件,各个文件中代码如下:
// before.js
let func = age => `age_${age}` + 3
let obj = {
  name: 'xiaoming'
}
console.log(obj.age?.value)
// babel.config.js
module.exports = {
  presets: [],
  plugins: []
}

此时,babel.config.js是一个默认的零配置文件,之后,我们要安装一些必要的npm包,@babel/core为核心包,@babel/cli为解析命令行的包。注意,当前用的是最新版本的@babel/core: 7.19.1

npm install @babel/core @babel/cli -D

然后在package.json文件夹中,我们配置一条命令:这个命令的意思是用babel将before.js中的代码,转译后创建并输出到after.js中

"scripts": {
  "babel": "babel before.js -o after.js"
}

我们执行npm run babel命令,调试一下,发现after.js在当前目录下被创建了,里边的内容和before.js竟然是一模一样的,当然一样啦,因为我们目前只是调通了整个流程,并没有对babel的配置文件做任何配置,所以babel就会把before.js文件中的代码,原封不动的输出到after.js文件中。

插件的基本使用

接下来我们进入正题,首先安装一个插件npm install @babel/plugin-transform-arrow-functions -D,从名字上我们不难看出,这个插件的作用就是转换箭头函数语法,安装好之后,我们只需要在babel.config.js文件中做如下配置即可:

// 将插件名字直接放在plugins数组中即可
module.exports = {
  presets: [],
  plugins: ["@babel/plugin-transform-arrow-functions"]
}

怎样,是不是非常简单?之后,我们执行npm run babel命令进行编译,发现after.js中文件发生了变化,箭头函数语法被转译成了function,说明我们配置的插件奏效了.

let func = function (age) {
  return `age_${age}` + 3;
};
let obj = {
  name: 'xiaoming'
};
console.log(obj.age?.value);          

此时此刻,眼神比较犀利的同学就会问了,箭头函数是转译成ES5了,但是可选链操作符和模板字符串,怎么还是被原封不动的输出到after.js中了呢?聪明的你这时候一定意识到了,其实plugins之所以被称之为是插件,从名字上就可以发现,他只能转译某一种语法,那么如果你想再转译其他的语法,我们就需要去安装相应的插件,比如我们想继续转译可选链,那么此时我们还需要安装另一个插件npm install @babel/plugin-proposal-optional-chaining -D,同样,相同的方式把这个插件放在babel配置文件中,此时共有2个插件。

// babel.config.js
module.exports = {
  presets: [],
  plugins: ["@babel/plugin-transform-arrow-functions", 
  "@babel/plugin-proposal-optional-chaining"]
}

我们再次运行npm run babel命令,发现转移后after.js文件的内容如下所示:可选链被成功的转译了

var _obj$age;
let func = function (age) {
  return `age_${age}` + 3;
};
let obj = {
  name: 'xiaoming'
};
console.log((_obj$age = obj.age) === null || _obj$age === void 0 ? void 0 : _obj$age.value);

那么接下来发现let声明,模板字符串,依旧没有被转译,那按照之前的套路,我们需要去寻找let声明转译对应的插件,模板字符串转译对应的插件,这些插件的目录都在官方文档上有列出来。看到这里可能就烦来了,会说,我又不知道我项目中会用到哪些新语法,难道每次用到一个新语法,都要去找相应的plugin,然后再安装配置么?一共有几十甚至上百个插件,找来找去,这也太麻烦了吧。

别急,当然有解决的办法,接下来就该presets(预设)出场了

预设的基本使用

什么是预设呢?一句话,就是插件的集合包,相当于里边集合了所有的插件,可以理解为安装了一个预设,就相当于安装了那几十上百个插件,管你用多少新语法,老夫一把梭。那预设怎么用呢?很简单还是安装@babel/preset-env这个预设包,为什么安装他呢,我们先用一下,后边再详细解释。使用命令npm install @babel/preset-env -D,安装完后,我们把babel.config.js文件做如下稍稍的调整即可:

module.exports = {
  presets: ["@babel/preset-env"],
  plugins: [] // 清不清空之前的插件无所谓,这里为了展示清爽方便,就清空了
}

配置预设是不是很简单,此时已经按捺不住内心的激动来进行代码转译了,同样,运行npm run babel,发现after.js文件中,所有的ES6(这里为了方便高版本的都统称ES6了,下文相同)语法,都被转移成了ES5的语法,妈妈再也不用担心我找不到相应的插件了~:

var _obj$age;
var func = function func(age) {
  return "age_".concat(age) + 3;
};
var obj = {
  name: 'xiaoming'
};
console.log((_obj$age = obj.age) === null || _obj$age === void 0 ? void 0 : _obj$age.value);

预设的选择

有同学可能问了,为啥预设要安装@babel/preset-env这个?一些资深玩家也会问我之前怎么见到过好多种预设什么stag-1 2 3啥的,别急,我们来详细捋一遍。

在Babel 6的时候,每当TC39发布新的ES标准语法的时候,都会有相应的预设,比如babel-preset-es2015、babel-preset-es2016、babel-preset-es2017,分别对应着每一年的新语法;

而babel-preset-stage-0、babel-preset-stage-1、babel-preset-stage-2这些个预设,分别对应着TC39发布新的ES标准语法的草案阶段,Babel 7之后babel-preset-stage-XXX版本的预设因为会造成理解混乱,所以也就不再更新;

在发布babel-preset-es2017之后,大概是觉得每年发一次,时间久了预设太多,会比较混乱,于是就新推出了babel-preset-latest预设,便是TC39每年发布的进入标准的ES语法的转换器预设集合,里边包含了之前所有的预设;

从Babel 7问世之后,@babel/preset-env这个预设包含了之前babel-preset-latest预设所有的功能,并且变得更加完善了,通过对预设的配置(后文会详细讲解),也可以转译草案中(babel-preset-stage-xxx)的语法,相当于一个预设,包含了所有;

经过一番对历史的梳理,我们不难发现,之前那么多的预设都用不到了,我们现在只需要用这一个@babel/preset-env预设,就能解决所有问题。是不是像是一群小虾米被几条鱼吃了,然后这几条鱼又被一条大鱼吃了的感觉?

除了对ES6语法转译的预设,现在常见的预设还有3个,对类型和react的特定语法进行转译,都在文档上详细说明,分别是

  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript

所以我们日常用到的预设也就只有这4个了,如果是vue+typescript项目的话,就仅仅需要安装@babel/preset-env和@babel/preset-typescript这两个预设即可。

插件的选择

那既然有了预设,我们需要插件么?当然需要啦,只不过不需要转译ES6语法的插件了,我们需要的插件,另有其功能,那么其实我们大部分的项目,只需要一个插件就好了,那就是@babel/plugin-transform-runtime,这个插件的作用和配置,后文也会进行详细的说明。

预设与插件其他使用说明

@babel/preset-env其实有个简写,可以省略preset-,直接写成@babel/env,而插件也有简写,可以省去plugin-,不过还是推荐使用全程,只是有些人会用简写,我们见到了知道怎么回事就行,不然说人家写错了,就闹笑话了。

预设和插件是有一定顺序要求的,如果插件和预设同时处理一段代码,那么就要遵循下边的规则:

  • 插件比预设先执行
  • 插件执行顺序是plugins数组从前向后执行
  • 预设执行顺序是presets数组从后向前执行

接下来,在将预设的配置和插件的使用配置之前,我们要先了解一个重要的概念,那就是时常听说的polyfill,很多人多polyfill的理解是模模糊糊,认为不是有babel语法转换就好了么,polyfill又是干嘛的呢?别急,我们接下来会进行详细的讲解。

polyfill概念与使用方法

什么是polyfill呢?从英文单词的含义来说,是一种填充物;我们可以把他理解为常说的'腻子',就是在装修刷墙的时候刷的那层白色的粉,有些地方坑坑洼洼,就需要抹上腻子,进行填平,风干之后,墙面就变得平整了。那么在我们前端来说,polyfill就是为了补齐ES6等高版本的API,啥意思呢?同样我们还是举个栗子:

// 我们babel.config.js文件中,只保留@babel/preset-env的预设,清空before.js中代码,做如下替换
let promise = Promise.resolve('success')
let obj = Object.assign({}, { age: 33 })

然后执行npm run babel命令,诶,很奇怪,只有let被转译成了var,但是Promise和Object.assign并没有被转译,如果我们把这个js代码,放到老版本的浏览器中运行,就会直接报错不识别这两种方法,为啥Babel没有把他们转译呢?

var promise = Promise.resolve('success');
var obj = Object.assign({}, {
  age: 33
});

好多人把语法和API的概念弄混了,Babel默认只是转化新的语法而并不转译新的API,比如之前提到的箭头函数,可选链,模板字符串等;而新的API大致分为两类,一类是Promise、Map、Symbol、Proxy等全局对象及其对象自身的方法,例如Object.assign,Promise.resolve;另一类是新的实例方法,例如Object.assign(),Array.prototype.find(),Array.prototype.flat()等方法。

如果我们想让这些新的API在版本比较老的浏览器中运行,那么就要使用polyfill来进行兼容,因为你不知道用户会用什么版本的浏览器。

那么该如何引入polyfill呢?其实polyfill并不是只一个文件,而是一个简称,我们使用最多的polyfill方式大致分为两类,一:引入已经构建打报好的polyfill.js文件;二:未构建,需要使用npm安装@babel/polyfill并且借助类似于webpack的打包工具,而@babel/polyfill的核心是core-js与regenerator-runtime这两个npm包组成的,在@babel/core7.4.0版本之后,已经不推荐引入@babel/polyfill这个包了,综上,引入polyfill的方法大致上有以下7种:

  1. 直接在html文件引入Babel官方的polyfill.js脚本文件;
  2. 在前端工程的入口文件里引入polyfill.js;
  3. 在前端工程的入口文件里引入@babel/polyfill;
  4. 在前端工程的入口文件里引入core-js/stable与regenerator-runtime/runtime;
  5. 在webpack等打包工具配置文件入口项引入polyfill.js;
  6. 在webpack等打包工具配置文件入口项引入@babel/polyfill;
  7. 在webpack等打包工具配置文件入口项引入core-js/stable与regenerator-runtime/runtime;

在详细讲解这7种方法之前,我们需要简单的安装配置下webpack,做好准备工作:

// step 1:安装webpack和webpack命令行工具
npm install webpack webpack-cli -D
// step 2:配置webpack.config.js文件
const path = require('path')
module.exports = {
  entry: './before.js',
  output: {
    clean: true,
    path: path.resolve(__dirname, 'output')
  },
  devtool: false,
  mode: 'development'
}
// step3: 配置package.json文件中script命令
{
  "script: "webpack"
}

方法1:直接在index.html文件中使用CDN或本地路径引入:

<!-- 使用CDN -->
<script src="https://cdn.bootcdn.net/ajax/libs/babel-polyfill/7.12.1/polyfill.js"></script>
<!-- 下载polyfill.js然后本地引用,例如:-->
<script src="./polyfill.js"></script>

方法2:在before.js文件(作为入口文件)顶部直接引入:

// 下载完polyfill.js文件后,引入到before.js中
import './polyfill.js'

方法3:先安装@babel/polyfill包,然后在入口文件引入:

// step 1: npm install @babel/polyfill -S
// step 2: before.js作为入口文件,顶部引入:
import '@babel/polyfill'

方法4:从@babel/core 7.4.0版本开始官方已经不再推荐使用@babel/polyfill包了,因为@babel/polyfill包其实就是core-js和regenerator-runtime这两个包组成的(可以查看@babel/polyfill的package.json进行验证),@babel/polyfill中用的core-js版本是2.0版本,以后也不会再升级更新,而2.0版本是不能够被转化一些API的,比如Array.prototype.flat()方法和Array.prototype.includes()方法,所以我们需要安装core-js的最新版本和regenerator-runtime这两个包:

// step 1:npm安装包
npm install core-js regenerator-runtime -S
// step 2:在入口文件顶部导入
import 'core-js/stable'
import 'regenerator-runtime/runtime'

注意:如果你的@babel/core版本是7.18.0以上的,则可以不安装regenerator-runtime并且不导入这个包,babel会自动将这个包中的代码添加到转译后的文件里,可以通过执行npm run babel查看转译结果进行验证;但是如果你的@babel/core版本是7.18.0以下,那么在你的代码中如果使用了async或generator语法,那么一定要安装并导入regenerator-runtime 这个包;后续示例,均以最新版本的配置来示范。

方法5:下载好polyfill.js文件后,在webpack配置项进行操作:

const path = require('path')
module.exports = {
  // 入口文件处做修改
  entry: ['./polyfill.js', './before.js'],
  output: {
    clean: true,
    path: path.resolve(__dirname, 'output')
  },
  devtool: false,
  mode: 'development'
}

方法6:和方法5类似,npm install @babel/polyfill之后修改入口文件

const path = require('path')
module.exports = {
  // 入口文件处做修改
  entry: ['@babel/polyfill', './before.js'],
  output: {
    clean: true,
    path: path.resolve(__dirname, 'output')
  },
  devtool: false,
  mode: 'development'
}

方法7: 和之前说的相似npm install core-js -S之后,在入口文件处修改

const path = require('path')
module.exports = {
  /* 入口文件处做修改,同样,如果@babel/core版本低于7.18.0,代码中使用了async,generator语法,
  一定要安装和引入regenerator-runtime包 */
  entry: ['core-js/stable', './before.js'],
  output: {
    clean: true,
    path: path.resolve(__dirname, 'output')
  },
  devtool: false,
  mode: 'development'
}

我们总结一下polyfill的引入方法,因为之前提到过的@babel/core在7.4.0之后,官方不再推荐使用@babel/polyfill包了,所以,我们最终的取舍是使用方法4或者方法7的方式来引入polyfill,但无论是方法4还是方法7,都是对polyfill文件进行全部的导入,什么意思呢?比如我们代码中没有使用到Promise,但是全部导入的话,还是会将Promise转译的API导入进来,因为polyfill完整的文件非常的大,所以就会大大增加了我们项目打包后的体积,那有什么办法么?当然有,那就是按需引入,我们后边会详细的讲解。

总的来说,polyfill的含义有很多,可以是polyfill.js这个文件,也可以指@babel/polyfill这个包,还可以指core-js和regenerator-runtime这两个包,我们见到polyfill,只需要明白,他的作用就是补齐浏览器缺失的API即可。

通过预设的配置,来进行polyfill的按需移入

前文我们已经提到过@babel/preset-env这个预设如何使用了,那么其实他还有第二个参数,通过对第二个参数中配置的修改,我们可以做到对polyfill按需引入等一些列灵活性配置,写法如下:

// babel.config.js文件
module.exports = {
  // 注意这里是一个二维数组,对象中可以添加属性
  presets: [["@babel/preset-env", {
  
  }]],
  plugins: []
}

我们常用的属性有targets,useBuiltIns,modules和corejs这四个配置项,剩下的属性我们几乎用不到,把这4个属性弄明白了,你基本就已经超过大多数开发者了。我们分别来讲这几个属性的作用与配置:

(1)targets

讲targets属性作用之前,我们需要了解一个预备的知识。如果你的项目是用Vue或React官方脚手架进行生成的,那么一定见过.browserslistrc这个文件,或者在package.json中有个browserslist的配置,从名字上我们不难发现,这个文件是目标环境配置表,也就是说通过配置,可以让我们的代码运行在哪些指定的浏览器或node环境,比如下边这种配置的意思就是,市场份额大于1%的浏览器,并且不考虑ie10以下版本的浏览器,并且不是已'死亡'(指24个月没有官方的更新)的浏览器。

// .browserslistrc 文件
> 1%
not ie <= 10
not dead

那么通过读取这个.browserslistrc文件的配置,我们的Babel可以对相应版本进行代码的转译;像webpack中配置的Autoprefixer,Postcss等插件,也可以读取这个文件的配置,对相应环境进行添加-webkit-的前缀进行Css的兼容。

那么第一个参数targets里边就可以填写.browserslistrc中的内容,效果是一样的,我们先设置targets的环境为一个比较老的chrome浏览器30版本:

// babel.config.js文件中配置
module.exports = {
  // 注意,preset如果要添加配置的话,是一个二维数组!
  presets: [["@babel/preset-env", {
    targets: {
      chrome: 30
    }
  }]],
  plugins: []
}
// before.js中的代码
let func = age => {
  console.log(`age:${age}`)
}

之后我们执行npm run babel命令进行转译,发现after.js中结果将箭头函数和模板字符串语法都进行了转译

var func = function func(age) {
  console.log("age:".concat(age));
};

接下来我们把targets中chrome的版本改成个比较新的版本,比如100,然后再次执行npm run babel,命令,发现结果和before.js文件中的代码,竟然一模一样,难道是出问题了么?并不是的,因为在chrome100这个版本中,已经能够支持let语法和箭头函数了,所以,就不用多此一举再进行ES6向低版本语法的转译了。其实一般在项目中,我们都会把这个配置,单独写在.browserslistrc文件或package.json文件中,正如前边所说,这样,其他的插件都会从这个配置文件中读取环境信息,进行相应的逻辑处理,如果没有这个文件,那么Babel就会把所有的ES6语法转化成ES5等低版本语法。

(2)useBuiltIns

很多小伙伴很奇怪,为啥要把@babel/preset-env配置项的讲解放在polyfill栏目中,而不是紧接着放在@babel/preset-env的使用讲解完毕之后。是因为只有先讲解了什么是polyfill,我们才能够去理解预设的参数,是什么意思,那么我们业内前端项目引入polyfill的方法,除了上文提到的全部引入之外,按需引入一般是有2种方案,对应着各自适合的场景,一种是需要借助@babel/preset-env配置项useBuiltIns等配置项,另一种是下文会提及的@babel/plugin-transform-runtime插件,我们一步一步来。useBuiltIns有三种取值,entry,usage和false,默认为false;

如果useBuiltIns设置成false的话,那么Babel就不会进行任何polyfill操作,在低版本浏览器中使用新的API,同样会报错,这个我们不多提;

如果useBuiltIns设置成entry的话,我们需要在入口文件处(before.js为例)或者webpack配置文件的入口(之前讲的方法7)引入core-js(同样如果@babel/core版本低于7.18.0的话,还需要安装和引入regenerator-runtime包),具体操作如下,我们这里姑且先将corejs配成3,如果没有这个配置,那么在进行转译的时候,就会抛出警告,让我们填写corejs的版本:

npm install core-js -S
npm install regenerator-runtime -S (根据@babel/core版本判断是否需要安装)
// babel.config.js
module.exports = {
  presets: [["@babel/preset-env", {
    useBuiltIns: 'entry',
    corejs: 3
  }]],
  plugins: []
}
// before.js,在第一行引入'core-js/stable'
import 'core-js/stable'
let promise = Promise.resolve('success')
//.browserslintrc文件中,我们配置一个chrome浏览器的低版本
chrome 60

之后我们运行npm run babel命令,查看转译后的结果,发现require了好多的包,我就截了一部分,同时因为我们使用了chrome 60版本不兼容的Promise,所以相应的polyfill也被require进来了

pro.png

很多心细的小伙伴就会发现一个问题,为啥会有这么多包被引入了进来?我明明只用了一个Promise,但是看打包结果,好多字符串和数组的新增API方法的polyfill也被require进来了,没错,这就是配置成entry的特性:Babel会根据我们.browserslistrc文件中的环境配置(这里是chrome 60版本),来补齐chrome 60版本所有不支持的新增ES6API,引入chrome60版本所有的polyfill,不管你的入口文件中有没有用到相应的API。 可以尝试改变不同chrome或其他浏览器版本,进行编译,查看结果。

如果useBuiltIns设置成usage的话,需要进行的配置便非常之简单,需要修改的配置如下:

npm install core-js -S
npm install regenerator-runtime -S (根据@babel/core版本判断是否需要安装)
// babel.config.js
module.exports = {
  presets: [["@babel/preset-env", {
    useBuiltIns: 'usage',
    corejs: 3
  }]],
  plugins: []
}
// before.js文件,不用import任何包了
let promise = Promise.resolve('success')
// .browserslistrc文件还是使用chrome 60版本,保持不变

再次使用npm run babel进行转译,发现这时候转移后的结果变得清爽了很多,因为我们只用到了Promise,所以就只把Promise进行了polyfill补齐。那么我们便知道usage的作用,仅仅会引入我们配置目标环境中,缺失并且入口文件中用到的API,并且剔除没有用到的冗余polyfill。

image.png

(3)corejs

这个参数是让我们选择使用core-js2还是core-js3版本,这里强烈推荐使用3版本,2版本对有些新的API无法进行polyfill,比如数组的flat方法:

corejs可以配置成字符串,如上边所示
corejs: 3 代表用core-js@3版本进行polyfill,那么我们需要手动npm install core-js -S来安装此版本
corejs: 2 代表用core-js@2版本进行polyfill,需要我们手动npm install core-js@2 -S来安装此版本
同时,corejs也可以配置成对象
corejs: {
  version: '3.25.2', //这里还可以具体指定版本,可以保持最新,单独配置也可以这样写
  proposals: true //默认为false,为true则会开启对题提案语法的polyfill,比如数组的flat方法,配置true才会被polyfill
}

这里我们推荐将corejs配置成对象,并将proposals设置为true,这样就能在代码中尽情的使用最新的语法了。

(4)modules

可以配置成"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为auto,这个参数的含义是,是否将ES6模块化语法转译成其他的语法,设置为false的话,转译的结果就还是ES6模块化语法,就是import,export default这种,不会使用require(CommonJs语法)这种进行导入导出,如果使用webpack或rollup等打包工具,推荐将modules设置为false,这样的话就有利于打包工具进行静态分析,从而做tree shaking等优化操作,如果不明白是啥意思的话,可以先这样配,之后我的webpack原理文章会讲到相关知识,到时候可以再关注。

讲完这4个常用属性,我们稍作总结:不难发现,targets为配置需要转译的目标环境,我们一般在.browserslistrc文件或package.json文件中配置,只需要了解这个参数有什么用即可;进行polyfill功能的,就是core-js这个包干的活,也就是corejs这个配置项;那么useBuiltIns配置项,作用就是选择用什么方式进行polyfill,false的话,就没有进行polyfill,需要手动引入全部的polyfill文件(极其不推荐),entry的话就是根据当前配置的运行环境,来根据其缺失的API进行polyfill补齐,usage的话,就是比较智能的引入polyfill,代码中用了哪些新的API,就只引入相应的polyfill。

所以使用@babel/preset-env进行polyfill的方法,就是useBuiltIns配成entry和usage这两种,那么有的小伙伴会说了,usage这么智能,直接用不就完事了,干嘛还用entry这种方法呢,这不是引入了好多没有用到的polyfill么?其实不然,entry当然有他的作用,如果你的项目中安装了一个npm包,里边用了Promise并且没有经过转译就发布了,那么如果你的项目里配置成了usage,而又恰好没有用到Promise,这不就出问题了么,转译完代码后,因为第三方包里使用了没有polyfill的Promise,可能有的低版本浏览器就直接报错了。

所以对于最种包体积不是很在意其大小的项目里,还是建议使用entry的方式来进行polyfill,这样就可以避免很多第三方模块因为没有polyfill从而导致的奇奇怪怪报错

那么不管是用@babel/preset-env的useBuiltIns配置成哪种方式进行polyfill,都会有个问题,那就是全局污染问题,什么意思呢?我们可以打开node_modules中core-js的某一个polyfill方法,这里以es.array.finds.js文件举例,内容如下图所示:

image.png

我们可以看到框出来的这块代码,这段代码的含义其实就相当于

Object.defineProperty(Array.prototype,'find', {
  value: // 自己实现的find函数
})

本质上是给Array原型上,添加了浏览器不支持的find方法,那么这会有什么问题呢?如果是在我们的自己的项目中采用这种方式进行polyfill,全局变量污染问题不大;但是如果开发了工具库,或者组件库,污染了全局变量,那么用到你这个包的人,如果在自己的项目中对Array.prototype.find方法进行了重写,添加了一些特殊逻辑,那不出所料,因为你库中的Array.prototype.find方法污染了全局,所以就影响到他自己定义的find方法了,可能会直接报错。那么如果我们在开发工具库或组件库的时候,有没有一种方式,可以不对全局对象进行污染呢?当然有,接下来,就到了我们介绍@babel/runtime这个包的作用了。

通过插件的配置,来进行polyfill的按需引入

我们之前曾经提到过,目前常用的插件只需要了解@babel/plugin-transform-runtime即可。在讲解这个插件之前,我们有必要先了解一下@babel/runtime这个包是干啥的。同样我们先进行代码的修改:

npm install @babel/runtime -S
// babel.config.js 我们移除@babel/preset-env的配置
module.exports = {
  presets: ["@babel/preset-env"],
  plugins: []
}
// before.js
class People {
  constructor(name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
let xiaoming = new People('xiaoming')
xiaoming.getName()
// .browserslistrc文件中选择低版本的firfox浏览器
firefox 26

然后执行npm run babel查看转译结果,发现有三个辅助函数被自动加进来进行声明定义了,那么这就会有个问题,在我们开发的过程中,会有很多个文件,如果每个文件中都使用了class语法,那么打包出来的文件,这几个辅助函数,都会在打包的文件上边定义声明了这几个辅助函数,那体积无疑是非常大的,此时我们想到了一个方法,就是有一个包,存放了这些辅助函数,哪个文件用到,就从哪里引入,这样在webpack之类的打包工具在进行打包的时候,就会只引入一次,这样就做到了复用,减少了打包后的体积。

image.png 那么@babel/runtime这个包,就完美的解决了我们想要复用的问题,从node_modules中打开@babel/runtime这个包,我们观察一下不难发现,上边的三个辅助函数,都有相应的文件与之对应,那么问题来了,我们怎么做到将这几个辅助函数,替换成require(xxxxxxx)引入的这种形式呢?这就不得不提到之前说过的@babel/plugin-transform-runtime这个插件了

image.png

那么我们接下来就详细讲解一下这个插件到底有啥作用,我们一定要学会从命名上来理解插件的意思,transform-runtime,从名字上就能看出来个大概意思,这个插件可以移动@babel/runtime这个包,也就是上边我们想要达成的那个效果。其实这个插件的作用不止于此,总结起来,一共有2个作用:

作用一:自动移除语法转换后的辅助函数,使用@babel/runtime/helpers里的辅助函数来替代; 我们还是修改代码去看下:

npm install @babel/runtime -S (前边如果已经安装过可以忽略)
npm install @babel/plugin-transform-runtime -D
// babel.config.js,还是用老方法,在plugin中将其名字添加进去即可
module.exports = {
  presets: ["@babel/preset-env"],
  plugins: ["@babel/plugin-transform-runtime"]
}

还是使用npm run babel命令,查看打包结果,我们可以发现,在这次打包结果的顶部,之前那三个辅助函数主体,没有被定义了,而是通过require的方法,从@babel/runtime包中进行了导入,完全符合我们的预期。

image.png

作用二:通过配置其配置项,进行代码的polyfill,和我们之前提到过的@babel/preset-env配置项很是类似; 配置插件的方法和配置预设的方法是一样的,只不过配置参数不同,@babel/plugin-transform-runtime常用的配置有:helpers,corejs,regenerator,useESModules,absoluteRuntime,version。

helpers:是否需要引入辅助函数包,默认为true,这个肯定要引入的,不然我们不是用了个寂寞么;

corejs,regenerator:和@babel/preset-env的配置类似的,都是控制用那个版本进行polyfill,只不过不同的是包的名字有一些小小的变化,如果用3版本,那么需要npm install --save @babel/runtime-corejs3用2版本,需要npm install --save @babel/runtime-corejs2,老规矩这里还是强烈推荐使用3版本,如果不配置corejs,即默认值为false的时候,那么就没有启用polyfill功能,而仅仅是能够将辅助函数从@babel/runtime中进行引用;regenerator保持默认值为true即可;

absoluteRuntime:用来自定义@babel/plugin-transform-runtime插件引入@babel/runtime/模块的路径规则,我们保持默认值false就好了; useESModules:设置是否使用ES6的模块化用法,取值是布尔值。默认是fasle,在用webpack等打包工具的时候,我们可以设置为true,以便做静态分析。官方文档上边写着此选项是被弃用的,不过经过我本地测试的时候,发现这个属性依旧是生效的,配成true的时候,会引入@babel/runtime的ESM的路径,看来目前还是没有被移除掉; version:这个配置和我们安装的@babel/runtime,@babel/runtime-corejs2,@babel/runtime-corejs3的安装版本有关系,根据上边corejs的配置,这三个包我们只需要选择一个安装即可,然后把安装那个包的版本写在version中,其实version这个配置项我们不填,默认就好,填上版本号主要是为了能够减少打包体积;

那么根据上述讲解,我们可以得到一个使用@babel/plugin-transform-runtime插件进行polyfill的打包配置,即:

我们之前提到过@babel/preset-env配置项里进行polyfill,会对全局对象进行污染,那为什么使用了@babel/runtime和@babel/plugin-transform-runtime之后,就能解决这个问题呢?我们可以修改下代码,然后执行npm run babel命令查看转译结果:

// babel.config.js
module.exports = {
  presets: ["@babel/preset-env"],
  plugins: [["@babel/plugin-transform-runtime", {
    corejs: 3
  }]]
}
// before.js
let a = [1, 2, 3]
a.includes(1)

我们可以看到,并不是直接在修改了Array.prototype原型上的方法,而是在'default'这个属性上定义了方法。

image.png

既然@babel/runtime配合@babel/plugin-transform-runtime插件不会污染全局变量,又有类似于useBuiltIns:usage的智能化导入,我们直接一把梭,不管是啥都用这种配置不就好了么?其实不然,官方之所以也提供了这种方法,完全是为了不同的场景来考虑(下文探索最佳方案会详细总结);在Babel7中,已经将@babel/plugin-transform-runtime中的useBuiltIns配置项移除了,所以是不能够像@babel/preset-env中配置使用entry还是usage的方式进行polyfill了,只能实现类似usage的智能化导入polyfill,还是那句话,他有其适用的场景。

探索polyfill不同场景下的最佳解决方案

讲到这里,关于Babel相关的配置,我们就差不多讲完了,看到这里可能对前边的知识有些遗忘,或者混淆了,没关系我们稍作总结,就像之前说的polyfill的方案其实可以分为日常的项目业务场景和自定义类库组件库(比如发布到npm上的包)的两种场景:

日常项目和业务

在日常业务开发中,全局污染的问题并不是那么重要,此时选择@babel/preset-env配置useBuiltIns进行polyfill,同时使用@babel/runtime和@babel/plugin-transform-runtime插件,进行辅助函数转化为引用,是非常不错的配置选择;最终如下:

module.exports = {
  presets: [["@babel/preset-env", {
    // targets不做配置,默认使用我们 .browserslistrc文件中的配置
    // targets: {},
    // 对所用语法进行polyfill
    useBuiltIns: 'usage',
    corejs: {
      // 使用最新版本的core-js进行polyfill
      version: '^3.25.2',
      // 开启草案中的polyfill转译
      proposals: true
    },
    // 使用es6模块进行
    modules: false
  }]],
  plugins: ["@babel/plugin-transform-runtime"]
}

那么至于useBuiltIns该配成entry还是usage,上文已经详细的说明其区别和场景利害,不要感觉usage有灵性,就任何场景一把梭。

类库与组件库

在开发我们的类库和组件库的时候,对于全局污染问题,需要尽量去避免,那么此时我们可以选择只使用@babel/preset-env的转译语法功能,不做任何配置,同时借助@babel/plugin-transform-runtime插件的corejs配置项,进行polyfill,具体配置如下:

// babel.config.js
module.exports = {
  presets: ["@babel/preset-env"],
  plugins: [["@babel/plugin-transform-runtime", {
    corejs: {
      // 注意,要安装@babel/runtime-corejs3的包
      version: 3,
      proposals: true
    }
  }]]
}

注意

这两种polyfill方式,官方针对不同场景下提出的解决方案,只能选择一种polyfill,不要在预设和插件中,同时配置corejs开启polyfill,不然可能会导致报错,不管是那种方式,都强烈推荐使用core-js@3或者@babel/runtime-corejs3的版本进行polyfill,这样可以涵盖最新版本API的polyfill。

阅读官方文档实践

为什么要在最后加上这一栏呢?其实也是我以小白的身份,看了这么多篇文章之后的一个心得体会,1.文章的教学说的可能不是很全面;2.随着时间的流逝,文章所说内容可能不适用包的最新版本的内容,比如我现在花了这么一大篇文章来讲解Babel7最新版本的配置,但是过了1年2年,Babel8 9 10可能都出来了,配置又不一样了,那对于大多数想学习Babel的人来说,看到这篇文章的时候,就会直接划走了,所谓授之于鱼,不如授之以渔,好多初中级前端小伙伴痛点之一就是不会看文档,那经过前边的教学,现在的你是不是已经对Babel的配置已经了然于胸了呢?那么基于大家现在这种对Babel算是'有基础的状态',我们现在再一起去读一下文档,当你能够读懂文档之后,不管之后的Babel版本再怎么更新,万变不离其宗,你都能轻松拿捏,也进一步印证了文章之前所写的内容是有所依据的。不要被英语所畏惧,要锻炼自己阅读英文文档的能力,没准能少踩很多的坑(之前被webpack中文文档坑过,查阅了英文文档才发现配置项变了,但中文文档没有及时更新,更何况Babel的文档还是中英混杂的...大概感觉翻译起来也没啥实际收益,所以好多地方还是英文,所以可能也会有很多新的信息不会被及时同步更新)。

预设的讲解:

我们打开Babel的官网babeljs.io/docs/en/ 找到文档,很明显,经过前文的学习,我们很快能找到预设和插件的所在地,前文也提到过,我们现在只需要去了解下@babel/preset-env这个预设,那么就开始吧~ image.png 我们看文档,需要抓重点,先看核心,比如定义,重点配置项,再去看其他内容,文档中对@babel/preset-env的作用解释如下:简单来说,@babel/preset-env就是一个智能化的预设,让我们能够使用最新版本的JavaScript语法,并且无需细颗粒度的管理哪些语法需要根据目标环境进行转译,我们的打包结果会变得更精小,这样我们就能欢快的敲代码了! image.png 我们再简单看下useBuiltIns这个配置项的说明:文档中详细的解释了entry,false,usage的区别,并且进行了示范,其实就是上文中提到过的,现在是不是能看懂了? image.png 我们继续看corejs这个配置项的说明,就和我们前边介绍的一样,介绍了配成string和对象的区别 image.png 其实Babel中关于预设的介绍还有很多,比如项目中是使用了vue-cli脚手架生成的话,那么在babel.config.js配置中,我们可以看到有@vue/cli-plugin-babel/preset这么一条预设,那么其实这个是vue-cli对@babel/preset-env的继承封装,感兴趣的同学可以深扒一下代码,总之,万变不离其宗,了解@babel/preset-env,一切都是纸老虎。 接下来我们再看下关于@babel/polyfill的说明,前文提到这个包已经废弃掉了,那么我就是从这里获取到的这个信息,文档中还介绍了@babel/polyfill的替代方案,经过上文的总结也可以发现,最新版本只需要引入core-js/stable即可。 image.png

插件的讲解:

最后我们再看下@babel/plugin-transform-runtime的说明: 发现里边提及到了如何安装,并且推荐我们使用最新版本的corejs进行polyfill,还提到了@babel/plugin-transform-runtime的作用,详细的内容可以自行去文档相应位置阅读。 image.png

image.png 相信随着我们对Babel文档大体结构的介绍,你对文档的畏惧,就不是那么的大了,反而可能会感觉,文档写的是真的太好了,你这篇文章前边废话一大堆,都不如人家官方文档几句话描述的全面和清楚

error

其实有这种心理是很正常的,在我弄懂Babel配置之前我也是花了好几天,查阅了各种资料,看遍了各种各样的文章,最后再去看Babel官方文档的时候,反而有种 就这?早知道官方文档写的这么清楚,我干嘛费那么大的力气去查资料看文章呢,直接看文档不就好了嘛,用不了一天就能学完。但这是种典型的马后炮行为,回想起最开始啥也不懂的时候,看文档那么多文字,也不知道哪条有用哪条没用,也没有看下去的兴趣,非常枯燥,反而是先看一些比较优秀的文章,有了基础后再去看文档,就知道哪些是重点,哪些是不常用不需要过多去关注的,同时还能根据文档查缺补漏。

别小看这小小的转变,如果你没有一个好的入门方式,而是从头到尾硬磕文档,大概率你是坚持不下去的,一直在浪费时间,所以回到文档开头说的,我认为,对于我们普通人来说,学习技术最好的方式就是找到靠谱的方法来带你入门,之后再凭自己的能力和本市深入研究和挖掘,而我,对于小白时期所经历的种种弯路和心酸的那种无依无靠,太了解了,所以今后也会继续坚持写一些既适合新手入门教学,又适合老手来回顾查缺补漏的文章~

文末

前前后后花了几个晚上和一个周末的时间,终于把这篇文章写完了,这个赛季王者都空没上50星就结束了,果然还是太菜了...如果你觉得文章不错 不妨点个赞,或者收藏一下,让这篇文章走进你的进收藏夹,吃灰去吧~

参考文章(感谢大佬们的无私奉献):

「前端基建」探索不同项目场景下Babel最佳实践方案

Babel教程