阅读 92

Babel 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"转换为目标环境的按需引入,举个例子:

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配置项为entryusage时生效,默认值为"2.0",建议配置为minor version的具体版本号

其它配置诸如includeexclude详见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,简单来说有如下几个步骤:

  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是如何配置的,配置很多,去掉注释之后如下所示:

 {
    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都有所支持,剔除掉这些,具体的配置可以精简如下:

{
    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
文章分类
前端
文章标签