Guide To Babel in 2021

Introduction

Babel 在 2021 年一共进行了 2 个minor版本的更新,增加了一些Stage 4 proposals的支持,以及一些Top-level的配置项(targets, assumptions)。伴随着这些更新,结合babel-loaderbabel-preset-react-app我们来探究一下在 2021 年该如何使用 Babel。

@babel/preset-env

@babel/preset-env是官方推荐的preset,只需要配置相关的targets就可以转换当前代码到目标环境的代码,遵循browserslist的相关配置,主要配置项如下:

targets

配置目标环境,如果不指定,则会转换所有 ES2015-ES2020 的代码到 ES5.而不是使用 browserslist 的defaults配置(> 0.5%, last 2 versions, Firefox ESR, not dead)。

useBuiltIns

配置@babel/preset-env如何处理polyfills,可选项为"usage"|"entry"|false

"entry"

这个配置会自动将import "core-js/stable";import "regenerator-runtime/runtime"转换为目标环境的按需引入,举个例子:

1
2
import 'core-js/stable';
import 'regenerator-runtime/runtime';

在不同环境下可能转换为:

1
2
import 'core-js/modules/es.string.pad-start';
import 'core-js/modules/es.string.pad-end';

但是有个缺点是用不到的 polyfill 也可能会引入进来,因为entry配置只针对目标环境,而不是具体代码

"usage"

这个配置则会自动引入代码中需要的 polyfill,且不需要显示声明import core-js,推荐使用该配置

false

不自动添加 polyfill,也不自动转换import core-js为按需引入

corejs

当 useBuiltIns 配置项为entryusage时生效,默认值为"2.0",建议配置为minor version的具体版本号

其它配置诸如includeexclude详见Options

@babel/runtime

@babel/runtime与其配套的@babel/plugin-transform-runtime主要有三个配置项,分别对应不同场景:

regenerator

使用 generator/async 函数时自动引用@babel/runtime/regenerator。默认值为true,默认开启

1
function* foo() {}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
'use strict';

var _regenerator = require('@babel/runtime/regenerator');

var _regenerator2 = _interopRequireDefault(_regenerator);

function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}

var _marked = [foo].map(_regenerator2.default.mark);

function foo() {
return _regenerator2.default.wrap(
function foo$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
case 'end':
return _context.stop();
}
}
},
_marked[0],
this
);
}

corejs

按需引入相关@babel/runtime-corejs的 helpers,避免生成污染全局空间和内置对象原型的代码,常用于开发类库/工具。可选值为false | 2 | 3{ version: 2 | 3 , proposals: boolean}格式,配置为false则不引入相关helpers

1
2
3
4
5
6
7
var sym = Symbol();

var promise = Promise.resolve();

var check = arr.includes('yeah!');

console.log(arr[Symbol.iterator]());

输出

1
2
3
4
5
6
7
8
9
10
11
12
import _getIterator from '@babel/runtime-corejs3/core-js/get-iterator';
import _includesInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/includes';
import _Promise from '@babel/runtime-corejs3/core-js-stable/promise';
import _Symbol from '@babel/runtime-corejs3/core-js-stable/symbol';

var sym = _Symbol();

var promise = _Promise.resolve();

var check = _includesInstanceProperty(arr).call(arr, 'yeah!');

console.log(_getIterator(arr));

helpers

自动移除 inline 格式的Babel helpers并替换为引入模式,好处是移除了冗余重复的代码。默认值为true,默认开启

1
class Person {}

通常情况下的转换结果为:

1
2
3
4
5
6
7
8
9
10
11
'use strict';

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function');
}
}

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

开启之后转换结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';

var _classCallCheck2 = require('@babel/runtime/helpers/classCallCheck');

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}

var Person = function Person() {
(0, _classCallCheck3.default)(this, Person);
};

babel-loader

babel-loader 除了支持 babel 相关的所有options,还增加了相关的缓存支持,相关的缓存配置主要如下:

cacheDirectory

默认值为false,如果设置为true或者其他地址,那么 webpack 后续的 build 会尝试从缓存中读取之前内容,避免了 babel 的重新编译环节

cacheIdentifier

默认值为@babel/core的版本,babel-loader的版本,以及.babelrc 的内容合在一起的 stringify 值,即:

1
2
3
4
5
cacheIdentifier = JSON.stringify({
options,
'@babel/core': transform.version,
'@babel/loader': version,
});

如果该值改变,则会强制刷新缓存

cacheCompression

默认值为true,设置之后 babel 的缓存结果会被 gzip 压缩

开启缓存如何加快 rebuild?

关于对缓存的支持以加快 rebuild,简单来说有如下几个步骤:

  1. 检测是否配置了cacheDirectory,如果配置,则调用cache()进一步处理,如果没有则直接transform()
  2. 配置了cacheDirectory之后,先根据每个文件内容(source)和配置(options)以及标识符(identifier)三部分内容JSON.stringify之后进行哈希,得到文件名称filename,即调用了filename(source, cacheIdentifier, options),再path.join对应的directory获得缓存文件的绝对路径file
  3. 获得文件的绝对路径之后,尝试读取文件内容,如果读取到说明之前相对应的source已经缓存过,直接返回对应的结果
  4. 没有的则将transform()之后的结果写到对应的文件file下,并将结果返回

感兴趣的可以阅读相关的cache源码

create-react-app

有了以上的基础知识铺垫,我们来看一下create-react-app和 babel 相关的内容是如何配置和处理的

react-scripts

先看一下 react-scripts 中关于 webpack 的babel-loader是如何配置的,配置很多,去掉注释之后如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
babelrc: false,
configFile: false,
cacheIdentifier: getCacheIdentifier(
isEnvProduction
? 'production'
: isEnvDevelopment && 'development',
[
'babel-plugin-named-asset-import',
'babel-preset-react-app',
'react-dev-utils',
'react-scripts',
]
),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
cacheDirectory: true,
cacheCompression: false,
compact: isEnvProduction,
},
},

总结如下:

  1. 引入了babel-preset-react-app,这个 preset 也是 create-react-app 维护的,细节我们后面讲
  2. 自定义了babel-loadercacheIdentifier,确保其值唯一,具体如上所示
  3. 设置cacheDirectory值为true,开启相关缓存
  4. 但是禁用了缓存相关的 gzip 压缩,原因见这个PR
  5. 正式环境开启了compact模式

babel-preset-react-app

babel-preset-react-app对 react 以及 flow 和 typescript 都有所支持,剔除掉这些,具体的配置可以精简如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
{
presets: [
isEnvTest && [
// ES features necessary for user's Node version
require('@babel/preset-env').default,
{
targets: {
node: 'current',
},
},
],
(isEnvProduction || isEnvDevelopment) && [
// Latest stable ECMAScript features
require('@babel/preset-env').default,
{
// Allow importing core-js in entrypoint and use browserlist to select polyfills
useBuiltIns: 'entry',
// Set the corejs version we are using to avoid warnings in console
corejs: 3,
// Exclude transforms that make all code slower
exclude: ['transform-typeof-symbol'],
},
],
].filter(Boolean),
plugins: [
// Experimental macros support. Will be documented after it's had some time
// in the wild.
require('babel-plugin-macros'),
// class { handleClick = () => { } }
// Enable loose mode to use assignment instead of defineProperty
// See discussion in https://github.com/facebook/create-react-app/issues/4263
[
require('@babel/plugin-proposal-class-properties').default,
{
loose: true,
},
],
// Adds Numeric Separators
require('@babel/plugin-proposal-numeric-separator').default,
// Polyfills the runtime needed for async/await, generators, and friends
// https://babeljs.io/docs/en/babel-plugin-transform-runtime
[
require('@babel/plugin-transform-runtime').default,
{
corejs: false,
helpers: areHelpersEnabled,
// By default, babel assumes babel/runtime version 7.0.0-beta.0,
// explicitly resolving to match the provided helper functions.
// https://github.com/babel/babel/issues/10261
version: require('@babel/runtime/package.json').version,
regenerator: true,
// https://babeljs.io/docs/en/babel-plugin-transform-runtime#useesmodules
// We should turn this on once the lowest version of Node LTS
// supports ES Modules.
useESModules,
// Undocumented option that lets us encapsulate our runtime, ensuring
// the correct version is used
// https://github.com/babel/babel/blob/090c364a90fe73d36a30707fc612ce037bdbbb24/packages/babel-plugin-transform-runtime/src/index.js#L35-L42
absoluteRuntime: absoluteRuntimePath,
},
],
// Optional chaining and nullish coalescing are supported in @babel/preset-env,
// but not yet supported in webpack due to support missing from acorn.
// These can be removed once webpack has support.
// See https://github.com/facebook/create-react-app/issues/8445#issuecomment-588512250
require('@babel/plugin-proposal-optional-chaining').default,
require('@babel/plugin-proposal-nullish-coalescing-operator').default,
].filter(Boolean),
};

总结如下:

  1. @babel/preset-env采用了useBuiltInsentry模式,而不是usage模式,这个comment解释了为什么,总的来说usage模式貌似有点问题?
  2. @babel/preset-envexclude 了transform-typeof-symbol,因为会导致代码变慢,具体见这个issue
  3. babel-plugin-macros: 支持了宏,具体是干嘛的有兴趣的可以自行了解一下babel-plugin-macros
  4. @babel/plugin-transform-runtime
    • core-js: false: 不引入core-js相关内容,因为上面@babel/preset-env提到了需要入口文件import core-js
    • version: require('@babel/runtime/package.json').version:固定了 version,这也是 babel 官方推荐做法
    • regenerator: true:支持@babel/runtime/regenerator的自动引入
  5. 对其它常用的和 webpack 4 不支持的 proposal 做了兼容,引入了这些 plugin

Conclusion

create-react-app相关的react-scriptsbabel-preset-react-app给出了 2021 年如何使用 babel 的标准范式,在日常的开发学习中有很多值得参考和借鉴的地方。当然在实际运用中需要掌握基础,才能遇到不同的情况灵活处理。最后来一波总结:

  1. 使用babel-loader时建议配置cacheDirectory,开启缓存,增加重新构建速度
  2. 项目中需要编译代码到指定环境时,善用@babel/preset-envtargetsuseBuiltIns配置项,可以减少很多不必要的代码编译和 polyfill
  3. 开发工具/类库时,建议使用@babel/runtime配合@babel/plugin-transform-runtime

Comments