Babel
Babel
Babel是一个编译器,主要用于将最新的 JavaScript 代码转化为向后兼容的代码,以便在老版本的浏览器或环境中运行。
Babel的核心功能是解析代码到 AST,然后再将 AST 转回 JavaScript 代码的所有的语法转换(例如将 ES6 转化为 ES5)和功能添加(例如 polyfills)都是通过各种插件实现。
在前端开发中,Babel 被广泛用于现代 JavaScript 项目,它能确保代码能在各种环境中运行,而不需要手动处理各种浏览器和 JavaScript 版本的兼容性问题。
主要功能
- 语法转换:将新的 JavaScript 语法(如 JSX,TypeScript,ES2015+ 特性等)转换为旧的 ES5 语法。
- 源码映射:在编译后的代码中添加源码映射,以方便调试。
- Polyfills:添加缺失的特性,如
Promise、Symbol等,这称为 polyfill。Babel 提供了一个 Polyfill 功能,能自动引入所需的 Polyfill。这个功能通过core-js模块实现(Babel v7.4.0 之前使用的是@babel/polyfill),可以模拟整个 ES2015+ 环境。 - 插件和预设:Babel 提供了大量的插件支持,可以通过插件来使用特定的 JavaScript 特性。预设是一组插件的集合,例如,
@babel/preset-env会根据环境自动决定需要使用哪些插件。
依赖包
@babel/parser: Babel 的解析器,用于将源代码转换为 AST。@babel/core: Babel 的核心包,提供了 Babel 的核心编译功能。这个包是使用 Babel 必须安装的。@babel/generator: Babel 的代码生成器,它接收一个 AST 并将其转换为代码和源码映射(sourcemap)。@babel/code-frame: 提供了一种用于生成 Babel 错误消息的方法,可以在代码帧中高亮显示错误。@babel/runtime: 提供了 Babel 运行时所需要的辅助函数和 polyfills,以避免在每个文件中都重复这些代码。@babel/template: 提供了一种编写带有占位符的 Babel AST 模板的方法。@babel/traverse: 是 Babel 的 AST 遍历器,它包含了一些用于处理 AST 的工具。@babel/types: 提供了一种用于 AST 节点的 Lodash-esque 实用程序库。@babel/cli:提供 Babel CLI 命令行工具。
配置文件
配置文件格式
在 Babel 中,配置文件可以分为两种:
项目范围
这种配置文件是针对整个项目生效的一个配置,一般放在项目根目录下面。Babel 对项目范围级别的配置文件是有格式要求的,一般是指 babel.config.* 这种格式,* 支持各种类型的扩展名:
.json。.js。.cjs。.mjs。.cts。
文件相关
这种类型的配置文件对特定的文件或者特定的目录以及子目录生效。在 Babel 中,如下格式的配置文件是文件级别:
.babelrc.js。.babelrc.json。.babelrc。package.json文件里面的babel键。
Babel 在对目录下的文件进行编译的时候,会自动的合并多个 Babel 配置文件。如在目录下,会合并目录下的配置文件和项目的配置文件。
配置选项
Babel 所支持的配置项比较多,可进行一个简单的分类:
- 主要选项。
- 配置加载选项。
- 插件和预设配置。
- 输出目标选项。
- 配置合并选项。
- 源码映射选项。
- 其他选项。
- 代码生成器选项。
- AMD / UMD / SystemJS 选项。
- 选项概念。
插件和预设配置
plugins:配置要使用的插件,对应的值为一个数组,可以配置多个,插件需要提前进行安装。presets:配置一个预设,对应的值也是一个数组,表示可以配置多个。
{
"plugins": [
["@babel/plugin-transform-arrow-functions", {}]
],
"presets": [
["@babel/preset-env"]
]
}
输出目标选项
targets:用于指定要兼容的浏览器版本范围。
{
"targets": "> 0.25%, not dead"
}
指定浏览器范围,有多种多样的形式,可以在项目根目录下创建一个 .browserslistrc 配置文件来指定范围,也可以在 package.json 中通过 browserslist 这个键来指定范围。优先级顺序如下:targets、.browserslistrc、package.json。
browserlistConfigFile
默认值是 true,表示允许 Babel 去搜寻项目中和 browserlist 相关的配置。
例如 Babel 配置文件中没有 targets 的配置,但是项目中有 .browserslistrc 这个文件,里面指定了浏览器范围,那么 Babel 在进行编译的时候,会去搜索和 browserlist 相关的配置,并在编译的时候应用对应的浏览器范围配置。这个配置对应的值还可以是一个字符串形式的路径,该路径就指定了具体的 browserlist 文件的位置。
{
"presets": [
["@babel/preset-env", {
"browserslistConfigFile": "./.browserslistrc"
}]
]
}
配置合并
extends:允许扩展其他的 Babel 配置文件。可以提供一个路径,该路径对应的 Babel 配置文件就会作为基础的配置。
{
"extends": "./base.babelrc.json"
}
env:可以为不同的环境提供不同的配置,例如在开发环境或者生成环境需要使用不同的插件或者预设,那么就可以通过env来指定环境。
{
"env": {
"development": {
"plugins": ["pluginA"]
},
"production": {
"plugins": ["pluginB"]
}
}
}
overrides:该配置项用于对匹配上的特定文件或者目录应用不同的配置。test:做匹配。include:包含哪些目录。exclude:排除哪些目录。
{
"overrides": [
{
"test": ["*.ts", "*.tsx"],
"exclude": "node_modules",
"presets": ["@babel/preset-typescript"]
}
]
}
ignore和only:ignore控制忽略文件,only指定特有文件。
{
"ignore": ["node_modules"],
"only": ["src"]
}
源码映射
sourceMaps:否要生成 source map。sourceFileName:指定 source map 文件的文件名。sourceRoot:source map 文件对应的 URL 前缀。
{
"sourceMaps": true,
"sourceFileName": "customFileName.js",
"sourceRoot": "/root/path/to/source/files/"
}
其他选项
-
sourceType:指定 Babel 应该如何去解析 JavaScript 代码。module:如果代码使用的 ESM 模块化,里面涉及到了export、import,那么应该指定为这个值。script:普通的 JS 脚本,没有使用模块化。unambiguous:让 Babel 自己来判断,Babel 检查到代码使用了export、import,就会视为模块文件,否则就会视为普通的script脚本。
-
assumptions:让开发者对自己的代码做一个假定。
{
"assumptions": {
"noClassCalls": true
}
}
CLI
Babel 也提供了 CLI,方便在命令行进行操作。
yarn add --save-dev @babel/core @babel/cli
在使用 Babel 的 CLI 命令的时候,有一个基本的格式:
babel [file | dir | glob] --out-[file | dir]
编译文件
如果你没有指定 --out,那么 Babel 会将编译后的结果输出到控制台。
// 编译结果输出到控制台
babel script.js
// 编译结果输出到指定文件
babel script.js --out-file script-compiled.js
// 编译整个目录到指定目录下
babel src --out-dir lib
// 编译整个目录下的文件,输出到一个文件里面
babel src --out-file script-compiled.js
// 监视文件,当文件发生变化时自动重新编译
babel script.js --watch --out-file script-compiled.js
在进行编译的时候,也可以指定是否要生成 source map:
babel script.js --out-file script-compiled.js --source-maps
babel script.js --out-file script-compiled.js --source-maps inline
忽略文件
// 忽略 src 目录下面的所有测试文件
babel src --out-dir lib --ignore "src/**/*.spec.js","src/**/*.test.js"
拷贝文件
有些文件要进行拷贝,而不需要 Babel 进行编译:
// 将 src 目录下的文件原封不动的复制到 lib 目录下
babel src --out-dir lib --copy-files
// 进行拷贝的时候忽略文件中匹配的文件不要拷贝
babel src --out-dir lib --copy-files --no-copy-ignored
插件和预设
// 指定插件
babel script.js --out-file script-compiled.js --plugins=@babel/transform-class-properties,@babel/transform-modules-amd
// 指定预设
babel script.js --out-file script-compiled.js --presets=@babel/preset-env,@babel/flow
配置文件
通过 --config-file 可以指定配置文件的位置:
babel --config-file /path/to/my/babel.config.json --out-dir dist ./src
如果想要忽略已经有了的配置文件中的配置,可以使用 --no-babelrc:
babel --no-babelrc script.js --out-file script-compiled.js --presets=@babel/preset-env,@babel/preset-react
预设
预设是一组的插件的组合,方便进行配置。
预设的对应的值是一个数组,因此可以配置多个预设:
yarn add --save-dev @babel/preset-env @babel/preset-react
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
预设的运行的顺序和插件是相反的,从后往前运行。
官方预设
官方提供了 4 套预设:
@babel/preset-env用于编译 ES2015 及以上版本的语法。@babel/preset-typescript用于 TypeScript。@babel/preset-react用于 React。@babel/preset-flow用于 Flow。
@babel/preset-env 是最常用的预设:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.22",
"modules": false,
"targets": "> 0.25%, not dead"
}
]
]
}
targets:指定浏览器需要支持的版本范围。useBuiltIns:决定如何使用 polyfills。entry:该选项值会根据项目中browserslist对应的浏览器版本范围来添加 polyfills,这个选项不会管你源码中是否用到缺失的特性,只要对应的浏览器版本是缺失的,那么就会添加对应的特性。而且在使用这个选项值的时候,还需要在源码的入口文件中手动引入core-js。usage:根据源码中是否使用了缺失的特性,如果使用到了缺失的特性,那么才添加对应的 polyfills。false:这个是默认值,关闭自动引入 polyfills。
corejs:指定定corejs版本,polyfills 是通过corejs来实现的,该配置项一般和useBuiltIns一起使用。2:使用 core-js 的版本2。这是旧版本的core-js,它包含 ES5、ES6 和 ES7 的特性。在 Babel 7.4.0 之前,这是默认值。3:使用 core-js 的版本3。这是新版本的core-js,它包含 ES5、ES6、ES7、ES8 和更高版本的特性。在 Babel 7.4.0 及更高版本,这是推荐的值。false:不使用 core-js。如果不想让 Babel 添加任何 polyfill,可以将corejs设置为false。
modules:设置模块的类型,默认值为auto,根据环境和代码自动来决定使用的模块版本。amdumdsystemjscommonjscjsautofalse
include:允许显式的指定要包含的插件。
{
"presets": [
["@babel/preset-env", {
"targets": "> 0.25%, not dead",
"include": ["@babel/plugin-proposal-optional-chaining"]
}]
]
}
APIs
babel/core 的 API 大致可分为三类:
transform:编译相关的操作。parse:将源码转为抽象语法树。load:配置文件的加载。
插件
插件是 Babel 的灵魂,Babel 中的插件很多。
使用插件
在 Babel 要使用一个插件,可分为两步:
- 安装插件。
- 在配置文件或者 CLI 中指定插件。
yarn add @babel/plugin-transform-arrow-functions -D
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
插件顺序
plugins 对应的是一个数组,因此可以指定多个插件,插件的运行会从左往右运行。
如果配置文件中既配置了插件,又配置了预设,那么 Babel 会先运行插件,然后在运行预设里面的插件,也就是说,插件运行的时机要早于预设插件。
插件选项
插件有三种配置方式:
{
"plugins": ["pluginA", ["pluginA"], ["pluginA", {}]]
}
上面的三种写法目前来讲是等价的,而第三种写法,数组第二项的对象可以用来传递插件配置项:
{
"plugins": [
[
"transform-async-to-module-method",
{
"module": "bluebird",
"method": "coroutine"
}
]
]
}
Babel 原理
Babel处理代码流程
Babel 在对代码进行处理的时候,核心的流程分为三步:
解析(parse)
将接收到的源代码转为抽象语法树,此步骤可分为两个小阶段:词法分析与语法分析。
词法分析,即将源码转为 token:
n * n;
会形成如下的 token:
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
]
每一个 token 中有一个 type 属性来描述这个 token:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
// ...
}
形成 token 之后,会进入到语法分析阶段,将所得到的 token 转为 AST 树结构,便于后续的操作。
转换(transform)
对 AST 树进行遍历,在遍历的时候,就可以对树里面的节点进行一些添加、删除、更新等操作,这其实就是 Babel 转换代码的核心。
生成(generate)
经历转换之后,得到的树结构已经和之前不一样,然后将这颗 AST 重新转为代码。
插件的核心原理:创建新的 AST 节点,替换旧的 AST 节点。
遍历
在对 AST 进行遍历的时候,采用的是深度优先遍历。
访问者
访问者其实是一个对象,该对象上面有一些特殊的方法,这些方法会在到达特定的节点的时候触发:
const MyVisitor = {
Identifier() {
console.log("Called!");
}
}
该访问者对象会在遍历 AST 树的时候,遇见 Identifier 节点的时候会被调用。
有些时候可以针对特定的节点定义进入时要调用的方法,退出时要调用的方法:
const MyVisitor = {
Identifier: {
enter(path, state) {
console.log("Entered!");
},
exit(path, state) {
console.log("Exited!");
}
}
}
路径
AST 是由一系列的节点组成,但是这些节点之间并非孤立的,而是彼此之间有一些联系。
因此有一个 path 对象,该对象主要记录节点和节点之间的一些关系。path 对象里面不仅仅包含了节点本身的信息,还包含了节点和父节点、子节点、兄弟节点之间的关系。
这样做的好处在于使用了一个相对简单的对象来表示节点之间复杂关系,而不需要在每个节点里面来保存节点之间关系的信息。
const babel = require("@babel/core");
const traverse = require("@babel/traverse").default;
const code = `function square(n) {
return n * n;
}`;
const ast = babel.parse(code);
traverse(ast, {
enter(path) {
console.log(path.node.type);
},
});
状态
在遍历和修改抽象语法树的时候,应该尽量避免全局状态的问题,可以在一个访问者对象里面再定义一个访问者对象专门拿来存储状态:
const updateParamNameVisitor = {
Identifier(path) {
if (path.node.name === this.paramName) {
path.node.name = "x";
}
}
};
const MyVisitor = {
FunctionDeclaration(path) {
const param = path.node.params[0];
const paramName = param.name;
param.name = "x";
path.traverse(updateParamNameVisitor, { paramName });
}
}
自定义插件
格式
自定义 Babel 插件,基本格式为:
module.exports = function(babel){
// 该函数会自动传入 babel 对象
// types 也是一个对象,该对象上面有很多的方法,方便对 AST 的节点进行操作
const { types } = babel;
return {
name: "Plugin Name",
visitor: {
// ...
// 这里书写不同类别的方法,不同的方法会被进入不同类别的节点触发
}
}
}
types 对象的一些方法:
t.callExpression(callee, arguments):这个函数用于创建一个表示函数调用的 AST 节点。callee参数是一个表示被调用的函数的表达式节点,arguments参数是一个数组,包含了所有的参数表达式节点。t.memberExpression(object, property, computed = false):这个函数用于创建一个表示属性访问的 AST 节点。object参数是一个表示对象的表达式节点,property参数是一个表示属性名的标识符或表达式节点,computed参数是一个布尔值,表示属性名是否是动态计算的。t.identifier(): 创建 AST 节点,只不过创建的是identifier类型的 AST 节点。
示例
创建一个自定义插件,该插件能够把 ES6 里面的 ** 转换为 Math.pow:
- plugins/transform-to-mathpow.js
- test.js
module.exports = function (babel) {
const { types: t } = babel
return {
name: 'transform-to-mathpow',
visitor: {
BinaryExpression(path) {
if (path.node.operator !== '**') {
return
}
const mathpowAstNode = t.callExpression(
t.memberExpression(
t.identifier('Math'),
t.identifier('pow')
),
[path.node.left, path.node.right]
)
path.replaceWith(mathpowAstNode)
}
}
}
}
const babel = require('@babel/core')
const myPlugin = require('./plugins/transform-to-mathpow')
const code = 'const result = 2 ** 3'
const result = babel.transform(code, {
plugins: [myPlugin]
})
// const result = Math.pow(2, 3);
console.log(result.code)