1.分享目的
通过实操,了解babel7.x的基本使用和配置,能够看懂项目中babel相关配置。在碰到相关babel问题时不是一头雾水,找不到解决问题的地方。
2.babel是什么
简单来说,babel就是一个js代码的转换器。它主要的使用场景有:
-
把ES6的代码转换为ES5代码,这样即使代码最终的运行环境(常见:浏览器)不支持ES6,在开发期间也能使用最新语法提升开发效率和质量;
-
有些ES6最新api,目标运行环境(常见:浏览器)还没有普遍提供实现,babel借助core-js对可以自动给js代码添加polyfill,以便最终在浏览器运行的代码能够正常使用那些api;babel始终能提供对最新es提案的支持;
-
.jsx文件和ts文件等转化为js代码
-
babel因为会对代码进行转换,所以可选地能够生成代码的sourcemap,便于排查特殊问题;
-
相关模块介绍
包名 功能 说明 @babel/core babel编译核心包 必装开发依赖 ,babel7版本的基础包 @babel/cli 命令行执行babel命令工具 非必装开发依赖,packages.json的script中使用了babel命令则需安装 babel-loader webpack中使用babel加载文件 非必装开发依赖,webpack项目中使用 @babel/plugin-* babel编译功能实现插件 开发依赖,按照需要的功能安装 @babel/preset-* 功能实现插件预设 开发依赖,插件集合。按照需要的功能安装,js语言新特性转换推荐使用preset-env @babel/plugin-transform-runtime 复用工具函数 非必装开发依赖,和@babel/runtime同时存在 @babel/runtime 工具函数库 非必装生产依赖,和@babel/plugin-transform-runtime同时存在提供 helpers 函数,并会去安装 regenerator-runtime 包,只做语法转换(helpers 和 regenerator), 没有新 api 的实现。 @babel/polyfill 低版本浏览器兼容库 非必装生产依赖,已不推荐使用,推荐通过preset-env的useBuiltIns属性按需引入 core-js@* 低版本浏览器兼容库 非必装生产依赖,通过preset-env引入polyfill需安装此包,并通过corejs指定版本 给低版本浏览器提供es6+新特性接口的库。分为 core-js@2和@core-js@3 regenerator-runtime regenerator-runtime模块来自facebook的regenerator模块,主要作用是生成器函数、async、await函数经babel编译后,regenerator-runtime模块用于提供功能实现 @babel/runtime-corejs* 不污染变量的低版本浏览器兼容库 非必装生产依赖,plugin-transform-runtime设置开启后,可以不污染变量的引入polyfill @babel/runtime-corejs3 @babel/runtime-corejs3 包含 @babel/runtime 的全部并额外安装 core-js-pure@3 @babel/preset-env js语言新特性转换 这是一个智能预设,只要安装这一个preset,就会根据你设置的目标浏览器,自动将代码中的新特性转换成目标浏览器支持的代码。 即使不设置targets,也会有一个默认值,规则为 > 0.5%, last 2 versions, Firefox ESR, not dead -
为什么放在@babel下,没什么特别意义。表示一定是官方推出的包,比较权威,可信赖。
3.plugin,prests
npm init 然后一路enter
npm i @babel/core 安装babel7核心模块
npm i @babel/cli 安装babel可执行文件
创建 src/index.js 放我们的源文件
packagejson中增加 build: rm -rf ./lib/index.js && babel src -d lib 使用babel编译src下所有文件
src的index.js下增加如下代码用于检验babel的编译结果。
let fn = async () => {}
const res = [1,2,3].includes(3)
class Test {}
const A = new Map()
此时我们执行 npm run build。然后查看新生成的lib下index文件。发现新的文件和源文件一模一样。why?
因为我们没有通过配置规则告诉babel如何转化
3.1 如何配置babel的规则
- package.json加入babel的配置信息就行。
- babelrc和.babelrc.js。一种是json,一种是js
- babel.config.json,babel.config.js
3.2 plugin:转化规则的配置
babel的plugin分为三类:
- syntax 语法类 如plugin-syntax-class-properties用于class语法转化
- transform 转换类 如@babel/plugin-transform-arrow-functions 用于箭头函数转码
- proposal 也是转换类,指代那些对ES Proposal进行转换的plugin。如@babel/plugin-proposal-decorators装饰器
// 首先npm i @babel/plugin-transform-arrow-functions
const presets = [];
const plugins = ['@babel/plugin-transform-arrow-functions'];
module.exports = {presets, plugins}
plugin的元素有两种类型:
- 纯字符串,用来标识一个plugin
- 另外一个数组,这个数组的第一个元素是字符串,用来标识一个plugin,第二个元素,是一个对象字面量,可以往plugin传递options配置
- 如 plugin: ['@babel/plugin-transform-arrow-functions']或者 [["@babel/plugin-transform-async-to-generator",{"module": "bluebird","method": "coroutine"}]]
此时我们npm run build 发现箭头函数被转化了。但是class等还未被转化,因为没有安装相关插件。那么如果我需要转化什么就安装什么插件岂不是很麻烦?
3.3 presets:插件的集合
目前官方推荐的preset,有下面四个:
- @babel/preset-env 所有项目都会用到的
- @babel/preset-flow flow需要的
- @babel/preset-react react框架需要的
- @babel/preset-typescript typescript需要的
当然也可以自己写presets,本质上就是一个插件数组。
4.@babel/preset-env
babel最重要的一个preset,而且功能比上面自定义的preset要复杂的多,所以有必要详细的学习。
"dependencies": {
"@babel/compat-data": "^7.16.8",
"@babel/helper-compilation-targets": "^7.16.7",
"@babel/helper-plugin-utils": "^7.16.7",
"@babel/helper-validator-option": "^7.16.7",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7",
......,
"@babel/plugin-transform-typeof-symbol": "^7.16.7",
"@babel/plugin-transform-unicode-escapes": "^7.16.7",
"@babel/plugin-transform-unicode-regex": "^7.16.7",
"@babel/preset-modules": "^0.1.5",
"@babel/types": "^7.16.8",
}
preset-env是会一直变化的,所以最好能保持其能一直更新。
并且preset-env不是万能的。 如果我们用到某一个新的ES特性,还是proposal阶段,而且preset-env不提供转码支持的话,就得自己单独配置plugins了。
此时我们修改babel.config.js,增加@babel/preset-env
const presets = [ ["@babel/preset-env"]];
module.exports = { presets };
// 转化后代码如下
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// require('core-js')var fn = function fn() {};
var res = [1, 2, 3].includes(3);
var Test = /#PURE/_createClass(function Test() { _classCallCheck(this, Test);});
var A = new Map();
我们发现所有的class,map,箭头函数都被转化了。正是我们想要的结果。但是发现includes方法还在,有些浏览器是不支持includes方法的。这需要我们进行额外的配置
在这之前我们了解下@babel/preset-env options的几个重要相关配置
target: 目标运行环境 如 ' > 0.5%, last 2 versions, Firefox ESR, not dead' 这也是其默认规则。也可以是对象,如 { chrome: 97 } 参考:Browserslist配置说明
modules: 这个用于配置是否启用将ES6的模块转换其它规范的模块 默认auto
corejs:指定preset-env进行polyfill时,要使用的corejs版本。默认2,建议修改为3
useBuiltIns:由于@babel/polyfill在babel7.4开始,也不支持使用了。 所以现在要用preset-env,必须是得单独安装core-js v3。主要有两个value: entry和usage。 这两个值,不管是哪一个,都会把core-js的modules注入到转换后的代码里面,充当polyfill。
下面演示一下useBuiltIns为usage和entry的用法。以及两者的优缺点。
当设置为entry时。需要手动在入口文件引入corejs
const presets = [
["@babel/preset-env", {
useBuiltIns: "entry",
corejs: 3
}]
];
入口文件: require('core-js')
打包后代码如下:
"use strict";
require("core-js/modules/es.symbol.js");
require("core-js/modules/es.symbol.description.js");
require("core-js/modules/es.symbol.async-iterator.js");
.......
require("core-js/modules/web.url-search-params.js");
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var fn = function fn() {};
var res = [1, 2, 3].includes(3);
var Test = /#PURE/_createClass(function Test() { _classCallCheck(this, Test);});
var A = new Map();
将所有的垫片全部引入了
这样的优点是全局彻底ployfill,就算 node_modules 中的依赖存在不兼容的代码,也能成功运行,缺点也很明显,引入过多不必要的代码
当设置为usage时
const presets = [
["@babel/preset-env", {
useBuiltIns: "usage",
corejs: 3
}]
];
生成代码如下
require("core-js/modules/es.object.define-property.js");
require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.map.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.string.iterator.js");
require("core-js/modules/web.dom-collections.iterator.js");
require("core-js/modules/es.promise.js");
require("regenerator-runtime/runtime.js");
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = genkey; var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
// require('core-js')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: case "end": return _context.stop(); } } }, _callee); }));
return function fn() { return _ref.apply(this, arguments); };}();
var res = [1, 2, 3].includes(3);
var Test = /#PURE/_createClass(function Test() { _classCallCheck(this, Test);});
var A = new Map();
我们发现不是所有的都被require了,仅仅部分。真正做到了按需加载。缺点是无法处理node_modules中内容
如果以上配置增加target: {chrome:97}的配置,会发现箭头函数,class等都继续存在。因为97版本的chrome以上都支持。
插件和presets的执行顺序
- 插件在 Presets 前运行。
- 插件顺序从前往后排列。
- Preset 顺序是颠倒的(从后往前)。
5.@babel/plugin-transform-runtime
官方说明:A plugin that enables the re-use of Babel's injected helper code to save on codesize.
helpers: true // 开启引入帮助函数
regenerator: true // 不用全局的regeneratorRuntime,关闭后打包代码会有regeneratorRuntime变量
corejs: false // 可设置为 { version: 2/3, proposals: false/true } 开启后表示开启了沙箱模式,不会污染全局变量。但是实验下来此选项会让useBuiltIns的usage失效。
但是以上的配置有存在两个问题。
一是污染了全局变量。比如regeneratorRuntime这个变量他直接使用了。说明在window下有regeneratorRuntime变量。包括includes方法。
二是增加了像_createClass这样的辅助函数。如果有多个文件,那么就会创建多个辅助函数。
增加runtime配置
["@babel/preset-env", {useBuiltIns: "usage",corejs: 3}]];
const plugins = [["@babel/plugin-transform-runtime",{corejs: 3}]];
module.exports = { presets, plugins };
// 然后执行build打包后代码如下
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/map"));
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
var _context2;
// require('core-js')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: case "end": return _context.stop(); } } }, _callee); }));
return function fn() { return _ref.apply(this, arguments); };}();
var res = (0, _includes["default"])(_context2 = [1, 2, 3]).call(_context2, 3);var Test = /#PURE/(0, _createClass2["default"])(function Test() { (0, _classCallCheck2["default"])(this, Test);});var A = new _map"default";
发现以上两个问题都解决了,同时所有模块都是引自@babel/runtime-corejs3模块下
如果使用了装饰器语法,该如何支持呢:['@babel/plugin-proposal-decorators', { legacy: true }], legacy:使用历史遗留(stage 1)的装饰器中的语法和行为
6.几种配置结果
结论:
- 在preset-env中配置useBuiltIns:usage,会按需引入相关的polyfill,但是不会提取公共的help函数,以及会污染全局对象和对象原型
- 在第一点的基础上,新增@babel/plugin-transform-runtime但是core-js指定为false,结果相对于第一点,不同的是这次提取了公共的help函数。
- 在第二点的基础上,runtime插件指定core-js版本3,会导致useBuiltIns:usage配置无效。并且也会按需引入相关polyfill,提取公共help函数,但是不会污染全局对象和对象原型。这种配置适合打包npm库。
- 实际业务需求开发中,采用第二点方案较为合适,但是这种混用的方式,在github上有相关issue(github.com/babel/babel…),并且bable团队成员不推荐这种混用的方式。
参考: