工程化篇 | 格式化与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 的工作流程分三步:
- 解析(Parse):用解析器(默认 Espree,也可以用 @typescript-eslint/parser 等)把源代码解析成抽象语法树(AST)
- 遍历 + 规则匹配(Traverse + Lint):遍历 AST 的每个节点,把每个节点交给已启用的规则去检查。如果规则发现问题,就报告一个 lint 错误
- 修复(Fix):如果规则提供了自动修复逻辑(fixer),ESLint 可以自动修改源代码
所以 ESLint 不是用正则表达式”看”你的代码,而是通过 AST 理解代码的结构,然后用规则去匹配特定的模式。
Q2:ESLint 和 Prettier 都能格式化代码,它们有什么区别?如果配置冲突了怎么办?
点击查看答案
ESLint 主要关注代码质量——未使用的变量、未声明的变量、潜在的 bug(如 == 而不是 ===)等。虽然 ESLint 也有一些格式化规则(如缩进、分号),但它的强项不在于格式化。
Prettier 专注于代码格式化——缩进、换行、引号风格、括号位置等。它是”固执的”(opinionated),刻意减少配置选项,让团队不用再争论格式。
如果两者的格式化规则冲突,用 eslint-config-prettier 关闭 ESLint 中与 Prettier 冲突的格式化规则,让 Prettier 全权负责格式化,ESLint 专注于代码质量。
Q3:husky 和 lint-staged 分别做什么?为什么两个都需要?
点击查看答案
- husky:让你可以方便地在 Git Hooks(如
pre-commit、commit-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 规则的三种级别
| 级别 | 值 | 含义 |
|---|---|---|
| off | 0 | 关闭规则 |
| warn | 1 | 警告(不影响退出码) |
| error | 2 | 错误(退出码为 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 都能格式化代码,但它们的关注点不同:
| 维度 | ESLint | Prettier |
|---|---|---|
| 关注点 | 代码质量(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-commit | git commit 之前 | 跑 lint 和格式化 |
commit-msg | 提交信息写完之后 | 检查 Commit Message 格式 |
pre-push | git 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
| 维度 | EditorConfig | Prettier |
|---|---|---|
| 工作时机 | 编辑时(输入即生效) | 保存/格式化时 |
| 覆盖范围 | 基础设置(缩进、换行符、字符集) | 完整的代码格式化 |
| 编辑器支持 | 几乎所有编辑器原生支持或有插件 | 需要编辑器插件 |
两者不冲突。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 确实有格式化规则(如 indent、quotes),但它的格式化能力远不如 Prettier 完善。ESLint 的格式化规则只覆盖了部分场景,而且对于复杂的换行策略(比如链式调用怎么换行、对象字面量怎么格式化)处理得不够好。更重要的是,ESLint 团队在 v8.53.0 之后已经弃用了所有格式化规则,明确建议用 Prettier 替代。
误区二:“lint-staged 会修改我没有改动的代码”
不会。lint-staged 只处理 Git 暂存区中的文件——也就是你 git add 过的文件。如果它执行了 eslint --fix 或 prettier --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 在编辑器层面统一基础配置。
核心要点
- ESLint 原理:源码 → AST → 规则遍历检查 → 报告/修复
- Prettier 的定位:固执的格式化器,不管代码质量,只管格式统一
- ESLint + Prettier 分工:用
eslint-config-prettier关闭冲突规则 - husky:管理 Git Hooks,在 commit/push 时触发脚本
- lint-staged:只对暂存区文件执行检查,提升效率
- EditorConfig:编辑器层面的基础配置统一
本章思维导图
- 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 双保险
练习挑战
第一题 ⭐(基础):工具选择
以下场景分别应该用哪个工具解决?
- 发现代码中有一个变量声明了但从未使用
- 团队有人用 Tab 缩进有人用 Space 缩进
- CSS 中有重复的属性声明
- 提交的 Commit Message 是 “fix bug”,不符合规范
点击查看答案与解析
- ESLint(
no-unused-vars规则)—— 这是代码质量问题 - Prettier(统一格式化)+ EditorConfig(编辑器层面统一)—— 这是格式问题
- Stylelint(
declaration-block-no-duplicate-properties规则)—— 这是 CSS 质量问题 - commitlint(配合 husky 的
commit-msghook)—— 这是提交规范问题
第二题 ⭐⭐(进阶):排查冲突
团队反馈:每次保存文件时,VS Code 先用 Prettier 格式化了一遍,然后 ESLint 又报了一堆格式错误,自动修复后代码变回了 Prettier 之前的样子。下次保存又重复这个过程,陷入死循环。请分析原因并提出解决方案。
点击查看答案与解析
原因: ESLint 的格式化规则和 Prettier 的规则冲突了。Prettier 按自己的规则格式化代码,ESLint 认为 Prettier 的格式化结果违反了 ESLint 的格式化规则,又把代码改回去。
典型的冲突例子:
- Prettier 设置了
semi: true(加分号),ESLint 的semi规则设置了never(不加分号) - Prettier 设置了
singleQuote: true,ESLint 的quotes规则设置了double
解决方案:
- 安装
eslint-config-prettier:
npm install -D eslint-config-prettier
- 在 ESLint 配置中引入(放在最后):
// eslint.config.js
import prettier from 'eslint-config-prettier';
export default [
// ... 其他配置
prettier // 放在最后,关闭所有和 Prettier 冲突的 ESLint 规则
];
- 确保 VS Code 中的格式化器设置为 Prettier(而不是 ESLint):
// .vscode/settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
核心原则:格式化的事全部交给 Prettier,ESLint 只管代码质量。
第三题 ⭐⭐⭐(综合):从零配置完整方案
请为一个使用 React + TypeScript 的新项目配置完整的代码质量方案。写出:
- 需要安装的 npm 包(列出包名)
eslint.config.js的核心配置lint-staged的配置- 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:
pre-commithook 触发lint-staged- lint-staged 对暂存区的 TS/TSX 文件跑 ESLint + Prettier
commit-msghook 触发 commitlint 检查提交信息格式- 所有检查通过后才允许提交
自我检测
- 能描述 ESLint 的工作原理(源码 → AST → 规则匹配 → 修复)
- 能解释 Prettier 和 ESLint 的定位差异,以及如何用 eslint-config-prettier 消除冲突
- 能说出 Stylelint 的作用和常见规则
- 能解释 husky 和 lint-staged 各自的职责以及为什么要配合使用
- 能配置一个完整的 pre-commit + commit-msg 检查流程
- 能区分 EditorConfig 和 Prettier 的工作时机和覆盖范围
- 能解释为什么本地 lint 和 CI lint 是”双保险”而不是”二选一”
- 能为一个新项目从零搭建完整的代码质量保障方案
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90