文章

代码规范工具深度解析:ESLint、Prettier与Husky的工程化实践

代码规范工具深度解析: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-commitpre-pushcommit-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.jsonlint-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.jsonhusky 字段,而是直接基于 .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;

解决方案

  1. 配置 eslint-config-prettier:关闭ESLint中所有与Prettier冲突的规则
  2. 在ESLint中使用 prettier 插件:将Prettier作为ESLint的一条规则运行(但不推荐,因为Prettier在ESLint中运行时效率较低)
  3. 最佳实践: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

总结与扩展

代码规范工具的工程化实践可以总结为“三线防御”

  1. 编辑器防线:VS Code扩展(ESLint + Prettier)提供实时反馈和自动格式化——开发者感知最轻
  2. Git Hook防线:Husky + lint-staged 在commit前做”最后一次检查”——防止不规范代码进入仓库
  3. CI防线:CI流水线中的全量检查——防止任何绕过Hook的情况

未来趋势

  • Rust/Go重写oxlint(Oxidation Compiler)、biome(前Rome团队新作)正用Rust重写Linter,性能提升100倍
  • AI辅助规范:Copilot等AI工具正在学习的”团队编码风格”,未来可能替代手动配置规则
  • 配置即规则:Flat Config的普及使得ESLint配置更加透明和可组合

学习路径

  1. 先从配置现有工具开始,理解每个配置项的含义
  2. 尝试写一个自定义ESLint规则(这是理解AST的最佳实践)
  3. 阅读 eslint-plugin-import 的源码,理解如何实现复杂的跨文件规则
  4. 学习 biomeoxlint 的架构,理解原生编译器的性能优势从何而来

代码规范不是限制自由,而是用工具的自动化约束,换取团队协作的自由

本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量