本章从整体视角介绍 Babel 生态的全貌,并非详细地介绍其中的每一个细节,如希望了解详细的介绍,可根据需要查看本书其他章节。
# 1 Babel 简介
# 1.1 Babel 是什么?
Babel 是 JavaScript 转译器,通过 Babel,开发者可以自由使用下一代 ECMAScript 语法。高版本 ECMAScript 语法将被转译为低版本语法,以便顺利运行在各类环境,如低版本浏览器、低版本 Node.js 等。
Babel 是转译器,不是编译器。下面是转译和编译的区别:
编译,一般指将一种语言转换为另一种语法和抽象程度等都不同的语言,常见的比如 gcc 编译器。 转译,一般指将一种语言转换为不同版本或者抽象程度相同的语言,比如 Babel 可以把 ECMAScript 6 语法转译为 ECMAScript 5 语法。
利用 Babel,开发者可以使用 ECMAScript 的各种新特性进行开发,同时花极少的精力关注浏览器或其他JS运行环境对新特性的支持。甚至,开发者可以根据自身需要,创造属于自己的 JavaScript 语法。
Babel 在转译的时候,会对源码进行以下处理: 语法转译(Syntax)和添加 API Polyfill。
语法(Syntax)部分
Babel 支持识别高版本语法,并通过插件将源码从高版本语法转译为低版本语法,如:
- 箭头函数
() => {}
转为普通函数function() {}
。 const / let
转译为var
- 箭头函数
API Polyfill
有些运行时相关的 API,语法转译无法解决它们对低版本浏览器等环境的兼容性问题,因此 Babel 通过与 core-js 等工具的配合,实现 API 部分对目标环境(通常是低版本浏览器等)的兼容。
例如
[1, 2, 3].include
、Promise
等 API,Babel 在处理时,如果目标环节可能不支持原生的include / Promise
的话,Babel 会在转译结果中嵌入include / Promise
的自定义实现。
有多种方式可以使用 Babel,如: 命令行(babel-cli、babel-node)、浏览器(babel-standalone)、API调用(babel-core)、webpack loader(babel-loader)等。
# 1.2 转译过程
和多数转译器相同,Babel 运行的生命周期主要是 3 个阶段: 解析、转换、代码生成。
这个过程涉及抽象语法树:
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。
AST 是树形对象,以结构化的形式表示编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
源码字符串需要经转译器生成 AST,转译器有很多种,不同转译器,生成的AST对象格式细节可能有差异,但共同点为: 都是树形对象、该树形对象描述了节点特征、各节点之间的关系(兄弟、父子等)。
以下是 Babel 生命周期的三个过程:
解析(Parsing): Code1 ==> 抽象语法树1
解析过程包括 2 个环节: 词法解析、语法解析,最终生成抽象语法树。
词法解析阶段,代码字符串被解析为 token 令牌数组,数组项是一个对象,包括: 代码字符碎片的值、位置、类型等信息。
token 数组是平铺式的数组,没有嵌套的结构信息,它是为语法解析阶段做准备的。
语法解析阶段,token 令牌数组被解析为结构化的抽放语法树对象(AST)。
babel-parser 完成该阶段的主要功能。
转换(Transformation): AST1 ==> AST2
Babel 生成 AST 后,会对 AST 进行遍历,遍历过程中,各类插件对原 AST 对象进行增删改查等操作,AST 结构被修改。
代码生成(Generation): AST2 ==> Code2
Babel 将修改后的 AST 对象转目标代码字符串。
babel-generator 完成该阶段的主要功能。
# 2 Babel 模块拆解
# 2.1 微内核架构
Babel 采用微内核架构,其内核保留核心功能,其余功能利用外部工具和插件机制实现,也体现了"开放-封闭"的设计原则。
除了微内核设计架构,Babel 的模块设计也可以做如下分类:
# 2.2 转译模块
转译模块位于 Babel 微内核架构的"微内核"部分,该部分主要负责代码转译,也就是上面提到的"解析-转换-代码生成"过程。
该模块主要包括: babel-parser、babel-traverse、babel-generator。
babel-parser
负责将代码字符串转为 AST 对象。
有 2 点值得一提:
- babel-parser 本身并不会对 AST 做转换操作,只是负责解析出 AST。AST 转换部分交由各类 plugins 和 presets 处理。
- babel-parser 内置了对 ESNext/TypeScript/JSX/Flow 最新版本语法的支持,但很多默认是不开启的,目前没有开放插件机制扩展新语法。
babel-traverse
在转译过程中,babel-traverse 负责遍历 AST 节点,并根据配置的 plugins/presets,在遍历过程中,对各个 AST 节点进行增删改查的操作。
AST 是一个树形对象,遍历 AST 对象的过程也是一个深度优先遍历的过程。
babel-generator
负责将 AST 节点,转为代码字符串,同时也可以生成 source-map。
# 2.3 插件模块
插件模块包括 plugins、presets。
plugins
丰富的插件,帮助 Babel 成为一个非常成功的转译工具。
对 AST 的遍历、转换是 Babel 转译的核心功能,但 Babel 本身并不参与该过程,将这些功能作为插件引入到运行时。
具体来说,babel-core 作为核心工具,不提供对 AST 的修改逻辑,通过调用各类插件,实现对 AST 的修改。
Babel的插件分为语法插件和转换插件。
语法插件
值得注意的是,babel-parser 负责将 JavaScript 代码解析出抽象语法树(AST),它支持全面识别 ESNext/TypeScript/JSX/Flow 等语法,目前由 Babel 团队开发维护,不支持插件化。
Babel 插件生态中的语法插件,其功能就是作为"开关",配置是否开启 babel-parser 的某些语法转译功能。
语法插件在 Babel 源码中,以
babel-plugin-syntax
开头。举个例子:
babel-plugin-syntax-decorators
负责开启 babel-parser 对装饰器的语法支持。
babel-plugin-syntax-dynamic-import
负责开启 babel-parser 对
import
语句的语法支持。babel-plugin-syntax-jsx
负责开启 babel-parser 对 jsx 语法的支持。
转换插件
转换插件就是社区里常说的 Babel 插件,负责转换 AST 节点。
在介绍 babel-traverse 时提到,它负责遍历 AST 对象,每个 AST 节点会被访问到,等待转换,转换的部分,由"转换插件"负责。
转换插件会提供一个叫做"Visitor"的对象,该对象的 Key 为节点名称,Value 部分提供进入该节点时、离开该节点时的回调函数,在回调函数里,可以对该节点进行一系列操作。
"Visitor" 又称为 "访问者"。
// plugin 提供 visitor,在 visitor 中对 AST 节点操作 const visitor = { Program: { enter() {}, exit() {}, }, CallExpression: { enter() {}, exit() {}, }, NumberLiteral: { enter() {}, exit() {}, } }; traverse(ast, visitor);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19转换插件在 Babel 源码中,以
babel-plugin-transform
开头。举个例子:
babel-plugin-transform-strict-mode
该插件拦截
Program
节点,也就是整个程序的根节点,添加"use strict"
指令。visitor 节点值为函数时,是 enter 回调的快捷方式。
{ name: "transform-strict-mode", visitor: { Program(path) { const { node } = path; for (const directive of (node.directives: Array<Object>)) { if (directive.value.value === "use strict") return; } path.unshiftContainer( "directives", t.directive(t.directiveLiteral("use strict")), ); }, }, }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19babel-plugin-transform-object-assign
该插件负责拦截函数调用表达式节点
CallExpression
,将Object.assign
转为extends
写法。{ name: "transform-object-assign", visitor: { CallExpression(path, file) { if (path.get("callee").matchesPattern("Object.assign")) { path.node.callee = file.addHelper("extends"); } }, }, }
1
2
3
4
5
6
7
8
9
10
11
presets
Babel 插件的功能是细粒度的,大部分插件承担了单一功能。
而在实际开发过程中,往往需要支持对各类语法的支持。此时,有两种做法:
- 需要支持哪些特性,就分别引入支持该特性的插件
- 直接引入一个插件集合,涵盖所需的各类插件功能
很显然,第一种做法是相对麻烦的。针对第二种做法,Babel 提供了插件集 preset。
preset 在 Babel 源码中,以
babel-preset
开头。例如,Babel 已经提供了几种常用的 preset 供开发者使用:
babel-preset-env
babel-preset-react
babel-preset-flow
babel-preset-typescript
插件运行顺序
Babel 配置项中,plugins 和 presets 均以数组的形式配置,执行时有先后顺序。
- plugins 在 presets之前运行
- plugins 按照数组正序执行
- presets 按照数组倒序执行
# 2.4 工具模块
工具模块提供 Babel 相关模块所需的各类工具,以下一一简要介绍:
babel-core
babel-core 对外提供了 Babel 多项功能的 API,如转译文件、转译代码、创建/获取配置等,在 Babel 官方文档介绍的比较详细,不再赘述。
值得注意的是,转译类的 API 均提供了同步和异步版本,如
transformSync/transfomAsync
、parseSync/parseAsync
。babel-cli
Babel 的命令行工具,可以直接转译文件夹/文件,它也提供了很多配置项做其他工作,官方文档介绍的比较详细,感兴趣的同学可以去 Babel 官网查看详细配置。
babel-standalone
Babel 对外服务的很多包是基于 node 环境下使用的,babel-standalone 提供了浏览器下转译的方案。
babel-standalone 内置了所有 Babel 插件,所以体积还是比较大的,而且在浏览器端转译需要时间,比较适合开发、学习使用,不适合在生产环境使用。
举个例子:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>test babel-standalone</title> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> const arr = [1, 2, 3]; console.log(...arr); </script> </head> <body> </body> </html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14在浏览器运行该 html,可以看到,页面结构变成了:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>test babel-standalone</title> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> const arr = [1, 2, 3]; console.log(...arr); </script> <script> "use strict"; var _console; var arr = [1, 2, 3]; (_console = console).log.apply(_console, arr); //# sourceMappingURL=data:application/json;charset=utf-8;base64... </script> </head> <body> </body> </html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21babel-node
提供在命令行执行高级语法的环境。
例如:
// index.js 里可以使用高级语法 babel-node -e index.js
1
2index.js 文件以及被其引入的其他文件均可以使用高级语法了。
和 babel-cli 不同的是,babel-cli 只负责转换,不在 node 运行时执行;babel-node 会在 node 运行时执行转换,不适合生产环境使用。
babel-register
在源文件中,引入
babel-register
,如 index.js:index.js
require('babel-register'); require('./run');
1
2run.js
import fs from 'fs'; console.log(fs);
1
2执行
node index
时,run.js 就不需要被转码了。babel-register 通过拦截 node require 方法,为 node 运行时引入了 Babel 的转译能力。
babel-loader
babel-loader 并不是 Babel 官方提供的对外服务工具,它是独立的另一个项目,利用 babel-core 的 API 封装的 webpack loader,用于 webpack 构建过程。
babel-types
babel-types 是一个非常强大的工具集合,它集成了节点校验、增删改查等功能,是 Babel 核心模块开发、插件开发等场景下不可或缺的工具。
例如:
const t = require('@babel/types'); const binaryExpression = t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2));
1
2babel-template
模板引擎,负责将代码字符串转为 AST 节点对象。
import { smart as template } from '@babel/template'; import generate from '@babel/generator'; import * as t from '@babel/types'; const buildRequire = template(` var %%importName%% = require(%%source%%); `); const ast = buildRequire({ importName: t.identifier('myModule'), source: t.stringLiteral("my-module"), }); const code = generate(ast).code console.log(code)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16运行结果:
var myModule = require("my-module");
1babel-code-frame
负责打印出错的代码位置,例如:
const { codeFrameColumns } = require('@babel/code-frame'); const testCode = ` class Run { constructor() {} } `; const location = { start: { line: 2, column: 2, } }; const result = codeFrameColumns(testCode, location); console.log(result);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
181 | class Run { > 2 | constructor() {} | ^ 3 | } 4 |
1
2
3
4
5babel-highlight
向控制台输出有颜色的代码片段。该工具可以识别 JavaScript 中的操作符号、标识符、保留字等类型的词法单元,并在终端环境下显示不同的颜色。
# 2.5 运行时相关模块
Babel 配合其插件可以对静态代码进行转译,但有一些遗漏点:
- 对于运行时涉及的一些高版本 API,并没有提供兼容目标环境的 Polyfill
- 转译产物代码可能有些臃肿
为此,运行时模块(runtime)关注的是转译产物的运行时环境,对运行时提供 API polyfill、代码优化等,该模块涉及几个子包:
- babel-preset-env
- babel-plugin-transform-runtime
- babel-runtime
- babel-runtime-corejs2/3
- core-js
接下来以案例解释 runtime 模块的作用。
源码文件 index.js 的内容:
const a = 1; // const 为语法部分
class Base {} // class 为语法部分
new Promise() // Promise 为 API 部分
2
3
这段源码包含了语法和 API 部分:
const
、class
为语法部分Promise
为 API 部分
如果希望这段源码转为 ES5 版本,使构建产物可以运行在不支持 ES6 和 Promise 的环境里,该怎么做呢?
用 babel 命令行执行转译,其中源文件为 index.js,转译产物文件为 index-compiled.js。
npx babel index.js --out-file index-compiled.js
安装 node 或 npm 后,可直接使用 npx 唤起各类命令行执行
需要配置 .babelrc
帮助 Babel 完成语法和 API 部分的转译:
.babelrc
:
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
简要解释下该配置的原理:
babel-preset-env 可以完成语法部分转译,即
const
转译为var
但构建产物中,有些辅助代码如
_classCallCheck
是以硬编码的形式直接写入转译产物的:"use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var a = 1; var Base = function Base() { _classCallCheck(this, Base); }; new Promise();
1
2
3
4
5
6
7
8
9
10
11这样的后果就是构建产物比较臃肿。
babel-plugin-transform-runtime 可以将上述
_classCallCheck
置于通用包中,以引用的形式写入转译产物:"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var a = 1; var Base = function Base() { (0, _classCallCheck2["default"])(this, Base); }; new Promise();
1
2
3
4
5
6
7
8
9
10
11
12
13babel-plugin-transform-runtime 的配置参数
corejs
用于转译 API 部分,如Promsie
"use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault"); var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck")); var a = 1; var Base = function Base() { (0, _classCallCheck2["default"])(this, Base); }; new _promise["default"]();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Babel 转译过程的运行时优化是一个繁琐的过程,为此将单独用一章讲解运行时优化,感兴趣的同学可以直接阅读 "Babel Runtime" 章节详细了解。
# 3 使用 Babel
# 3.1 对外工具集
在介绍 Babel 微内核架构时,面向用户使用的工具,有这些:
- babel-cli
- babel-node
- babel-register
- babel-standalone
- babel-parser
- babel-loader
由此看出,Babel 官方和社区提供了众多工具以满足开发者在不同场景下使用 Babel 的需要,下图是 Babel 可以使用的场景分类:
# 3.2 配置 Babel
# 多种配置方式
Babel 支持多种配置方式。
# 文件式配置
配置文件列表
babel.config.
为前缀的文件- babel.config.json
- babel.config.js
- babel.config.cjs
- babel.config.mjs
.babelrc
为前缀的文件- .babelrc.json
- .babelrc.js
- .babelrc.cjs
- .babelrc.mjs
其他工具如 ESLint、Prettier 也有类似的配置文件: .eslintrc、.prettierrc。
package.json 中
package.json 中的 "babel" 字段。
配置文件格式
不同后缀名的配置文件,其配置内容需遵循不同的格式规范。
.cjs
文件,内容是 CommonJS 规范const presets = [ ... ]; const plugins = [ ... ]; module.exports = { presets, plugins };
1
2
3
4.mjs
文件,内容是 ECMAScript modules 格式可以在 Node.js 13.2 及以上版本试用,也可以在较老版本 Node.js 的
--experimental-modules
标记下使用。.js
文件内容,可以是 CommonJS 规范,也可以是 ECMAScript modules 格式如果在 package.json 中指定
"type": "module"
,babel.config.js
和.babelrc.js
文件即可使用 ECMAScript modules。否则,
babel.config.js
和.babelrc.js
文件需要使用 CommonJS 规范。.json
文件,内容可以是: JSON5 规范、CommonJS 规范JSON5 规范的写法:
{ "name": "my-package", "version": "1.0.0", "babel": { "presets": [ ... ], "plugins": [ ... ], } }
1
2
3
4
5
6
7
8
# 命令行式配置
Babel 支持执行 babel
命令行时指定配置。
babel --plugins @babel/plugin-transform-arrow-functions script.js
# API 式配置
通过调用 Babel 提供的 API,并为其传参。
require("@babel/core").transformSync("code", {
plugins: ["@babel/plugin-transform-arrow-functions"]
});
2
3
# 项目级配置与局部配置
Babel7 新增了项目级配置和局部配置的概念,其配置文件有两种作用范围: 完整的项目范围、子项目范围。
babel.config.
系列的配置文件,影响范围是整个项目。
.babelrc
系列的配置文件,影响范围是子项目。
官方文档对此有详细的描述,以下是笔者整理后的配置文件作用范围描述:
项目级配置文件(
babel.config.
系列)局部配置文件(
.babelrc
系列)对于当前正在被转译的文件,Babel 会逐级向上查找
.babelrc
系列的配置文件,直到第一个有package.json
的目录、或当前项目的root
目录。找到局部配置文件后,会将其内容和
babel.config.json
之类的配置内容合并。
# 配置项
Primary options
这些配置在 API 中使用,相对于文件形式(
.babelrc
等)的配置,它们的优先级更高些,因此成为Primary options
。cwd: string
默认值是:
process.cwd()
Babel 运行过程中,所有子路径的相对地址,应用于 API 调用时。
caller: CallerData
interface CallerData { name: string; supportsStaticESM?: boolean; supportsDynamicImport?: boolean; supportsTopLevelAwait?: boolean; supportsExportNamespaceFrom?: boolean; }
1
2
3
4
5
6
7提供给 Babel 自身配置项、
plugins
、presets
的一些快捷灵活配置项。filename: string
这是个非必填项。可用于 API 调用时,一些其他配置项依赖
filename
的配置。这里有一些场景可能需要配置
filename
:filename
的值可以暴露给插件做进一步处理test
、exclude
、ignore
等配置项需要filename
有值用于匹配- 不提供
filename
的情况下,查找.babelrc.json
、.babelrc
这些配置文件时,会相对于被转译的目标文件;如果提供了filename
,会默认执行babelrc: false
的逻辑
filenameRelative: string
默认值:
path.relative(opts.cwd, opts.filename)
。filenameRelative
的值会是配置项sourceFileName
的默认值。code: boolean
默认是
true
。默认情况下,api 如
transformSync
在转译后会返回转译后的代码,也就是code
。但可能存在不需要code
的时候,比如在多级转译时,前后逻辑仅需要 AST 传递即可。ast: boolean
默认是
false
,api 如transformSync
并不会返回转移后代码的 AST 对象,也是出于性能考虑,因为 Babel 会优先为转译结果添加本地缓存文件,而 AST 对象需要占用挺大空间的。如果需要在多级转译时,利用中间过程产生的 AST 对象,就可以设置为
true
了:const filename = "example.js"; const source = fs.readFileSync(filename, "utf8"); // Load and compile file normally, but skip code generation. const { ast } = babel.transformSync(source, { filename, ast: true, code: false, }); // Minify the file in a second pass and generate the output code here. const { code, map } = babel.transformFromAstSync(ast, source, { filename, presets: ["minify"], babelrc: false, configFile: false, });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17cloneInputAst: boolean
默认值是
true
。例如:
const { code, map } = babel.transformFromAstSync(ast, source, { filename, presets: ["minify"], babelrc: false, configFile: false, });
1
2
3
4
5
6babel.transformFromAstSync
会接受一个 ast 对象进行各种操作,内部会复制一个新的 ast 对象避免修改传入的 ast 对象。当然开发者可以决定是否需要复制,毕竟复制的开销是比较大的。
Config Loading options
root
仅用于 API 调用环境。类型是
string
,默认值是opts.cwd
的值,opts.cwd
的默认值是process.cwd()
。这是一个基准路径,与
rootMode
配合使用,它决定了当前 Babel 作用项目的根路径(root folder)是什么。这个基准值可以用来:
- 作为查找
configFile
配置项的基准路径 - 作为
babelrcRoots
配置项的默认值
- 作为查找
rootMode
仅用于 API 调用环境。类型是
"root" | "upward" | "upward-optional"
。默认值是"root"
。rootMode
与root
配合使用,它决定了当前 Babel 作用项目的根路径。"root"
: 和默认值一样,描述当前 Babel 作用项目的根路径"upward"
: 沿着root
配置项持续向上查找,发现babel.config.json
文件后停止向上遍历,否则会一直遍历到系统根目录"upward-optional"
: 和"upward"
一样,但如果遍历到系统根目录也没有发现babel.config.json
文件,会退回到"root"
值
envName
仅用于 API 调用环境。类型是
string
。默认值是process.env.BABEL_ENV || process.env.NODE_ENV || "development"
。可以在配置文件的
"env"
配置项里,或api.env()
方法使用。configFile
仅用于 API 调用环境。类型是
string | boolean
。默认值是path.resolve(opts.root, "babel.config.json")
,但内部会判断该路径的文件是否存在,如果不存在,值会被调整为false
。babelrc
仅用于 API 调用环境。类型是
boolean
。默认值是true
。babelrcRoots
Plugin and Preset options
plugins/presets
plugin/preset
配置规范相同。对于
plugin/preset
,如下配置方式的含义是一致的:{ "plugins": [ "pluginA", [ "pluginA" ], [ "pluginA", {} ] ] }
1
2
3
4
5
6
7
8
9[ "pluginA", {} ]
这种写法可以为 plugin/preset 描述更具体的配置,如:{ "plugins": [ [ "transform-async-to-module-method", { "module": "bluebird", "method": "coroutine" } ] ], "presets": [ [ "env", { "loose": true, "modules": false } ] ] }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20passPerPreset
Output targets
targets
类型
string | Array<string> | { [string]: string }
。默认值{}
。举例
{ "targets": "> 0.25%, not dead" }
1
2
3要适配的目标环境的最小版本:
{ "targets": { "chrome": "58", "ie": "11" } }
1
2
3
4
5
6目标环境可以是:
chrome
,opera
,edge
,firefox
,safari
,ie
,ios
,android
,node
,electron
。没有设置
targets
时如果没有设置
targets
,Babel 默认会假设你要适配最古老的浏览器版本,当然这样是不推荐的,因为转译后的文件会比较大。截至本书截稿时,如果使用者没有指定
targets
的值,Babel 也不会像 browserslist 那样提供一个"defaults"
的默认值。targets.esmodules
类型
boolean
。默认值 ??可以指定支持 esmodules 的目标浏览器。
targets.node
类型
string | "current" | true
。targets.safari
类型
string | "tp"
。可以通过设置为
"tp"
来指定 safari 的technology preview
版本。targets.browsers
类型
string | Array<string>
。它的语法是 browserslist 的查询语法。
browserslistConfigFile
类型
boolean
。默认值是true
。用于 browserslist 查询配置用。browserslistEnv
类型是
string
。默认undefined
。
Config Merging options
extends
类型是
string
。env
类型是
{ [envKey: string]: Options }
。overrides
类型是
Array<Options>
。test
类型是
MatchPattern | Array<MatchPattern>
。include
类型是
MatchPattern | Array<MatchPattern>
。同
test
。exclude
类型是
MatchPattern | Array<MatchPattern>
。ignore
类型
Array<MatchPattern>
。ignore: ["./lib"];
1only
only: ["./src"];
1
Source Map options
inputSourceMap
类型
boolean | SourceMap
。默认值true
。true
: 如果存在注释//# sourceMappingURL=...
,会尝试在 Babel 要转译的文件中寻找 sourceMap 的信息,如果加载失败的话就忽略SourceMap
: 也可以传入一个 sourceMap 对象
sourceMaps
类型
boolean | "inline" | "both"
。默认值false
。true
: 转译时会生成 sourceMap,并在转译结果对象中添加 sourceMap 结果对象"inline"
: 转译时会生成 sourceMap,并在转译结果代码中添加以data
开头的 sourceMap 代码"both"
: 综合了true
和"inline"
sourceMap
同
sourceMaps
配置项,推荐使用sourceMaps
。sourceFileName
类型
string
。默认值path.basename(opts.filenameRelative)
,如果不可用,会被重置为unknown
。用于 sourceMap 对象内部。
sourceRoot
类型
string
。设置生成的 sourceMap 中的
sourceRoot
字段。
Misc options
sourceType
类型
"script" | "module" | "unambiguous"
。默认"module"
。"script"
: 使用 ECMAScript Script 语法转译。不允许出现import/export
语句的写法。"module"
: 使用 ECMAScript Module 语法转译。允许出现import/export
语句的写法,并默认转译为严格模式。"unambiguous"
: 自动化判断,如果源码中出现了import/export
语句,就用"module"
模式,否则用"script"
模式。
assumptions
类型
{ [assumption: string]: boolean }
。默认{}
。允许使用在 API 使用、配置文件、
presets
中。它可以用来减少转译结果大小。
{ "assumptions": { "iterableIsArray": true }, "presets": ["@babel/preset-env"] }
1
2
3
4
5
6highlightCode
类型
boolean
。默认true
。在错误提示里对源码高亮展示。
wrapPluginVisitorMethod
类型
(key: string, nodeType: string, fn: Function) => Function
。该方法可用于 AST 节点被内部访问等处理时,开发者做拦截处理。
key
: ??nodeType
: AST 节点类型fn
: AST 节点内部处理函数
开发者需要返回一个新的函数,并在新函数里执行旧的处理函数
fn
。parserOpts
类型
{}
。用于给 babel-parser 提供配置项。generatorOpts
类型
{}
。用于给 babel-generator 提供配置项。
Code Generator options
retainLines
类型
boolean
。默认值false
。??
compact
类型
boolean | "auto"
。默认值"auto"
。??
minified
类型
boolean
。默认值false
。如果设置为
true
,会省略块级代码结尾的分号、省略new Foo()
中的()
、生成更短形式的字面量描述方式等。auxiliaryCommentBefore
类型
string
。auxiliaryCommentAfter
comments
类型
boolean
。默认true
。shouldPrintComment
类型
(value: string) => boolean
。如果同时设置了
minified
,那么其默认值是(val) => opts.comments || /@license|@preserve/.test(val)
。如果没有设置
minified
,那么其默认值是() => opts.comments
。用于决定哪些注释可以在最终输出的转译结果中保留(通常注释是会被删除的)。
AMD / UMD / SystemJS module options
moduleIds
类型
boolean
。默认!!opts.moduleId
。是否开启
module ID
的转译。moduleId
类型
string
。用于描述当前转译的
module
ID,这是硬编码的。不能与getModuleId
同时使用。getModuleId
类型
(name: string) => string
。??
moduleRoot
类型
string
。为生成的 module 名字上添加根路径。
Options Concepts
MatchPattern
类型
string | RegExp | (filename: string | void, context: { caller: { name: string } | void, envName: string, dirname: string ) => boolean
。
# 配置合并规则
Babel 配置的合并遵循以下方式:
Array#concat
针对
plugins/presets
,用Array#concat
合并,因为它们均为数组。Object.assign
plugins/presets
之外的配置,使用Object.assign
做覆盖式合并。
下面是官方文档的案例:
const config = {
plugins: [
[
"plugin-1a", { loose: true }
],
"plugin-1b"
],
presets: [
"preset-1a"
],
sourceType: "script"
}
const newConfigItem = {
plugins: [
[
"plugin-1a", { loose: false }
],
"plugin-2b"
],
presets: [
"preset-1a",
"preset-2a"
],
sourceType: "module"
}
BabelConfigMerge(config, newConfigItem);
// returns
({
plugins: [
[
"plugin-1a", { loose: true }
],
"plugin-1b",
[
"plugin-1a", { loose: false } // concat 推入的配置
],
"plugin-2b" // concat 推入的配置
],
presets: [
"preset-1a",
"preset-1a", // concat 推入的配置
"preset-2b" // concat 推入的配置
],
sourceType: "module" // sourceType: "script" 值被新值覆盖
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Plugins/Presets 配置
Babel 支持对 Plugins 和 Presets 多种形式的配置,可配置的方式有:
EntryTarget
[EntryTarget, EntryOptions]
[EntryTarget, EntryOptions, string]
ConfigItem
plugins: [
// EntryTarget
'@babel/plugin-transform-classes',
// [EntryTarget, EntryOptions]
['@babel/plugin-transform-arrow-functions', { spec: true }],
// [EntryTarget, EntryOptions, string]
['@babel/plugin-transform-for-of', { loose: true }, "some-name"],
// ConfigItem
babel.createConfigItem(require("@babel/plugin-transform-spread")),
],
2
3
4
5
6
7
8
9
10
11
12
13
EntryTarget: string | {} | Function
string
: 要求是 Babel plugin/preset 的路径、名字(如env / @babel/preset-env
,会经过格式化的)。{} | Function
: 要求是 Babel plugin/preset 对象或函数。
EntryOptions: undefined | {} | false
undefined
: 没有设置,内部会重置为空对象{}
。false
: 禁用该 plugin/preset,这个可用于提升转译速度等情况下,由开发者决定是否开启。但是
overrides
配置项的优先级高于它:plugins: [ 'one', [ 'two', false ], 'three', ], overrides: [ { test: "./src", plugins: [ 'two', ] } ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16以上配置的效果是:
two
会参与对./src
路径下文件的转译,其他路径不参与two
的转译依然发生在one
和three
之间
对 plugin/preset 名称的格式化
Babel 支持多种对 plugin/preset 名称的简写,当然要遵循一定的规则:
- 绝对路径
- 以
./
开头的相对路径 - 以
moudle:
开头的名称 - 以
@babel
开头的名称内,会默认为添加plugin-
和preset-
前缀,如@babel/env
会被格式化为@babel/preset-env
| 输入 | 格式化结果 | 规则 | |
"/dir/plugin.js"
|"/dir/plugin.js"
| 绝对路径不处理 | |"./dir/plugin.js"
|"./dir/plugin.js"
| 相对路径不处理 | |"mod"
|"babel-plugin-mod"
| 未声明命名空间@babel
,且没有前缀的,会新增babel-plugin
前缀 | |"mod/plugin"
|"mod/plugin"
| | |"babel-plugin-mod"
|"babel-plugin-mod"
| 有babel-plugin
前缀,不处理 | |"@babel/mod"
|"@babel/plugin-mod"
| 以@babel
命名空间开头,会添加plugin
前缀 | |"@babel/plugin-mod"
|"@babel/plugin-mod"
| 以@babel
命名空间开头,且有plugin
前缀,不处理 | |"@babel/mod/plugin"
|"@babel/mod/plugin"
| | |"@scope"
|"@scope/babel-plugin"
| 在@
开头的某个命名空间上,添加babel-plugin
前缀 | |"@scope/babel-plugin"
|"@scope/babel-plugin"
| | |"@scope/mod"
|"@scope/babel-plugin-mod"
| 在@
开头的某个命名空间上,添加babel-plugin
前缀 | |"@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"
| 以module:
开头,会移除module:
|
# 4 Babel 的项目管理
# 4.1 monorepo
如果一个大型项目由多个子工程组成,该如何管理呢?
通常来说,有以下方式:
monorepo
monorepo 指的是,该项目所有工程统一在一个仓库中,维护、发布等操作统一进行。
目前 Babel、React、Angular 等均采用该方式。
monorepo 的代码结构为:
├── packages | ├── pkg1 | | ├── package.json | ├── pkg2 | | ├── package.json ├── package.json
1
2
3
4
5
6multirepo
和 monorepo 相反,该项目的每个子工程独立运行,每个子工程相互独立,自由选择构建工具,独立发布。
multirepo 的代码结构为:
├── project1 | ├── package.json ├── project2 | ├── package.json ├── project3 | ├── package.json
1
2
3
4
5
6submodules
submodules 是借助 git 的实现,在
.gitmodules
中写明引用的仓库,在主仓库中只保留必要的索引。
在此主要关注 monorepo 和 multirepo。二者对比如下:
monorepo | multirepo |
---|---|
所有项目在同一个分支内开发 | 所有项目独立开发 |
统一的 issue 管理 | 分散的 issue 管理 |
统一的开发环境 | 分散的开发环境 |
统一管理所有项目的版本 | 各项目难以统一版本 |
项目所需存储空间大 | 项目分散,存储空间较小 |
Babel 项目涉及的子工程很多,其采用了 monorepo 方式开发,并且沉淀出了自动化工具 lerna。
# 4.2 lerna
两种模式
monorepo 有两种模式:
fixed
和independent
,默认为fixed
模式。fixed
模式该模式强制所有的包都使用在根目录 lerna.json 中指定的版本号。
lerna 默认使用该模式。
该模式下,每个 package 呈现"黑盒"状态,所有包统一迭代。
independent
模式该模式允许各个包独立指定自己的版本号。
可以使用
lerna init --independent
声明该模式,也可以将lerna.json
的version
字段指定为independent
。对于需要暴露内部细节、或者迭代频率显著不一致的包,比较适合该模式。
命令集
以下是 lerna 的部分命令及功能:
lerna init
: 初始化一个项目lerna create
: 创建一个子 packagelerna import
: 将已有项目导入为子 packagelerna list
: 列举当前项目所有子 packagelerna add
: 添加公共依赖lerna clean
: 删除子 package 的 node_moduleslerna bootstrap
: 安装依赖、创建子 package 间的 symlinklerna diff
: 类似 git difflerna changed
: 列出子 package 的所有变更lerna exec
: 在所有子 package 下执行 shelllerna run
: 在所有子 package 下执行 npm scripts 命令lerna link
: 将彼此依赖的子 package 关联起来lerna publish
: 发布子 packagelerna version
: 确认发布的版本号
后面的章节对 lerna 有更详细的介绍。
# 5 标准化
Babel 生态涉及的一些标准化组织。无论是 JavaScript、HTML、DOM、URL 等领域,均需要统一的标准,才能在不同的运行环境下有统一的表现。Babel 转译也需要遵循这些标准,包括 ECMAScript、web 标准等。
// JavaScript 20年: https://cn.history.js.org/part-2.html
https://zhuanlan.zhihu.com/p/22557749
# 5.1 ECMAScript
# JavaScript诞生
1995 年,JavaScript 的第一个版本发布。用时间线的方式描述 JavaScript 的诞生过程会更清晰:
# ECMAScript发布
1996 年,微软模仿 JavaScript 实现了 JScript 并内置在 IE3.0,随后,Netscape 公司着手推动 JavaScript 标准化。
这里涉及几个组织:
Ecma International
Ecma International 是一家国际性会员制度的信息和电信标准组织。1994年之前,名为欧洲计算机制造商协会(European Computer Manufacturers Association)。因为计算机的国际化,组织的标准牵涉到很多其他国家,因此组织决定改名表明其国际性。
Ecma International 的任务包括与有关组织合作开发通信技术和消费电子标准、鼓励准确的标准落实、和标准文件与相关技术报告的出版。
Ecma International 负责多个国际标准的制定:
- CD-ROM 格式(之后被国际标准化组织批准为ISO 9660)
- C# 语言规范
- C++/CLI 语言规范
- 通用语言架构(CLI)
- Eiffel 语言
- 电子产品环境化设计要素
- Universal 3D 标准
- OOXML
- Dart 语言规范
- ECMAScript 语言规范(以 JavaScript 为基础)ECMA-262
其中就包括 JavaScript 标准语言规范 ECMAScript。
Ecma International 拥有 ECMAScript 的商标。
ECMA TC39
「TC39」全称「Technical Committee 39」译为「第 39 号技术委员会」,是 Ecma International 组织架构中的一部分。
TC39 负责迭代和发展 ECMAScript,它的成员由各个主流浏览器厂商的代表组成,通常每年召开约 6 次会议来讨论未决提案的进展情况,会议的每一项决议必须得到大部分人的赞同,并且没有人强烈反对才可以通过。
TC39 负责:
- 维护和更新 ECMAScript 语言标准
- 识别、开发、维护 ECMAScript 的扩展功能库
- 开发测试套件
- 为 ISO/IEC JTC 1 提供标准
- 评估和考虑新添加的标准
ISO
国际标准化组织(英语: International Organization for Standardization,简称: ISO)成立于 1947 年 2 月 23 日,制定全世界工商业国际标准的国际标准建立机构。
ISO 总部设于瑞士日内瓦,现有 164 个会员国。该组织定义为非政府组织,官方语言是英语、法语和俄语。参加者包括各会员国的国家标准机构和主要公司。
ISO 的国际标准以数字表示,例如: "ISO 11180:1993" 的 "11180" 是标准号码,而 "1993" 是出版年份。
ISO/IEC JTC 1 是国际标准化组织和国际电工委员会联合技术委员会。其目的是开发、维护和促进信息技术以及信息和通信技术领域的标准。JTC 1 负责了许多关键的 IT 标准,从 MPEG 视频格式到 C++ 編程語言。
ECMAScript 发展过程中的关键节点
# ECMAScript 各版本
ECMAScript 经历了多个版本,每个版本有自己的特点,简单列举如下:
# ECMAScript 迭代过程
https://erasermeng.github.io/2017/07/12/ECMAScript%E6%B5%81%E7%A8%8B%E8%A7%84%E8%8C%83%E7%AE%80%E4%BB%8B/
一个 ECMAScript 标准的制作过程,包含了 Stage 0 到 Stage 4 共 5 个阶段,每个阶段提交至下一阶段都需要 TC39 审批通过。
特性进入 Stage-4 后,才有可能被加入标准中,还需要 ECMA General Assembly 表决通过才能进入下一次的 ECMAScript 标准中。
# 5.2 如何阅读 ECMAScript
# ECMAScript 文档结构
https://timothygu.me/es-howto/
http://www.ruanyifeng.com/blog/2015/11/ecmascript-specification.html
ECMAScript 的规格,可以在 ECMA 国际标准组织的官方网站免费下载和在线阅读。ECMAScript 不同版本的规格有独立的网址,网址格式为: www.ecma-international.org/ecma-262/{version}/。比如 ECMAScript 6.0 版本的网址为 www.ecma-international.org/ecma-262/6.0/
。截止本书截稿时,官方网站支持的版本有: 6.0/7.0/8.0/9.0/10.0/11.0
。
ECMAScript 6.0
、ECMAScript 7.0
有 26 章,之后的版本有 27 章,虽然章节数量不同,规格章节的分布是保持一定规律的,以 ECMAScript 11.0
版本为例:
Introduction: 介绍部分
该章节简要描述了: JavaScript 和 ECMAScript 的发展历史、不同 ECMAScript 规格的主要更新内容。
第 1 章到第 3 章: 描述了规格文件本身,而非语言
第 1 章用一句话描述了该规格的描述范围。
第 2 章描述了基于规格的"实现"的一致性要求:
- "实现"必须支持规格中描述的所有类型、值、对象、属性、函数以及程序的语法和语义
- "实现"必须按照 Unicode 标准和 ISO/IEC 10646 的最新版本处理文本输入
- "实现"如果提供了应用程序编程接口(API),那么该 API 需要适应不同的人文语言和国家差异,且必须实现最新版本的 ECMA-402 所定义的与本规范相兼容的接口
- "实现"可以支持该规格中没有提及的类型、值、对象、属性、函数、正则表达式语法以及其他编程写法
- "实现"不能实现该规格中禁止的写法
第 3 章描述了该规格的一些参考资料:
- ISO/IEC 10646
- ECMA-402
- EMCA-404 JSON 数据交换格式规范
第 4 章: 对这门语言总体设计的描述。
第 5 章到第 8 章: 语言宏观层面的描述。
第 5 章是规格的名词解释和写法的介绍。
第 6 章介绍数据类型。
第 7 章介绍语言内部用到的抽象操作。
第 8 章介绍代码如何运行。
第 9 章到第 27 章: 介绍具体的语法。
一般而言,除非写编译器,开发者无需阅读 ECMAScript 的规格,规格的内容非常多,如无必要也无需通读。只是在遇到一些奇怪的问题时,阅读官方规格,是最稳妥的办法。
# 通过阅读规格解决一些问题
识别关键词和保留字,并高亮
Babel 工具集中的 babel-highlight,可以实现在终端对代码块中的目标字符单元显示不同的颜色。这里需要识别不同字符单元的类型,如关键字、保留字、标识符、数字、字符串等。
标识符、数字、字符串都很好理解和识别,但哪些字符应该被识别为关键字、保留字,而不是标识符呢?
此时可以阅读 ECMAScript 规格了,ECMAScript 11.0 规格的 11.6.2 节介绍了关键词和保留字列表。
关键词(keywords)
关键词首先是标识符,同时有语义,包括
if、while、async、await...
,个别关键词是可以用作变量名的。保留字(reserved word)
保留字首先是标识符,但不能用作变量名。
部分关键词是保留字,但部分不是:
if、while
是保留字;await
只有在async
方法和模块中才是保留字;async
不是保留字,它可以作为普通的变量名使用。保留字列表
await break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null returns uper switch this throw true try typeof var void while with yield
1
读完上述规格,也就知道哪些字符单元是需要识别为保留字与关键词,并高亮的了。
识别全局对象,并高亮
继续使用 babel-highlight 实现代码块中的全局对象高亮,那么,我们需要知道哪些是规格中描述的全局变量。
规格的 18 章介绍了全局对象,通过该章的描述,可以知道:
全局属性
全局属性有:
globalThis
、Infinity
、NaN
、undefined
。全局方法
全局方法有:
eval(x)
、isFinite
、isNaN
、parseFloat
、parseInt
、decodeURIComponent
、encodeURIComponent
等。全局构造函数
全局的构造函数有:
Array
、ArrayBuffer
、BigInt
、BigInt64Array
、BigUnit64Array
、Boolean
、DataView
、Date
、Error
、EvalError
、Float32Array
、Float64Array
、Function
、Int8Array
、Int16Array
、Int32Array
、Map
、Number
、Object
、Promise
、Proxy
、RangeError
、ReferenceError
、RegExp
、Set
、SharedArrayBuffer
、String
、Symbol
、SyntaxError
、TypeError
、Uint8Array
、Uint8ClampedArray
、Uint16Array
、Uint32Array
、URIError
、WeakMap
、WeakSet
。其他的全局属性
Atomics
、JSON
、Math
、Reflect
。
很显然,当字符单元的名称是上述名称中的一员时,我们可以对其进行高亮处理了(若上下文中无重新定义的同名变量)。
自定义 Error
在介绍 Babel 工具集中的 babel-loader 时,babel-loader 自身维护了私有的 LoaderError 对象,该对象继承自原生 Error 类,并且订制了部分实例属性。代码如下:
class LoaderError extends Error { constructor(err) { super(); const { name, message, codeFrame, hideStack } = format(err); this.name = "BabelLoaderError"; this.message = `${name ? `${name}: ` : ""}${message}\n\n${codeFrame}\n`; this.hideStack = hideStack; Error.captureStackTrace(this, this.constructor); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15可以看到,babel-loader 自定义了错误实例的
name
、message
、hideStack
属性,那么,问题是,原生的Error
类有哪些属性和方法,哪些是开发者可以自定义的呢?规格的 19.5 章节,详细介绍了
Error
的各类规范:Error
作为函数被调用时(Error(...)
),表现和new Error(...)
一致,均会创建并返回Error
的新实例Error
可以被继承,比如通过extends
的方式,子类必须提供constructor
方法,且该方法内必须提供super()
调用Error
构造函数必须有prototype
属性Error.prototype
属性需有以下属性Error.prototype.constructor
: 指向构造函数Error.prototype.message
: 描述错误信息,默认是空字符串Error.prototype.name
: 描述错误名称,默认值是Error
Error.prototype.toString
:
从
LoaderError
的源码可以看到,LoaderError
做了以下几件事情:LoaderError
继承自Error
- 实例自定义了
name
、message
属性,明确 babel-loader 的信息 - 实例自定义的
hideStack
属性是非标准属性,用于 babel-loader 内部
# 5.3 web标准
是在解决 API Polyfil 的时候,Babel 配合使用的 core-js 除了提供 ECMAScript 标准下的 JavaScript API 实现,也提供了 DOM/URL 等实现。而 DOM/URL 所属的 web 标准,由 W3C/WHATWG 制定。
标准化组织 | 介绍 |
---|---|
W3C | W3C(World Wide Web Consortium) 致力于实现所有的用户都能够对 web 加以利用(不论其文化教育背景、能力、财力以及其身体残疾)。 W3C 包含了众多标准,包括 HTML/CSS/DOM/WebAssembly/WebDriver/IndexedDB 等等。 |
WHATWG | WHATWG(Web Hypertext Application Technology Working Group) 是浏览器厂商(包括苹果、谷歌、微软、Mozilla)于 2004 年组成的行业组织。 专注于浏览器领域的标准制定、测试等工作,如 HTML/DOM/streams/fetch/url/encoding 等标准的起草和发布。 |
经过多年发展,WHATWG 和 W3C 目前是合作关系,其中,WHATWG 维护 HTML 和 DOM 标准,W3C 使用 WHATWG 存储库中的 HTML 和 DOM 标准描述,W3C 在 HTML 部分的工作集中在 XHTML/XML 上。
# 6 总结
本章从整体层面介绍了 Babel 的转译过程及其生态中各个模块的作用。
其中涉及的部分知识点,后续章节有详细解析,读者可以根据自身需要查看相应内容。