-
Notifications
You must be signed in to change notification settings - Fork 39
Description
前言
作为前端工程化工具,无论是Babel还是Webpack,在前端工程化中都扮演非常重要的角色。但是这类工具除了在项目初始创建时会频繁接触,到了后期的功能开发和维护中却又鲜有涉及,加之这类工具可配置属性多,随着工具更新配置项又会经常变化,因此对我个人而言,一直掌握的并不是很好。本次在升级组件库中遇到了一系列问题,借此机会记录问题的解决。
对于Babel涉及以下相关的问题:
- Babel在项目中起到了什么作用?
- Babel的preset是什么?@babel/preset-env起到了什么作用?@babel/preset-env为什么要配置corejs参数?
- @babel/plugin-transform-runtime起到了什么作用?
- Library和Application中该如何分别配置polyfill
Babel是什么?
按照Babel官方的说法:
Babel is a JavaScript compiler
Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. Here are the main things Babel can do for you:
- Transform syntax
- Polyfill features that are missing in your target environment (through @babel/polyfill)
- Source code transformations (codemods)
- And more!
如官方所说,Babel是JavaScript编译器,作为工具链主要用来将ECMAScript2015+的代码兼容为能运行在当前和旧版本的浏览器或其他环境中的代码。其实在上面的官方介绍中就表示了Babel所提供的两大功能:
- Transform syntax
- Browser feature
Babel作为开箱即用工具链,在不做任何配置的情况下,Babel并不会做任何处理,而Babel所需要处理的任何工作都需要借助插件(plugin)完成。例如当我们在.babelrc中配置:
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}时,Babel便可以用来编译箭头函数:
// before compile
const fn = () => console.log('a')
// after compile
var fn = function fn() {
return console.log('a');
};当我们在项目中想使用Babel支持众多特性和语法,一条条插件配置过于繁重,因此Babel提供了Preset(预设),其本质就是一系列Babel插件的集合,例如:
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
@babel/preset-env是什么?
关于@babel/preset-env官方介绍:
@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s).
@babel/preset-env允许我们在不需要微观管理的情况下,根据目标浏览器环境,进行语法转换(polyfill)从而使用最新的JavaScript。
在 babel@6 版本中,一般使用的是stage,例如:babel/preset-stage-0,对stage其实只会语法转化,对应的polyfill对应的API则交给 babel-plugin-transform-runtime 或者 babel-polyfill 来实现。
在 babel@7版本,废弃了stage,转而引入了@babel/preset-env,@babel/preset-env不仅提供了语法转化,同时也提供了polyfill的能力。
target
target参数表明我们项目需要适配到的环境,比如可以声明适配到的浏览器版本(例如IE11),这样 babel 会根据浏览器的支持情况自动引入所需要的 polyfill。
@babel/preset-env实际上依赖了类似于:browserslist、compat-table、electron-to-chromium这类第三方库数据,@babel/preset-env利用这些数据并按照我们配置target,获得我们目标浏览器所支持的JavaScript语法和浏览器特性,从而对应这些JavaScript语法和浏览器特性所需要的Babel插件和Polyfills。
当@babel/preset-env的target参数为空时,默认指向的是项目层级的browserslistrc配置。
一般来说,项目层级的浏览器支持配置可以通过 .browserslistrc 文件来设定目标浏览器,例如:
// .browserslistrc
> 0.25%
not dead表示目标是包括浏览器市场份额超过0.25%且忽略没有安全更新的浏览器(如 IE10 和 BlackBerry)的用户所需的Polyfills 和代码转换。如果未设置,则默认使用browserslist配置源。browserslist的默认配置为:
> 0.5%, last 2 versions, Firefox ESR, not dead需要注意的是,@babel/preset-env目前不支持stage-x阶段的插件,需要单独引入相应的插件。
corejs
按照core-js官方的介绍
Modular standard library for JavaScript. Includes polyfills for ECMAScript up to 2021: promises, symbols, collections, iterators, typed arrays, many other features, ECMAScript proposals, some cross-platform WHATWG / W3C features and proposals like URL. You can load only required features or use it without global namespace pollution.
core-js是JavaScript的模块化标准库,包含到ECMAScript 2021的polyfill,例如:promises、symbols、collections、iterators 等特性及提案。
目前core-js分为v2和v3两个大的版本,v3版本并不向后兼容v2版本,目前推荐使用core-js@3的主要原因在于:
- core-js@2.0的版本已经被冻结,所有的新特性只会添加到3.0的分支中
- core-js@3.0增加了proposals配置项,对处在提案阶段的api提供支持,但是因为提案阶段并不稳定,在正式加入标准之前,可能会有大的改动,因此需要谨慎使用
- core-js@3.0增加了对一些web标准的支持,比如URL 和 URLSearchParams
- core-js@3.0尽可能支持模块化,可以按需引入,不污染全局环境
@babel/preset-env的corejs属性仅当配置 useBuiltIns: usage 或 useBuiltIns: entry 时才对应生效,corejs对应的属性值为 core-js 所支持的版本,从而 决定 @babel/preset-env 如何注入polyfill。
useBuiltIns
useBuiltIns作为@babel/preset-env的配置项,支持一下三个值:
- false
- entry
- usage
useBuiltIns: false时,@babel/preset-env不会引入polyfill,需要你在项目中主动引入:
import 'core-js/stable';
import 'regenerator-runtime/runtime';通过这种方式,会把所有的polyfill全部引入,造成包体积庞大。
useBuiltIns: entry时,我们需要在项目入口中主动引入polyfill库,babel 根据 targets 替换成浏览器不兼容的所有 polyfill。
import "core-js";
const ps = new Promise.resolve();会被编译成:
"use strict";
require("core-js/modules/es.symbol");
require("core-js/modules/es.symbol.description");
require("core-js/modules/es.symbol.async-iterator");
// ......省略
require("core-js/modules/web.url-search-params");
var ps = new Promise.resolve();useBuiltIns: usage时,无序引入任何的polyfill库,babel 根据 targets ,以及项目代码中用到的 API 实现按需添加,例如:
Promise.resolve();会被编译成
"use strict";
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var ps = new Promise.resolve();此时@babel/preset-env则会按需引入polyfill。
需要注意的是,无论使用的哪一种useBuiltIns,preset-env 注入的 polyfill 是会污染全局的。
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime的官方文档介绍如下:
A plugin that enables the re-use of Babel's injected helper code to save on codesize.
- Automatically requires @babel/runtime/regenerator when you use generators/async functions (toggleable with the regenerator option).
- Can use core-js for helpers if necessary instead of assuming it will be polyfilled by the user (toggleable with the corejs option)
- Automatically removes the inline Babel helpers and uses the module @babel/runtime/helpers instead (toggleable with the helpers option).
@babel/plugin-transform-runtime的功能可以总结为三点:
- 自动处理generators/async函数
- 避免polyfill污染全局变量
- 自动移除Babel的helpers
自动处理generators/async函数
对于不支持的generators/async的浏览器,我们必须使用polyfill兼容处理。例如我们通过@babel/preset-env配置useBuiltIns为usage:
"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
require("regenerator-runtime/runtime");
function _regeneratorRuntime() {//....}
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { // ...... }
function _asyncToGenerator(fn) { //...... }
var fn = /*#__PURE__*/function () {
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
console.log('Hello World');
case 1:
case "end":
return _context.stop();
}
}, _callee);
}));
return function fn() {
return _ref.apply(this, arguments);
};
}();但是这种方式会造成全局环境污染,利用@babel/plugin-transform-runtime配置regenerator属性则可以避免该情况。
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
require("regenerator-runtime/runtime");
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
var fn = /*#__PURE__*/function () {
var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
console.log('Hello World');
case 1:
case "end":
return _context.stop();
}
}, _callee);
}));
return function fn() {
return _ref.apply(this, arguments);
};
}();避免polyfill污染全局变量
@babel/plugin-transform-runtime插件的另外一个目的是构建代码的沙盒环境。我们之前提到的,引入 core-js 或者 @babel/polyfill都会污染全局环境(对于@babel/preset-env而言,无论使用的哪一种useBuiltIns),如果是应用开发不会造成额外的影响,但如果你的代码目的是发布给其他人使用的库,这就会造成其他的问题。
例如Promise的polyfill会被编译成:
"use strict";
require("core-js/modules/es.symbol");
require("core-js/modules/es.symbol.description");
require("core-js/modules/es.symbol.async-iterator");
// ......省略
require("core-js/modules/web.url-search-params");
var ps = new Promise.resolve();而引入 @babel/plugin-transform-runtime 插件后,则会被编译成:
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
const ps = new _Promise.resolve();相比于添加全局的实例或者修改原型,而是通过的统一模块(@babel/runtime-corejs3)引入并替换,避免了对全局变量及其原型的污染,更符合类库或者工具库的定义。
如果@babel/plugin-transform-runtime 和 @babel/preset-env 共同使用,且@babel/plugin-transform-runtime 开启corejs,@babel/preset-env开启 useBuiltIns,实际效果会是怎么阳?事实上,polyfill 将会采用不污染全局的,且 @babel/preset-env 的 targets 设置将会失效。但是 @babel/plugin-transform-runtime 也并不是没有缺点,因为其导致了targets的失效,因此无法带来打包体积的优势。
自动移除Babel的helpers
Babel使用非常小的助手函数(helper)实现常见的函数,例如:_extend。默认情况下,helper会被添加到所需的每个文件中。这种逻辑会造成打包体积的膨胀。
例如:
class Person {}会被编译为:
"use strict";
function _typeof(obj) { // ...... }
function _defineProperties(target, props) { // ......}
function _createClass(Constructor, protoProps, staticProps) { // ...... }
function _toPropertyKey(arg) { // ...... }
function _toPrimitive(input, hint) { // ...... }
function _classCallCheck(instance, Constructor) { // ...... }
var Person = /*#__PURE__*/_createClass(function Person() {
_classCallCheck(this, Person);
});Babel为Class创造了_classCallCheck作为辅助函数(helpers),但是项目中存在多个文件,Babel就会为每个文件创建单独的辅助函数,这无疑会大大增加打包体积。这就是@babel/plugin-transform-runtime出现的主要原因,所有的helper将会引用@babel/runtime模块从而避免编译输出的内容的重复。
同样上面的内容,引入 @babel/plugin-transform-runtime,上面的类会被转译为:
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var Person = /*#__PURE__*/(0, _createClass2.default)(function Person() {
(0, _classCallCheck2.default)(this, Person);
});因此@babel/plugin-transform-runtime插件能够复用Babel的注入helper代码从而节省资源体积。
@babel/plugin-transform-runtime关于polyfill的副作用
@babel/plugin-transform-runtime 插件实现的 polyfill 不会污染全局环境,但是采用 @babel/plugin-transform-runtime后, @babel/preset-env 中的 targets 将会失效,这会导致最终包的体积变大。
应用项目和Library中该如何分别配置polyfill
应用项目
useBuiltIns推荐设置设置为entry,将最低环境不支持的所有 polyfill 都引入到入口文件(即使你在你的业务代码中并未使用),这是一种兼顾最终打包体积和稳妥的方式,因为我们很难保证引用的三方包有处理好polyfill这些问题。除非充分保证你的三方依赖 polyfill处理得当,那么也可以把 useBuiltIns 设置为 usage。
建议配置如下:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": {
"version": 3,
"proposals": true
}
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false
}
]
]
}并在项目开头处引入:
import 'core-js/stable';
import 'regenerator-runtime/runtime';Library
Library与应用项目不同在会被发布给第三方使用,因而无法确定使用环境,因而要求整个环境必须是不会污染全局环境的沙盒。因此必须借助babel/plugin-transform-runtime插件,议开启 corejs,polyfill 由 @babel/plugin-transform-runtime 引入。@babel/preset-env 关闭 useBuiltIns。
建议配置如下:
{
"presets": [
[
"@babel/preset-env",
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": {
"version": 3,
"proposals": true
}
}
]
]
}