开发 babel 插件
可能有用的链接
https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md
babel 知识点:
1. plugins、preset 调用优先级
plugins 会从上到下顺序执行。
preset 会从下到上逆序执行。
plugins 先于 preset 执行。
2. babel 转换过程
@babel/core 将源代码转为 ast 抽象语法树。
经过 plugins、preset 的操作生成新的 ast。
最后反向将新的 ast 转换为新的代码。
3. 在线查看并分析生成 ast
插件基础入门
典型的一个 babel 插件结构:
export default function({ types: babelTypes }) {
return {
visitor: {
Identifier(path, state) {
}
}
};
};解释:
babelTypes:类似于 loadash 那样的工具函数库。主要用来操作 AST 节点,比如创建、校验、转变等。举例:判断某个节点是不是标识符(identifier)。
path:AST 中有很多节点,每个节点可能有不同的属性,并且节点之间可能存在关联。path 是个对象,它代表了两个节点之间的关联。你可以在 path 上访问到节点的属性,也可以通过 path 来访问到关联的节点(比如父节点、兄弟节点等)。path.node 就是当前遍历到的节点对象。可以通过这个网址查看。
state:代表了插件的状态,你可以通过 state 来访问插件的配置项。
visitor:Babel 采取递归的方式访问 AST 的每个节点,之所以叫做 visitor,只是因为有个类似的设计模式叫做访问者模式,不用在意背后的细节。
Identifier、ASTNodeTypeHere:AST 的每个节点,都有对应的节点类型,比如标识符(Identifier)、函数声明(FunctionDeclaration)等,可以在 visitor 上声明同名的属性,当 Babel 遍历到相应类型的节点,属性对应的方法就会被调用,传入的参数就是 path、state。
实现插件 bad-to-good
写插件:
module.exports = (params) => {
// undefined
console.log(params)
return {
name: 'bad-to-good',
// babel 用了访问者设计模式来遍历节点。
// 访问到一个节点时,visitor 下的属性方法钩子就会视情况被调用。
visitor: {
// 如果节点类型是标识符,就会调用这个钩子
Identifier(path, state) {
// path 是个对象。可以通过 path 访问到当前节点的信息,也可以获取相关联的节点信息。
// state 代表插件状态。可以通过 state 访问到传入插件的配置参数。
if (path.node.name === 'bad') {
path.node.name = 'good'
}
}
}
}
}引入插件(三种引入方式)
const badToGoodPlugin = require('./my-plugin')
module.exports = {
plugins: [
// badToGoodPlugin() // 这种方式拿不到参数
// badToGoodPlugin
'./my-plugin'
]
}实现插件传参 bad-to-good-2
插件函数:
module.exports = (params) => {
// undefined
// console.log(params)
return {
name: 'bad-to-good-2',
visitor: {
Identifier(path, state) {
// 通过 state.opts 拿到配置参数
const name = path.node.name
if (state.opts[name]) {
path.node.name = state.opts[name]
}
}
}
}
}实现辅助插件:替换 process.env.NODE_ENV
插件函数:
// path.get('xx.xxx') // 返回当前节点下的 xx.xx 属性。等同于 path.node.xx.xxx
// path.node // 当前访问的节点对象
// path.toComputedKey // 获取成员表达式的 key。比如 process.env.NODE_ENV 的是 NODE_ENV。
// process.env 是 env。
// matchesPattern // 传入正则或字符串。如果匹配则返回 true。
module.exports = function({ types: babelTypes }) {
return {
name: "node-env-replacer",
visitor: {
// 成员表达式
// 表示一个对象的成员访问
MemberExpression(path, state) {
// 如果为 true,说明当前节点为 `process.env.NODE_ENV`
if (path.get("object").matchesPattern("process.env")) {
// 这里返回结果为字符串字面量类型的节点
const keyPath = path.toComputedKey();
// 如果是字符串字面量
if (babelTypes.isStringLiteral(keyPath)) {
// path.replaceWith(newNode) 用来替换当前节点
// babelTypes.valueToNode(value) 用来创建节点,
// 如果 value 是字符串,则返回字符串字面量类型的节点
path.replaceWith(babelTypes.valueToNode(process.env[keyPath.value]));
}
}
}
}
};
};生产模式下去除 DEBUG
比如下面这段代码,在生产模式下是会被清除的。
if (DEBUG) {
console.log(123)
}插件写法:
const t = require("@babel/types");
module.exports = () => {
return {
visitor: {
// 标识符(变量名)
Identifier: (path, state) => {
const nodeName = path.node.name;
const isDebug = nodeName === "DEBUG";
const parentPath = path.parentPath;
const parentIsIf = t.isIfStatement(parentPath);
if (isDebug && parentIsIf) {
const isPro = process.env.NODE_ENV === 'production'
if (isPro) {
path.parentPath.remove();
}
}
}
},
};
};生成 ast 树
执行下面一段 js 代码:
import parser from "@babel/parser";
const code = `function square(n) {
return n * n;
}`;
const res = parser.parse(code);
console.log(res)打印的 res 如下:

与通过网站生成的 ast 对象结构是一样的。

path 的方法
- traverse(visitor, state) 遍历当前节点的子节点,传入 visitor 和 state(state 是不同节点间传递数据的方式)
使用 api
# 将代码解析为 ast
pnpm install --save @babel/parser
# 操作 ast
pnpm install --save @babel/traverse
# 工具库
pnpm install --save @babel/types
# 将 ast 转为源代码
pnpm install --save @babel/generator将变量 n 替换为 x
import parser from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types";
// 源代码
const code = `function square(n) {
return n * n;
}`
// 将源代码解析为 ast
const ast = parser.parse(code)
// 访问并操作 ast 的节点
traverse.default(ast, {
enter(path, state) {
if (
// 如果节点类型是变量名
path.node.type === "Identifier" &&
// 并且节点名字是 n
path.node.name === "n"
) {
// 名字替换为 x
path.node.name = "x";
}
},
// 节点类型作为函数属性名
})
// 将处理后的 ast 方向生成代码。
const outputCode = generate.default(ast, {
// 保留行数
retainLines: false,
// 压紧
compact: "auto",
// 引号
quotes: "double",
// concise,简明的
concise: false,
}, code)
console.log(outputCode)
替换全等符号两边的变量
源码:
foo === bar;生成的 ast:
{
type: "BinaryExpression",
operator: "===",
left: {
type: "Identifier",
name: "foo"
},
right: {
type: "Identifier",
name: "bar"
}
}插件:
import parser from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types";
// 源代码
const code = `foo === bar;`
// 将源代码解析为 ast
const ast = parser.parse(code);
// 访问并操作 ast 的节点
traverse.default(ast, {
// 节点类型作为函数属性名
// 注意不要写 visitor!!
// visitor: {},
BinaryExpression(path) {
if (path.node.operator !== "===") {
return
}
path.node.left = t.identifier("leftValue");
path.node.right = t.identifier("rightValue");
}
})
// 将处理后的 ast 方向生成代码。
const outputCode = generate.default(ast, {}, code)
console.log(outputCode)获取子节点的 path
当匹配到节点类型函数时,我们能获取到节点的 path 对象。
path.node 就是当前匹配的 ast 节点对象。
如果想获取子节点的 path 对象,通过 path.get() 函数获取。
还是以这段代码为例:
const code = `foo === bar;`生成的 ast 如下:
{
type: "BinaryExpression",
operator: "===",
left: {
type: "Identifier",
name: "foo"
},
right: {
type: "Identifier",
name: "bar"
}
}// 名字为 foo 的节点 path 对象。
path.get('left')
// 名字为 bar 的节点 path 对象。
path.get('right')import parser from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types";
const code = `foo === bar;`
const ast = parser.parse(code);
traverse.default(ast, {
BinaryExpression(path) {
// 获取子节点的 path
const leftPath = path.get('left')
if(leftPath) {
console.log(`isFoo: ${leftPath.node.name === 'foo'}`)
}
}
})
const outputCode = generate.default(ast, {}, code)检查一个节点的确切的节点类型
1、传入 t.isIdentifier 一个参数
BinaryExpression(path) {
// 检查当前节点的 left 节点类型,是否是标识符
if (t.isIdentifier(path.node.left)) {
// ...
}
}2、传入 t.isIdentifier 两个参数
除了比较节点类型,也可以多比较一些属性:
BinaryExpression(path) {
if (t.isIdentifier(path.node.left, { name: "n" })) {
// ...
}
}这相当于:
BinaryExpression(path) {
if (
path.node.left != null &&
path.node.left.type === "Identifier" &&
path.node.left.name === "n"
) {
// ...
}
}当前节点路径对象也能检查当前节点的类型
一个节点的路径对象也能判断当前节点的类型。
BinaryExpression(path) {
if (path.get('left').isIdentifier({ name: "n" })) {
// ...
}
}等效于:
BinaryExpression(path) {
if (t.isIdentifier(path.node.left, { name: "n" })) {
// ...
}
}如果是变量节点,检查在上下文中是否有引用关系
api
Identifier(path) {
if (path.isReferencedIdentifier()) {
// ...
}
}或者
Identifier(path) {
if (t.isReferenced(path.node, path.parent)) {
// ...
}
}什么情况下一个变量会被认为与代码中的其它部分有引用关系呢?
- 使用一个变量
- 从变量中取值
比如下面这个例子:
import babel from '@babel/core'
import * as t from "@babel/types";
const soureCode =
`const a = 123 // 1
function fn() { // 2
console.log(a) // 3
const a = 123 // 4
} // 5
[b][0] // 6
fn() // 7`
const visitor = {
Identifier(path) {
// 当前节点是否引用了其它变量。
// 注意,即使其它变量在代码中没有被显示声明,也会进入该 if 判断。
if(path.isReferencedIdentifier()) {
console.log(path.node.loc.start.line, path.node.name)
}
},
}
const {code} = babel.transform(soureCode, {
plugins: [{ visitor }],
})
// console.log('res', code)