前端向架构突围系列 - 编译原理 [6 - 3]:ESLint 原理、自定义规则与 Codemod
写在前面
很多团队面临这样的困境: 架构师制定了规范:“所有业务组件禁止直接引用
lodash,必须引用src/utils。” 结果呢?文档写在 Wiki 里吃灰,新同事照样写import _ from 'lodash'。Code Review 时如果你没看出来,这代码就溜上线了。口头规范是软弱的,代码规范才是强硬的。
真正的高手,会把架构规范写成 ESLint 插件。这一节,我们将把“文档里的规范”变成“编辑器里的红色波浪线”。
![]()
一、 ESLint 的原理:找茬的艺术
ESLint 的工作流程和 Babel 惊人地相似,只有最后一步不同。
1.1 流程对比
- Babel: Parse -> Transform (修改 AST) -> Generate (生成新代码)
- ESLint: Parse -> Traverse (遍历 AST) -> Report (报告错误)
ESLint 默认使用 Espree 作为解析器(Parser)。它遍历 AST,当遇到不符合规则的节点时,不是去修改它,而是记录一个“错误对象”(包含行号、列号、错误信息)。
1.2 Fix 的原理
你一定用过 eslint --fix。既然 ESLint 不生成新代码,它是怎么修复错误的? 其实,ESLint 的规则在报错时,可以提供一个 fixer 对象。
context.report({
node: node,
message: "缺少分号",
fix: function(fixer) {
// 告诉 ESLint:在当前节点后面插入一个 ";"
return fixer.insertTextAfter(node, ";");
}
});
ESLint 收集所有的 fix 操作,最后在源码字符串上进行字符串拼接(而不是重新 Generate),从而保留原本的格式(空格、注释)。
二、 实战:编写你的第一条 ESLint 规则
假设你的团队有一个死规定:代码中禁止使用 var,必须用 let 或 const。 虽然现有的规则集里有 no-var,但为了学习,我们自己写一个。
2.1 规则结构
一个 ESLint 规则就是一个导出的对象,包含 meta(元数据)和 create(访问者)。
// eslint-plugin-no-var-custom.js
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "禁止使用 var",
},
fixable: "code", // 表示这个规则支持自动修复
},
create(context) {
return {
// 监听 VariableDeclaration 节点
VariableDeclaration(node) {
// 如果声明类型是 "var"
if (node.kind === "var") {
// 报警!
context.report({
node,
message: "大清亡了,别用 var 了!",
// 自动修复逻辑
fix(fixer) {
// 把 "var" 替换成 "let"
// sourceCode.getFirstToken(node) 获取到的就是 "var" 这个关键词
const varToken = context.getSourceCode().getFirstToken(node);
return fixer.replaceText(varToken, "let");
}
});
}
}
};
}
};
2.2 架构级应用:防腐层治理
架构师可以利用自定义规则做更高级的事情。 场景: 项目中分层架构,UI 层(src/components)严禁直接导入数据库层(src/db)。
// rule: no-ui-import-db.js
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value; // e.g., '@/db/user'
const currentFilename = context.getFilename(); // 当前正在检查的文件
// 如果当前文件在 components 目录下,且引用了 db 目录
if (currentFilename.includes('/src/components/') && importPath.includes('/db/')) {
context.report({
node,
message: "架构报警:UI 组件禁止直接触碰数据库层!请通过 Service 层调用。"
});
}
}
};
}
把这个规则加入 CI/CD,你的架构分层就有了强制力。
三、 Codemod:自动化重构的核武器
ESLint 的 fix 适合修补小问题。但如果你面临的是大规模破坏性重构,比如:
- 把项目中 5000 个文件的
React.createClass全部重写为class extends React.Component。 - 把所有的
import { Button } from 'my-ui'变成import Button from 'my-ui/button'。
这时候,你需要 Codemod。最著名的工具是 Facebook 推出的 jscodeshift。
3.1 jscodeshift 的优势
它不仅仅是 AST 解析器,它提供了一套类似 jQuery 的 API 来操作 AST。你不需要关心复杂的节点结构,只需要链式调用。
3.2 实战:API 签名变更
需求: 旧的 API myApi.get(id, type) 升级了,参数变了,必须改成对象传参 myApi.get({ id, type })。
Codemod 脚本:
// transformer.js
export default function(file, api) {
const j = api.jscodeshift; // 获取 jscodeshift 实例
return j(file.source) // 1. 解析源码
.find(j.CallExpression, { // 2. 查找所有的函数调用
callee: {
object: { name: 'myApi' },
property: { name: 'get' }
}
})
.forEach(path => { // 3. 遍历找到的节点
const args = path.node.arguments;
// 如果参数数量是 2 个,说明是旧代码
if (args.length === 2) {
// 创建一个新的对象表达式 { id: arg0, type: arg1 }
const newObjArg = j.objectExpression([
j.property('init', j.identifier('id'), args[0]),
j.property('init', j.identifier('type'), args[1])
]);
// 替换参数
path.node.arguments = [newObjArg];
}
})
.toSource(); // 4. 生成新代码
}
运行:
npx jscodeshift -t transformer.js src/**/*.js
瞬间,你完成了全项目几千个文件的 API 升级。这就是架构师的效率。
四、 总结:架构师的“法治”思维
这一节我们从“写代码”进阶到了“管代码”。
- ESLint 是日常执勤的警察,通过 Linting(检查)和 Fixing(微修补)维持代码风格和架构边界。
- Codemod 是特种部队,通过 AST Transformation 解决大规模的技术债务和破坏性升级。
架构师不应该仅仅是那个“写文档告诉大家怎么做”的人,而应该是那个“提供工具让大家没法做错”的人。
Next Step: 我们已经把 AST 在工具链(Babel, ESLint)中的应用学完了。 最后,我们要看看 AST 是如何在现代前端框架中发挥作用的。Vue 的
<template>是怎么变成 JS 的?React 的 JSX 到底是怎么回事? 下一节,我们将揭秘**《第四篇:应用——框架的魔法:Vue 模板编译与 React JSX 转换背后的编译艺术》**。