普通视图
中泰证券:2025年净利同比预增40%-60%
智翔金泰:预计2025年净利润亏损4.81亿元-5.87亿元
现代汽车去年销售额创新高,营业利润同比降19.5%
天顺风能:六家全资子公司停产
晨曦航空:预计2025年归母净利润亏损5500万元-7060万元
百奥赛图:2025年净利同比预增384%-444%
盛和资源:2025年净利同比预增281.28%-339.20%
恒指收涨0.51%,恒生科技指数跌1%
方正证券:预计2025年净利润同比增长75%-85%
大众公用:预计2025年归母净利润同比增长50.12%-114.46%
桐昆股份:2025年净利同比预增62.24%-78.88%
春雪食品:预计2025年归母净利润同比增长340.90%-426.63%
中科磁业:2025年净利同比预增75.11%-108.32%
TypeScript 类型体操练习笔记(一)
福田汽车:2025年净利同比预增1551%左右
德国政府下调2026年经济增长预期至1%
瑞可达:2025年净利同比预增64.20%-81.43%
前端向架构突围系列 - 编译原理 [6 - 2]:Babel 插件开发与访问者模式
写在前面
很多高级的前端库都在用 Babel 插件做“魔法”。
- React: JSX 语法根本不是 JS,是 Babel 把它变成了
React.createElement。- Vue:
v-model的语法糖,是在编译阶段展开的。- babel-plugin-import: 为什么 Ant Design 可以按需加载?因为它悄悄把
import { Button } from 'antd'改写成了引用具体文件的路径。学会写 Babel 插件,意味着你拥有了改写语言规则的能力。
![]()
一、 核心设计模式:访问者模式 (Visitor Pattern)
AST 是一棵深度极深、结构复杂的树。如果让你手动写递归函数去遍历每一个节点,还得判断“这是不是函数”、“这是不是变量”,你会疯掉的。
Babel 采用 访问者模式 来解决这个问题。
1.1 什么是 Visitor?
想象 AST 是一个巨大的迷宫。
- Babel (Traverser): 是一个不知疲倦的导游,他负责走遍迷宫的每一个角落(深度优先遍历)。
- 你 (Visitor): 是游客。你不需要自己走,你只需要在特定的“景点”等着。
- 工作流: 导游走到一个节点(比如“函数声明节点”),就会大喊:“这里有个函数声明!”如果你对这个节点感兴趣,你就处理它;不感兴趣,导游就继续走。
1.2 代码中的 Visitor
在 Babel 插件中,Visitor 就是一个对象,对象的Key 是你感兴趣的 AST 节点类型。
const visitor = {
// 当遍历到 Identifier(标识符/变量名)节点时,执行这个函数
Identifier(path) {
console.log("我发现了一个变量:", path.node.name);
},
// 当遍历到 BinaryExpression(二元表达式,如 a + b)节点时...
BinaryExpression(path) {
console.log("我发现了一个运算");
}
};
二、 手术刀的核心:Path 与 Types
在编写插件时,有两个最重要的概念:path 和 @babel/types。
2.1 Path:节点之间的桥梁
注意,Visitor 函数接收的参数不是 node,而是 path。
-
Node (节点): 只是静态的数据(JSON 对象),比如
{ type: "Identifier", name: "a" }。它没有灵魂。 -
Path (路径): 是一个响应式对象。它不仅包含当前节点的信息,还包含父节点、作用域以及增删改查的方法。
-
path.node: 获取当前节点数据。 -
path.parent: 获取父节点。 -
path.remove(): 自杀(把自己从树中移除)。 -
path.replaceWith(newNode): 变身(把自己替换成新节点)。 -
path.stop(): 告诉导游(Babel),停止遍历,不要往下走了。
-
2.2 @babel/types:节点的生成器与验证器
如果你想把 a 替换成 b,你不能直接写 path.node.name = 'b'(虽然有时候也能跑,但不规范)。你需要创建一个标准的 AST 节点。 @babel/types (通常简写为 t) 就是干这个的。
-
t.isIdentifier(node): 判断是不是标识符。 -
t.stringLiteral("hello"): 创建一个字符串字面量节点。
三、 实战演练:编写一个“去除 console.log”的插件
这是 Babel 插件开发的 "Hello World"。 需求: 生产环境构建时,自动删除代码里所有的 console.log,但保留 console.error。
3.1 第一步:在 AST Explorer 中观察
输入源码:
console.log('hello');
console.error('oops');
观察 AST 结构,发现 console.log('hello') 是一个 CallExpression (调用表达式)。
-
callee是一个 MemberExpression (成员表达式console.log)。-
object:console(Identifier) -
property:log(Identifier)
-
3.2 第二步:编写插件代码
一个 Babel 插件就是一个函数,返回一个包含 visitor 的对象。
// my-babel-plugin.js
module.exports = function({ types: t }) {
return {
name: "remove-console-log",
visitor: {
// 监听 CallExpression 节点
CallExpression(path) {
// 1. 获取 callee (被调用的函数,比如 console.log)
const { callee } = path.node;
// 2. 判断是否是成员表达式 (MemberExpression),且不是计算属性 (a['b'])
if (t.isMemberExpression(callee) && !callee.computed) {
// 3. 检查 object 是不是 'console',property 是不是 'log'
if (t.isIdentifier(callee.object, { name: 'console' }) &&
t.isIdentifier(callee.property, { name: 'log' })) {
// 4. 这里的 path 就是 console.log('...') 这一整行
// 直接由手术刀切除!
path.remove();
}
}
}
}
};
};
3.3 第三步:测试效果
输入:
function add(a, b) {
console.log('debug:', a);
console.error('fatal error');
return a + b;
}
输出:
function add(a, b) {
console.error('fatal error');
return a + b;
}
Bingo! 你刚刚成功完成了一次代码外科手术。
四、 进阶:作用域 (Scope) 的魔咒
架构师和普通开发者的区别在于对副作用的考虑。 上面的插件有个 Bug:如果用户自己定义了一个叫 console 的变量怎么办?
function test() {
const console = { log: () => {} };
console.log('这不应该被删除'); // 我们的插件会错误地删除这一行!
}
4.1 作用域检查
Babel 的 path.scope 提供了强大的作用域分析能力。 我们需要检查:console 这个引用,是否绑定(Binding) 到了全局?还是被局部变量覆盖了?
优化后的代码:
CallExpression(path) {
const { callee } = path.node;
if (t.isMemberExpression(callee) &&
t.isIdentifier(callee.object, { name: 'console' }) &&
t.isIdentifier(callee.property, { name: 'log' })) {
// 【新增】核心检查:获取 'console' 这个标识符的绑定信息
const binding = path.scope.getBinding('console');
// 如果没有绑定(说明是全局变量),才删除
if (!binding) {
path.remove();
}
}
}
现在,Babel 能够识别出局部变量 console,从而放过它。
五、 总结与脑洞:Babel 还能干什么?
通过这个简单的例子,你掌握了 Babel 插件的精髓:Find (Visitor) -> Check (Path/Types) -> Modify (Remove/Replace) 。
在架构设计中,Babel 插件有无限的潜力:
-
自动埋点: 找到所有的 Click 事件函数,在函数体第一行自动插入
track('click')代码。 -
国际化 (i18n) 提取: 扫描所有中文字符串,自动提取到 JSON 文件中,并替换为
t('key')。 - 代码卫士: 禁止使用某些落后的 API,一旦发现直接构建报错(比 ESLint 更暴力)。
结语:从手术刀到守门员
我们现在已经拥有了修改代码的手术刀(Babel Plugin)。 但是,手术刀太锋利了,容易伤人。在日常开发中,我们更多时候不需要“修改”代码,而是需要“检查”代码,或者进行大规模的、安全的“自动化重构”。
这时候,我们需要另一套基于 AST 的工具体系。
Next Step: 既然我们能分析代码结构,那是不是可以制定一套“代码法律”? 下一节,我们将探讨**《第三篇:工具——代码质量的守门员:ESLint 原理、自定义规则与 Codemod 自动化重构》**。我们将学习如何编写自定义的 ESLint 规则,以及如何用 Codemod 瞬间修改 1000 个文件。