Introduction
Babel在2021年一共进行了2个minor版本的更新,增加了一些Stage 4 proposals的支持,以及一些Top-level的配置项(targets, assumptions)。伴随着这些更新,结合babel-loader和babel-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"转换为目标环境的按需引入,举个例子:
import "core-js/stable";
import "regenerator-runtime/runtime";
在不同环境下可能转换为:
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配置项为entry或usage时生效,默认值为"2.0",建议配置为minor version的具体版本号
其它配置诸如include,exclude详见Options
@babel/runtime
@babel/runtime与其配套的@babel/plugin-transform-runtime主要有三个配置项,分别对应不同场景:
regenerator
使用generator/async函数时自动引用@babel/runtime/regenerator。默认值为true,默认开启
function* foo() {}
输出
"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
var sym = Symbol();
var promise = Promise.resolve();
var check = arr.includes("yeah!");
console.log(arr[Symbol.iterator]());
输出
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,默认开启
class Person {}
通常情况下的转换结果为:
"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);
};
开启之后转换结果为:
"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值,即:
cacheIdentifier = JSON.stringify({
options,
"@babel/core": transform.version,
"@babel/loader": version,
})
如果该值改变,则会强制刷新缓存
cacheCompression
默认值为true,设置之后babel的缓存结果会被gzip压缩
开启缓存如何加快rebuild?
关于对缓存的支持以加快rebuild,简单来说有如下几个步骤:
- 检测是否配置了
cacheDirectory,如果配置,则调用cache()进一步处理,如果没有则直接transform() - 配置了
cacheDirectory之后,先根据每个文件内容(source)和配置(options)以及标识符(identifier)三部分内容JSON.stringify之后进行哈希,得到文件名称filename,即调用了filename(source, cacheIdentifier, options),再path.join对应的directory获得缓存文件的绝对路径file - 获得文件的绝对路径之后,尝试读取文件内容,如果读取到说明之前相对应的
source已经缓存过,直接返回对应的结果 - 没有的则将
transform()之后的结果写到对应的文件file下,并将结果返回
感兴趣的可以阅读相关的cache源码
create-react-app
有了以上的基础知识铺垫,我们来看一下create-react-app和babel相关的内容是如何配置和处理的
react-scripts
先看一下react-scripts中关于webpack的babel-loader是如何配置的,配置很多,去掉注释之后如下所示:
{
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,
},
},
总结如下:
- 引入了
babel-preset-react-app,这个preset也是create-react-app维护的,细节我们后面讲 - 自定义了
babel-loader的cacheIdentifier,确保其值唯一,具体如上所示 - 设置
cacheDirectory值为true,开启相关缓存 - 但是禁用了缓存相关的gzip压缩,原因见这个PR
- 正式环境开启了
compact模式
babel-preset-react-app
babel-preset-react-app对react以及flow和typescript都有所支持,剔除掉这些,具体的配置可以精简如下:
{
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),
};
总结如下:
@babel/preset-env采用了useBuiltIns的entry模式,而不是usage模式,这个comment解释了为什么,总的来说usage模式貌似有点问题?@babel/preset-envexclude了transform-typeof-symbol,因为会导致代码变慢,具体见这个issuebabel-plugin-macros: 支持了宏,具体是干嘛的有兴趣的可以自行了解一下babel-plugin-macros@babel/plugin-transform-runtimecore-js: false: 不引入core-js相关内容,因为上面@babel/preset-env提到了需要入口文件import core-jsversion: require('@babel/runtime/package.json').version:固定了version,这也是babel官方推荐做法regenerator: true:支持@babel/runtime/regenerator的自动引入
- 对其它常用的和webpack 4不支持的proposal做了兼容,引入了这些plugin
Conclusion
create-react-app相关的react-scripts和babel-preset-react-app给出了2021年如何使用babel的标准范式,在日常的开发学习中有很多值得参考和借鉴的地方。当然在实际运用中需要掌握基础,才能遇到不同的情况灵活处理。最后来一波总结:
- 使用
babel-loader时建议配置cacheDirectory,开启缓存,增加重新构建速度 - 项目中需要编译代码到指定环境时,善用
@babel/preset-env的targets和useBuiltIns配置项,可以减少很多不必要的代码编译和polyfill - 开发工具/类库时,建议使用
@babel/runtime配合@babel/plugin-transform-runtime