preset-env和plugin-transform-runtime的异同点

本文参考了但不限于以下文章,参考文章有些纰漏在本文已纠正
useBuiltIns 和 @babel/plugin-transform-runtime 是互斥的…
Babel 不同配置的实验报告]
@babel/preset-env 与@babel/plugin-transform-runtime 使用及场景区别
babel-plugin-transform-runtime官网

写在前面

babel一直在迭代,配置项也一直在变。其中配置有两个重要的概念:预设、插件。

这两个概念中又有两个高频词汇:@babel/preset-env@babel/plugin-transform-runtime

几个月前我以为我弄懂了这两个东西,并且实践出了一个最优配置。

直到有一天我发现打包产物比我预期大,还发现了一个长久以来大家都存在的误区。

具体代码、结论都在文末。本文默认读者对babel的polyfill有基本的了解

基本流程:代码 babel 产物

一. 使用预设(preset)

  1. @babel/preset-env默认配置只会处理ES6+新增的语法,如箭头函数letconst

    module.exports = {
    	presets: ['@babel/preset-env'],
    };
  2. 要实现实现新增的内置基本函数(类)实例方法类静态方法生成器函数,代码中按需引用core-jsregenerator-runtime/runtime(以下统称垫片总入口文件,老版本是@babel/polyfill,@babel7+版本已不推荐使用)。

    总体分如下2步骤:

    • 在配置中添加presetuseBuiltIns参数:

      module.exports={
      	presets: [
      		[
      			'@babel/preset-env',
      			{
      				useBuiltIns: false // false是默认值
      			}
      		]
      	]
      }

      useBuiltIns有三个选项:

      1. false(默认值):不处理polyfill文件,不管代码中是引入了import "core-js";import 'regenerator-runtime/runtime';
      2. 'usage':代码中不需要写垫片总入口文件,因为babel会按需引用对应的polyfill。
      3. 'entry':代码顶部必须写垫片总入口文件(手动引入)。但是会把垫片总入口文件按照目标浏览器的功能需要,转换成大量polyfill的引用。
    • 按照useBuiltIns配置不同,可选在代码顶部引入垫片总入口文件:

      import "core-js"
      import 'regenerator-runtime/runtime'
      // 其他代码
      ...

    PS: 如果你非要引入垫片总入口文件useBuiltIns的3个选项按产物大小排序是:false>='entry'>'usage'

  3. corejs就是各个api的polyfill的集合,由于corejs@2已经不再添加新特性,如Array.prototype.flat(),所以我们要用corsjs@3

    module.exports = {
    	presets: [
    		[
    			'@babel/preset-env',
    			{
    				useBuiltIns: 'usage',
    				corejs: 3 // 默认是2
    			}
    		]
    	]
    }

    配置之后产物中会生成垫片文件的引用。(但是这些垫片会污染全局变量,这个后文中第三点会解决。)

二. 优化帮助函数(使用插件@babel/plugin-transform-runtime

场景:

如果a.js和b.js中都用到了类(如 new Promise()),那么在babel转译完2个js之后,2个产物文件中都会有一个_createClass的帮助函数来实现特性的兼容。如果转译的文件过多,就会出现多个_createClass帮助函数,这显然就不合理。

解决办法:

使用 @babel/plugin-transform-runtime 插件(为了行文方便,后文把 @babel/plugin-transform-runtime 插件简称为btr插件。)

  • 此插件的功能:1. 按需引入ESNextapi的垫片,并且不污染全局。 2. 抽离公共的帮助函数。后文把此2点特性称之为沙箱化

  • 使用btr插件后(默认配置),产物中会引入@babel/runtime/xxxx等垫片,即生产环境使用@babel/runtime去实现特性的兼容。

抽离帮助函数

  1. 安装依赖,要安装
    • @babel/plugin-transform-runtime(开发时使用的)。
    • @babel/runtime(生产用到)。(根据插件自身corejs配置不同,这里可替换为@babel/runtime-corejs2@babel/runtime-corejs3,详见后文)
  2. 配置:
    module.exports = {
      presets: [
    	[
    		'@babel/preset-env', {
    		  useBuiltIns: 'usage',
    		  corejs: 3
    		}
    	]
      ],
      plugins: [
    	['@babel/plugin-transform-runtime']
      ],
    };
  3. 上述编译时会创建一个沙箱环境,各个产物文件最终用的帮助函数都是同一个,防止污染到全局变量。如编译产物中的class帮助函数:
    var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
    var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

三. ESNest的API沙箱化

问题

preset-env并没有对 Promise 等api的变量名做沙箱化处理,生成的垫片会污染全局变量。

解决:

上述btr插件示例只对帮助函数做了沙箱化处理,要想给ESNext的API也做沙箱化处理,需要给btr插件加一个corejs的配置项:

module.exports = {
  presets: ['@babel/preset-env'],
  plugins: [
	['@babel/plugin-transform-runtime', {
	  'corejs': 3 // 默认值false,使用2、3时,会抽离ESNEXT的api到沙箱环境
	}]
  ]
};

插件的corejs选项有3个:

  • false(默认值):仅支持帮助函数的沙箱化
  • 2: 除了帮助函数,还支持全局变量(例如 Promise)和静态属性(例如 Array.from)沙箱化
  • 3: 除了上述的,还支持实例属性(例如 [].includes)沙箱化

注意,一旦btr插件的corejs配置不为false:

  • 默认就有preset-envuseBuiltIns: usage按需引用的效果。

做个测试,btr插件的corejs配置不为false

preset-envuseBuiltIns: usage配置垫片总入口文件结果
手动引入1. corejs全量引入。2. 还重复引入了用到的api垫片(来自插件)
entry手动引入1. corejs根据浏览器配置引用。2. 还重复引入了用到的api垫片(来自插件)
usage手动引入只输出了来自插件api垫片(按需引用)
只输出了来自插件api垫片(按需引用)
entry只输出了来自插件api垫片(按需引用)
usage只输出了来自插件api垫片(按需引用)

所以关于btr插件的结论【划重点!!!】:

  1. 一旦btr插件配置了corejs(不为false),就没必要给preset-env配置corejsuseBuiltIns了。
  2. 更不要在代码中引入垫片总入口文件,一旦写了,产物就会被重复引入polyfill文件。

四. 其他

browserslistrc

.browserslistrcplugin-transform-runtimepreset-env都生效

细节

要注意一点就是useBuiltIns: 'usage'不会处理第三方依赖包的引入模块,所以如果第三方依赖包使用了ESNext api而未处理兼容性的话,可能会出bug。

关于两个corejs配置

  • preset-envcorejs最终生成污染全局环境的垫片。
  • plugin-transform-runtimecorejs也是生成垫片,不过还把垫片沙箱化了。
  • 同时配这两个地方的corejs配置项会带来极大的心智负担,极力推荐只配一个!

两个配置的输出产物与生产依赖

  1. preset-env:

    Promise为例

    corejs配置项转译产物生产需依赖
    2(默认值)require("core-js/modules/es6.promise.js")npm install --save core-js@2
    3require("core-js/modules/es.promise.js")npm install --save core-js@3
  2. plugin-transform-runtime:

    Promise为例:

    corejs配置项转译产物生产需依赖
    false(默认值)npm install --save @babel/runtime
    2_interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"))npm install --save @babel/runtime-corejs2
    3_interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"))npm install --save @babel/runtime-corejs3

五. 终极配置

没有所谓的终极配置,因为babel一直在更迭。这里只是暂时的一个比较合适的配置:

  1. 使用preset-env时,需要安装依赖项:

    • @babel/core
    • @babel/cli
    • @babel/preset-env
    • core-js@3 (生产用到。如果配置了preset-envcorejs,转译产物会依赖此包)

    如果用到@babel/plugin-transform-runtime,除了btr插件本身:

    • 根据btr插件的corejs配置不同,@babel/runtime@babe/runtime-corejs2@babel/runtime-corejs3这三个库三选一即可,帮生产环境依赖。
    • 并且不再需要core-js@3了(因为没有用到preset-envcorejs配置)【划重点!】。
  2. 配置文件:babel.config.js

    module.exports = {
      presets: ['@babel/preset-env'], 
      plugins: [
    	['@babel/plugin-transform-runtime', {
    	  'corejs': 3 // 默认值false
    	}]
      ]
    };

    就这么简单。懒得动脑子的话,记住corejs只在配置文件中出现一次就行。

一些用来测试的文件

  1. .browserslistrc
    # chrome 31 # 不支持class
     chrome 50 # 不支持promise
    # chrome 67 # 支持promise
  2. babel.config.js
    module.exports = {
    	 presets: [
    		 [
    			 "@babel/preset-env",
    			 {
    				corejs: 3
    			 },
    		 ],
    	 ],
    	 plugins: [
    		 ['@babel/plugin-transform-runtime', {
    			'corejs': 3 // 默认值false
    		 }]
    	 ]
    };
  3. src/index.js
    const p1 = new Promise();
    class A {
     #a = 1
    }
    const a = new A()
  4. 指令:npx babel src/*.js --out-dir lib/