Babel插件开发小试:支持模块化、代码复用化

Stone926 Lv1

一、解决上期问题

上期提到,如果用如下方式导出插件会报错:

1
2
3
export default function ({ types: t }) {
// ...
}

解决方式是在package.json根对象下加入

1
"type": "module"

这样,所有.js文件都会当作ES module处理。如果想按CommonJS处理,将文件扩展名改为.cjs。同理,如果typecommonjs,所有ES module文件的扩展名应为.mjs

注意,用ESM引入CJS模块时:

1
2
3
4
5
6
7
8
9
10
// lib.js
exports.default = function traverse() {
console.log("successfully imported")
}
traverse.foo = foo
traverse.bar = bar

// index.js
import traverse from "./lib.js"
traverse()

如果这样引用模块就会报错traverse不是函数。因为这里引入的是整个exports对象,所以需要如下写:

1
2
import traverse from "./lib.js"
traverse.default(); // output on console: successfully imported

这点我们后面会用到。

二、支持导入重载

1
2
3
import { $operator as $ } from "./operator.js"

console.log(1 + "1")

现在会被编译为如下代码(如果$operator重载了加法):

1
2
3
import { $operator as $ } from "./operator.js"

console.log($.plus(1, "1"))

编译后重载对象的名称都是as后面的名称,import必须为具名导入,as不是必需的,不能为:

1
2
import $operator from "./operator.js"
import * as $operator from "./operator.js"

这与插件的工作原理有关。

三、导入的工作原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Program(path, state) {
path.traverse({
ImportDeclaration(path) {
for (let i = 0; i < (path.node.specifiers.length ?? 0); i++) {
let specifier = path.node.specifiers[i];
if (specifier.imported.name === outer.operatorObjectName) {
let x = path.node.source.value;
if (!x.endsWith(".js")) x += ".js"
operatorFileName = nodePath.join(state.filename, "../", x);
operatorObjName = specifier.local.name;
return;
}
}
}
});
}

插件会先遍历import语句,并判断import进来的名字是不是babel配置文件中配置的重载对象名operatorObjectName。然后将$operator所在的文件解析为绝对路径,用于后续读取。specifier.local.name在有as的情况下为as后的值,没有则是原来的值,即specifier.imported.name

1
2
3
4
5
6
7
8
9
10
11
12
import parser from "@babel/parser";
import traverse from "@babel/traverse";

const VariableDeclaration = (path) => { /* ... */ };

if (operatorFileName) {
let operatorFile = fs.readFileSync(operatorFileName, { encoding: outer.encoding });
const ast = parser.parse(operatorFile, { sourceType: "module" });
traverse.default(ast, { VariableDeclaration });
} else {
path.traverse({ VariableDeclaration });
}

接下来读取$operator所在的文件为字符串,并讲其解析为抽象语法树,然后遍历其抽象语法树,找到重载对象的声明并注册重载。

我们需要在Babel配置文件中配置被读取的文件的编码,默认值为utf8

1
"plugins": [["./plugin-operator/main.js", { "encoding": "utf8" }]]

此处我们用到了两个Babel APIparser.parse读取字符串并将其解析为AST,只有在第二个参数中传入sourceType: "module"才能解析ESM,否则会报错。

四、复用代码

注意到,本插件中所有的visitor都哦有类似的如下结构:

1
2
3
4
const operatorObjectParent = path.findParent((parentPath) => t.isVariableDeclaration(parentPath) && 	operatorObjName == parentPath.node.declarations?.[0].id.name);
if (operatorObjectParent) return;
const operator = outer.registeredOperators.get(path.node.operator /* + 特殊处理自增减 */);
if (operator) { path.replaceWithMultiple(/* replacement */); }

我们可以将这些代码提取出来以优化代码结构。每个visitor都是一个函数,因此我们可以创建一个高阶函数,他接受一个函数replacement,这是所有visitor唯一不同的地方,并返回一个拥有相应replacement的函数。

1
2
3
4
5
6
7
8
const visitorFactory = (replacement) => (path) => {
const operatorObjectParent = path.findParent((parentPath) => t.isVariableDeclaration(parentPath) && operatorObjName == parentPath.node.declarations?.[0].id.name);
if (operatorObjectParent) return;
const operator = outer.registeredOperators.get(path.node.operator + path.node.prefix ?? "");
if (operator) {
path.replaceWithMultiple(replacement);
}
}

但只有这样是不够的,因为replacement需要访问pathoperator,所以我们需要将replacement改成一个函数,将pathoperator传递给他。

1
2
3
4
5
6
7
8
const visitorFactory = (replacement) => (path) => {
const operatorObjectParent = path.findParent((parentPath) => t.isVariableDeclaration(parentPath) && operatorObjName == parentPath.node.declarations?.[0].id.name);
if (operatorObjectParent) return;
const operator = outer.registeredOperators.get(path.node.operator + path.node.prefix ?? "");
if (operator) {
path.replaceWithMultiple(replacement(operator, path));
}
}

于是visitor可以改造成这样:

1
2
3
4
5
6
7
8
AssignmentExpression: visitorFactory((operator, path) => t.parenthesizedExpression(
t.assignmentExpression(
"=", path.node.left, t.callExpression(
t.memberExpression(t.identifier(operatorObjName), t.identifier(operator)),
[path.node.left, path.node.right]
)
), path.node.left
)),

对于自增减,我们进行如下的特殊处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (path.node.prefix) {
// ...
} else {
path.replaceWith(path.node.argument);
path.insertAfter(
t.expressionStatement(
t.assignmentExpression(
"=", path.node, t.callExpression(
t.memberExpression(t.identifier(operatorObjName), t.identifier(operator)),
[path.node]
)
)
)
);
return path.node;
}

我们直接在这里操作抽象语法树节点,并返回节点自身,这样在visitorFacotry中就不会再修改了。

  • 标题: Babel插件开发小试:支持模块化、代码复用化
  • 作者: Stone926
  • 创建于 : 2024-11-17 18:38:15
  • 更新于 : 2025-04-17 23:54:59
  • 链接: https://stone926.github.io/2024/11/17/babel-plugin-operator2/
  • 版权声明: 本文为公有领域作品,可自由转载、引用。
目录
Babel插件开发小试:支持模块化、代码复用化