再学babel配置

babel 每次学习都有新的理解,哪怕是其配置都与我们前端生态中的各种概念息息相关。近期再次复习 babel 知识从而更好的编写 js 类库,本文是学习过程所做的记录。

babel 生态里的一些 npm 模块

  • babel/core 核心转义功能
  • babel/cli 命令行工具,可以通过 babel 命令来转换代码,或者转换文件,输出转义后的文件。如 babel src –out-dir lib –watch
  • babel/node 是用他来执行 es6+代码,以 node 方式执行。适合于去执行你写的 nodejs 代码。

那么,我们使用 webpack 来打包 js 代码时,是使用的什么呢?答案是 babel-loader。

babel-lodaer 是适配 webpack 的一个模块,其内部就是调起 babel/core 来实现代码转译。

理解转译其实有 2 种类型的转译

记住:babel 自身唯一能做的事情叫:代码转译。
至于新增 api 那些,那是需要引入 polyfill 垫片的事情(下文会讲)。

babel 本身所做的事情,一般都叫他代码转译。那么所谓的转译都有哪些种类呢?如下:

1、单纯的语法解析和转换。例如箭头函数转成 function
2、需要一点辅助的语法解析和转换,例如 class 语法,给你转成 prototype 原型语法

解释下第二点:我们知道,一种新的语法意味着新的一些关键字和符号等,那么对于一些特殊符号如箭头函数,那么他恰好有 es5 对应的 function 关键字,所以 babel 只需简单换一下字符。

然而有些却不是简单的换一下,例如 class 语法,babel 给你转成 es5 之后,他必须改造成函数和 prototype 的写法,在这种情况下,babel 编译后的代码中会加入一些辅助函数(也就是下文说的 babel/runtime 所干的事儿)以协助来用 es5 的方式实现 class。

举个栗子:
image.png

看到了吗,babel 生成的原型式写法中,需要在构造函数中对调用者的实例化方式进行检测,这些都封装成了 helper 函数 _classCallCheck。

除此之外,还有更复杂的场景 babel 转译之后甚至需要依赖 polyfill 垫片:例如 async 转换。 babel 会把 async 语法代码转成 generator 辅助函数,而 generator 辅助函数的功能需要依赖 regenerator-runtime 这个 polyfill 垫片。如下图,babel 给你把 async 语法转换后,多了很多辅助函数;甚至其中有一个 regeneratorRuntime 的函数是没有找到定义的,而这个函数其实就是依赖全局需要引入 regenerator-runtime 这个 polyfill 才行。

image.png

理解什么是 corejs 和 babel/runtime

首先,我们要理解什么是 polyfill 什么是辅助函数。

像上文所说的 2 种 babel 转译,其中转换之后所出现的那些辅助函数,叫做运行时所需要的 helper,他们其实就是 runtime。 而这些运行时函数,有一个单独的 npm 包去实现他们,叫做 babel/runtime。

而像那些 es6 新增的 API,例如 Promise、Map、WeakMap。数据新方法 flat、includes 等等 api,这些东西不属于 babel 语法编译的范畴,是属于一些新的 api。这些叫做 垫片,英文名叫 polyfill。在社区里,polyfill 垫片通过另外 2 个 npm 包来实现:一个叫做 corejs(用来实现除了 generator 之外的垫片,现在要使用他的版本 3),另一个叫做 regenerator-runtime(用来实现 generator 垫片)。

实战:配置 babel 的最佳实践

其实最佳实践的前提就是正确的理解上文中两个概念。从而正确的使用 babel/runtime 和 corejs3.

如果比喻成做饭,babel/runtime 和 corejs3 就是我们的食材。有了食材,我们就要用一个配套的刀具来加工他们。

  • babel/runtime 配套的刀具是:babel/plugin-transform-runtime
  • corejs3 的配套刀具是 preset/env 预设。

我们接下来所讲的实践,其背景是在 webpack + babel 下的实践。因为,毕竟我们通常不会单据来用 babel 操作一个 js 文件。

webpack 打包一个网站应用 js 时

这种场景的要求是:

  • 按需转译语法和辅助函数。例如如果我的目标平台支持了箭头函数,那么 babel 请不要给我转换成 es5 那种语法。
  • 全局 polyfill 垫片即可,不怕污染,因为我期望面向我产品所有用户使用的浏览器
  • 尽可能的少打包垫片。即按需 polyfill。因为我们期望尽可能减少不必要的体积。比如我面向的用户浏览器大于 IE10,那么 IE9 的 polyfill 不要打;同时,如果我代码中没有使用 promise,那么 promise 的垫片也不要给我打
  • helper 那些辅助函数,不要每个 js 模块中都写一份(因为 babel 本身肯定是针对每个 js 编译的,所以默认每个 js 肯定都会出现辅助函数)

解决方法:

1、 为了能按需转译语法。在最新 babel7 之后,我们只需使用 preset/env 预设就很简单了。

1
2
3
4
5
6
7
8
9
10
11
12
// babel.config.js
module.exports = {
presets: [
[
"@babel/env",
{
targets: "last 50 Chrome versions",
useBuiltIns: false, // polyfill配置先关掉,后面再讲
},
],
],
};

我们可以通过修改 targets ,来决定 babel 编译哪些语法。例如你把 chrome 版本号调到最新 1 个版本,那么箭头函数必然是不会转换的。

2、 3、按需加载 polyfill

按需加载 polyfill 有 2 种方式,一种是把 useBuiltIns 配置成 entry,另一种是配置成 usage.

使用上的区别是:

  • entry:需要你手工在你的 webpack 入口 js 里,引入一下 corejs 和 regenerator-runtime 这俩 polyfill。babel 编译后,会自动在入口 js 里,把你那 2 行换成面向目标 targets 按需引用的 corejs 模块。
  • usage:不需要你手工引入。babel 会自动把 corejs 库的模块放到你的每个 js 模块里。放置的原则就是:不仅面向目标 targets 来按需引入,而且还按照你代码中是否使用来引入。假如你的 a.js 里用了 promise,那么他会把 corejs 中 promise 模块引入。

如下是 usage 的方式:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
presets: [
[
"@babel/env",
{
targets: "last 50 Chrome versions",
useBuiltIns: "usage",
corejs: 3, // 写死就好!当前阶段就该用3
},
],
],
};

如下是 entry 方式

1
2
3
4
5
6
7
8
9
10
11
12
13
//babel.config.js
module.exports = {
presets: [
[
"@babel/env",
{
targets: "last 50 Chrome versions",
useBuiltIns: "entry",
corejs: 3,
},
],
],
};

entry 模式时,需要在代码里手工引入一下。

1
2
3
4
// index.js
// 手工引入
import "core-js/stable";
import "regenerator-runtime/runtime";

既然 usage 这么好,我们何苦要用 entry 呢? 所以,就用 usage 模式吧!

4、问题来了:怎么解决辅助函数的冗余问题。

babel/runtime 是干啥的,要杂用?

上文的配置,我们解决了 polyfill 的问题。下一步,我们要解决辅助函数在每个 js 里都有冗余的问题。因为现在的配置下,如果你有 a.js 和 b.js,那么两个 js 中都会被 babel 放入那些 helper 辅助函数。当 webpack 打包完成一个 bundle。学过 webpack 原理的应该指到:最终的 bundle 里也会有 a.js 和 b.js 模块,每个模块中必然也存在重复的辅助函数。

怎么解决? 那就用 babel/plugin-transform-runtime 插件。
这个插件的工作就是:在 babel 编译每个 js 时,把里面的辅助函数给换成 对 babel/runtime 的 require 引用。

当每个 js 里的辅助函数,都变成 babel/runtime 的引用。那么 webpack 打包后的 bundle,必然这些辅助函数就变成一个公共模块了,解决了冗余问题。

配置方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = {
presets: [
[
"@babel/env",
{
targets: "last 50 Chrome versions",
useBuiltIns: "usage",
corejs: 3,
},
],
],
plugins: [
[
"@babel/plugin-transform-runtime",
{
helpers: true, // 这就是抽离helper 的配置
corejs: false, // 先设置false,后面再讲
regenerator: false, // 先设置false,后面再讲
},
],
],
};

上面把 helper 配置为 true,就可以实现 helper 函数抽到 babel/runtime。

至此,网站打包讲解配置完成!开发网站时就用上述配置即可。

webpack 打包一个 jssdk 时

开发 jssdk 跟开发网站对 bundle.js 有不同的要求。

  • 我们期望我们的 sdk 可以支持很多 target 环境,因此需要 polyfill 垫片。但我们不知道 jssdk 的运行环境是否有垫片。这里有 2 个方法,方法 1:通过文档告诉开发者你需要加哪些垫片;方法 2:我们自己 polyfill,但是不能污染全局
  • jssdk 如果自己进行 polyfill。那当然也希望按需 polyfill,减少体积
  • 辅助函数同样需要减少冗余,跟上文网站相同。

可以显而易见,对于开发 jssdk,我们第一要做的,就是先把全局 polyfill 给他关掉。比如关掉那个 usage:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
presets: [
[
"@babel/env",
{
targets: "last 50 Chrome versions",
useBuiltIns: false,
},
],
],
};

幸运的是,jssdk 的另外几个要求,靠 babel/plugin-transform-runtime 都可以搞定。如下配置即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
presets: [
[
"@babel/env",
{
targets: "last 50 Chrome versions",
useBuiltIns: false,
},
],
],
plugins: [
[
"@babel/plugin-transform-runtime",
{
helpers: true, // 这就是抽离helper 的配置
corejs: 3, // 这是局部polyfill的配置
regenerator: true, // 这是局部polyfill generator的配置
},
],
],
};

我们只需把该插件的配置全开。把 preset/env 的 polyfill 配置关掉,就是一个适配 jssdk 开发的配置了。