babel 是一个 Javascript 编译器,是目前前端开发最常用的工具之一,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境。
a ?? 1;
a?.b;
//babel转换后
("use strict");
var _a;
(_a = a) !== null && _a !== void 0 ? _a : 1;
(_a2 = a) === null || _a2 === void 0 ? void 0 : _a2.b;
可以在babel 官网自己测试。
转译 esnext、typescript、flow 等到目标环境支持的 js。
一些特定用途的代码转换。 babel 暴露了很多 api,用这些 api 可以完成代码到 AST 的 parse,AST 的转换,以及目标代码的生成。 开发者可以用它来来完成一些特定用途的转换,比如函数插桩(函数中自动插入一些代码,例如埋点代码)、自动国际化、default import 转 named import 等。
代码的静态分析。 对代码进行 parse 之后,能够进行转换,是因为通过 AST 的结构能够理解代码。理解了代码之后,除了进行转换然后生成目标代码之外,也同样可以用于分析代码的信息,进行一些检查。
linter 工具就是分析 AST 的结构,对代码规范进行检查。
api 文档自动生成工具,可以提取源码中的注释,然后生成文档。
type checker 会根据从 AST 中提取的或者推导的类型信息,对 AST 进行类型是否一致的检查,从而减少运行时因类型导致的错误。
压缩混淆工具,这个也是分析代码结构,进行删除死代码、变量名混淆、常量折叠等各种编译优化,生成体积更小、性能更优的代码。
babel 整体编译流程分为三步:
parse 阶段的目的是把源码字符串转换成机器能够理解的 AST,这个过程分为词法分析、语法分析。
比如 let name = ‘guang’; 这样一段源码,我们要先把它分成一个个不能细分的单词(token),也就是 let, name, =, ‘guang’,这个过程是词法分析,按照单词的构成规则来拆分字符串成单词。
transform 阶段是对 parse 生成的 AST 的处理,会进行 AST 的遍历,遍历的过程中处理到不同的 AST 节点会调用注册的相应的 visitor 函数,visitor 函数里可以对 AST 节点进行增删改。
generate 阶段会把 AST 打印成目标代码字符串,并且会生成 sourcemap。
while (node condition) {
// node contet
}
字面量、标识符、表达式、语句、模块语法、class 语法都有各自的 AST。
比如 字面 Literal:
‘a’ 就是 StringLiteral,123 就是 NumberLiteral,变量名,属性名等标识符都是 Identifier。
我们可以去 AST 可视化工具了解各个节点。
也可以去 @babel/types 查看所有 AST 类型。
它提供了有两个 api:parse 和 parseExpression。两者都是把源码转成 AST,不过 parse 返回的 AST 根节点是 File(整个 AST),parseExpression 返回的 AST 根节点是是 Expression(表达式的 AST)。可以指定 parse 的内容以及 parse 的方式。最常用的 option 就是 plugins、sourceType 这两个。
plugins: 指定 jsx、typescript、flow 等插件来解析对应的语法。
sourceType: 指定是否支持解析模块语法,有 module、script、unambiguous 3 个取值,module 是解析 es module 语法,script 则不解析 es module 语法,当作脚本执行,unambiguous 则是根据内容是否有 import 和 export 来确定是否解析 es module 语法。
require("@babel/parser").parse("code", {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
parse 出的 AST 由 @babel/traverse 来遍历和修改,babel traverse 包提供了 traverse 方法:
function traverse(parent, opts)
parent 指定要遍历的 AST 节点,opts 指定 visitor 函数。babel 会在遍历 parent 对应的 AST 时调用相应的 visitor 函数。enter 是在遍历当前节点的子节点前调用,exit 是遍历完当前节点的子节点后调用。
visitor: {
Identifier (path, state) {},
StringLiteral: {
enter (path, state) {},
exit (path, state) {}
}
}
参数 path 是遍历过程中的路径,会保留上下文信息。通过 path 可以获取节点信息:
path.scope 获取当前节点的作用域信息
path.isXxx 判断当前节点是不是 xx 类型
path.assertXxx 判断当前节点是不是 xx 类型,不是则抛出异常
isXxx、assertXxx 系列方法可以用于判断 AST 类型
path.insertBefore、path.insertAfter 插入节点
path.replaceWith、path.replaceWithMultiple、replaceWithSourceString 替换节点
path.remove 删除节点
这些方法可以对 AST 进行增删改
参数 state 可以在不同节点之间传递数据。
创建 AST 和判断 AST 的类型。
如果要创建 IfStatement 就可以调用
t.ifStatement(test, consequent, alternate);
而判断节点是否是 IfStatement 就可以调用 isIfStatement 或者 assertIfStatement
t.isIfStatement(node, opts);t.assertIfStatement(node, opts);
批量创建节点
const ast = template(code, [opts])(args);
const ast = template.ast(code, [opts]);
const ast = template.program(code, [opts]);
打印成目标代码字符串
const { code, map } = generate(ast, { sourceMaps: true })
function (ast: Object, opts: Object, code: string): {code, map}
基于上面的包来完成 babel 的编译流程,可以从源码字符串、源码文件、AST 开始。
npm i @babel/core @babel-cli
{
"scripts": {
"build": "babel src -d dist"
}
}
// webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/app.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
module: {
rules: [{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }],
},
};
我们需要告诉 babel 要怎么去编译,编译哪些内容。 配置文件的方式有以下几种:
//package.json
{
"name":"babel-test",
"version":"1.0.0",
"devDependencies": {
"@babel/core":"^7.4.5",
"@babel/cli":"^7.4.4",
"@babel/preset-env":"^7.4.5"
}
"babel": {
"presets": ["@babel/preset-env"]
}
}
.babelrc
{
"presets": ["@babel/preset-env"]
}
.babelrc.js
//webpack的配置文件也是这种写法
module.exports = {
presets: ["@babel/preset-env"],
};
同.babelrc.js,但是 babel.config.js 是针对整个项目。
通过配置文件告诉 babel 编译哪些内容,然后还要引入对应的编译插件(Plugins),比如箭头函数的转换需要的是 @babel/plugin-transform-arrow-functions 这个插件。
npm i @babel/plugin-transform-arrow-functions
// babel.config.js
module.exports = {
plugins: ["@babel/plugin-transform-arrow-functions"],
};
现在我们代码中的箭头函数就会被编译成普通函数。
当 plugin 比较多或者 plugin 的 options 比较多的时候就会导致使用成本升高。这时候可以封装成一个 preset,用户可以通过 preset 来批量引入 plugin 并进行一些配置。preset 就是对 babel 配置的一层封装。
只要安装这一个 preset,就会根据你设置的目标浏览器,自动将代码中的新特性转换成目标浏览器支持的代码。
npm i @babel/preset-env
// babel.config.js
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
chrome: "58",
},
},
],
],
};
当用 babel 编译 class 的时候,需要一些工具函数来辅助实现。
class People {}
// babel编译后
("use strict");
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var People = function People() {
_classCallCheck(this, People);
};
每个 class 都会生成 _classCallCheck,最后就会产生大量重复代码。plugin-transform-runtime
就是为了解决这个问题。
npm i @babel/plugin-transform-runtime
//生产依赖
npm i @babel/runtime
module.exports = {
presets: ["@babel/preset-env"],
plugins: ["@babel/plugin-transform-runtime"],
};
"use strict";
// babel 编译后
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var People = function People() {
(0, _classCallCheck2["default"])(this, People);
};
babel 可以转化一些新的特性,但是对于新的内置函数(Promise,Set,Map),静态方法(Array.from,Object.assign),实例方法(Array.prototype.includes)这些就需要 babel-polyfill 来解决,babel-polyfill 会完整模拟一个 ES2015+环境。
因为 @babel/polyfill 体积比较大,整体引入既增加项目体积,又污染了过多的变量,所以更推荐使用 preset-env 来按需引入 polyfill。
// corejs 是一个给低版本的浏览器提供接口的库
npm i core-js@2
// babel.config.js
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage", // usage-按需引入 entry-入口引入(整体引入) false-不引入polyfill
corejs: 2, // 2-corejs@2 3-corejs@3
},
],
],
};
const a = Array.from([1]);
//babel编译后
("use strict");
require("core-js/modules/es6.string.iterator");
require("core-js/modules/es6.array.from");
var a = Array.from([1]);
希望通过 babel 能够自动在 console.log 等 api 中插入文件名和行列号的参数,方便定位到代码,这段代码不影响其他逻辑,这种函数插入不影响逻辑的代码的手段叫做函数插桩。
思路: 在遍历 AST 的时候对 console.log 等 api 自动插入一些参数,也就是要通过 visitor 指定对函数调用表达式 CallExpression 做一些处理。CallExrpession 节点有两个属性,callee 和 arguments,分别对应调用的函数名和参数, 所以我们要判断当 callee 是 console.xx 时,在 arguments 的数组中插入一个 AST 节点,创建 AST 节点需要用到 @babel/types 包。
主要代码:
const ast = parser.parse(sourceCode, {
sourceType: "unambiguous",
plugins: ["jsx"],
});
const targetCalleeName = ["log", "info", "error", "debug"].map((item) => `console.${item}`);
traverse(ast, {
CallExpression(path, state) {
const calleeName = generate(path.node.callee).code;
if (targetCalleeName.includes(calleeName)) {
const { line, column } = path.node.loc.start;
path.node.arguments.unshift(types.stringLiteral(`filename: (${line}, ${column})`));
}
},
});
// babel编译前
const sourceCode = `
console.log(1);
function func() {
console.info(2);
}
export default class Clazz {
say() {
console.debug(3);
}
}
`;
// babel编译后
console.log("filename: (2, 4)", 1);
function func() {
console.info("filename: (5, 8)", 2);
}
export default class Clazz {
say() {
console.debug("filename: (10, 12)", 3);
}
}
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章