ESLint 自定义规则与插件开发实战 2026 | 代码质量工程化指南

ESLint 是前端代码质量保障的基石。当内置规则和社区插件无法满足团队需求时,自定义规则和插件就成为了必要的工程化手段。本文将从 AST 基础到插件发布,系统讲解 ESLint 自定义规则开发的全流程。
一、AST 抽象语法树基础
1.1 什么是 AST
AST(Abstract Syntax Tree)是源代码的树形结构表示。ESLint 通过分析 AST 来检测代码模式:
javascript
// 源代码
const x = 1 + 2
// AST 结构(简化)
{
type: "Program",
body: [{
type: "VariableDeclaration",
kind: "const",
declarations: [{
type: "VariableDeclarator",
id: { type: "Identifier", name: "x" },
init: {
type: "BinaryExpression",
operator: "+",
left: { type: "Literal", value: 1 },
right: { type: "Literal", value: 2 }
}
}]
}]
}1.2 AST 可视化工具
- AST Explorer — 在线 AST 查看工具
- 选择
JavaScript→@typescript-eslint/parser或espree
1.3 常用 AST 节点类型
| 节点类型 | 说明 | 示例 |
|---|---|---|
Identifier | 标识符 | foo, bar |
Literal | 字面量 | 1, "str", true |
VariableDeclaration | 变量声明 | const x = 1 |
FunctionDeclaration | 函数声明 | function foo() {} |
ArrowFunctionExpression | 箭头函数 | () => {} |
CallExpression | 函数调用 | foo() |
MemberExpression | 成员访问 | obj.prop |
BinaryExpression | 二元表达式 | a + b |
IfStatement | if 语句 | if (true) {} |
ImportDeclaration | import 语句 | import x from 'y' |
二、ESLint 规则结构
2.1 规则基本结构
javascript
// rule.js
module.exports = {
meta: {
type: 'suggestion', // problem | suggestion | layout
docs: {
description: '禁止使用 var 声明变量',
category: 'Best Practices',
recommended: true
},
fixable: 'code', // code | whitespace | null
schema: [ // 规则参数定义
{
type: 'object',
properties: {
preferConst: { type: 'boolean' }
},
additionalProperties: false
}
],
messages: {
unexpected: "禁止使用 '{{type}}',请使用 'const' 或 'let'"
}
},
create(context) {
return {
// AST 节点选择器:当遇到对应节点时触发
VariableDeclaration(node) {
if (node.kind === 'var') {
context.report({
node,
messageId: 'unexpected',
data: { type: node.kind },
fix(fixer) {
return fixer.replaceText(
node.kind,
node.declarations.every(d =>
!d.init || d.init.type === 'Literal'
) ? 'const' : 'let'
)
}
})
}
}
}
}
}2.2 meta 字段说明
javascript
meta: {
// 规则类型
type: 'problem', // problem: 代码错误(必须修复)
// suggestion: 改进建议
// layout: 格式问题
// 文档信息
docs: {
description: '规则描述',
category: 'Possible Errors',
recommended: true,
url: 'https://github.com/your/repo/blob/main/docs/rules/your-rule.md'
},
// 是否可自动修复
fixable: 'code', // code: 可修复代码
// whitespace: 仅修复空白
// null: 不可修复
// 是否有副作用(影响其他规则)
hasSuggestions: true, // 提供修复建议
// 参数 Schema(JSON Schema 格式)
schema: [
{
type: 'object',
properties: {
option1: { type: 'string', enum: ['a', 'b'] },
option2: { type: 'number' }
},
required: ['option1'],
additionalProperties: false
}
],
// 错误消息模板(支持占位符)
messages: {
errorId: "变量 '{{name}}' 不能使用下划线前缀",
warningId: "建议重命名变量 '{{name}}'"
},
// 弃用标记
deprecated: false,
replacedBy: ['new-rule-name']
}2.3 context 对象
javascript
create(context) {
// context 提供的 API
// 1. 获取配置选项
const options = context.options[0] || {}
const preferConst = options.preferConst !== false
// 2. 获取文件信息
const filename = context.getFilename()
const sourceCode = context.getSourceCode()
// 3. 获取源码
const sourceText = sourceCode.getText()
// 4. 报告错误
context.report({
node: someNode,
messageId: 'errorId',
data: { name: 'someVar' },
loc: { line: 1, column: 1 },
fix(fixer) {
return fixer.replaceText(node, 'newCode')
},
suggest: [{
desc: '使用 const 替代',
fix(fixer) {
return fixer.replaceText(node, 'const')
}
}]
})
// 5. 获取设置
const settings = context.settings // .eslintrc 中的 settings
// 6. 获取解析器服务(TypeScript)
const parserServices = context.parserServices
return { /* AST 选择器 */ }
}三、实战规则开发
3.1 规则:禁止 console.log
javascript
// rules/no-console-log.js
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: '禁止使用 console.log',
category: 'Best Practices',
recommended: false
},
fixable: null,
schema: [
{
type: 'object',
properties: {
allow: {
type: 'array',
items: { type: 'string', enum: ['warn', 'error', 'info'] }
}
},
additionalProperties: false
}
],
messages: {
unexpected: '不允许使用 console.{{method}}'
}
},
create(context) {
const options = context.options[0] || {}
const allowed = options.allow || []
return {
// 匹配 console.xxx() 调用
'CallExpression > MemberExpression'(node) {
if (
node.object.type === 'Identifier' &&
node.object.name === 'console' &&
node.property.type === 'Identifier'
) {
const method = node.property.name
if (!allowed.includes(method)) {
context.report({
node: node.parent,
messageId: 'unexpected',
data: { method }
})
}
}
}
}
}
}3.2 规则:强制组件名大写
javascript
// rules/pascal-case-component.js
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Vue 组件名必须使用 PascalCase',
category: 'Vue'
},
fixable: 'code',
schema: [],
messages: {
invalid: '组件名 "{{name}}" 必须使用 PascalCase'
}
},
create(context) {
return {
// 检查 Vue 组件注册
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'Vue' &&
node.callee.property.name === 'component'
) {
const nameArg = node.arguments[0]
if (nameArg && nameArg.type === 'Literal' && typeof nameArg.value === 'string') {
const name = nameArg.value
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
context.report({
node: nameArg,
messageId: 'invalid',
data: { name },
fix(fixer) {
const pascalName = name
.split(/[-_]/)
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
.join('')
return fixer.replaceText(nameArg, `'${pascalName}'`)
}
})
}
}
}
}
}
}
}3.3 规则:禁止直接修改 props
javascript
// rules/no-mutating-props.js
module.exports = {
meta: {
type: 'problem',
docs: {
description: '禁止在组件中直接修改 props',
category: 'Vue'
},
fixable: null,
schema: [],
messages: {
unexpected: '不允许直接修改 prop "{{name}}"'
}
},
create(context) {
// 获取 Vue 组件中的 props
let props = new Set()
return {
// 收集 props 定义
'Property[key.name="props"] > ObjectExpression > Property'(node) {
if (node.key.type === 'Identifier') {
props.add(node.key.name)
}
},
// 检查赋值操作
'AssignmentExpression'(node) {
if (node.left.type === 'MemberExpression') {
const obj = node.left.object
if (
obj.type === 'Identifier' &&
(obj.name === 'props' || props.has(obj.name))
) {
const propName = node.left.property.name
context.report({
node,
messageId: 'unexpected',
data: { name: propName }
})
}
}
}
}
}
}3.4 规则:API 调用必须有错误处理
javascript
// rules/api-call-error-handling.js
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'async 函数中的 API 调用必须有错误处理',
category: 'Best Practices'
},
fixable: null,
schema: [{
type: 'object',
properties: {
apiPatterns: {
type: 'array',
items: { type: 'string' }
}
},
additionalProperties: false
}],
messages: {
missingTryCatch: 'API 调用 "{{api}}" 必须包含在 try/catch 中',
missingCatch: 'API 调用 "{{api}}" 的 catch 中必须有错误处理逻辑'
}
},
create(context) {
const options = context.options[0] || {}
const apiPatterns = options.apiPatterns || ['fetch', 'axios', 'request']
function isApiCall(node) {
if (node.type === 'CallExpression') {
const name = node.callee.type === 'Identifier'
? node.callee.name
: node.callee.property?.name
return apiPatterns.includes(name)
}
return false
}
function isInTryBlock(node) {
let parent = node.parent
while (parent) {
if (parent.type === 'TryStatement') return true
parent = parent.parent
}
return false
}
return {
AwaitExpression(node) {
if (isApiCall(node.argument)) {
const apiName = node.argument.callee.property?.name ||
node.argument.callee.name
if (!isInTryBlock(node)) {
context.report({
node,
messageId: 'missingTryCatch',
data: { api: apiName }
})
}
}
}
}
}
}四、ESLint 插件结构
4.1 插件目录结构
eslint-plugin-my-rules/
├── package.json
├── index.js # 插件入口
├── rules/
│ ├── no-console-log.js
│ ├── pascal-case-component.js
│ ├── no-mutating-props.js
│ └── api-call-error-handling.js
├── configs/
│ ├── recommended.js # 推荐配置
│ ├── strict.js # 严格配置
│ └── vue.js # Vue 专用配置
├── tests/
│ └── rules/
│ ├── no-console-log.test.js
│ ├── pascal-case-component.test.js
│ └── ...
└── docs/
└── rules/
├── no-console-log.md
└── ...4.2 插件入口文件
javascript
// index.js
const noConsoleLog = require('./rules/no-console-log')
const pascalCaseComponent = require('./rules/pascal-case-component')
const noMutatingProps = require('./rules/no-mutating-props')
const apiCallErrorHandling = require('./rules/api-call-error-handling')
module.exports = {
meta: {
name: 'eslint-plugin-my-rules',
version: '1.0.0'
},
// 注册规则
rules: {
'no-console-log': noConsoleLog,
'pascal-case-component': pascalCaseComponent,
'no-mutating-props': noMutatingProps,
'api-call-error-handling': apiCallErrorHandling
},
// 注册可共享配置
configs: {
recommended: require('./configs/recommended'),
strict: require('./configs/strict'),
vue: require('./configs/vue')
}
}4.3 配置文件
javascript
// configs/recommended.js
module.exports = {
plugins: ['my-rules'],
rules: {
'my-rules/no-console-log': 'warn',
'my-rules/pascal-case-component': 'error',
'my-rules/no-mutating-props': 'error',
'my-rules/api-call-error-handling': 'warn'
}
}
// configs/strict.js
module.exports = {
extends: ['./recommended'],
rules: {
'my-rules/no-console-log': 'error',
'my-rules/api-call-error-handling': 'error'
}
}4.4 package.json
json
{
"name": "eslint-plugin-my-rules",
"version": "1.0.0",
"description": "自定义 ESLint 规则插件",
"main": "index.js",
"peerDependencies": {
"eslint": ">=8.0.0"
},
"devDependencies": {
"eslint": "^8.57.0",
"mocha": "^10.0.0"
},
"scripts": {
"test": "mocha tests/**/*.test.js",
"lint": "eslint ."
},
"keywords": ["eslint", "eslint-plugin", "vue", "typescript"]
}五、规则测试
5.1 使用 RuleTester
javascript
// tests/rules/no-console-log.test.js
const { RuleTester } = require('eslint')
const rule = require('../../rules/no-console-log')
const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2022 }
})
ruleTester.run('no-console-log', rule, {
// 有效的代码(不应报错)
valid: [
'console.warn("warning")',
'console.error("error")',
{
code: 'console.log("debug")',
options: [{ allow: ['log'] }]
},
'Math.max(1, 2)'
],
// 无效的代码(应该报错)
invalid: [
{
code: 'console.log("hello")',
errors: [{ messageId: 'unexpected', data: { method: 'log' } }]
},
{
code: 'console.info("info")',
options: [{ allow: ['warn', 'error'] }],
errors: [{ messageId: 'unexpected', data: { method: 'info' } }]
}
]
})5.2 测试自动修复
javascript
ruleTester.run('pascal-case-component', rule, {
valid: [
'Vue.component("MyComponent", {})',
'Vue.component("UserCard", {})'
],
invalid: [
{
code: 'Vue.component("my-component", {})',
output: 'Vue.component("MyComponent", {})',
errors: [{ messageId: 'invalid', data: { name: 'my-component' } }]
},
{
code: 'Vue.component("user_card", {})',
output: 'Vue.component("UserCard", {})',
errors: [{ messageId: 'invalid', data: { name: 'user_card' } }]
}
]
})5.3 运行测试
bash
# 运行所有测试
npm test
# 运行特定规则测试
npx mocha tests/rules/no-console-log.test.js六、在项目中使用
6.1 ESLint 配置
javascript
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es2022: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:my-rules/recommended' // 使用插件推荐配置
],
plugins: ['my-rules'],
rules: {
// 单独配置规则
'my-rules/no-console-log': ['error', { allow: ['warn', 'error'] }],
'my-rules/pascal-case-component': 'error',
'my-rules/no-mutating-props': 'error',
'my-rules/api-call-error-handling': ['warn', {
apiPatterns: ['fetch', 'axios', '$http']
}]
},
settings: {
// 插件可用的全局设置
'my-rules': {
vueVersion: 3
}
}
}6.2 与 Vite 集成
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import eslint from 'vite-plugin-eslint'
export default defineConfig({
plugins: [
eslint({
fix: false, // 是否自动修复
cache: true, // 启用缓存
include: ['src/**/*.{js,ts,vue}'],
exclude: ['node_modules', 'dist']
})
]
})6.3 本地开发链接
bash
# 在插件目录
npm link
# 在项目目录
npm link eslint-plugin-my-rules
# 测试完成后取消链接
npm unlink eslint-plugin-my-rules七、TypeScript 规则开发
7.1 使用 @typescript-eslint/utils
typescript
// rules/no-explicit-any.ts
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'
const createRule = ESLintUtils.RuleCreator(
name => `https://github.com/your/repo/blob/main/docs/rules/${name}.md`
)
export default createRule({
name: 'no-explicit-any',
meta: {
type: 'problem',
docs: {
description: '禁止使用 any 类型',
recommended: 'error'
},
fixable: 'code',
schema: [],
messages: {
unexpected: '禁止使用 any 类型,请使用 unknown 替代'
}
},
defaultOptions: [],
create(context) {
return {
TSAnyKeyword(node: TSESTree.TSAnyKeyword) {
context.report({
node,
messageId: 'unexpected',
fix(fixer) {
return fixer.replaceText(node, 'unknown')
}
})
}
}
}
})7.2 类型感知规则
typescript
// rules/no-floating-promises.ts
import { ESLintUtils } from '@typescript-eslint/utils'
const createRule = ESLintUtils.RuleCreator(
name => `https://github.com/your/repo/blob/main/docs/rules/${name}.md`
)
export default createRule({
name: 'no-floating-promises',
meta: {
type: 'problem',
docs: { description: 'Promise 必须被处理' },
schema: [],
messages: {
floating: 'Promise 必须被 await、catch 或 void 处理'
}
},
defaultOptions: [],
create(context) {
const services = ESLintUtils.getParserServices(context)
const checker = services.program.getTypeChecker()
return {
ExpressionStatement(node) {
const tsNode = services.esTreeNodeToTSNodeMap.get(node.expression)
const type = checker.getTypeAtLocation(tsNode)
if (type.symbol?.name === 'Promise') {
context.report({
node,
messageId: 'floating'
})
}
}
}
}
})八、插件发布
8.1 发布到 npm
bash
# 登录 npm
npm login
# 发布
npm publish
# 发布 beta 版本
npm publish --tag beta8.2 发布前检查
bash
# 检查 package.json
npm pack --dry-run
# 检查 .npmignore
# node_modules
# tests
# .git
# *.md(docs 目录除外)九、最佳实践
9.1 规则设计原则
- 单一职责:每条规则只检测一个问题
- 性能优先:避免深度遍历 AST
- 可修复:尽可能提供
fix函数 - 消息清晰:错误信息要明确可操作
- 测试完整:覆盖有效和无效用例
9.2 常用 AST 选择器
javascript
// ESLint 支持的选择器语法(类似 CSS)
{
// 所有函数声明
'FunctionDeclaration'(node) {},
// 名为 foo 的函数
'FunctionDeclaration[id.name="foo"]'(node) {},
// 异步函数
'FunctionDeclaration[async=true]'(node) {},
// 赋值表达式的左侧
'AssignmentExpression > MemberExpression.left'(node) {},
// 子选择器
'VariableDeclaration VariableDeclarator'(node) {},
// 组合选择器
'IfStatement, WhileStatement, ForStatement'(node) {},
}9.3 性能优化技巧
javascript
create(context) {
// ✅ 使用选择器精确匹配,避免遍历所有节点
return {
'CallExpression[callee.property.name="forEach"]'(node) {
// 只处理 forEach 调用
}
}
// ❌ 避免在所有节点上做过滤
// return {
// '*'(node) {
// if (isWhatWeWant(node)) { ... }
// }
// }
}十、总结
- ✅ 理解 AST 抽象语法树与常用节点类型
- ✅ 掌握 ESLint 规则结构与 meta 配置
- ✅ 实战 4 条自定义规则(console、组件命名、props、API 错误处理)
- ✅ 完整插件结构(入口、配置、测试、文档)
- ✅ 使用 RuleTester 编写规则测试
- ✅ 在项目中集成自定义插件(ESLint 配置 + Vite 集成)
- ✅ TypeScript 规则开发(类型感知规则)
- ✅ 插件发布到 npm
- ✅ 规则设计最佳实践与性能优化
自定义 ESLint 规则是代码质量工程化的高级技能,它能将团队的代码规范自动化,从源头保障代码质量。
相关阅读: