跳到主要内容
预计阅读 41 分钟

工程化篇 | 格式化与Lint

前言

代码风格之争,可能是程序员世界里最”无聊”却最频繁的争论——Tab 还是 Space?单引号还是双引号?分号要不要?每次 Code Review 一半的评论都在讨论格式问题。

解决这个问题的方法很简单:交给工具。

ESLint 管代码质量,Prettier 管代码格式,Stylelint 管 CSS 规范,husky + lint-staged 保证每次提交都经过检查。这套组合拳在现代前端项目中几乎是标配。

但面试中被问到这些工具,很多人的回答停留在”知道有这些工具”的层面。面试官继续追问:

  • ESLint 是怎么工作的?它是怎么”看懂”你的代码并发现问题的?
  • Prettier 和 ESLint 都能格式化代码,它们之间怎么分工?会不会冲突?
  • husky 和 lint-staged 各自做什么?为什么需要两个工具配合?
  • EditorConfig 和 Prettier 有什么区别?

很多人就含糊了。

本章,我们从 ESLint 的工作原理(AST 解析 → 规则匹配)讲起,理清 Prettier 与 ESLint 的分工,再聊 Stylelint 和 Git Hooks 方案。


诊断自测

Q1:ESLint 是怎么发现你代码中的问题的?它的内部工作流程是什么?

点击查看答案

ESLint 的工作流程分三步:

  1. 解析(Parse):用解析器(默认 Espree,也可以用 @typescript-eslint/parser 等)把源代码解析成抽象语法树(AST)
  2. 遍历 + 规则匹配(Traverse + Lint):遍历 AST 的每个节点,把每个节点交给已启用的规则去检查。如果规则发现问题,就报告一个 lint 错误
  3. 修复(Fix):如果规则提供了自动修复逻辑(fixer),ESLint 可以自动修改源代码

所以 ESLint 不是用正则表达式”看”你的代码,而是通过 AST 理解代码的结构,然后用规则去匹配特定的模式。

Q2:ESLint 和 Prettier 都能格式化代码,它们有什么区别?如果配置冲突了怎么办?

点击查看答案

ESLint 主要关注代码质量——未使用的变量、未声明的变量、潜在的 bug(如 == 而不是 ===)等。虽然 ESLint 也有一些格式化规则(如缩进、分号),但它的强项不在于格式化。

Prettier 专注于代码格式化——缩进、换行、引号风格、括号位置等。它是”固执的”(opinionated),刻意减少配置选项,让团队不用再争论格式。

如果两者的格式化规则冲突,用 eslint-config-prettier 关闭 ESLint 中与 Prettier 冲突的格式化规则,让 Prettier 全权负责格式化,ESLint 专注于代码质量。

Q3:huskylint-staged 分别做什么?为什么两个都需要?

点击查看答案
  • husky:让你可以方便地在 Git Hooks(如 pre-commitcommit-msg)上执行自定义脚本
  • lint-staged只对 Git 暂存区(staged files)中的文件运行 lint/格式化命令

为什么两个都需要?husky 负责”在什么时机执行”(commit 前),lint-staged 负责”对哪些文件执行”(只检查改动的文件,而不是整个项目)。如果没有 lint-staged,每次 commit 都跑全项目的 lint,大项目会非常慢。


一、ESLint:代码质量的守护者

1.1 工作原理

ESLint 的核心工作流程可以用一张图概括:

源代码 → [Parser 解析] → AST → [Rules 遍历检查] → 错误报告 → [Fixer 修复]

第一步:解析为 AST

AST(Abstract Syntax Tree,抽象语法树)是代码的结构化表示。比如这行代码:

const x = 1 + 2;

会被解析成类似这样的 AST:

{
  "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 }
    }
  }]
}

ESLint 默认使用 Espree 解析器。如果你的代码是 TypeScript,需要换成 @typescript-eslint/parser;如果是 Vue 的 SFC 文件,需要用 vue-eslint-parser

第二步:规则匹配

每条 ESLint 规则本质上就是一个函数,它接收 AST 节点并检查是否符合预期。例如 no-unused-vars 规则会遍历所有的变量声明节点,检查每个变量是否在后续代码中被引用过。

// 简化版的规则实现
module.exports = {
  create(context) {
    return {
      // 当遍历到 VariableDeclarator 节点时触发
      VariableDeclarator(node) {
        const varName = node.id.name;
        // 检查这个变量是否被使用过
        const scope = context.getScope();
        const variable = scope.variables.find(v => v.name === varName);
        if (variable && variable.references.length === 0) {
          context.report({
            node,
            message: `'${varName}' is defined but never used.`
          });
        }
      }
    };
  }
};

第三步:修复

有些规则提供了自动修复逻辑。运行 eslint --fix 时,ESLint 会应用这些修复。

context.report({
  node,
  message: 'Use === instead of ==',
  fix(fixer) {
    return fixer.replaceText(node, '===');
  }
});

1.2 配置方式

ESLint 9.x 引入了扁平配置(Flat Config),使用 eslint.config.js

// eslint.config.js (ESLint 9+ 扁平配置)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['**/*.{ts,tsx}'],
    rules: {
      'no-unused-vars': 'warn',
      'no-console': ['error', { allow: ['warn', 'error'] }],
      '@typescript-eslint/no-explicit-any': 'warn'
    }
  },
  {
    ignores: ['dist/**', 'node_modules/**']
  }
];

传统的 .eslintrc.* 配置方式(ESLint 8 及之前):

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "rules": {
    "no-unused-vars": "warn",
    "no-console": "error"
  }
}

1.3 规则的三种级别

级别含义
off0关闭规则
warn1警告(不影响退出码)
error2错误(退出码为 1,CI 会失败)

二、Prettier:固执的格式化器

2.1 设计哲学

Prettier 的核心理念是**“少配置,多统一”**。它故意减少可配置的选项(只有十几个),让团队没办法争论格式问题——因为没得选。

Prettier 的格式化是全量重写的——它不是在你现有代码上做微调,而是完全按照自己的规则重新输出代码。所以无论你的代码原来是什么样,经过 Prettier 处理后都是统一的风格。

2.2 支持的语言

Prettier 不仅仅是 JavaScript 格式化器,它支持:

  • JavaScript / TypeScript
  • JSX / TSX
  • CSS / SCSS / Less
  • HTML / Vue / Angular
  • JSON / YAML / Markdown
  • GraphQL

2.3 配置

// .prettierrc
{
  "semi": true,           // 行尾加分号
  "singleQuote": true,    // 使用单引号
  "tabWidth": 2,          // 缩进 2 个空格
  "trailingComma": "es5", // 尾随逗号
  "printWidth": 80,       // 每行最大 80 字符
  "arrowParens": "always" // 箭头函数参数总是加括号
}

.prettierignore 文件指定忽略的文件:

dist
node_modules
pnpm-lock.yaml

2.4 与 ESLint 的关系

ESLint 和 Prettier 都能格式化代码,但它们的关注点不同:

维度ESLintPrettier
关注点代码质量(bug、最佳实践)代码格式(缩进、换行、引号)
工作方式AST 分析 + 规则匹配全量重新格式化
灵活性高度可配置刻意减少配置
自动修复部分规则支持全部可自动修复

最佳实践:让两者各司其职。

npm install -D eslint-config-prettier

eslint-config-prettier 的作用是关闭 ESLint 中所有与 Prettier 冲突的格式化规则。这样 ESLint 只管代码质量,Prettier 只管格式,互不干扰。

// eslint.config.js
import prettier from 'eslint-config-prettier';

export default [
  // ... 其他配置
  prettier  // 放在最后,关闭冲突规则
];

三、Stylelint:CSS 的 ESLint

3.1 为什么需要 Stylelint?

ESLint 管 JS,Prettier 管格式,但 CSS 呢?CSS 也有自己的代码质量问题:

  • 使用了浏览器不支持的属性
  • 选择器权重过高
  • 重复的属性声明
  • 颜色值不统一(有的用 hex,有的用 rgb)
  • 属性书写顺序混乱

Stylelint 就是 CSS 世界的 ESLint。

3.2 基本配置

// .stylelintrc.json
{
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-css-modules"
  ],
  "rules": {
    "color-hex-length": "short",
    "selector-class-pattern": "^[a-z][a-zA-Z0-9]+$",
    "declaration-block-no-duplicate-properties": true,
    "no-descending-specificity": true
  }
}

3.3 与 Prettier 的配合

和 ESLint 一样,Stylelint 的格式化规则也可能和 Prettier 冲突。用 stylelint-config-prettier 解决:

npm install -D stylelint-config-prettier
{
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-prettier"   // 放在最后
  ]
}

四、husky + lint-staged:Git Hooks 自动化

工具再好,如果开发者忘了跑或者偷懒不跑,等于白搭。所以我们需要在 Git 提交的时候自动执行检查——这就是 husky + lint-staged 的组合。

4.1 Git Hooks 是什么?

Git 提供了一系列 Hooks(钩子),让你在特定的 Git 操作前后执行自定义脚本:

Hook触发时机常见用途
pre-commitgit commit 之前跑 lint 和格式化
commit-msg提交信息写完之后检查 Commit Message 格式
pre-pushgit push 之前跑测试

4.2 husky:管理 Git Hooks

husky 让你可以方便地在项目中配置 Git Hooks:

# 安装
npm install -D husky

# 初始化
npx husky init

# 添加 pre-commit hook
echo "npx lint-staged" > .husky/pre-commit

4.3 lint-staged:只检查改动的文件

如果每次 commit 都对整个项目跑 ESLint + Prettier,大项目可能要等好几分钟。lint-staged 只对 Git 暂存区(staged)中的文件 运行检查。

// package.json
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss}": [
      "stylelint --fix",
      "prettier --write"
    ],
    "*.{json,md,yaml}": [
      "prettier --write"
    ]
  }
}

4.4 完整工作流

开发者修改代码 → git add → git commit

                        pre-commit hook 触发

                        lint-staged 启动

                    只对暂存区的文件执行:
                    - JS/TS: ESLint --fix + Prettier --write
                    - CSS:   Stylelint --fix + Prettier --write
                    - JSON:  Prettier --write

                    检查通过 → 提交成功
                    检查失败 → 提交被阻止,开发者需要修复问题

4.5 commit-msg Hook

配合 commitlint 检查提交信息格式:

# .husky/commit-msg
npx commitlint --edit $1

这样不符合 Conventional Commits 规范的提交信息会被阻止。


五、EditorConfig:编辑器级别的统一

5.1 它解决什么问题?

团队成员可能用不同的编辑器(VS Code、WebStorm、Vim),每个编辑器的默认设置不同。EditorConfig 在编辑器层面统一基础的代码风格。

5.2 配置

# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

5.3 EditorConfig vs Prettier

维度EditorConfigPrettier
工作时机编辑时(输入即生效)保存/格式化时
覆盖范围基础设置(缩进、换行符、字符集)完整的代码格式化
编辑器支持几乎所有编辑器原生支持或有插件需要编辑器插件

两者不冲突。EditorConfig 保证你在输入代码的那一刻就是正确的缩进和换行符,Prettier 在保存或提交时做完整的格式化。建议两者都配置。


六、完整方案:从零搭建

把上面所有工具串起来,一个现代前端项目的代码质量保障方案是:

# 1. 安装所有工具
npm install -D eslint prettier stylelint \
  eslint-config-prettier \
  stylelint-config-standard stylelint-config-prettier \
  husky lint-staged \
  @commitlint/cli @commitlint/config-conventional

# 2. 初始化 husky
npx husky init

# 3. 配置 pre-commit hook
echo "npx lint-staged" > .husky/pre-commit

# 4. 配置 commit-msg hook
echo "npx commitlint --edit \$1" > .husky/commit-msg

配置文件一览:

项目根目录/
├── eslint.config.js         ← ESLint 配置
├── .prettierrc              ← Prettier 配置
├── .prettierignore          ← Prettier 忽略文件
├── .stylelintrc.json        ← Stylelint 配置
├── .editorconfig            ← EditorConfig 配置
├── commitlint.config.js     ← commitlint 配置
├── .husky/
│   ├── pre-commit           ← 执行 lint-staged
│   └── commit-msg           ← 执行 commitlint
└── package.json             ← lint-staged 配置

常见误区

误区一:“ESLint 能格式化代码,不需要 Prettier”

ESLint 确实有格式化规则(如 indentquotes),但它的格式化能力远不如 Prettier 完善。ESLint 的格式化规则只覆盖了部分场景,而且对于复杂的换行策略(比如链式调用怎么换行、对象字面量怎么格式化)处理得不够好。更重要的是,ESLint 团队在 v8.53.0 之后已经弃用了所有格式化规则,明确建议用 Prettier 替代。

误区二:“lint-staged 会修改我没有改动的代码”

不会。lint-staged 只处理 Git 暂存区中的文件——也就是你 git add 过的文件。如果它执行了 eslint --fixprettier --write,修改的也只是你已经改动过的文件。不过要注意,修复后的改动会自动添加回暂存区。

误区三:“有了 CI 上的 lint 检查,本地就不需要 husky + lint-staged 了”

理论上 CI 可以兜底,但问题是:如果你等到 push 到远端、CI 跑完才发现 lint 不通过,你还得改代码、重新 commit、重新 push、重新等 CI。本地 pre-commit hook 可以在提交的那一刻就拦截问题,反馈周期从几分钟缩短到几秒钟。两者不是替代关系,而是双保险

误区四:“EditorConfig 和 Prettier 功能重复,用一个就行”

它们工作在不同的时机。EditorConfig 在你敲键盘的时候就确保了缩进和换行符是对的(编辑器实时应用),而 Prettier 在保存或格式化时才会执行。如果没有 EditorConfig,新文件在被 Prettier 格式化之前可能有错误的缩进。两者配合使用是最佳实践。


小结

本章我们梳理了前端代码质量和格式化的完整工具链:ESLint 管代码质量,Prettier 管格式化,Stylelint 管 CSS,husky + lint-staged 在 Git 提交时自动化执行检查,EditorConfig 在编辑器层面统一基础配置。

核心要点

  1. ESLint 原理:源码 → AST → 规则遍历检查 → 报告/修复
  2. Prettier 的定位:固执的格式化器,不管代码质量,只管格式统一
  3. ESLint + Prettier 分工:用 eslint-config-prettier 关闭冲突规则
  4. husky:管理 Git Hooks,在 commit/push 时触发脚本
  5. lint-staged:只对暂存区文件执行检查,提升效率
  6. EditorConfig:编辑器层面的基础配置统一

本章思维导图

格式化与 Lint
  • ESLint
    • 工作原理:源码 → Parser → AST → Rules → 报告/修复
    • 关注点:代码质量(bug、最佳实践)
    • 配置:eslint.config.js / .eslintrc.*
    • 规则级别:off / warn / error
  • Prettier
    • 设计哲学:少配置、多统一、全量重写
    • 关注点:代码格式(缩进、换行、引号)
    • 与 ESLint 的关系:eslint-config-prettier 消除冲突
  • Stylelint
    • CSS 的 ESLint
    • 与 Prettier 配合:stylelint-config-prettier
  • Git Hooks 方案
    • husky:管理 Git Hooks
    • lint-staged:只检查暂存区文件
    • commitlint:检查 Commit Message 格式
    • 工作流:commit → pre-commit → lint-staged → 通过/拦截
  • EditorConfig
    • 编辑器层面的基础配置
    • 与 Prettier 互补(编辑时 vs 保存时)
  • 完整方案
    • ESLint + Prettier + Stylelint + EditorConfig
    • husky + lint-staged + commitlint
    • 本地检查 + CI 双保险

练习挑战

第一题 ⭐(基础):工具选择

以下场景分别应该用哪个工具解决?

  1. 发现代码中有一个变量声明了但从未使用
  2. 团队有人用 Tab 缩进有人用 Space 缩进
  3. CSS 中有重复的属性声明
  4. 提交的 Commit Message 是 “fix bug”,不符合规范
点击查看答案与解析
  1. ESLintno-unused-vars 规则)—— 这是代码质量问题
  2. Prettier(统一格式化)+ EditorConfig(编辑器层面统一)—— 这是格式问题
  3. Stylelintdeclaration-block-no-duplicate-properties 规则)—— 这是 CSS 质量问题
  4. commitlint(配合 husky 的 commit-msg hook)—— 这是提交规范问题

第二题 ⭐⭐(进阶):排查冲突

团队反馈:每次保存文件时,VS Code 先用 Prettier 格式化了一遍,然后 ESLint 又报了一堆格式错误,自动修复后代码变回了 Prettier 之前的样子。下次保存又重复这个过程,陷入死循环。请分析原因并提出解决方案。

点击查看答案与解析

原因: ESLint 的格式化规则和 Prettier 的规则冲突了。Prettier 按自己的规则格式化代码,ESLint 认为 Prettier 的格式化结果违反了 ESLint 的格式化规则,又把代码改回去。

典型的冲突例子:

  • Prettier 设置了 semi: true(加分号),ESLint 的 semi 规则设置了 never(不加分号)
  • Prettier 设置了 singleQuote: true,ESLint 的 quotes 规则设置了 double

解决方案:

  1. 安装 eslint-config-prettier
npm install -D eslint-config-prettier
  1. 在 ESLint 配置中引入(放在最后):
// eslint.config.js
import prettier from 'eslint-config-prettier';

export default [
  // ... 其他配置
  prettier  // 放在最后,关闭所有和 Prettier 冲突的 ESLint 规则
];
  1. 确保 VS Code 中的格式化器设置为 Prettier(而不是 ESLint):
// .vscode/settings.json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true
}

核心原则:格式化的事全部交给 Prettier,ESLint 只管代码质量。

第三题 ⭐⭐⭐(综合):从零配置完整方案

请为一个使用 React + TypeScript 的新项目配置完整的代码质量方案。写出:

  1. 需要安装的 npm 包(列出包名)
  2. eslint.config.js 的核心配置
  3. lint-staged 的配置
  4. husky 的 Hook 配置
点击查看答案与解析

一、安装依赖

npm install -D \
  eslint \
  @eslint/js \
  typescript-eslint \
  eslint-plugin-react-hooks \
  eslint-config-prettier \
  prettier \
  husky \
  lint-staged \
  @commitlint/cli \
  @commitlint/config-conventional

二、eslint.config.js

import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactHooks from 'eslint-plugin-react-hooks';
import prettier from 'eslint-config-prettier';

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['**/*.{ts,tsx}'],
    plugins: {
      'react-hooks': reactHooks,
    },
    rules: {
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
      'no-unused-vars': 'off',
      '@typescript-eslint/no-unused-vars': 'warn',
    },
  },
  {
    ignores: ['dist/**', 'node_modules/**'],
  },
  prettier, // 最后放,关闭冲突的格式化规则
];

三、lint-staged 配置(package.json)

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss}": [
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

四、husky 配置

# 初始化 husky
npx husky init

# pre-commit hook
echo "npx lint-staged" > .husky/pre-commit

# commit-msg hook
echo "npx commitlint --edit \$1" > .husky/commit-msg

# commitlint 配置
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

五、.prettierrc

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 80
}

这样配置后,每次 git commit

  1. pre-commit hook 触发 lint-staged
  2. lint-staged 对暂存区的 TS/TSX 文件跑 ESLint + Prettier
  3. commit-msg hook 触发 commitlint 检查提交信息格式
  4. 所有检查通过后才允许提交

自我检测

  • 能描述 ESLint 的工作原理(源码 → AST → 规则匹配 → 修复)
  • 能解释 Prettier 和 ESLint 的定位差异,以及如何用 eslint-config-prettier 消除冲突
  • 能说出 Stylelint 的作用和常见规则
  • 能解释 husky 和 lint-staged 各自的职责以及为什么要配合使用
  • 能配置一个完整的 pre-commit + commit-msg 检查流程
  • 能区分 EditorConfig 和 Prettier 的工作时机和覆盖范围
  • 能解释为什么本地 lint 和 CI lint 是”双保险”而不是”二选一”
  • 能为一个新项目从零搭建完整的代码质量保障方案

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90