Babel
Babel是js的一个编译器,主要用来将esnext新语法转换为向后兼容的语法。Babel这种“源码到源码”编译, 也被称为转换编译。
主要功能:
- 转换语法
- polyfill新特性
- 源码修改
- 支持语法扩展,比如jsx等
使用
现在Babel自身已经被拆成了若干个核心模块和官方的插件。简单介绍下几个常用的模块和插件。
@babel/core
@babel/core
负责接受和转换代码。
源码
// esnext.js
// es2015
const num = 1
class Person {
sayHi() {
console.log('hi')
}
}
转换
import babel from '@babel/core'
// 加载esnext.js文件
const result = babel.transformFileSync('./esnext.js')
console.log(result.code)
结果
// es2015
const num = 1;
class Person {
sayHi() {
console.log('hi');
}
}
// es2022
const temp = {
name: 'name'
};
console.log(Object.hasOwn(temp, 'name'));
对比发现,babel只是添加了换行符,其他啥好像都没做。因为还需要告诉babel该怎么处理这些代码,需要配合@babel/preset-env
预设。
@babel/preset-env
@babel/preset-env
本质为一堆插件的集合,用来生成辅助代码。
转换
import babel from '@babel/core'
const babelOptions = {
presets: ['@babel/preset-env'],
// 兼容的最低浏览器版本
targets: { ie: 11 }
}
const result = babel.transformFileSync('./esnext.js', babelOptions)
console.log(result.code)
结果
"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); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
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, _toPropertyKey(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 _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
// es2015
var num = 1;
var Person = /*#__PURE__*/function () {
function Person() {
_classCallCheck(this, Person);
}
_createClass(Person, [{
key: "sayHi",
value: function sayHi() {
console.log('hi');
}
}]);
return Person;
}();
// es2022
var temp = {
name: 'name'
};
console.log(Object.hasOwn(temp, 'name'));
对比发现,babel通过生成一堆辅助代码,使esnext的语法兼容旧浏览器(当前设置的为ie11)。@babel/preset-env
会根据targets设置,生成对应的代码,如果目标浏览器支持其中部分新语法,则不会转译。比如将targets设置为{chrome: 111}
,则不会产生辅助代码。
可是有个问题,如果将第一次测试生成的代码,跑在低版本浏览器会报错,因为Object.hasOwn()
为es2022的语法,Babel没法生成一些比如Object.hasOwn, Array.prototype.includes()
等方法的辅助代码,还需要一些polyfill代码。
转换
import babel from '@babel/core'
const babelOptions = {
presets: [
['@babel/preset-env',
{
"useBuiltIns": "usage",
"corejs": "3.22"
}
]
],
targets: { chrome: 50 }
}
const result = babel.transformFileSync('./esnext.js', babelOptions)
console.log(result.code)
结果
"use strict";
// 自动导入core-js中相应的模块
require("core-js/modules/es.object.has-own.js");
// es2015
const num = 1;
class Person {
sayHi() {
console.log('hi');
}
}
// es2022
const temp = {
name: 'name'
};
console.log(Object.hasOwn(temp, 'name'));
通过配置@babel/preset-env
的core-js
和useBuiltIns
选项,Babel自动引入了目标浏览器原生缺少的方法。
小结
通过以上几个代码实践,可以发现Babel转译代码的大概流程。@babel/core
主要负责接受代码、解析、生成。
@babel/preset-env
中内置了一些插件,主要用来加工代码,来生成兼容性的代码。
详解
配置
@babel/core
中的转换方法可以接受一个配置选项参数,配置支持代码硬编码或者配置文件。
配置文件
- 项目范围内的配置(仓库结构为
monorepo
、支持编译node_modules
)babel.config.*
(后缀:.json
,.js
,.cjs
,.mjs
,.cts
)
- 文件相关配置(只编译项目中一部分代码)
.babelrc.*
(后缀:.json
,.js
,.cjs
,.mjs
,.cts
).babelrc
package.json
中的"babel"
字段
.json
无法添加代码,.js
可以添加一些代码。
js文件配置的一些额外功能
module.exports = function(api) {
const plugins = [];
// 动态添加一些插件
if (process.env.NODE_ENV === "production") {
plugins.push('one-plugin');
}
// 缓存配置,避免每次都执行获取,提升性能
api.cache.using(() => process.env.NODE_ENV)
return {
plugins
};
};
配置选项(部分)
rootMode
值有"root" | "upward" | "upward-optional"
,默认root。决定Babel怎么决定项目根目录。
envName
值有process.env.BABEL_ENV || process.env.NODE_ENV || "development"
plugins
插件
presets
预设
targets
支持的目标环境
"targets": "> 0.25%, not dead"
"targets": {
"chrome": "58",
"ie": "11"
}
如果没设置targets,则默认目标环境是低版本浏览器,转为兼容ES5的代码。
extends
继承其他配置
overrides
覆盖
test
overrides
中常用
include
同test
exclude
overrides
中常用
ignore
忽略
ignore: ["./lib"];
only
only: ["./src"];
Name Normalization 名称规范化
默认情况下,Babel希望插件名称有babel-plugin-
或者 babel-preset-
的前缀。为了避免重复,Babel有个名称转换的阶段,当加载每一项时,会自动添加这些前缀。
- 绝对路径原封不动
- 相对路径原封不动
- 引用的文件原封不动
module:
为前缀的,module:会被移除- @babel-没有
plugin-
/preset-
前缀,会自动添加plugin-
/preset-
前缀 - 一些情况下,
babel-plugin-
/babel-preset-
前缀自动注入
例子:
Input | Normalized |
---|---|
"/dir/plugin.js" | "/dir/plugin.js" |
"./dir/plugin.js" | "./dir/plugin.js" |
"mod" | "babel-plugin-mod" |
"mod/plugin" | "mod/plugin" |
"babel-plugin-mod" | "babel-plugin-mod" |
"@babel/mod" | "@babel/plugin-mod" |
"@babel/plugin-mod" | "@babel/plugin-mod" |
"@babel/mod/plugin" | "@babel/mod/plugin" |
"@scope" | "@scope/babel-plugin" |
"@scope/babel-plugin" | "@scope/babel-plugin" |
"@scope/mod" | "@scope/babel-plugin-mod" |
"@scope/babel-plugin-mod" | "@scope/babel-plugin-mod" |
"@scope/prefix-babel-plugin-mod" | "@scope/prefix-babel-plugin-mod" |
"@scope/mod/plugin" | "@scope/mod/plugin" |
"module:foo" | "foo" |
Presets
Babel的预设是一些插件和配置选项的集合
。
官方预设
- @babel/preset-env
- @babel/preset-typescript
- @babel/preset-react
- @babel/preset-flow
使用预设
babel.config.json
{
"presets": ["babel-preset-myPreset", "@babel/preset-env"]
}
或者使用项目中自定义的预设
{
"presets": ["./myProject/myPreset"]
}
创建一个预设
导出一个配置对象。
集成若干个插件的预设
module.exports = function() {
return {
plugins: ["pluginA", "pluginB", "pluginC"],
};
};
包括其他预设和插件的预设
module.exports = () => ({
presets: [require("@babel/preset-env")],
plugins: [
[require("@babel/plugin-proposal-class-properties"), { loose: true }],
require("@babel/plugin-proposal-object-rest-spread"),
],
});
预设的顺序
预设的顺序是反过来的,从后向前。
{
"presets": ["a", "b", "c"]
}
运行的顺序为c、b、a
配置预设的选项
{
"presets": [
"presetA", // bare string
["presetA"], // wrapped in array
["presetA", {}] // 第二个对象可配置presetA预设
]
}
官方@babel/preset-env
主要根据设置的目标环境,转换语法。推荐使用.browserslistrc
设置目标环境。
配置选项(部分)
targets
目标浏览器
modules
md" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false
, 默认"auto"
.
debug
默认false
include
包括的插件
有效值
@babel/plugin-transform-spread
@babel/transform-spread
babel-transform-spread
transform-spread
es.map
es.math.sign
es.math.*
/^transform-.*$/
exclude
排除的插件
useBuiltIns
"usage"
| "entry"
| false
, 默认为false
.
用来处理polyfills,当使用usage
或者entry
,会引用core-js
模块
useBuiltIns: 'entry'
项目最开始时,引入core-js
import "core-js";
useBuiltIns: 'usage'
在具体的文件中添加指定的polyfills
corejs
指定corejs的版本
forceAllTransforms
强制都转为es5代码
Plugins
Babel系统已经插件化,大的功能可以通过一个个小的插件组合起来实现。
使用插件
如果插件是维护在npm仓库中,可以把包的名称传递给plugins
{
"plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"]
}
也可以指定一个路径
{
"plugins": ["./node_modules/asdf/plugin"]
}
Transform Plugins转换插件
转换插件主要是转换代码
Syntax Plugins语法糖插件
Babel会转换大多数的语法。在极少情况下(转换没实现),可以使用插件,比如像@babel/plugin-syntax-bigint
,来允许Babel来解析特殊的语法类型。
插件顺序
- 插件在预设前运行
- 插件顺序是从前往后
- 预设是从后往前
插件选项
{
"plugins": ["pluginA", ["pluginA"], ["pluginA", {}]]
}
开发插件
一个简单的例子
export default function() {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
// reverse the name: JavaScript -> tpircSavaJ
path.node.name = name
.split("")
.reverse()
.join("");
},
},
};
}
官方插件列表
主要分为提案中的和已经发布成为ES标准。还在提案中的特性,就需要安装对应的插件。而已经成为ES标准的特性,更新@babel/preset-env
版本就行,其中已经内置对应插件,不需要单独再安装。
比如要使用装饰器的语法,还在提案中,就需要
- 安装插件
npm install --save-dev @babel/plugin-proposal-decorators
- 使用插件
{ "plugins": ["@babel/plugin-proposal-decorators", {version: '2023-01'}]}
Compiler assumptions 编译断定
Babel会尽可能地把代码编译成原生表现一样。然而,为了支持一些极限场景,就会生成很多额外、运行很慢的代码。从Babel 7.13.0开始,可以指定assumptions
,来更好地优化编译结果。
比如:
{
"targets": ">0.5%",
"assumptions": {
// 配置noDocumentAll
"noDocumentAll": true
},
"presets": ["@babel/preset-env"]
}
noDocumentAll
When using operators that check for null
or undefined
, assume that they are never used with the special value document.all
当使用操作符来检查null
或者undefined
时,断定他们不会用在document.all
源码
let score = points ?? 0;
let name = user?.name;
noDocumentAll
开启时,输出代码
var _points, _user;
let score = (_points = points) != null ? _points : 0;
let name = (_user = user) == null ? void 0 : _user.name;
noDocumentAll
关闭时,输出代码
var _points, _user;
let score = (_points = points) !== null && _points !== void 0 ? _points : 0;
let name = (_user = user) === null || _user === void 0 ? void 0 : _user.name;
对比发现,开启断定时,会少生成一些判断代码。
其他
@babel/plugin-transform-runtime
通常Babel转换后,会在当前文件头部生成辅助代码,但是这会导致每个文件头部都会产生相同辅助代码的问题,所以需要将Babel辅助代码单独引入。@babel/plugin-transform-runtime
插件可以再次利用Babel注入的额外代码,来减少生成的代码大小。
// 开发下的依赖
npm install --save-dev @babel/plugin-transform-runtime
// 生产下的依赖(程序运行时需要)
npm install --save @babel/runtime
当这个插件启用时,
@babel/preset-env
的useBuiltIns
不能设置,否则沙盒环境会失效
转换
import babel from '@babel/core'
const babelOptions = {
presets: ['@babel/preset-env'],
targets: { ie: 11 },
// 启用@babel/plugin-transform-runtime插件
plugins: ['@babel/plugin-transform-runtime']
}
const result = babel.transformFileSync('./esnext.js', babelOptions)
console.log(result.code)
结果
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
// es2015
var num = 1;
var Person = /*#__PURE__*/function () {
function Person() {
(0, _classCallCheck2.default)(this, Person);
}
(0, _createClass2.default)(Person, [{
key: "sayHi",
value: function sayHi() {
console.log('hi');
}
}]);
return Person;
}();
// es2022
var temp = {
name: 'name'
};
console.log(Object.hasOwn(temp, 'name'));
辅助代码通过@babel/runtime
依赖导入。
总结
文章主要介绍了Babel的使用和配置,通过代码演示对比,更容易理解每个模块的作用,方便脚手架中的配置。