[3] Babel 最佳实践

385 阅读8分钟

一、基本概念

概念作用特殊说明
@babel/polyfill (污染全局的垫片)一个纯运行时的包,不是babel插件 。新API的转化,如 Set、Maps、Proxy。@babel/polyfill可以看作是:core-jsregenerator-runtime注意:Babel v7.4.0[5]开始, @babel/polyfill被废弃了,推荐直接引用core-jsregenerator-runtime替代。原因:@babel/polyfill,因为它只包含core-js 2.0的版本。
@babel/preset-env:插件预设**基础语法synctx的转化。**core.js 的按需引入。注意。1.无论如何设置,该插件require的 polyfill 代码都将污染全局的原型对象2. @babel/preset-env 也因 core-js@3 的原因,需要配置 corejs 参数去指定使用的corejs 版本,否则 webpack 运行时会报 warning。
@babel/runtime语法转译通常使得原语法的部分语义由编译期来到运行时,因此需要提供额外的运行时代码(称为 Helper)。此库提供了所有babel可复用的 helper方法。需要搭配插件@babel/plugin-transform-runtime使用,从而避免重复生成 helper。如果并不想复用 helper,该库可以不安装
@babel/runtime-corejs2 或者**@babel/runtime-corejs3:**转化新的API。结合了 @babel/runtimecore-js两个库的内容。需要搭配插件@babel/plugin-transform-runtime使用(配置了corejs选项),为前者提供 helper 和 polyfill。core.js2 与core.js3 的区别:支持实例上的方法,例如iarray.includes(something)
@babel/plugin-transform-runtime三个作用:1.自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代;2.当代码里使用了core-js的API,自动引入@babel/runtime-corejs3/core-js-stable/,以此来替代全局引入的core-js/stable;3.当代码里使用了Generator/async函数,自动引入@babel/runtime/regenerator,以此来替代全局引入的regenerator-runtime/runtime;这将无效化@babel/preset-env中targets属性(因为 plugin 先于 preset 运行)。

二、业务项目开发

  1. @babel/polyfill 改为:core-js/stable + regenerator-runtime/runtime

  2. @babel/preset-env: 配置tagets + 设置useBuiltIns: 'usage'选项。

2.1 polyfill(垫片) 简介

作为前端,最常见的Babel生态的库想必是 @babel/polyfill(垫片)@babel/preset-env

从上文我们知道,Babel本身只是JS的编译器,以上两者的转换功能是谁实现的呢?

答案是:core-js

2.2 core-js简介

core-js是一套模块化的JS标准库,包括:

  • 一直到ES2021polyfill。
  • promisesymbolsiterators等一些特性的实现。
  • ES提案中的特性实现。
  • 跨平台的WHATWG / W3C特性,比如URL。

目前有版本2 和版本3。

对于版本3:

2.3 @babel/polyfill与core-js关系

image.png @babel/polyfill可以看作是:core-jsregenerator-runtime

regenerator-runtimegenerator以及async/await的运行时依赖

单独使用 @babel/polyfill会将core-js全量导入,造成项目打包体积过大。

为了解决全量引入core-js造成打包体积过大的问题,我们需要配合使用 @babel/preset-env

2.4 优化打包体积

使用 @babel/preset-env,可以**「按需」core-js**中的特性打包,这样可以显著减少最终打包的体积。

这里的**「按需」**,分为两个粒度:

  • 宿主环境的粒度。根据不同宿主环境将该环境下所需的所有特性打包
  • 按使用情况的粒度。仅仅将使用了的特性打包

我们来依次看下。

宿主环境的粒度

指定应用所要支持的浏览器环境,以避免不必要的补丁,减少打包输出体积。

当我们按如下参数在项目目录下配置browserslist文件(或在 @babel/preset-envtargets属性内设置,或在package.jsonbrowserslist属性中设置):

not IE 11
maintained node versions

会将**「非IE11」「所有Node.js基金会维护的node版本」**下需要的特性打入最终的包。

按使用情况的粒度

更理想的情况是只打包我们使用过的特性。

这时候可以设置 @babel/preset-envuseBuiltIns属性为usage-按需导入。

useBuiltIns取值

false: 默认值,不注入垫片。

entry: 需要在整个项目入口,main.ts, 导入 core.js。会注入目标环境不支持的所有语法。

usage: 按需导入core.js。会注入目标环境的所有被用到的类型语法。

比如:
a.jsvar a = new Promise();
b.jsvar b = new Map();

当宿主环境不支持promise与Map时,输出的文件为:
a.jsimport "core-js/modules/es.promise";
var a = new Promise();
b.jsimport "core-js/modules/es.map";
var b = new Map();

当宿主环境支持这两个特性时,输出的文件为:
a.jsvar a = new Promise();
b.jsvar b = new Map();

2.5 分析

优点缺点使用场景
可控能力强(targets)全局变量污染。helpers函数无法提出。(少可忽略)工程业务项目,自己控制,污染也无所谓。

三:类库、组件库开发

  1. @babel/preset-env: 基本语法转化
  2. @babel/plugin-transform-runtime + @babel/runtime-corejs2(3):配套使用: helpers + 新API

@babel/runtime-corejs2(3) = @babel/runtime + corejs2(3)

3.1 @babel/runtime

一句话:将preset-env所产生的helpers函数提出到一个独立文件中,从而减少代码量

@babel/runtime内部集成了:

  1. regenerator: 作为 core-js 的拾遗补漏,主要是 generator/yield 和 async/await 两组的支持。当代码中有使用 generators/async 时自动引入。
  2. helpers, 如上面的 asyncToGenerator 就是其中之一,其他还有如 jsx, classCallCheck 等等,可以查看 babel-helpers

会发现编译出的结果为:

image.png 其中 _classCallCheck为辅助方法。

如果多个文件都使用了class特性,那么每个文件打包对应的module中都将包含 _classCallCheck

为了减少打包体积,更好的方式是:需要使用**「辅助方法」module**都从同一个地方引用,而不是自己维护一份。

@babel/runtime包含了Babel所有**「辅助方法-工具函数」**。

单纯引入 @babel/runtime还不行,因为Babel不知道何时引用 @babel/runtime中的**「辅助方法」**。

所以,还需要引入 @babel/plugin-transform-runtime

这个插件会在编译时将所有使用**「辅助方法-工具函数」的地方从「自己维护一份」**改为从 @babel/runtime中引入。 image.png

从定义方法改成引用,那重复定义就变成了重复引用,就不存在代码重复的问题了。

注意:

  • 所以我们需要将 @babel/plugin-transform-runtime置为devDependence,因为他在编译时使用。
  • @babel/runtime置为dependence,因为他在运行时使用。
  • runtime方式不支持类似 Array.prototype.includes 的实例方法 如果想要支持,还是要使用 core-js@3。

一句话:将preset-env所产生的helpers函数提出到一个独立文件中,从而减少代码量

3.2 @babel/runtime-corejs2(3)

@babel/polyfill 和@babel/runtime-corejs2 都使用了 core-js(v2)这个库来进行 api 的处理。core-js(v2)这个库有两个核心的文件夹,分别是 library 和 modules。

@babel/polyfill 使用 modules 这个文件夹,@babel/runtime-corejs2 使用 library 这个文件夹。

  1. library 使用 helper 的方式,局部实现某个 api,不会污染全局变量;
  2. modules 以污染全局变量的方法来实现 api;
  3. library 和 modules 包含的文件基本相同,最大的不同是_export.js 这个文件。

image.png library下的这个$export方法,会实现一个wrapper函数,防止污染全局变量。

例如:对Promise的转译,@babel/polyfill和@babel/runtime-corejs2的转译方式差异如下

var p = new Promise();

// @babel/polyfill
require("core-js/modules/es6.promise");
var p = new Promise(); // 直接修改全局变量,上面加了Promise一个方法。

// @babel/runtime-corejs2
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise")); // 实现了一个新的_promise
var a = new _promise.default();
  1. 从上面这个例子可以看出,对于Promise这个api,@babel/polyfill引用了core-js/modules中的es6.promise.js文件,因为是对全局变量进行处理,所以赋值语句不用做处理;
  2. @babel/runtime-corejs2会生成一个局部变量_promise,然后把Promise都替换成_promise,这样就不会污染全局变量了。

babel-polyfill 是在原有的JS内置对象及方法上做向后兼容的处理。

比如说ES5里面的 Object 是没有自带 assign 方法的,那么你加载了babel-polyfill 之后,它就给 Object 扩展了一个 assign 方法,这样你就可以直接使用 Object.assign(obj1, obj2) 了

而 babel-runtime 的方式则需要babel作为工具。

在转换的过程中,检测到你使用了 Object.assign,而且你的 .babelrc 配置中需要对其做ES5兼容处理,那么结合 babel-plugin-transform-runtime,在该JS文件中引入 Object.assign 的Polyfill,这样也能实现 Object.assign 的功能,但是你无法在 Object 上直接找到 assign 的方法。

3.3 @babel/plugin-transform-runtime

corejs 可设置为3个参数:

取值含义说明
false需安装 @babel/runtime ,不需要开启core-js相关API转换功能的时候。不包含core.js ,所以只做语法的转换。做API转换,对内置对象进行重命名,以防止污染全局环境。
2需安装 @babel/runtime-corejs2,开启core-js相关API转换功能的时候,避免全局变量污染时。
3需安装 @babel/runtime-corejs3,开启core-js相关API转换功能的时候,避免全局变量污染。

3.4 分析

优点缺点使用场景
helpers函数可提出。避免全局变量污染。可控能力弱,targets 不会起作用。组件库,类库。