代码规范工具深度解析:ESLint、Prettier与Husky的工程化实践
一句话概括
前端代码规范体系的核心在于Linter(ESLint)的抽象语法树分析能力与Formatter(Prettier)的确定性格式化规则的职责分离,再通过Husky + lint-staged组成代码提交前的自动检查防线,最终构建出一套从”写法约束”到”格式统一”再到”自动修复”的完整质量保障闭环。
背景与意义
前端研发的”熵增”问题
一个没有代码规范的前端项目,经过3个迭代、5个开发者贡献后,往往会出现以下症状:
- 一半文件用单引号,一半用双引号
- 有的人写
function() {},有的人写() => {} - CSS顺序混乱,选择器深度超过5层
console.log和未使用的变量提交到生产代码- 不同开发者的缩进大小不同
这些问题单独看都不致命,但它们共同增加了代码的认知负担:你每次阅读一个文件,都需要重新适应它的风格。Ralph Johnson(GoF设计模式作者之一)说过:”代码被阅读的次数远多于它被编写的次数。”代码规范的真正价值不在于”谁更对”,而在于消除不必要的差异。
从规范化到自动化
传统的方式是在Code Review中人工检查规范,但这有两大问题:一是低效(需要Reviewer具备全部规范的记忆),二是延迟(写代码到审查之间有较长反馈周期)。
现代前端工程化通过工具链实现了代码规范的自动化:
| 维度 | 工具 | 作用 |
|---|---|---|
| 代码质量 | ESLint | 检测逻辑错误、不良做法 |
| 代码风格 | Prettier | 自动格式化(换行、缩进、空格) |
| 提交前检查 | Husky + lint-staged | 在git commit前自动执行检查 |
| 提交信息规范 | Commitlint | 校验commit message格式 |
概念与定义
ESLint:基于AST的代码质量分析器
ESLint的核心机制是规则(Rule)。每条规则是一个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 一条ESLint规则的简化结构
module.exports = {
meta: {
type: 'suggestion', // problem / suggestion / layout
docs: { description: '禁止var' },
fixable: 'code', // 是否需要自动修复
schema: [], // 配置参数
},
create(context) {
return {
// 当遍历到VariableDeclaration节点时触发
VariableDeclaration(node) {
if (node.kind === 'var') {
context.report({
node,
message: '不要使用var,请用let或const',
fix(fixer) {
// 修复逻辑:将 var 替换为 const
if (node.declarations.every(d => d.init)) {
return fixer.replaceTextRange(
[node.range[0], node.range[0] + 3],
'const'
);
}
},
});
}
},
};
},
};
ESLint的实质是:解析源码为AST → 遍历AST → 针对每种节点类型检查 → 报告问题(可选修复)。
Prettier:固执己见的格式器
Prettier与ESLint最大的不同在于:ESLint是”你应该(不)做什么”,Prettier是”你必须看起来像什么”。
Prettier没有”规则”的概念,它是一个”黑盒”——你把不规范的代码放进去,它输出规范格式的代码。它只有几十个可配置参数(打印宽度、引号风格、尾分号等),没有”警告级别”或”自动修复”(因为所有格式化都是自动修复)。
Husky:Git Hook管理器
Git提供了Hook机制:pre-commit、pre-push、commit-msg 等。Husky的作用是让这些Hook的管理变得更简单——你只需要在 .husky/ 目录下放脚本文件即可。
lint-staged:只检查暂存区文件
核心思想:在 pre-commit 阶段,只对 git add 过的文件运行检查,而不是全量检查。这在大型项目中能节省大量时间。
最小示例
项目搭建
1
2
3
mkdir code-quality-demo && cd code-quality-demo
npm init -y
npm install eslint prettier husky lint-staged -D
配置ESLint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// eslint.config.js - ESLint 9+ 扁平配置格式(Flat Config)
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import pluginImport from 'eslint-plugin-import';
export default [
// 1. 基础推荐配置
js.configs.recommended,
// 2. TypeScript配置
...typescript.configs.recommended,
// 3. React插件配置
pluginReact.configs.flat.recommended,
// 4. 项目自定义规则
{
files: ['src/**/*.{js,jsx,ts,tsx}'],
plugins: {
import: pluginImport,
},
rules: {
// 自定义规则
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'no-var': 'error',
'eqeqeq': ['error', 'always'],
// TypeScript规则
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/no-explicit-any': 'warn',
// React规则
'react/prop-types': 'off', // TypeScript不需要prop-types
'react/react-in-jsx-scope': 'off', // React 17+不需要
// Import规则
'import/order': ['warn', {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
}],
},
},
// 5. 忽略配置
{
ignores: ['dist/**', 'build/**', 'node_modules/**', '.next/**'],
},
];
配置Prettier
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// prettier.config.js
/** @type {import('prettier').Config} */
const config = {
// 基础
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
// JSX & 对象
jsxSingleQuote: false,
trailingComma: 'all',
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'always',
// 其他
endOfLine: 'lf',
quoteProps: 'consistent',
embeddedLanguageFormatting: 'auto',
};
export default config;
集成ESLint与Prettier
Prettier和ESLint的格式化规则可能存在冲突(比如Prettier坚持加尾逗号,ESLint禁止尾逗号)。解决方案:
1
2
3
4
5
6
7
8
9
10
// 方式1(推荐):使用 eslint-config-prettier 关闭冲突规则
npm install eslint-config-prettier -D
// 在eslint.config.js中
import eslintConfigPrettier from 'eslint-config-prettier';
export default [
// ...其他配置
eslintConfigPrettier, // 必须放在最后
];
配置Husky + lint-staged
1
2
3
4
5
# 初始化husky
npx husky init
# 创建pre-commit hook
# 编辑 .husky/pre-commit
.husky/pre-commit:
1
2
3
4
5
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 只对暂存区文件运行lint-staged
npx lint-staged --concurrent false
配置 package.json 或 lint-staged.config.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lint-staged.config.js
export default {
// 对不同文件类型运行不同的检查
'*.{js,jsx,ts,tsx,vue}': [
'eslint --fix', // ESLint检查并修复
'prettier --write', // Prettier格式化
],
'*.{json,md,yaml,yml,css,scss}': [
'prettier --write', // 只格式化
],
'*.{png,jpg,jpeg,gif,svg}': [
'imagemin-lint-staged', // 图片压缩检查
],
};
配置Commitlint
1
2
3
4
5
6
7
8
9
10
11
12
13
// commitlint.config.js
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style',
'refactor', 'perf', 'test',
'build', 'ci', 'chore', 'revert',
]],
'subject-case': [0], // 不限制subject大小写
'subject-full-stop': [0], // 不要求全文结束
},
};
.husky/commit-msg:
1
2
3
4
5
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 校验commit message
npx --no -- commitlint --edit $1
核心知识点拆解
1. ESLint的扁平配置体系(Flat Config)
ESLint 9+从传统的 .eslintrc 层级配置转向了Flat Config。关键变化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 旧配置:.eslintrc.js(层级继承)
module.exports = {
extends: ['eslint:recommended'],
overrides: [{ files: ['*.ts'], parser: '@typescript-eslint/parser' }],
};
// 新配置:eslint.config.js(扁平数组,明确顺序)
export default [
js.configs.recommended,
{
rules: {
'no-console': 'error',
},
},
{ files: ['*.ts'], ...typescriptConfig },
{ ignores: ['dist/*'] },
];
Flat Config的优势:
- 顺序明确:数组顺序就是应用顺序,后续可以覆盖前面的规则
- 无继承复杂性:不再有
extends的”隐式继承”——每条配置都是显式的 - 根目录可并列:不同目录可以有独立的
eslint.config.js,不需要复杂的层级继承逻辑
2. Prettier的格式化算法原理
Prettier的核心是一个确定性算法,它用”最小成本”模型决定代码的换行方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 伪代码:Prettier的换行决策算法
function format(code, options) {
// 1. 将代码解析为AST(使用自定义解析器)
const ast = parser.parse(code);
// 2. 将AST转换为Doc(中间表示)
// Doc是一系列"打印指令"
const doc = toDoc(ast, {
// 例如:
// group(concat(["if (", indent(concat([softline, condition, softline])), ") {"]))
});
// 3. 将Doc输出为字符串
// 关键算法:找到最优的换行方案
return printDocToString(doc, {
printWidth: options.printWidth,
tabWidth: options.tabWidth,
});
}
// Doc类型:
// group - 如果一行放不下则换行
// indent - 嵌套增加缩进
// softline - 可选换行(只在group范围内换)
// hardline - 强制换行
// breakParent - 强制父级换行
Prettier的”确定性”意味着同样的代码 + 同样的配置 = 永远同样的输出。这消除了人类对格式化决策的争议。
3. Husky 9+ 的Hook机制
Husky 9+彻底重写了Hook管理机制,不再依赖 package.json 的 husky 字段,而是直接基于 .git/hooks 的符号链接:
1
2
3
4
5
6
7
8
9
10
# Husky的安装流程
# 1. 在 .husky/ 目录下创建钩子文件
# 2. Husky将 .husky/_/husky.sh 链接到 .git/hooks/
# 3. 当git触发hook时,执行对应的脚本
# .git/hooks/pre-commit (自动生成)
# 这是一个包装器,指向.husky/pre-commit
# 手动创建新的hook
npx husky add .husky/pre-push "npm run test"
Husky 9+ 比旧版本更轻量:它不再需要在 package.json 中声明 husky 属性,也没有运行时依赖,纯粹是文件操作。
4. lint-staged的多任务并行与失败处理
lint-staged 支持并行和串行两种模式:
1
2
3
4
5
6
7
8
9
10
11
12
// lint-staged.config.js - 高级配置
export default {
// 并行模式(默认):多个globs同时执行
'*.ts': ['eslint --fix', 'prettier --write'],
'*.css': ['stylelint --fix', 'prettier --write'],
// 这些命令是并行的,但每个文件组内部的命令是串行的
// 串行模式:使用 '&&' 连接
'*': ['some-command && another-command'],
// 可以用 --concurrent 控制
};
失败处理:如果ESLint失败(发现错误),lint-staged会阻止commit。如果只有警告,可以配置:
1
2
3
4
5
6
export default {
'*.ts': [
'eslint --max-warnings 50', // 最多允许50个警告
'prettier --write --check', // --check验证而非写入
],
};
实战案例
场景:大型企业级Monorepo的ESLint配置
在一个包含100+包的Monorepo中,需要对不同包设置不同的规则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 根目录 eslint.config.js(Monorepo适用)
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import globals from 'globals';
// 读取所有workspace包
const packages = fs.readdirSync(path.join(__dirname, 'packages'))
.filter(p => fs.statSync(path.join(__dirname, 'packages', p)).isDirectory());
export default [
// 全局配置
{
ignores: ['**/node_modules/**', '**/dist/**', '**/.next/**'],
},
// 服务器端代码
{
files: ['packages/server/**/*.ts'],
languageOptions: {
globals: { ...globals.node },
},
rules: {
'no-console': 'off', // 服务端允许console日志
'no-process-exit': 'warn',
'@typescript-eslint/no-require-imports': 'error',
},
},
// 浏览器端代码
{
files: ['packages/web/**/*.{ts,tsx}'],
languageOptions: {
globals: { ...globals.browser },
},
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'react/react-in-jsx-scope': 'off',
},
},
// 测试文件 - 宽松规则
{
files: ['**/*.{test,spec}.{ts,tsx}'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'max-lines': 'off',
'max-lines-per-function': 'off',
},
},
// 每个包的自定义配置
...packages.map(pkgName => ({
files: [`packages/${pkgName}/src/**/*.{ts,tsx}`],
rules: getPackageSpecificRules(pkgName),
})),
];
function getPackageSpecificRules(pkgName) {
const pkgPath = path.join(__dirname, 'packages', pkgName, 'eslint.override.js');
if (fs.existsSync(pkgPath)) {
const override = require(pkgPath);
return override.rules || {};
}
return {};
}
场景:CI流水线中的代码质量门禁
在GitHub Actions中,将代码规范检查作为CI的必经门槛:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# .github/workflows/code-quality.yml
name: Code Quality Check
on:
pull_request:
branches: [main, develop]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取所有提交记录
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
# ESLint检查
- name: ESLint
run: npx eslint src/ --max-warnings 0
# --max-warnings 0: 任何警告都会被视为失败
# Prettier检查(--check模式不修改文件)
- name: Prettier Check
run: npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,scss}"
# TypeScript类型检查(如果使用了ts)
- name: TypeScript Check
run: npx tsc --noEmit
场景:自定义eslint规则——禁止魔数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// eslint-local-rules/no-magic-numbers.js
// 项目中经常有直接使用数字常量的问题,如: if (status === 3)——
// 这段代码没人知道3代表什么
export default {
meta: {
type: 'suggestion',
docs: {
description: '禁止魔数(未命名的数字常量)',
},
schema: [
{
type: 'object',
properties: {
allowedNumbers: {
type: 'array',
items: { type: 'number' },
},
},
additionalProperties: false,
},
],
},
create(context) {
const allowedNumbers = new Set(context.options[0]?.allowedNumbers || [0, 1]);
return {
Literal(node) {
if (typeof node.value !== 'number') return;
// 允许已知的安全数字
if (allowedNumbers.has(node.value)) return;
// 允许默认值、数组索引等场景
const parent = node.parent;
if (parent.type === 'VariableDeclarator') return; // const x = 3;
if (parent.type === 'Property') return; // const obj = { value: 3 }
if (parent.type === 'SwitchCase') return; // case 3:
if (parent.type === 'ArrayExpression') return; // [1, 2, 3]
// 检查是否在二元表达式中
if (parent.type === 'BinaryExpression') {
const sibling = node === parent.left ? parent.right : parent.left;
if (sibling.type === 'Identifier') {
// status === 3 这种可以快速判断是未命名的魔数
context.report({
node,
message: `发现魔数 ${node.value}。建议定义为有意义的常量。`,
suggest: [
{
desc: '提取为变量',
fix(fixer) {
const sourceCode = context.getSourceCode();
const scope = sourceCode.getScope(node);
const existing = scope.variables.find(
v => v.defs[0]?.node?.init?.value === node.value
);
if (existing) {
return fixer.replaceText(node, existing.name);
}
return null;
},
},
],
});
}
}
},
};
},
};
底层原理
ESLint的AST遍历与Rule执行
ESLint内部使用 eslint-scope 进行作用域分析和 espree(基于acorn)进行解析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ESLint Linter的核心流程(源码简化)
class Linter {
verify(code, config) {
// 1. 解析代码
const ast = this.parse(code);
// 2. 创建规则数组
const rules = this.getRules(config);
// 3. 创建SourceCode对象(包含AST)
const sourceCode = new SourceCode(code, ast);
// 4. 使用Traverser遍历AST
const traverser = new Traverser();
traverser.traverse(sourceCode.ast, {
// 对于每种节点类型,检查所有规则
enter(node) {
const type = node.type;
rules.forEach(rule => {
if (rule.visitor[type]?.enter) {
rule.visitor[type].enter(node, {
report: (problem) => problems.push(problem),
getSourceCode: () => sourceCode,
options: rule.options,
});
}
});
},
});
return problems;
}
}
Prettier的Doc IR(中间表示)
Prettier的核心学术贡献在于它的”Doc IR”理论,基于Philip Wadler的论文”A prettier printer”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Prettier内部Doc类型
// Doc是一个代数数据类型(ADT)
type Doc =
| { type: 'string', value: string }
| { type: 'concat', parts: Doc[] }
| { type: 'group', contents: Doc, break: boolean }
| { type: 'if-break', breakContents: Doc, flatContents: Doc }
| { type: 'indent', contents: Doc }
| { type: 'line', soft: boolean, hard: boolean, literal: boolean }
| { type: 'line-suffix-boundary' }
| { type: 'line-suffix', contents: Doc }
| { type: 'break-parent' };
// 格式化算法:寻找最佳换行方案
function fits(size, doc, options) {
// 判断doc在指定宽度内能否"fit"
if (doc.type === 'group' && doc.shouldBreak) {
return false; // 强制换行的组无论如何都不fit
}
// 递归判断...
}
function formatDoc(doc, options) {
// 对于group,尝试两种方案:
// 1. 平铺模式(不换行)
// 2. 展开模式(换行)
// 选择先尝试平铺,如果超宽则选展开
if (doc.type === 'group') {
if (fits(options.printWidth, doc.flat, options)) {
return formatDoc(doc.flat, options);
} else {
doc.shouldBreak = true;
return formatDoc(doc.break, options);
}
}
}
Husky的Git Hook符号链接机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Husky的工作原理
# .git/hooks/ 目录下的每个hook文件被替换为一个符号链接
# 指向 .husky/_/husky.sh
# 当git触发 pre-commit 时:
# 1. git执行 .git/hooks/pre-commit
# 2. 该文件是一个shell脚本,加载了 husky 的运行时
# 3. 运行时查找 .husky/pre-commit 文件
# 4. 如果存在,执行它
# .husky/_/husky.sh (简化)
husky() {
local hook="$(basename "$0")"
local hookPath="${HUSKY_PATH:-.husky}/$hook"
if [ -f "$hookPath" ]; then
echo "husky - $hook hook..."
sh "$hookPath"
fi
}
husky
这个设计的巧妙之处在于:钩子文件在仓库版本控制中(.husky/ 目录),而 .git/hooks/ 不提交。开发者 clone 仓库后运行 npm install,Husky的install脚本会自动建立符号链接。
高频面试题解析
面试题1:ESLint和Prettier的冲突如何处理?为什么会产生冲突?
答案要点:
冲突的根本原因是:ESLint同时关心代码质量和代码风格,而Prettier只关心代码风格。两者在风格规则上可能重叠但规则不同。
例如,ESLint的 max-len 规则默认 printWidth: 80,而Prettier如果配置 printWidth: 120,就会产生冲突。更典型的冲突是分号:
1
2
3
4
5
// ESLint 要求无分号(semi: never)
const a = 1
// Prettier 默认加分号
const a = 1;
解决方案:
- 配置
eslint-config-prettier:关闭ESLint中所有与Prettier冲突的规则 - 在ESLint中使用
prettier插件:将Prettier作为ESLint的一条规则运行(但不推荐,因为Prettier在ESLint中运行时效率较低) - 最佳实践:ESLint只做代码质量检查,不做格式化,格式化全部交给Prettier:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default [
// 只启用质量相关的规则
js.configs.recommended,
// 关闭所有可能和Prettier冲突的样式规则
eslintConfigPrettier,
// 自定义质量规则
{
rules: {
'no-unused-vars': 'error', // ✅ 质量规则
'no-undef': 'error', // ✅ 质量规则
'no-extra-boolean-cast': 'warn', // ✅ 质量规则
// ❌ 不要加 'max-len', 'indent', 'quotes', 'comma-dangle' 这些
},
},
];
面试题2:lint-staged的原理是什么?为什么要用它而不是直接跑全量检查?
答案要点:
lint-staged 的核心原理是:读取 git diff --staged --name-only 的输出,只对暂存区有变更的文件运行指定命令。
为什么不用全量检查? 假设一个项目有1000个文件,其中只有3个文件在当前commit中被修改了。如果跑全量ESLint检查:
- 1000个文件 → 可能需要30秒
- 如果第501号文件有历史遗留的lint错误,会阻止commit(但这些错误不是本次修改引入的)
用 lint-staged 的话:
- 只检查3个修改的文件 → 1秒
- 不会因为其他文件的遗留问题而阻塞commit
进阶用法:
1
2
3
4
5
6
7
8
9
10
export default {
'*.ts': (stagedFiles) => {
// stagedFiles 是当前暂存区中所有匹配的文件路径数组
// 可以动态生成命令行
return [
`eslint --fix ${stagedFiles.join(' ')}`,
`prettier --write ${stagedFiles.join(' ')}`,
];
},
};
面试题3:在你的Monorepo项目中,如何管理多个包的ESLint配置,确保各包有独立的规则但基线与统一?
答案要点:
在Monorepo中管理ESLint配置有三种策略:
策略1:根目录单配置(简单但不够灵活)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 根目录 eslint.config.js
export default [
// 统一配置适用于所有包
{
files: ['packages/*/src/**/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
},
},
// 针对特定包覆写
{
files: ['packages/legacy/src/**/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off', // 遗留代码允许any
},
},
];
策略2:每个包独立配置(灵活但分散)
每个包的目录下有自己的 eslint.config.js,ESLint自动检测:
1
2
3
4
5
6
7
8
9
project/
├── eslint.config.js # 根(仅基础规则)
├── packages/
│ ├── server/
│ │ ├── eslint.config.js # 继承根 + 覆写
│ │ └── ...
│ └── web/
│ ├── eslint.config.js
│ └── ...
策略3(推荐):共享配置包 + 各包覆写
创建 packages/eslint-config-base/ 作为共享配置:
1
2
3
4
5
6
7
8
// packages/eslint-config-base/index.js
export default {
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'eqeqeq': 'error',
},
};
各包引入后覆写:
1
2
3
4
5
6
7
8
9
10
11
// packages/server/eslint.config.js
import baseConfig from 'eslint-config-base';
export default [
...baseConfig,
{
rules: {
'no-console': 'off', // 服务端需要console
},
},
];
CI统一校验:在CI中强制所有包必须通过ESLint检查,无论各包的配置如何:
1
2
3
4
5
6
7
8
# CI流程
- name: Lint All Packages
run: |
for dir in packages/*/ ; do
echo "Linting $dir"
cd $dir && npx eslint . --max-warnings 0
cd ../..
done
总结与扩展
代码规范工具的工程化实践可以总结为“三线防御”:
- 编辑器防线:VS Code扩展(ESLint + Prettier)提供实时反馈和自动格式化——开发者感知最轻
- Git Hook防线:Husky + lint-staged 在commit前做”最后一次检查”——防止不规范代码进入仓库
- CI防线:CI流水线中的全量检查——防止任何绕过Hook的情况
未来趋势
- Rust/Go重写:
oxlint(Oxidation Compiler)、biome(前Rome团队新作)正用Rust重写Linter,性能提升100倍 - AI辅助规范:Copilot等AI工具正在学习的”团队编码风格”,未来可能替代手动配置规则
- 配置即规则:Flat Config的普及使得ESLint配置更加透明和可组合
学习路径
- 先从配置现有工具开始,理解每个配置项的含义
- 尝试写一个自定义ESLint规则(这是理解AST的最佳实践)
- 阅读
eslint-plugin-import的源码,理解如何实现复杂的跨文件规则 - 学习
biome和oxlint的架构,理解原生编译器的性能优势从何而来
代码规范不是限制自由,而是用工具的自动化约束,换取团队协作的自由。