乐趣区

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

标题党了哈哈哈~~~
原文地址
相信很多人和笔者从前一样,babel 的配置都是从网上复制黏贴或者使用现成的脚手架,虽然这能够工作但还是希望大家能够知其所以然,因此本文将对 babel(babel@7)的配置做一次较为完整的梳理。
语法和 api
es6 增加的内容可以分为语法和 api 两部分,搞清楚这点很重要,新语法比如箭头函数、解构等:
const fn = () => {}

const arr2 = […arr1]
新的 api 比如 Map、Promise 等:
const m = new Map()

const p = new Promise(() => {})
@babel/core
@babel/core,看名字就知道这是 babel 的核心,没他不行,所以首先安装这个包
npm install @babel/core
它的作用就是根据我们的配置文件转换代码,配置文件通常为.babelrc(静态文件)或者 babel.config.js(可编程),这里以.babelrc 为例,在项目的根目录下创建一个空文件命名为.babelrc,然后创建一个 js 文件(test.js)测试用:
/* test.js */
const fn = () => {}
这里我们安装下 @babel/cli 以便能够在命令行使用 babel
npm install @babel/cli
安装完成后执行 babel 编译,命令行输入
npx babel test.js –watch –out-file test-compiled.js
结果发现 test-compiled.js 的内容依然是 es6 的箭头函数,不用着急,我们的.babelrc 还没有写配置呢
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 配置项即可,值可以是 entry 和 usage,假如是 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-js 和 regenerator-runtime 这两个包,这两个包提供了 es6+ 规范的运行时环境。因此当我们不需要按需 polyfill 时直接引入 @babel-polyfill 就行了,它会把 core-js 和 regenerator-runtime 全部导入,当我们需要按需 polyfill 时只需配置下 useBuiltIns 就行了,它会根据目标环境自动按需引入 core-js 和 regenerator-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 的配置差不多已经够了,你可能需要添加一些特定用途的 Plugin 和 Preset,比如 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
以上基本是本文的全部内容了,最后再来个总结和需要注意的地方:

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

… 待补充

全文完

退出移动版