主题
ESLint 与 Prettier
概述
在现代前端工程中,代码质量保障和格式统一是不可忽视的基础设施。ESLint 负责静态代码分析——发现潜在 Bug、强制编码规范;Prettier 负责代码格式化——统一代码风格、消除格式争论。两者各司其职,又需要协同工作。
代码质量保障体系
┌─────────────────────────────────────────────────┐
│ 开发者编写代码 │
└──────────────────────┬──────────────────────────┘
↓
┌──────────────┴──────────────┐
↓ ↓
┌───────────────┐ ┌────────────────┐
│ ESLint │ │ Prettier │
│ │ │ │
│ 逻辑检查 │ │ 格式化 │
│ - 未使用变量 │ │ - 缩进 │
│ - 类型错误 │ │ - 引号 │
│ - 最佳实践 │ │ - 分号 │
│ - 可能的 Bug │ │ - 换行 │
└───────┬───────┘ └───────┬────────┘
↓ ↓
└──────────────┬─────────────┘
↓
┌─────────────────────────┐
│ lint-staged + husky │
│ Git pre-commit 拦截 │
└────────────┬────────────┘
↓
┌─────────────────────────┐
│ commitlint │
│ 提交信息规范检查 │
└────────────┬────────────┘
↓
代码合入仓库本文将从底层原理到工程实践,全面深入地剖析 ESLint、Prettier 及整套前端代码规范工具链。
一、ESLint 深入
1.1 ESLint 工作原理
ESLint 的核心工作流程可以概括为四个阶段:解析(Parse)→ 遍历 AST → 规则检查 → 报告/修复。
ESLint 工作流程
源代码字符串
│
▼
┌──────────────────────────┐
│ Parser(解析器) │
│ │
│ 默认: espree │
│ TS: @typescript-eslint │
│ /parser │
│ Vue: vue-eslint-parser │
│ Babel: @babel/eslint │
│ -parser │
└────────────┬─────────────┘
│ 生成 AST
▼
┌──────────────────────────┐
│ AST(抽象语法树) │
│ │
│ 遵循 ESTree 规范 │
│ TypeScript 使用 │
│ TSESTree 扩展规范 │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Traverser(遍历器) │
│ │
│ 深度优先遍历 AST 节点 │
│ 对每个节点触发: │
│ - 进入事件 (enter) │
│ - 离开事件 (exit) │
└────────────┬─────────────┘
│ 触发对应规则回调
▼
┌──────────────────────────┐
│ Rules(规则集合) │
│ │
│ 每条规则监听特定 AST 节点 │
│ 检查是否违反约定 │
│ 收集 lint 问题列表 │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Linting Results │
│ │
│ - error / warning │
│ - 定位: 文件/行/列 │
│ - 可自动修复的问题标记 │
└──────────────────────────┘解析阶段:从源码到 AST
ESLint 默认使用 espree 作为解析器,espree 是 Acorn 的一个分支,严格遵循 ECMAScript 标准。解析器将源代码字符串转换为抽象语法树(AST),这棵树的每个节点都代表源码中的一个语法结构。
以一段简单代码为例:
js
const x = 42解析后生成的 AST 结构(简化):
Program
└── VariableDeclaration (kind: "const")
└── VariableDeclarator
├── Identifier (name: "x")
└── Literal (value: 42, raw: "42")对应的 AST JSON(核心字段):
json
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "x"
},
"init": {
"type": "Literal",
"value": 42,
"raw": "42"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}不同解析器的适用场景
┌──────────────────────┬────────────────────────────────────┐
│ 解析器 │ 适用场景 │
├──────────────────────┼────────────────────────────────────┤
│ espree │ 纯 JavaScript(ESLint 默认) │
├──────────────────────┼────────────────────────────────────┤
│ @typescript-eslint │ TypeScript 项目 │
│ /parser │ 生成 TSESTree(ESTree 超集) │
├──────────────────────┼────────────────────────────────────┤
│ vue-eslint-parser │ Vue SFC(.vue 文件) │
│ │ 内部委托 TS parser 处理 <script> │
├──────────────────────┼────────────────────────────────────┤
│ @babel/eslint-parser │ 使用 Babel 实验性语法 │
│ │ 如 decorators / pipeline operator │
└──────────────────────┴────────────────────────────────────┘规则检查阶段:访问者模式
ESLint 的规则系统基于访问者模式(Visitor Pattern)。每条规则声明它关心哪些 AST 节点类型,当遍历器访问到对应节点时,自动调用规则的回调函数。
AST 遍历与规则触发示意
Program (enter)
│
▼
VariableDeclaration (enter) ← no-var 规则监听此节点
│
▼
VariableDeclarator (enter)
┌──┴──┐
↓ ↓
Identifier Literal (enter) ← no-magic-numbers 规则监听此节点
(name:"x") (value:42)
↓ ↓
Identifier Literal (exit)
└──┬──┘
↓
VariableDeclarator (exit)
↓
VariableDeclaration (exit)
↓
Program (exit)每条规则独立运行、互不干扰。一次遍历完成后,ESLint 汇总所有规则产出的问题列表,按严重程度(error / warning)和位置信息输出。
1.2 配置体系
Flat Config(eslint.config.js)—— ESLint v9+ 新标准
ESLint v9 将 Flat Config 设为默认配置方式。相比旧的 .eslintrc,Flat Config 使用纯 JavaScript 模块导出一个配置对象数组,每个对象描述一组文件的 lint 规则。
js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
export default [
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
},
},
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
},
},
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
},
},
},
{
ignores: ['dist/', 'node_modules/', '*.config.js'],
},
]Flat Config 核心设计理念:
Flat Config 配置合并策略
配置数组:[ config1, config2, config3, ... ]
对于每个被 lint 的文件:
1. 从左到右遍历配置数组
2. 检查 files / ignores 是否匹配当前文件
3. 匹配的配置按顺序合并(后者覆盖前者)
4. 最终得到该文件的有效配置
┌──────────┐ ┌──────────┐ ┌──────────┐
│ config1 │ │ config2 │ │ config3 │
│ │ │ │ │ │
│ files: │ │ files: │ │ files: │
│ **/*.js │ │ **/*.ts │ │ **/*.ts │
│ │ │ │ │ │
│ rules: │ │ rules: │ │ rules: │
│ semi:err │ │ semi:off │ │ no-any: │
│ │ │ │ │ warn │
└──────────┘ └──────────┘ └──────────┘
对于 foo.ts:
config1 不匹配(*.js)→ 跳过
config2 匹配 → { semi: off }
config3 匹配 → { semi: off, no-any: warn }.eslintrc(旧配置)—— Legacy Config
旧配置支持 .eslintrc.js、.eslintrc.json、.eslintrc.yml 等格式,使用 extends 继承、overrides 覆盖、env 声明环境变量。
json
{
"root": true,
"env": {
"browser": true,
"es2024": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"rules": {
"react/react-in-jsx-scope": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
"overrides": [
{
"files": ["**/*.test.ts", "**/*.spec.ts"],
"env": { "jest": true },
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
]
}Flat Config vs Legacy Config 对比
┌────────────────────┬───────────────────────┬───────────────────────┐
│ 特性 │ Flat Config │ Legacy Config │
├────────────────────┼───────────────────────┼───────────────────────┤
│ 配置文件 │ eslint.config.js │ .eslintrc.* │
├────────────────────┼───────────────────────┼───────────────────────┤
│ 格式 │ 纯 JS/TS ESM 模块 │ JSON/YAML/JS │
├────────────────────┼───────────────────────┼───────────────────────┤
│ 配置继承 │ 数组展开 (spread) │ extends 字段 │
├────────────────────┼───────────────────────┼───────────────────────┤
│ 插件注册 │ 配置对象中直接引入 │ plugins 字段 (字符串) │
├────────────────────┼───────────────────────┼───────────────────────┤
│ 级联查找 │ 无(单一配置入口) │ 逐级向上查找到 root │
├────────────────────┼───────────────────────┼───────────────────────┤
│ 环境声明 (env) │ 移除,用 globals 替代 │ env 字段 │
├────────────────────┼───────────────────────┼───────────────────────┤
│ 全局变量 │ languageOptions.globals│ env + globals │
├────────────────────┼───────────────────────┼───────────────────────┤
│ 文件匹配 │ files / ignores │ overrides.files │
├────────────────────┼───────────────────────┼───────────────────────┤
│ ESLint 版本 │ v9+ 默认 │ v8 及以下默认 │
└────────────────────┴───────────────────────┴───────────────────────┘1.3 规则分类
ESLint 内置规则按功能分为三大类:
Possible Problems(可能的错误)
检测代码中可能存在的逻辑错误和运行时问题。
┌────────────────────────┬──────────────────────────────────────┐
│ 规则 │ 说明 │
├────────────────────────┼──────────────────────────────────────┤
│ no-undef │ 禁止使用未声明的变量 │
├────────────────────────┼──────────────────────────────────────┤
│ no-unused-vars │ 禁止定义未使用的变量 │
├────────────────────────┼──────────────────────────────────────┤
│ no-dupe-keys │ 禁止对象字面量中出现重复的 key │
├────────────────────────┼──────────────────────────────────────┤
│ no-unreachable │ 禁止在 return/throw 后出现不可达代码 │
├────────────────────────┼──────────────────────────────────────┤
│ no-constant-condition │ 禁止在条件判断中使用常量表达式 │
├────────────────────────┼──────────────────────────────────────┤
│ no-duplicate-case │ 禁止 switch 中出现重复的 case │
├────────────────────────┼──────────────────────────────────────┤
│ use-isnan │ 要求使用 isNaN() 检查 NaN │
├────────────────────────┼──────────────────────────────────────┤
│ valid-typeof │ 强制 typeof 与有效字符串比较 │
└────────────────────────┴──────────────────────────────────────┘Suggestions(建议)
提出更好的编码方式,不一定是错误但建议改进。
┌────────────────────────┬──────────────────────────────────────┐
│ 规则 │ 说明 │
├────────────────────────┼──────────────────────────────────────┤
│ eqeqeq │ 要求使用 === 和 !== │
├────────────────────────┼──────────────────────────────────────┤
│ no-var │ 要求使用 let 或 const 代替 var │
├────────────────────────┼──────────────────────────────────────┤
│ prefer-const │ 对不再赋值的变量要求使用 const │
├────────────────────────┼──────────────────────────────────────┤
│ no-eval │ 禁止使用 eval() │
├────────────────────────┼──────────────────────────────────────┤
│ no-implied-eval │ 禁止使用隐式 eval (setTimeout 字符串) │
├────────────────────────┼──────────────────────────────────────┤
│ curly │ 要求 if/else/for/while 使用花括号 │
├────────────────────────┼──────────────────────────────────────┤
│ no-throw-literal │ 禁止 throw 字面量,要求 throw Error │
├────────────────────────┼──────────────────────────────────────┤
│ prefer-template │ 建议使用模板字符串代替字符串拼接 │
└────────────────────────┴──────────────────────────────────────┘Layout & Formatting(排版格式)
控制代码的视觉呈现,如缩进、空格、换行等。ESLint v8.53+ 已废弃全部格式化规则,推荐交由 Prettier 处理。
已废弃的格式化规则(部分):
indent, semi, quotes, comma-dangle,
no-trailing-spaces, eol-last,
max-len, object-curly-spacing ...
ESLint 官方建议:
"格式化交给 Prettier,ESLint 专注于代码逻辑"1.4 常用 Shareable Config
Shareable Config 是预定义好的规则集合,团队可以直接复用。
eslint-config-airbnb
Airbnb 的 JavaScript 编码规范,是最流行的 ESLint 配置之一,规则非常严格。
eslint-config-airbnb 特点
┌──────────────────────────────────────┐
│ 核心规则选择 │
│ │
│ ✓ 强制 const / let,禁止 var │
│ ✓ 强制使用单引号 │
│ ✓ 强制使用分号 │
│ ✓ 强制使用 === / !== │
│ ✓ 强制解构赋值 │
│ ✓ 强制箭头函数简写 │
│ ✓ 禁止 console(需显式关闭) │
│ ✓ 强制 import 排序 │
│ │
│ 附带: │
│ eslint-config-airbnb-base(无 React)│
│ eslint-config-airbnb/hooks │
└──────────────────────────────────────┘@antfu/eslint-config
Anthony Fu(Vue/Vite 核心成员)维护的配置,特点是开箱即用、支持自动检测项目类型。
js
import antfu from '@antfu/eslint-config'
export default antfu({
typescript: true,
vue: true,
jsonc: false,
yaml: false,
stylistic: {
indent: 2,
quotes: 'single',
},
rules: {
'no-console': 'warn',
},
})@antfu/eslint-config 特点
┌──────────────────────────────────────┐
│ ✓ Flat Config 原生设计 │
│ ✓ 内置 Prettier 替代方案 │
│ (用 ESLint Stylistic 管格式) │
│ ✓ 自动检测 TS / Vue / React │
│ ✓ 单引号、无分号风格 │
│ ✓ 自动排序 import / 对象 key │
│ ✓ 支持 JSON / YAML / Markdown lint │
└──────────────────────────────────────┘1.5 与 TypeScript 集成
TypeScript 项目需要专用的解析器和插件,因为 ESLint 默认解析器 espree 无法理解 TypeScript 语法。
TypeScript + ESLint 集成架构
TypeScript 源码 (.ts / .tsx)
│
▼
┌─────────────────────────────┐
│ @typescript-eslint/parser │
│ │
│ 内部调用 TypeScript 编译器 │
│ 生成 TSESTree AST │
│ (ESTree 的超集) │
│ │
│ 可选:开启类型信息 │
│ parserOptions.projectService│
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ TSESTree AST │
│ │
│ 包含 TS 特有节点: │
│ - TSTypeAnnotation │
│ - TSInterfaceDeclaration │
│ - TSTypeAliasDeclaration │
│ - TSEnumDeclaration │
│ - TSAsExpression │
│ ... │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ @typescript-eslint │
│ /eslint-plugin │
│ │
│ 提供 TS 专用规则: │
│ - no-unused-vars (增强) │
│ - no-explicit-any │
│ - consistent-type-imports │
│ - no-floating-promises │
│ - strict-boolean-expressions│
│ ... │
└─────────────────────────────┘Flat Config 下的 TypeScript 配置:
js
import tseslint from 'typescript-eslint'
export default tseslint.config(
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/strict-boolean-expressions': 'warn',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' },
],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
)类型感知规则(Type-Aware Rules)需要 TypeScript 程序实例,能够检查更深层的问题:
普通规则 vs 类型感知规则
普通规则(仅 AST):
no-unused-vars → 只看变量是否在当前文件被引用
no-explicit-any → 只看是否出现 any 关键字
类型感知规则(AST + 类型信息):
no-floating-promises → 检测 Promise 是否未被 await
需要知道函数返回值的类型 → 需要类型信息
no-misused-promises → 检测 Promise 是否传给了非异步上下文
需要推断表达式的类型 → 需要类型信息
strict-boolean-expressions → 检测条件判断中是否存在隐式转换
需要知道变量的精确类型 → 需要类型信息
性能代价:
类型感知规则需要加载完整的 TypeScript 程序
会显著增加 lint 耗时(通常 2-5 倍)
建议仅在 CI 中启用全量类型检查规则1.6 自定义 ESLint 规则开发
ESLint 的强大之处在于其规则的可扩展性。每条规则是一个对象,包含 meta(元信息)和 create(规则逻辑)两个核心部分。
Rule 结构
ESLint Rule 结构
module.exports = {
meta: {
type: "problem" | "suggestion" | "layout",
docs: {
description: "规则描述",
recommended: true/false,
},
fixable: "code" | "whitespace" | null,
schema: [...], ← 规则选项的 JSON Schema
messages: { ← 错误消息模板
messageId: "..."
},
},
create(context) {
return {
"AST节点类型"(node) {
← 检查逻辑
← context.report() 报告问题
}
}
}
}手写规则:禁止 Magic Number
Magic Number 指代码中直接出现的、含义不明的数字字面量。我们来实现一个规则,强制使用命名常量代替 magic number。
js
const rule = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow magic numbers, require named constants',
},
fixable: null,
schema: [
{
type: 'object',
properties: {
ignore: {
type: 'array',
items: { type: 'number' },
},
ignoreArrayIndexes: {
type: 'boolean',
},
ignoreDefaultValues: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
messages: {
noMagicNumber:
"No magic number: {{value}}. Extract to a named constant.",
},
},
create(context) {
const options = context.options[0] || {}
const ignoreSet = new Set(options.ignore || [0, 1, -1])
const ignoreArrayIndexes = options.ignoreArrayIndexes || false
const ignoreDefaultValues = options.ignoreDefaultValues || false
function isNumericLiteral(node) {
return node.type === 'Literal' && typeof node.value === 'number'
}
function isConstDeclaration(node) {
let current = node.parent
while (current) {
if (
current.type === 'VariableDeclaration' &&
current.kind === 'const'
) {
return true
}
if (current.type === 'Property' || current.type === 'AssignmentPattern') {
current = current.parent
continue
}
break
}
return false
}
function isEnumMember(node) {
return node.parent && node.parent.type === 'TSEnumMember'
}
function isArrayIndex(node) {
return (
node.parent &&
node.parent.type === 'MemberExpression' &&
node.parent.computed &&
node.parent.property === node
)
}
function isDefaultValue(node) {
return node.parent && node.parent.type === 'AssignmentPattern'
}
return {
Literal(node) {
if (!isNumericLiteral(node)) return
if (ignoreSet.has(node.value)) return
if (isConstDeclaration(node)) return
if (isEnumMember(node)) return
if (ignoreArrayIndexes && isArrayIndex(node)) return
if (ignoreDefaultValues && isDefaultValue(node)) return
context.report({
node,
messageId: 'noMagicNumber',
data: { value: node.value },
})
},
}
},
}使用这条规则后的效果:
js
const timeout = 3000
setTimeout(callback, timeout)
const MAX_RETRIES = 5
for (let i = 0; i < MAX_RETRIES; i++) {
retry()
}
setTimeout(callback, 3000)
for (let i = 0; i < 5; i++) {
retry()
}测试自定义规则
ESLint 提供 RuleTester 工具来测试规则:
js
import { RuleTester } from 'eslint'
import rule from './no-magic-number.js'
const tester = new RuleTester({
languageOptions: { ecmaVersion: 2024 },
})
tester.run('no-magic-number', rule, {
valid: [
'const MAX = 100',
'const arr = [0, 1]',
'if (x === 0) {}',
'if (x === -1) {}',
],
invalid: [
{
code: 'const timeout = x + 3000',
errors: [{ messageId: 'noMagicNumber', data: { value: 3000 } }],
},
{
code: 'if (x > 42) {}',
errors: [{ messageId: 'noMagicNumber', data: { value: 42 } }],
},
],
})开发插件
将自定义规则打包为 ESLint 插件发布:
js
import noMagicNumber from './rules/no-magic-number.js'
const plugin = {
meta: {
name: 'eslint-plugin-my-team',
version: '1.0.0',
},
rules: {
'no-magic-number': noMagicNumber,
},
configs: {},
}
plugin.configs.recommended = [
{
plugins: {
'my-team': plugin,
},
rules: {
'my-team/no-magic-number': 'warn',
},
},
]
export default plugin二、Prettier
2.1 设计哲学:Opinionated
Prettier 的核心理念是 Opinionated(有主见的)——它故意减少配置选项,用一套经过深思熟虑的默认规则统一所有代码风格。
Prettier 设计哲学
传统格式化工具(如 ESLint formatting rules):
┌───────────────────────────────────────┐
│ 几十个格式相关选项 │
│ indent? tab? 2? 4? │
│ quotes? single? double? │
│ semicolons? always? never? ASI? │
│ trailing commas? none? all? es5? │
│ bracket spacing? same-line? ... │
│ │
│ → 团队争论不休 │
│ → 配置地狱 │
│ → 每个项目风格不同 │
└───────────────────────────────────────┘
Prettier:
┌───────────────────────────────────────┐
│ 极少的配置选项(约 20 个) │
│ 大部分选项不推荐修改 │
│ "停止争论代码风格,让工具做决定" │
│ │
│ → 所有项目风格一致 │
│ → 零配置或极少配置即可使用 │
│ → 开发者专注于逻辑而非格式 │
└───────────────────────────────────────┘Prettier 的格式化原理:
Prettier 格式化流水线
源代码
│
▼
┌──────────────┐
│ Parser │
│ 解析为 AST │
│ (多语言支持) │
└──────┬───────┘
│
▼
┌──────────────┐
│ IR (Doc) │
│ 中间表示 │
│ 与原始格式 │
│ 完全无关 │
└──────┬───────┘
│
▼
┌──────────────┐
│ Printer │
│ 根据 IR │
│ 重新输出代码 │
│ 应用格式规则 │
└──────┬───────┘
│
▼
格式化后的代码关键特性:Prettier 完全丢弃原始代码格式,从零重建输出。这意味着不管你的代码之前是什么样子,经过 Prettier 处理后,结果是完全确定的(幂等性)。
2.2 .prettierrc 配置
json
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"singleAttributePerLine": false
}配置项详解:
┌───────────────────────┬──────────┬────────────────────────────────────┐
│ 选项 │ 默认值 │ 说明 │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ printWidth │ 80 │ 每行最大字符宽度,超出则换行 │
│ │ │ 不是硬限制,Prettier 尽力而为 │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ tabWidth │ 2 │ 缩进空格数 │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ useTabs │ false │ 是否使用 Tab 缩进 │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ semi │ true │ 语句末尾是否加分号 │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ singleQuote │ false │ 是否使用单引号(JSX 单独控制) │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ trailingComma │ "all" │ 尾随逗号: "all" | "es5" | "none" │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ bracketSpacing │ true │ 对象花括号内是否加空格 │
│ │ │ true: { foo: bar } │
│ │ │ false: {foo: bar} │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ bracketSameLine │ false │ HTML 标签 > 是否与属性同行 │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ arrowParens │ "always" │ 箭头函数单参数是否加括号 │
│ │ │ "always": (x) => x │
│ │ │ "avoid": x => x │
├───────────────────────┼──────────┼────────────────────────────────────┤
│ endOfLine │ "lf" │ 换行符: "lf" | "crlf" | "auto" │
└───────────────────────┴──────────┴────────────────────────────────────┘配置文件优先级(从高到低):
1. package.json 中的 "prettier" 字段
2. .prettierrc (JSON / YAML)
3. .prettierrc.json / .prettierrc.yml
4. .prettierrc.js / .prettierrc.cjs / .prettierrc.mjs
5. prettier.config.js / prettier.config.cjs / prettier.config.mjs
6. .prettierrc.toml2.3 Prettier 与 ESLint 的冲突解决
当 ESLint 和 Prettier 同时使用时,某些规则会产生冲突——ESLint 要求一种格式,Prettier 要求另一种格式,导致两个工具互相"打架"。
冲突示例
ESLint (indent: 4): Prettier (tabWidth: 2):
┌──────────────────┐ ┌──────────────────┐
│ function foo() { │ │ function foo() { │
│ return 1 │ ←→ │ return 1 │
│ } │ 冲突! │ } │
└──────────────────┘ └──────────────────┘
ESLint 修复后 → Prettier 又改回来 → 无限循环解决方案有两种思路:
方案一:eslint-config-prettier(推荐)
关闭 ESLint 中与 Prettier 冲突的规则,让 Prettier 全权负责格式化。
eslint-config-prettier 原理
┌──────────────────────────────────────────────┐
│ ESLint 规则集 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 逻辑规则 │ │ 建议规则 │ │ 格式规则 │ │
│ │ │ │ │ │ xxxxxxxx │ │
│ │ 保留 ✓ │ │ 保留 ✓ │ │ 关闭 ✗ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ eslint-config-prettier 做的事情: │
│ 将所有 与 Prettier 冲突的规则设为 "off" │
│ 涵盖 ESLint 核心 + 多个插件的格式规则 │
└──────────────────────────────────────────────┘Flat Config 中使用:
js
import eslintConfigPrettier from 'eslint-config-prettier'
export default [
...otherConfigs,
eslintConfigPrettier,
]Legacy Config 中使用:
json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
]
}注意:prettier 必须放在 extends 数组的最后一个,这样它才能覆盖前面所有配置中的格式化规则。
方案二:eslint-plugin-prettier
将 Prettier 作为一条 ESLint 规则运行——在 ESLint 中集成 Prettier,将格式差异报告为 ESLint 错误。
eslint-plugin-prettier 原理
源代码 ──→ ESLint 处理
│
├──→ 常规 ESLint 规则检查
│
└──→ prettier/prettier 规则
│
├── 调用 Prettier 格式化
├── 对比原始代码与格式化结果
└── 差异部分报告为 ESLint error
并提供 autofixjs
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'
export default [
...otherConfigs,
eslintPluginPrettier,
]两种方案对比
┌─────────────────────┬──────────────────────┬──────────────────────┐
│ │ eslint-config-prettier│ eslint-plugin-prettier│
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 做法 │ 关闭冲突规则 │ 把 Prettier 当规则跑 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 格式化由谁负责 │ Prettier 单独运行 │ ESLint 内集成 Prettier│
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 性能 │ ✓ 好(各司其职) │ ✗ 较慢(双重处理) │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 编辑器体验 │ 需分别配置 │ 统一通过 ESLint 报错 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 官方推荐 │ ✓ 是 │ 可用但非首推 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 红色波浪线 │ 只有 ESLint 逻辑错误 │ 格式问题也显示红线 │
└─────────────────────┴──────────────────────┴──────────────────────┘2.4 推荐方案:职责分离
最佳实践:Prettier 管格式,ESLint 管逻辑
┌──────────────────────────────────────────────┐
│ 代码质量 │
│ │
│ ┌─── ESLint 负责 ───┐ ┌─ Prettier 负责 ─┐│
│ │ │ │ ││
│ │ ✓ 未使用变量 │ │ ✓ 缩进 ││
│ │ ✓ 类型安全 │ │ ✓ 引号风格 ││
│ │ ✓ 最佳实践 │ │ ✓ 分号 ││
│ │ ✓ 代码复杂度 │ │ ✓ 换行 ││
│ │ ✓ import 规范 │ │ ✓ 尾随逗号 ││
│ │ ✓ React hooks 规则│ │ ✓ 括号间距 ││
│ │ ✓ 可达性检查 │ │ ✓ 对象/数组 ││
│ │ │ │ 格式排列 ││
│ └───────────────────┘ └─────────────────┘│
│ │
│ 执行时机: │
│ - 保存时:Prettier 格式化 + ESLint 修复 │
│ - pre-commit:lint-staged 同时运行两者 │
│ - CI:两者都检查,任一失败则阻断 │
└──────────────────────────────────────────────┘三、完整工程化配置
3.1 ESLint + Prettier + TypeScript + React
一个完整的 React + TypeScript 项目配置方案:
package.json 依赖
json
{
"devDependencies": {
"eslint": "^9.0.0",
"@eslint/js": "^9.0.0",
"typescript-eslint": "^8.0.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.0",
"globals": "^15.0.0"
}
}eslint.config.js
js
import js from '@eslint/js'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import eslintConfigPrettier from 'eslint-config-prettier'
export default tseslint.config(
{ ignores: ['dist', 'node_modules', 'coverage'] },
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
globals: {
...globals.browser,
...globals.es2024,
},
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
files: ['**/*.{tsx,jsx}'],
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
settings: {
react: { version: 'detect' },
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
},
},
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' },
],
'@typescript-eslint/no-explicit-any': 'warn',
},
},
eslintConfigPrettier,
).prettierrc
json
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf"
}.prettierignore
dist
node_modules
coverage
pnpm-lock.yaml
package-lock.jsonpackage.json scripts
json
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\""
}
}3.2 ESLint + Prettier + TypeScript + Vue
eslint.config.js
js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
import eslintConfigPrettier from 'eslint-config-prettier'
export default [
{ ignores: ['dist', 'node_modules'] },
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
extraFileExtensions: ['.vue'],
},
},
},
{
rules: {
'vue/multi-word-component-names': 'warn',
'vue/no-unused-vars': 'error',
'vue/define-macros-order': [
'error',
{ order: ['defineProps', 'defineEmits'] },
],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
},
},
eslintConfigPrettier,
]3.3 lint-staged + husky:Git pre-commit 钩子
lint-staged 只对 Git 暂存区(staged)的文件运行检查,避免全量 lint 的性能问题。husky 提供 Git hooks 管理能力。
pre-commit 工作流
git add file1.ts file2.tsx
│
▼
git commit -m "feat: ..."
│
▼
┌──────────────────────────┐
│ husky → pre-commit hook │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ lint-staged │
│ │
│ 获取暂存区文件列表 │
│ file1.ts, file2.tsx │
│ │
│ 按 glob 匹配执行命令: │
│ *.{ts,tsx} → │
│ eslint --fix │
│ prettier --write │
│ │
│ 检查通过? │
│ ├── Yes → 继续 commit │
│ └── No → 中止 commit │
└──────────────────────────┘安装与配置
bash
npm install -D husky lint-staged
npx husky init执行后会在 .husky/pre-commit 生成 hook 文件:
bash
npx lint-stagedlint-staged 配置
在 package.json 中:
json
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
],
"*.css": [
"prettier --write"
]
}
}也可以使用独立配置文件 .lintstagedrc.js:
js
export default {
'*.{ts,tsx,js,jsx}': (filenames) => [
`eslint --fix ${filenames.join(' ')}`,
`prettier --write ${filenames.join(' ')}`,
],
'*.{json,md,yml,yaml,css}': (filenames) => [
`prettier --write ${filenames.join(' ')}`,
],
}3.4 commitlint:提交信息规范
commitlint 配合 Conventional Commits 规范,强制约束 Git 提交信息格式。
Conventional Commits 格式
<type>(<scope>): <subject>
│ │ │
│ │ └── 简短描述,不超过 72 字符
│ └── 可选,影响范围(模块/组件名)
└── 类型(必填)
常用 type:
┌──────────┬────────────────────────────┐
│ type │ 说明 │
├──────────┼────────────────────────────┤
│ feat │ 新功能 │
│ fix │ Bug 修复 │
│ docs │ 文档变更 │
│ style │ 代码格式(不影响逻辑) │
│ refactor │ 重构(非 feat/fix) │
│ perf │ 性能优化 │
│ test │ 添加/修改测试 │
│ build │ 构建系统或依赖变更 │
│ ci │ CI 配置变更 │
│ chore │ 其他杂项 │
│ revert │ 回滚提交 │
└──────────┴────────────────────────────┘
示例:
feat(auth): add OAuth2 login support
fix(router): resolve navigation guard loop
refactor(utils): extract date formatting helpers
docs(readme): update installation instructions安装与配置
bash
npm install -D @commitlint/cli @commitlint/config-conventionalcommitlint.config.js:
js
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'build', 'ci', 'chore', 'revert',
],
],
'subject-max-length': [2, 'always', 72],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
},
}添加 commit-msg hook:
bash
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg3.5 .editorconfig:编辑器统一配置
.editorconfig 是跨编辑器的配置文件标准,确保不同编辑器(VS Code、WebStorm、Vim 等)使用相同的基础格式设置。
ini
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.editorconfig vs Prettier vs ESLint 的职责边界
┌─────────────────┬──────────────────────────────────┐
│ .editorconfig │ 编辑器基础设置 │
│ │ 缩进方式、字符编码、换行符、 │
│ │ 尾部空格、最后空行 │
│ │ → 在输入时就生效 │
├─────────────────┼──────────────────────────────────┤
│ Prettier │ 代码格式化 │
│ │ 基于 AST 的完整格式重排 │
│ │ → 保存时 / 命令行运行 │
├─────────────────┼──────────────────────────────────┤
│ ESLint │ 代码逻辑检查 │
│ │ 静态分析,发现潜在 Bug │
│ │ → 保存时 / 命令行运行 │
└─────────────────┴──────────────────────────────────┘
三者配合:
.editorconfig → 保证输入一致
Prettier → 保证输出一致
ESLint → 保证逻辑正确3.6 完整工具链集成图
完整的代码规范工具链
┌─────────── 开发时 ──────────────────────────────┐
│ │
│ 编辑器 │
│ ├── .editorconfig → 统一缩进/编码 │
│ ├── ESLint 插件 → 实时显示逻辑错误 │
│ └── Prettier 插件 → 保存时自动格式化 │
│ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────── 提交时 ──────────────────────────────┐
│ │
│ git commit │
│ ├── husky pre-commit │
│ │ └── lint-staged │
│ │ ├── eslint --fix (暂存区文件) │
│ │ └── prettier --write (暂存区文件) │
│ │ │
│ └── husky commit-msg │
│ └── commitlint │
│ └── 检查提交信息是否符合规范 │
│ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────── CI/CD ───────────────────────────────┐
│ │
│ GitHub Actions / GitLab CI │
│ ├── eslint . (全量检查) │
│ ├── prettier --check . (格式检查) │
│ ├── tsc --noEmit (类型检查) │
│ └── npm test (单元测试) │
│ │
│ 任一步骤失败 → 阻止合入 │
│ │
└─────────────────────────────────────────────────┘VS Code 设置(.vscode/settings.json)
json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.useFlatConfig": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
]
}执行顺序:保存文件时,先执行 ESLint autofix(source.fixAll.eslint),再执行 Prettier format(editor.formatOnSave)。
GitHub Actions CI 配置
yaml
name: Code Quality
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx eslint .
- run: npx prettier --check .
- run: npx tsc --noEmit四、新一代工具
4.1 Biome(原 Rome)
Biome 是用 Rust 编写的 All-in-one 前端工具链,集成了 Linter、Formatter、Import Sorter 等功能于一体。
Biome 架构
┌──────────────────────────────────────────┐
│ Biome (Rust) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Formatter │ │ Linter │ │ Import │ │
│ │ │ │ │ │ Sorter │ │
│ │ 替代 │ │ 替代 │ │ │ │
│ │ Prettier │ │ ESLint │ │ │ │
│ └──────────┘ └──────────┘ └────────┘ │
│ │
│ 统一的 AST 解析(一次解析,多次使用) │
│ Rust 实现 → 极快的执行速度 │
│ 零依赖 → 无 node_modules 膨胀 │
│ 97%+ Prettier 兼容 │
│ 200+ lint 规则 │
└──────────────────────────────────────────┘Biome 配置
json
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noBannedTypes": "error",
"noExtraBooleanCast": "error"
},
"correctness": {
"noUnusedVariables": "warn",
"noUnusedImports": "error",
"useExhaustiveDependencies": "warn"
},
"suspicious": {
"noExplicitAny": "warn",
"noDoubleEquals": "error"
},
"style": {
"useConst": "error",
"noNonNullAssertion": "warn"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "asNeeded",
"trailingCommas": "all"
}
}
}Biome CLI
bash
npx @biomejs/biome check .
npx @biomejs/biome check --write .
npx @biomejs/biome format --write .
npx @biomejs/biome lint .
npx @biomejs/biome ci .4.2 oxlint(Oxc Linter)
oxlint 是 Oxc 项目的 Linter,同样用 Rust 编写,专注于高性能的 JavaScript/TypeScript 代码检查。
oxlint 定位
┌──────────────────────────────────────────┐
│ Oxc 项目 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ oxlint │ │ oxc-parser│ │ oxc- │ │
│ │ Linter │ │ 解析器 │ │ minifier│ │
│ └──────────┘ └──────────┘ └────────┘ │
│ │
│ 特点: │
│ - 比 ESLint 快 50-100 倍 │
│ - 无需配置即可使用 │
│ - 默认开启约 100 条高质量规则 │
│ - 设计为 ESLint 的补充而非替代 │
│ - 不支持自定义规则/插件 │
└──────────────────────────────────────────┘bash
npx oxlint@latest
npx oxlint@latest --fix
npx oxlint@latest -D correctness -D perfoxlint 推荐的使用策略是与 ESLint 互补:先用 oxlint 快速检查通用规则,再用 ESLint 检查项目特定规则(如 React Hooks、Vue 等框架规则)。
json
{
"scripts": {
"lint": "oxlint && eslint .",
"lint:fix": "oxlint --fix && eslint . --fix"
}
}4.3 Biome vs ESLint + Prettier 对比
┌─────────────────────┬──────────────────────┬──────────────────────┐
│ 维度 │ Biome │ ESLint + Prettier │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 语言实现 │ Rust │ JavaScript │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 速度 │ 极快(10-100x) │ 较慢 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 安装体积 │ 单一二进制文件 │ 大量 node_modules │
│ │ (~50MB) │ (~200MB+) │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 配置复杂度 │ 单一 biome.json │ 多个配置文件 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 规则数量 │ 200+ │ 1000+(含插件生态) │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 插件/自定义规则 │ 不支持 │ 丰富的插件生态 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 框架专用规则 │ 有限 │ 完善(React/Vue/...) │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ TypeScript 支持 │ 内置 │ 需额外解析器+插件 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 类型感知 Lint │ 不支持 │ 支持 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ Prettier 兼容性 │ 97%+ │ 100%(就是 Prettier) │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 社区生态 │ 快速增长中 │ 非常成熟 │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ 适用场景 │ 新项目 / 追求速度 │ 成熟项目 / 需要丰富 │
│ │ / 简化工具链 │ 插件生态 │
└─────────────────────┴──────────────────────┴──────────────────────┘选型建议
选型决策树
你的项目需要...
├── 框架专用规则(react-hooks / vue / angular)?
│ ├── Yes → ESLint + Prettier
│ │ (框架插件生态无可替代)
│ └── No ─┐
│ │
├── 自定义 lint 规则?
│ ├── Yes → ESLint + Prettier
│ │ (Biome 不支持自定义规则)
│ └── No ─┐
│ │
├── 类型感知 lint(如 no-floating-promises)?
│ ├── Yes → ESLint + typescript-eslint
│ │ (Biome 不支持类型感知)
│ └── No ─┐
│ │
├── 追求极致速度 / 简化工具链?
│ ├── Yes → Biome
│ └── No ─┐
│ │
└── 默认推荐:
新项目 → 尝试 Biome,不够再加 ESLint
存量项目 → ESLint + Prettier(迁移成本低)
大型 Monorepo → Biome(速度优势明显)渐进式迁移方案
从 ESLint + Prettier 迁移到 Biome 可以分步进行:
渐进式迁移路径
阶段 1:替换 Prettier
┌──────────────────────────────┐
│ Formatter: Prettier → Biome │
│ Linter: ESLint(保留) │
└──────────────────────────────┘
阶段 2:补充 Biome Lint
┌──────────────────────────────┐
│ Formatter: Biome │
│ Linter: Biome + ESLint │
│ (Biome 跑通用规则) │
│ (ESLint 跑框架规则)│
└──────────────────────────────┘
阶段 3:(如果可能)全面切换
┌──────────────────────────────┐
│ Formatter: Biome │
│ Linter: Biome │
│ (仅当 Biome 覆盖所有需求时) │
└──────────────────────────────┘Biome 提供了从 ESLint/Prettier 迁移的命令:
bash
npx @biomejs/biome migrate prettier --write
npx @biomejs/biome migrate eslint --write五、面试高频问题
问题 1:ESLint 的工作原理是什么?
回答思路:
ESLint 的工作流程分为四个阶段。首先,解析阶段:ESLint 通过解析器(默认 espree,TypeScript 用 @typescript-eslint/parser)将源代码转换为 AST(抽象语法树),AST 遵循 ESTree 规范。其次,遍历阶段:ESLint 使用深度优先遍历 AST 的每一个节点,在进入和离开每个节点时触发相应事件。接着,规则检查阶段:每条规则通过访问者模式注册对特定 AST 节点类型的监听,当遍历到对应节点时执行检查逻辑,违反规则则调用 context.report() 记录问题。最后,输出/修复阶段:汇总所有规则报告的问题,输出 error/warning 信息;如果规则提供了 fixer 函数,可以通过 --fix 自动修复。
追问:ESLint 的 --fix 是怎么实现的?
修复机制是通过规则在 context.report() 时提供一个 fix 函数,该函数返回一个描述文本替换操作的对象(包含替换范围和替换文本)。ESLint 收集所有 fix 操作后,检查是否存在范围冲突,无冲突的修复直接应用到源码文本上。如果存在冲突,ESLint 会多轮迭代(最多 10 次)直到没有新的修复产出。
问题 2:Flat Config 与 .eslintrc 有什么区别?为什么要迁移?
回答思路:
Flat Config 是 ESLint v9+ 的新配置标准。核心区别在于:第一,配置格式——Flat Config 使用 eslint.config.js 导出一个配置数组,每个元素是一个配置对象;旧配置使用 .eslintrc.* 文件加 extends/overrides 嵌套结构。第二,配置合并——Flat Config 采用扁平化的数组合并策略,配置对象按数组顺序合并,后者覆盖前者,逻辑简单可预测;旧配置依赖级联查找(从文件目录向上找到 root:true),extends 的深层嵌套容易造成配置优先级混乱。第三,插件注册——Flat Config 直接在配置对象中 import 插件对象,类型安全、可追溯;旧配置通过字符串名称引用,依赖 ESLint 内部解析机制。第四,环境变量——Flat Config 移除了 env 字段,改用 languageOptions.globals 显式声明全局变量。迁移的原因是 Flat Config 更简单、更透明、更利于工具链集成。
问题 3:ESLint 和 Prettier 冲突怎么解决?
回答思路:
冲突的根本原因是 ESLint 的格式化规则(如 indent、semi、quotes)和 Prettier 的格式化产出不一致。推荐解决方案是使用 eslint-config-prettier,它将所有与 Prettier 冲突的 ESLint 规则设为 off,让 ESLint 专注于逻辑检查,Prettier 专注于格式化。配置时需要将 eslint-config-prettier 放在配置数组的最后(或 extends 的最后),确保它能覆盖前面所有配置中的格式化规则。另一种方案是 eslint-plugin-prettier,它将 Prettier 作为一条 ESLint 规则运行,但这种方式性能较差(ESLint 和 Prettier 同时处理同一份代码),且会在编辑器中将格式问题显示为红色波浪线,干扰开发体验,目前不推荐。
问题 4:如何自定义一个 ESLint 规则?
回答思路:
一个 ESLint 规则由两部分组成:meta 对象描述规则的元信息(类型、文档、是否可修复、选项 schema、消息模板),create 函数接收 context 参数并返回一个访问者对象。访问者对象的 key 是 AST 节点类型(如 Literal、CallExpression、VariableDeclaration),value 是回调函数。当 ESLint 遍历 AST 遇到对应节点时调用回调,在回调中通过 context.report({ node, messageId, data, fix }) 报告问题。开发时可以使用 AST Explorer(astexplorer.net)查看代码对应的 AST 结构,用 RuleTester 编写单元测试,最终打包为 eslint-plugin 发布。
问题 5:Prettier 的 printWidth 是硬限制吗?
回答思路:
不是硬限制。printWidth 是 Prettier 的目标行宽,Prettier 会尽力将代码控制在这个宽度内,但在某些情况下会超出。例如,一个很长的字符串字面量、一个不可拆分的 URL、一个长链式调用如果拆行后反而降低可读性,Prettier 可能选择保持单行。Prettier 的算法本质上是:先尝试把整个表达式放在一行,如果超出 printWidth 则按照预定义的换行策略拆分。这不同于 ESLint 的 max-len 规则那种硬性限制。
问题 6:lint-staged 的工作原理是什么?为什么不直接 lint 整个项目?
回答思路:
lint-staged 通过 git diff --staged --name-only 获取暂存区文件列表,然后仅对这些文件运行配置的命令(如 eslint、prettier)。核心原因是性能——大型项目可能有成千上万个文件,全量 lint 可能需要几分钟甚至更长时间,而每次提交通常只修改几个文件。通过只检查变更文件,可以将 pre-commit 检查时间控制在秒级,不影响开发体验。同时 lint-staged 还会在命令执行后自动将修改后的文件重新 git add,保证 stage 区的一致性。全量 lint 则放在 CI 环节执行,作为最终兜底。
问题 7:Biome 能完全替代 ESLint + Prettier 吗?
回答思路:
目前还不能完全替代,但在很多场景下已经可以。Biome 的优势是速度极快(Rust 实现,比 ESLint 快 10-100 倍)、配置简单(单一 biome.json)、零 node_modules 依赖。但它也有局限:不支持自定义规则和插件、不支持类型感知的 lint 规则(如 no-floating-promises)、框架专用规则不如 ESLint 生态丰富(React Hooks 规则已支持,但 Vue/Angular 等支持有限)。实际选型建议:新项目如果不需要框架专用规则可以优先考虑 Biome;存量项目可以渐进迁移,先用 Biome 替代 Prettier,再逐步替代 ESLint 的通用规则;大型 Monorepo 中 Biome 的速度优势非常明显。
问题 8:eslint.config.js 中配置数组的合并顺序是怎样的?如果两个配置对象都匹配同一个文件,规则如何合并?
回答思路:
Flat Config 的合并策略是线性扁平合并:对于每个被 lint 的文件,ESLint 从数组第一个元素开始,依次检查每个配置对象的 files/ignores 是否匹配当前文件。匹配的配置对象会按出现顺序合并,后面的覆盖前面的。具体来说:rules 字段是浅合并——同名规则后者覆盖前者;plugins 是合并而非覆盖;languageOptions 会深度合并 parserOptions、globals 等子字段。没有 files 字段的配置对象视为匹配所有文件。这种扁平化设计避免了旧配置中 extends 层层嵌套导致的优先级混乱问题。
六、延伸阅读
- ESLint 官方文档 —— Flat Config 完整指南与规则列表
- typescript-eslint 官方文档 —— TypeScript 集成配置与类型感知规则
- Prettier 官方文档 —— 配置选项、编辑器集成、与 Linter 集成
- ESLint Stylistic —— ESLint 官方分离出的格式化规则项目,@antfu/eslint-config 的基础
- Biome 官方文档 —— 配置参考、规则列表、迁移指南
- Oxc 项目 —— oxlint 及 Oxc 工具链全览
- AST Explorer —— 在线查看 AST 结构,开发自定义规则的必备工具
- Conventional Commits —— 提交信息规范
- lint-staged 文档 —— 配置参考与高级用法
- husky 文档 —— Git hooks 管理工具