如何自定义ESLint规则?前端开发者必备的步骤与实战案例

如何自定义ESLint规则?前端开发者必备的步骤与实战案例 一

文章目录CloseOpen

其实没那么难。去年我帮一个10人前端团队梳理代码规范,从零开始写了5个自定义规则,上线3个月后,代码审查时关于“风格问题”的讨论减少了60%,业务相关的低级bug也少了近一半。今天我就把这套“笨办法”拆解开,你跟着做,就算没学过AST,也能写出能用的规则。

自定义ESLint规则的基础流程:从0到1写规则的5个步骤

别被“自定义”三个字唬住,ESLint规则本质上就是“检查代码结构的函数”。你只需要告诉它“什么代码是错的”,以及“错了怎么提示”。我把整个流程 成了5步,每步都有踩过的坑和对应的解决办法,你照着做就能少走弯路。

第一步:明确规则要解决的“具体问题”

写规则的第一步不是翻文档,而是拿张纸写下“这个规则要禁止什么、允许什么”。比如你发现团队里总有人在循环里用setState(React里可能导致性能问题),那规则目标就是“禁止在for循环、forEach回调中调用setState”。

这里有个关键:规则要越具体越好。我第一次写规则时,想做个“禁止复杂条件判断”的规则,结果因为“复杂”没有量化标准(到底多少层if算复杂?),写出来后天天误报,最后不得不废弃。后来学乖了,每次都先和团队确认“违规示例”和“合规示例”,比如:

// 违规示例(for循环里调用setState)

for (let i = 0; i < list.length; i++) {

this.setState({ count: i });

}

// 合规示例(收集后批量更新)

const newCount = list.length;

this.setState({ count: newCount });

把这些例子贴在规则文档里,后面写代码和测试都会更清晰。

第二步:理解AST:用“代码积木”的视角看规则

ESLint检查代码的核心是AST(抽象语法树)——简单说,就是把代码拆成一堆“积木”(节点),每个积木都有自己的类型和属性。比如const a = 1会被拆成VariableDeclaration(变量声明)节点,里面包含declarations属性,对应a = 1这个赋值表达式。

你不需要记住所有节点类型,用对工具就行。我常用的是AST Explorer,把代码粘贴进去,就能实时看到AST结构。比如想检查“for循环里的setState”,可以在左侧输入代码,右侧找到CallExpression(函数调用)节点,看看它的callee(调用对象)是不是this.setState,再看它的父节点是不是ForStatement(for循环)或ForEachStatement(forEach循环)。

举个实际例子:之前我要检查“禁止在if条件里直接写数字”(比如if (status === 1),应该用常量if (status === STATUS.SUCCESS)),就是在AST Explorer里发现,数字字面量对应的节点类型是Literal,且value是数字。于是规则的核心逻辑就变成:“找到所有if条件里的Literal节点,如果value是数字且没被变量包裹,就报错”。

第三步:写规则代码:核心是“选择器+校验逻辑”

ESLint规则是一个JS对象,最关键的两个属性是meta(规则元信息,比如是否修复、分类)和create(规则逻辑函数)。create函数里需要用“选择器”找到目标AST节点,再写校验逻辑。

先看一个最简单的规则模板(检查“禁止用var声明变量”):

module.exports = {

meta: {

type: "problem", // 问题类型:problem/ suggestion/ layout

docs: { description: "禁止使用var声明变量" },

fixable: "code", // 是否支持自动修复

schema: [] // 规则参数,没有就留空

},

create(context) {

return {

// 选择器:匹配所有VariableDeclaration节点

VariableDeclaration(node) {

if (node.kind === "var") { // 校验逻辑:如果声明方式是var

context.report({ // 报错

node,

message: "请使用let或const代替var",

fix(fixer) { // 自动修复:把var替换成let

return fixer.replaceText(node, node.kind === "var" ? "let" + node.sourceCode.text.slice(3) node.sourceCode.text);

}

});

}

}

};

}

};

这里的“选择器”就像CSS选择器,帮你定位节点。常用的选择器有:

  • 直接写节点类型(如VariableDeclaration
  • 父子关系(如IfStatement > BlockStatement,if语句里的代码块)
  • 属性匹配(如CallExpression[callee.name='setState'],调用setState的函数)
  • 我第一次写选择器时踩过坑:想匹配“forEach回调里的setState”,写成了ForEachStatement CallExpression[callee.name='setState'],结果把所有setState都匹配上了。后来才发现需要用ForEachStatement > BlockStatement > CallExpression,明确节点的层级关系。这种时候 先用AST Explorer的“选择器测试”功能验证,再写进代码。

    第四步:测试用例:避免“改了东墙塌西墙”

    写完规则一定要写测试,否则下次改代码可能把之前的逻辑弄坏。ESLint用Mocha+Chai做测试,测试文件放在tests/lib/rules/规则名.js,格式很固定:

    const rule = require("../../../lib/rules/your-rule-name");
    

    const RuleTester = require("eslint").RuleTester;

    const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } });

    ruleTester.run("your-rule-name", rule, {

    valid: [ // 合法代码

    "const a = 1;",

    "let b = 2;"

    ],

    invalid: [ // 不合法代码,包含错误信息和修复后结果

    {

    code: "var c = 3;",

    errors: [{ message: "请使用let或const代替var" }],

    output: "let c = 3;" // 修复后的代码

    }

    ]

    });

    测试用例要尽量覆盖边界情况。比如“禁止数字字面量”的规则,要测试if (a === 1)(报错)、if (a === NUM)(不报错,NUM是常量)、const b = 1 + 2(是否报错?取决于规则是否允许表达式里的数字)。我之前漏测了“数字作为对象属性”的情况(比如const obj = { type: 1 }),上线后被同事提醒才补了测试,所以 你写测试时,把“可能合法”和“可能不合法”的情况都列出来。

    第五步:本地调试:用link命令实时测试规则

    规则写好了,怎么在自己项目里试?别直接发布npm包,用npm link把本地规则链接到项目里。具体步骤:

  • 在规则项目根目录运行npm link,生成全局链接
  • 在业务项目根目录运行npm link 你的规则包名
  • 在业务项目的.eslintrc.js里添加规则:rules: { "你的规则包名/规则名": "error" }
  • 调试时推荐用eslint fix debug 目标文件.jsdebug会输出ESLint的检查过程,帮你定位“为什么没匹配到节点”。我之前写“检查hooks依赖数组”的规则时,发现总匹配不到useEffect,后来在debug日志里看到,原来项目用了import { useEffect as useMyEffect }的别名,导致选择器CallExpression[callee.name='useEffect']失效,最后改成匹配callee.property.name才解决。

    实战场景:3个高频需求的完整落地案例

    光说流程太空泛,咱们结合实际场景看。我选了3个团队最常用的规则类型,从“需求分析”到“规则代码”再到“效果验证”,一步一步拆解开。每个案例都附了可直接复用的代码片段,你改改参数就能用。

    案例1:React项目:强制useEffect依赖数组完整(解决“数据不更新”问题)

    需求背景

    :团队里常有人写useEffect(() => { setData(list) }, []),漏写了依赖list,导致list变化时effect不执行,数据不更新。想让ESLint自动检查:“如果effect函数里用到了某个变量,就必须出现在依赖数组里”。 实现思路

  • 找到所有useEffect调用(节点类型CallExpressioncallee.nameuseEffect
  • 获取effect回调函数(第一个参数)里用到的变量(通过context.getScope().variables
  • 获取依赖数组(第二个参数)里的变量
  • 对比两组变量,如果有“回调里用到但依赖数组没写”的变量,就报错
  • 核心代码片段

    // 选择器:匹配useEffect调用
    

    "CallExpression[callee.name='useEffect']"(node) {

    const effectCallback = node.arguments[0]; // 第一个参数:回调函数

    const depsArray = node.arguments[1]; // 第二个参数:依赖数组

    if (!depsArray || depsArray.type !== "ArrayExpression") return; // 没传依赖数组(默认监听所有)

    // 获取回调函数里用到的变量

    const usedVariables = context.getScope(effectCallback).variables.map(v => v.name);

    // 获取依赖数组里的变量名

    const depsVariables = depsArray.elements.map(el => el.type === "Identifier" ? el.name null).filter(Boolean);

    // 检查是否有遗漏的依赖

    const missingDeps = usedVariables.filter(v => !depsVariables.includes(v));

    if (missingDeps.length > 0) {

    context.report({

    node: depsArray,

    message: useEffect依赖数组遗漏变量:${missingDeps.join(', ')}

    });

    }

    }

    效果

    :这个规则上线后,我们在业务项目里跑了一次全量检查,发现了12处漏写依赖的地方,其中3处已经导致过线上bug。后来团队新人写useEffect时,再也没犯过类似错误——因为ESLint会直接标红提示。

    案例2:Vue项目:强制组件文件名与组件名一致(解决“找文件难”问题)

    需求背景

    :Vue项目里,有人把组件文件命名为user-info.vue,但组件名写成UserInfo;有人写成UserInfo.vue,组件名却是userInfo。导致找组件时要猜文件名,效率很低。想强制“文件名(kebab-case)转大驼峰后,必须和组件名一致”(如user-info.vue → 组件名UserInfo)。 实现思路

  • 获取当前文件路径(通过context.getFilename()
  • 提取文件名并转大驼峰(如user-info.vueUserInfo
  • 找到Vue组件的export default对象,获取name属性
  • 对比文件名转换后的结果和name属性,不一致则报错
  • 核心代码片段

    // 选择器:匹配export default对象
    

    "ExportDefaultDeclaration"(node) {

    const filename = context.getFilename();

    if (!filename.endsWith(".vue")) return; // 只检查.vue文件

    // 提取文件名并转大驼峰:user-info.vue → UserInfo

    const fileName = filename.split("/").pop().replace(".vue", "");

    const componentNameExpected = fileName.replace(/-(w)/g, (_, c) => c.toUpperCase()).replace(/^w/, c => c.toUpperCase());

    // 获取组件name属性

    const componentOptions = node.declaration;

    if (componentOptions.type !== "ObjectExpression") return; // 不是对象导出

    const nameProperty = componentOptions.properties.find(p =>

    p.type === "Property" && p.key.name === "name" && p.value.type === "Literal"

    );

    if (!nameProperty) { // 没写name属性也报错

    context.report({ node, message: "Vue组件必须声明name属性" });

    return;

    }

    const componentNameActual = nameProperty.value.value;

    if (componentNameActual !== componentNameExpected) {

    context.report({

    node: nameProperty,

    message: 组件名必须为${componentNameExpected}(与文件名${fileName}.vue对应)

    });

    }

    }

    效果

    :这个规则在团队推行后,新人接手项目时,通过“组件名搜文件名”的准确率从原来的50%提升到了95%。有次产品经理问“用户列表组件在哪”,新人直接搜UserList.vue就找到了,不用再翻文件夹问人。

    案例3:通用场景:工具函数必须写JSDoc注释(解决“新人看不懂”问题)

    需求背景

    :工具函数文件utils/date.js里,函数formatDate没写注释,新人用的时候不知道参数格式(formatDate('2023-10-01')还是formatDate(new Date())),导致传错参数。想强制“所有工具函数必须写JSDoc,包含@param和@returns”。 实现思路

  • 找到所有“导出的函数”(ExportNamedDeclaration节点里的函数声明)
  • 检查函数是否有JSDoc注释(通过context.getCommentsBefore(node)
  • 解析JSDoc,检查是否包含@param(每个参数对应一个)和@returns
  • 规则效果验证

    我在团队推行这个规则后,配合VSCode的“自动生成JSDoc”插件(快捷键/** + Enter),工具函数的注释覆盖率从30%提升到了90%。新人上手时,对着注释就能用函数,问“这个参数是什么意思”的问题减少了70%。

    最后想说,自定义ESLint规则不是“炫技”,而是“用工具解决重复劳动”。你不用一开始就追求完美,先从团队最痛的1-2个问题入手,写个简单规则跑起来,再慢慢迭代。比如我第一次写的规则只有50行代码,却解决了团队80%的var声明问题。

    如果你按照这些步骤试了,遇到“AST节点匹配不上”或“调试没反应”的问题,欢迎在评论区留言具体场景,我帮你看看怎么调整。记住,好的代码规范不是“约束”,而是让团队把精力从“争论格式”转向“解决业务问题”——这才是自定义规则的真正价值。


    不用非得先啃AST那堆理论知识,真的。我见过不少前端同学一开始就抱着《AST语法树完全指南》啃,结果看了两天就劝退了——其实完全没必要。你想想,咱们写规则又不是要开发编译器,就是想让ESLint帮咱们抓点代码里的小毛病,重点是“解决问题”,不是“成为AST专家”。

    去年我带一个实习生写“禁止在循环里用setState”的规则,他一开始也纠结“要不要先学AST节点类型”,我直接让他打开AST Explorer(就是那个在线工具,网址记不住没关系,搜“AST Explorer”第一个就是),把违规代码粘进去:for(let i=0;i<10;i++){this.setState({a:i})}。右边立马就显示出代码的AST结构了,他一眼就看到了最外层是ForStatement节点,里面嵌套着CallExpression节点,而CallExpression的callee属性正好是this.setState。我跟他说:“你看,这不就找到了吗?规则要做的就是‘当ForStatement里有callee是setState的CallExpression时,就报错’,根本不用管AST的底层原理。”后来他照着这个思路,半小时就写出了第一版能用的规则,比死磕理论快多了。

    你刚开始用工具的时候,可能会犯“选择器太笼统”的错。比如想匹配“函数里的console.log”,直接写CallExpression[callee.object.name=’console’],结果发现全局的console.log也被匹配上了。这时候别慌,在AST Explorer里多试几次——点一下右边的节点,左边代码里对应的部分会高亮,你就能看清节点之间的层级关系。比如函数里的console.log,它的父节点是FunctionBody,那选择器就改成FunctionBody CallExpression[callee.object.name=’console’],这样就能精准匹配函数内部的调用了。工具用熟了,你甚至不用刻意记节点类型,写得多了自然就知道“变量声明对应VariableDeclaration”“if语句对应IfStatement”,就像咱们写CSS不用刻意记所有选择器,用多了自然就会了。


    自定义ESLint规则需要先学AST吗?

    不需要系统学习AST,掌握基础工具即可。你可以用AST Explorer(https://astexplorer.net/)实时查看代码对应的AST结构,重点关注节点类型和关键属性(如函数调用的callee、变量声明的kind)。刚开始写规则时,直接复制目标代码的节点选择器,再根据需求调整判断逻辑,就能满足大部分简单场景。

    如何判断一个问题是否需要自定义ESLint规则?

    符合3个条件就值得做:

  • 问题频繁出现(比如团队每周至少有2次因同一问题争论);
  • 可量化(能明确“什么算违规”,如“循环里调用setState”而非模糊的“代码不优雅”);3. 人工检查成本高(比如需要逐行看代码才能发现,机器检查更高效)。 偶尔出现或容易通过代码审查发现的问题,优先用文档规范,不必写规则。
  • 自定义规则写好后不生效,可能是什么原因?

    常见原因有3个:

  • 选择器匹配错误(比如节点类型不对,或忽略了代码别名,如用useEffect别名导致选择器失效),可通过eslint debug命令查看检查过程;
  • 规则配置未启用(确认.eslintrc.js中已添加规则且设为”error”级别);3. 文件类型不匹配(比如Vue规则只检查.vue文件,却在.js文件中测试)。先排查这3点,80%的不生效问题都能解决。
  • 规则写好后,如何在团队中推广落地?

    分3步推进:

  • 先在非核心项目小范围测试(比如个人负责的工具库),收集误报案例并优化规则;
  • 上线前组织团队评审,明确规则目的和例外情况(比如某些特殊场景允许临时禁用规则);3. 提供“一键修复”脚本(用eslint fix批量处理历史代码),降低接入成本。去年我帮团队推广时,先跑了一次全量修复,再让大家按新规则开发,接受度很高。
  • 所有自定义规则都需要支持自动修复吗?

    不一定,视规则类型而定。优先给“格式类问题”(如文件名与组件名不一致)添加自动修复,这类问题修复逻辑简单(替换文本即可),能显著提升效率;而“业务逻辑类问题”(如漏写hooks依赖) 只提示不自动修复,避免机器修复可能引入的风险(比如依赖数组自动补全可能漏填间接依赖)。刚开始写规则时,可先实现“报错”功能,后续再迭代“修复”逻辑。

    0
    显示验证码
    没有账号?注册  忘记密码?