随着JavaScript的不断发展,新语法特性相继出现。为了弥合低版本浏览器不支持新语言特性的问题,Babel应运而生。时至今日,Babel已经成了前端开发必不可少的工具。依靠Babel的优秀能力,前端工程师可以在开发中自如的运用各种最新的语言特性而不必担心浏览器兼容问题。本文以Babel 7为例,对Babel的基本概念、基本功能做一些梳理,帮助没有Babel使用经验的前端工程师更好的了解Babel
什么是Babel
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中,对于个性化的非标准特性,Babel 支持插件化架构。Babel 中最核心的功能是语法转换。以如下源码为例
[1, 2, 3].map((n) => n + 1);
因为源码中用到了箭头函数,而包含箭头函数的代码是无法直接在IE 11及以下版本的IE浏览器上运行,而 Babel 可以做到将上述代码转化为如下形式以便代码可在相应环境正确运行
[1, 2, 3].map(function(n) {
return n + 1;
});
Babel基础用法
- 安装依赖:
$ npm install --save-dev @babel/core @babel/preset-env
API方式调用
- 编译源码(api调用)
/* eslint-disable no-console */
const babel = require('@babel/core');
const result = babel.transform('[1, 2, 3, 4].forEach(item => console.log(item))', {
"presets": [
[
"@babel/preset-env"
]
]
});
console.log(result.code);
命令行或第三方调用
对于命令行方式调用,或例如 Webpack 、 jest 等第三方调用,需要设置配置文件,配置文件有2类:
- 全局配置:
babel.config(.json|.js|.cjs|.mjs) 放置在根目录下,影响范围全局
- 文件相关:
- .
babelrc(.json|.js|.cjs|.mjs) 放置在需要编辑的目录下,影响范围限制在该目录及子目录下 package.json配置Babel字段
- .
需要注意的是,根据配置文件命名方式及所在位置的不同,影响范围不同,babel.config.json需要放置在根目录,影响范围是全局的;.babelrc.json一般放置在子目录下,影响范围仅限制在子目录下。
Babel核心概念
Babel 中有两个最重要的概念,Plugins和Presets。在大多数Babel应用场景中都需要对这两个对象做配置,其含义如下:
-
Plugins:
Babel中进行语法转换的实体。如@babel/plugin-transform-arrow-functions负责转换箭头函数、@babel/plugin-transform-classes用于转换Class等。不设置具体的Plugins,则Babel的输入和输出是相同的 -
Presets:若干
Plugins的集合。一个一个plugin去配置太耗时,Presets可以认为是使用Plugins的快捷方式。如@babel/preset-react实际上是3个plugin的集合:@babel/plugin-syntax-jsx、@babel/plugin-transform-react-jsx、@babel/plugin-transform-react-display-name
Presets与Plugins的执行顺序
Plugins先于Presets执行Plugins中的插件从前到后执行(例如设置{"plugins": ["transform-decorators-legacy", "transform-class-properties"]},则transform-decorators-legacy先于transform-class-properties执行)Presets执行顺序与Plugins相反(例如设置{"presets": ["@babel/preset-env", "@babel/preset-react"]},则@babel/preset-react先于@babel/preset-env执行)
重要的Preset
@babel/preset-env 是Babel使用中必不可少的Preset,主要功能是负责将高版本JavaScript语法特性转换。由于整合了Browserslist所以@babel/preset-env可以自动按照配置好的兼容性来自动对语法特性进行转化。以箭头函数为例,如果希望输出的代码要兼容Chrome 38,则进行如下配置:
{
"presets": [
["@babel/preset-env", {
"targets": {
"chrome": "38"
}
}]
]
}
由于Chrome 38不支持箭头函数,而输出代码又要兼容,所以Babel会自动将箭头函数编译替换。但如果我们将输出的兼容环境设置为Chrome 48,由于这个版本已经支持箭头函数,所以此时的输出将不会对箭头函数做处理
重要的Plugin
@babel/plugin-transform-runtime 在Babel编译优化中经常会用到。改插件可以将helper源码以依赖的方式引入,从而减小代码编译后的代码体积。
我们可以通过引入@babel/plugin-transform-runtime插件启用:
// babel.config.json
{
"presets": ["@babel/preset-env" ],
"plugins": ["@babel/plugin-transform-runtime"]
}
以如下代码为例,我们可以观察下启用@babel/plugin-transform-runtime与否的效果:
源码
// 源码
class Rectangle {}
async function* agf() {
await 1;
yield 2;
}
启用@babel/plugin-transform-runtime
// 启用@babel/plugin-transform-runtime
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _awaitAsyncGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/awaitAsyncGenerator"));
var _wrapAsyncGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/wrapAsyncGenerator"));
var Rectangle = function Rectangle() {
(0, _classCallCheck2.default)(this, Rectangle);
};
function agf() {
return _agf.apply(this, arguments);
}
function _agf() {
_agf = (0, _wrapAsyncGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
return _regenerator.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return (0, _awaitAsyncGenerator2.default)(1);
case 2:
_context.next = 4;
return 2;
case 4:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return _agf.apply(this, arguments);
}
未启用@babel/plugin-transform-runtime
// 未启用@babel/plugin-transform-runtime
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _awaitAsyncGenerator(value) { return new _AwaitValue(value); }
function _wrapAsyncGenerator(fn) { return function () { return new _AsyncGenerator(fn.apply(this, arguments)); }; }
function _AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; var wrappedAwait = value instanceof _AwaitValue; Promise.resolve(wrappedAwait ? value.wrapped : value).then(function (arg) { if (wrappedAwait) { resume(key === "return" ? "return" : "next", arg); return; } settle(result.done ? "return" : "normal", arg); }, function (err) { resume("throw", err); }); } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } }
if (typeof Symbol === "function" && Symbol.asyncIterator) { _AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; }
_AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); };
_AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); };
_AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); };
function _AwaitValue(value) { this.wrapped = value; }
var Rectangle = function Rectangle() {
_classCallCheck(this, Rectangle);
};
function agf() {
return _agf.apply(this, arguments);
}
function _agf() {
_agf = _wrapAsyncGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return _awaitAsyncGenerator(1);
case 2:
_context.next = 4;
return 2;
case 4:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return _agf.apply(this, arguments);
}
可以明显看出,启用@babel/plugin-transform-runtime后输出的代码体积明显变小。但需留意的是,开发公共组件库时通常不启用,因为启用就需要额外配套安装@babel/runtime,这对使用者来说是不友好的。
个性化定制Preset
因为Preset只是若干Plugin和Preset的集合,所以定制个性化的Preset就尤为简单。Babel官方鼓励社区创建自己的Preset,如果希望这个Preset以npm包的形式发布,其名称需要遵循babel-preset-*的命名规范,同时创建一个index.js将Preset的配置导出,内容格式类似Babel的配置文件,拥有presets和plugins配置,但需要注意的是,index.js必须导出一个函数(Babel 7)。以babel-preset-mypreset为例:
// babel-preset-mypreset/index.js
module.exports = () => ({
presets: [
["@babel/preset-env", {
"targets": {
"chrome": "38"
}
}]
],
plugins: ["@babel/plugin-transform-runtime"]
});
在项目的babel.config.json中作如下引用:
// babel.config.json
{
"presets": ["babel-preset-mypreset"]
}
上面的用法和下面的等价:
// babel.config.json
{
"presets": [
["@babel/preset-env", {
"targets": {
"chrome": "38"
}
}]
],
"plugins": ["@babel/plugin-transform-runtime"]
}
个性化Plugin开发
如果现有的Plugin无法满足需要,我们可以按照规范定制符合自身需要的Plugin,因为Plugin是Babel最核心的内容,是Babel实现语法转换的基础,也是Preset底层的基础,所以Plugin的复杂程度远远高于Preset。
在实现Plugin之前,我们需了解Babel的运行过程:
- Parse(解析):接收源码并输出
AST- Lexical Analysis(词法分析):把字符串形式的源代码转换为
tokens流 - Syntactic Analysis(语法分析):把令牌流转换成 AST 。 该阶段会利用令牌中的信息把它们转换成一个 AST结构,以便于后续的操作。
- Lexical Analysis(词法分析):把字符串形式的源代码转换为
- Transform(转换):接收
AST并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。是Babel或是其他编译器中最复杂的部分,同时也是插件的主要工作 - Generate(生成):将转换后的
AST重新生成字符串形式的代码,同时生成source maps。该过程比较简单,只需要做对AST的深度遍历,并生成字符串代码即可
Plugin开发示例
假设我们希望实现将源码中的所有变量名称都转化为大写,如何用Plugin来实现这个功能呢?首先,我们可以在Babel配置文件的同级目录下创建文件夹myplugin并在此文件夹内新建index.js,并写如下代码:
// 插件
module.exports = () => {
return {
visitor: {
Identifier(path) {
path.node.name = path.node.name.toUpperCase("");
}
},
};
}
然后配置Babel文件如下:
// babel.config.json
{
"plugins": ["./myplugin"]
}
对如下源码进行Babel编译:
// input
function name(start, end) {
let value = start + end;
return value;
}
name();
得到如下结果:
// output
function NAME(START, END) {
let VALUE = START + END;
return VALUE;
}
NAME();
通过如上示例,可以发现Babel已经帮我们做好了Parse和Generate,作为Plugin开发者,我们只需要做Transform即可
Babel高阶工具函数
对于Babel Plugin开发者这个级别的Babel来说,接下来的内容其实不需要了解,但如果希望借助Babel提供的功能做一些更加强大的开发,则以下内容需要深入了解
@babel/parser
@babel/parser是一个具备非标准特性插件化支持架构,脱胎于 Acorn 的JavaScript Parser,用于生成JavaScript AST。支持ES2017、JSX、Flow、Typescript,执行快速,使用简单。
安装:
$ npm install --save @babel/parser
用法示例:
import parser from "@babel/parser";
const code = `function square(n) {
return n * n;
}`;
parser.parse(code);
// Node {
// type: "File",
// start: 0,
// end: 38,
// loc: SourceLocation {...},
// program: Node {...},
// comments: [],
// tokens: [...]
// }
@babel/traverse
@babel/traverse模块维护了AST的状态,并且负责替换、移除和添加节点
安装:
$ npm install --save @babel/traverse
用法示例:
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
const code = `function square(n) {
return n * n;
}`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
}
});
@babel/types
@babel/types是一个针对AST节点的工具库。包含构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用,API详情可见
安装:
$ npm install --save @babel/types
用法示例:
import traverse from "@babel/traverse";
import * as t from "@babel/types";
traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: "n" })) {
path.node.name = "x";
}
}
});
@babel/generator
@babel/generator是Babel的代码生成器,用于将AST转换成字符串型的源码(附带source maps)
安装:
$ npm install --save @babel/generator
用法示例:
import parser from "@babel/parser";
import generate from "@babel/generator";
const code = `function square(n) {
return n * n;
}`;
const ast = parser.parse(code);
generate(ast, {}, code);
// {
// code: "...",
// map: "..."
// }
@babel/template
@babel/template支持编写字符串形式且带有占位符的代码来代替手动编码, 在生成的大规模 AST的时候尤其有用。在计算机科学中,这种能力被称为准引用(quasiquotes)
安装:
$ npm install --save @babel/template
用法示例:
import template from "@babel/template";
import generate from "@babel/generator";
import * as t from "@babel/types";
const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`);
const ast = buildRequire({
IMPORT_NAME: t.identifier("myModule"),
SOURCE: t.stringLiteral("my-module")
});
console.log(generate(ast).code);
// var myModule = require("my-module");
上面这个示例,通常的想法可能是用字符串的replace方法,可一旦仔细思考可能存在的各种情况后(如字符串嵌套等)就会发现问题并不能用replace来简单的处理,此时@babel/template就是很好的解决方案
Babel高级工具综合用法示例
我们以 babel-eslint 10.x 这个parser为例,简单分析下上述工具的综合用法,babel-eslint主要功能是支持Babel支持的语法特性,替换Eslint的默认parser,以便让Eslint拥有更强大的语法支持能力。
通过查阅babel-eslint的dependencies我们可以发现,该代码库依赖了以下@babel/code-frame、@babel/parser、@babel/traverse、@babel/types、eslint-visitor-keys、resolve。其中最核心的文件parse.js代码如下:
// parse.js
"use strict";
var babylonToEspree = require("./babylon-to-espree");
var parse = require("@babel/parser").parse;
var tt = require("@babel/parser").tokTypes;
var traverse = require("@babel/traverse").default;
var codeFrameColumns = require("@babel/code-frame").codeFrameColumns;
module.exports = function (code, options) {
const legacyDecorators =
options.ecmaFeatures && options.ecmaFeatures.legacyDecorators;
var opts = {
// 略
};
var ast;
try {
ast = parse(code, opts);
} catch (err) {
// 略
}
babylonToEspree(ast, traverse, tt, code);
return ast;
};
追溯babylon-to-espree.js可见:
// babylon-to-espree.js
"use strict";
var attachComments = require("./attachComments");
var convertComments = require("./convertComments");
var toTokens = require("./toTokens");
var toAST = require("./toAST");
module.exports = function (ast, traverse, tt, code) {
// 略
// transform esprima and acorn divergent nodes
toAST(ast, traverse, code);
// 略
};
继续追溯toAST.js
// toAST.js
"use strict";
var t = require("@babel/types");
var convertComments = require("./convertComments");
module.exports = function (ast, traverse, code) {
var state = {
source: code
};
// Monkey patch visitor keys in order to be able to traverse the estree nodes
// 略
traverse(ast, astTransformVisitor, null, state);
// 略
};
var astTransformVisitor = {
noScope: true,
enter(path) {
// 略
},
exit(path) {
// 略
}
};
可以看出,babel-eslint直接借用了@babel/parser生成AST,并借助@babel/traverse进行AST遍历,是Babel高阶工具的一次具体用法示例
后记
Babel的使用有三个层次:
- 能通过配置各种
Presets和Plugins来应对经典常见需求,如高版本JavaScript的编译,与Webpack、Jest、Eslint的配合等 - 能够配置个性化的
Presets - 能够结合
Babel高阶工具,按照需求实现自己的Plugin
通常而言,在开盒即用的今天,开发者可以直接使用已存在的工程化脚手架,这导致大多数开发者其实是停留在0.5层级上,只知道个大概但并不清楚具体细节。能达到第三个层次的就更少了。本文抛砖引玉,帮助大家对Babel有个初步的了解。要想达到第三层次的水平,还需额外阅读大量的编译资料,并且对AST的数据结构以及JavaScript语法有深入的了解。