一、问题:配置了规范,代码还是乱
先交代背景。2024年8月,我入职一家B轮电商公司,前端团队38人,主力技术栈是Vue3 + TypeScript + Vite。入职第一天,我建了个分支改了个组件,提交前跑了 npm run lint,屏幕上一片红:
✖ 216 problems (198 errors, 18 warnings)
9 errors and 0 warnings potentially fixable with the `--fix` flag.
这不是我的问题。项目里同时装着 @vue/eslint-config-typescript、eslint-plugin-prettier、prettier,三套规则互相打架。更头疼的是CI流水线:push代码后GitLab Runner跑一次lint+test+build,要50分钟,一半时间耗在ESLint上。
我翻了翻 package.json,ESLint配置用的是老式的 .eslintrc.cjs,里面 extends 了四层:eslint:recommended → @vue/typescript/recommended → plugin:vue/vue3-recommended → prettier,然后又被 eslint-plugin-prettier 覆盖。规则优先级混乱到没法追溯。
这活儿没法干。我把ESLint配置整个重写了一遍,换成ESLint 9的flat config + Prettier 3的独立格式化,CI耗时从50分钟干到13分钟,lint报错从200+归零。下文是完整过程。
二、方案对比:三套主流配置的取舍
重写之前,我先对比了三条路线。用我们仓库里120个Vue文件做基准,跑了三次全量lint,时间取中位数。
方案A:eslint-config-airbnb + eslint-plugin-prettier
这是2021年前后的主流做法,现在不推荐。Airbnb规则是为React写的,对Vue项目需要大量override。eslint-plugin-prettier 把Prettier当作ESLint规则运行,单文件多跑一次格式化,lint速度直接翻倍。
| 指标 | 数据 |
|---|---|
| 全量lint耗时 | 2分48秒 |
| 配置文件行数 | 217行 |
| 规则冲突 | 14处(缩进、引号、行宽) |
方案B:@antfu/eslint-config 零配置方案
Anthony Fu的零配置方案,确实是趋势,v2.0之后直接输出flat config,支持Vue、React、TS开箱即用。团队刚成立可以选这个,但我们的场景不行:
- 我们已经有两套存量代码,而antfu的规则比eslint:recommended严得多,全量lint报错3000+,改造量巨大
- 它内置了perfectionist插件对import排序、对象属性排序做自动修正,这改变了提交diff,code review被淹没在排序变更里
- 升级节奏快,它的v2到v3有过一次breaking change,我们是业务团队,没人力追版本
| 指标 | 数据 |
|---|---|
| 全量lint耗时 | 3分21秒(含排序修正) |
| 配置文件行数 | 0(默认配置) |
| 规则冲突 | 0(但存量代码报错3000+) |
方案C:自研规则集(最终选择)
基于 @vue/eslint-config-typescript 做基底,只保留必要规则,关闭与Prettier重叠的格式类规则,用Prettier单独管格式。ESLint只管代码质量(未使用变量、any泄漏、空值处理),Prettier管风格(缩进、引号、分号)。
| 指标 | 数据 |
|---|---|
| 全量lint耗时 | 18秒 |
| 配置文件行数 | 96行 |
| 规则冲突 | 0 |
三个方案的核心差异在于:把「格式」和「质量」混在一个工具里,必然产生冲突。ESLint 9之前,eslint-plugin-prettier 是主流方案,但它本质上是「用ESLint跑Prettier」,性能差、规则冲突多。正确做法是让Prettier独立运行,ESLint只保留一个 eslint-config-prettier 关掉冲突规则。
三、规则冲突的本质
为什么ESLint和Prettier老打架?因为它们的关注点有重叠:
- ESLint的
indent规则控制缩进,Prettier的tabWidth也控制缩进 - ESLint的
quotes规则控制引号,Prettier的singleQuote也控制引号 - ESLint的
max-len控制行宽,Prettier的printWidth也控制行宽
这些规则如果同时生效,会出现「ESLint说改成单引号,Prettier改成双引号,ESLint再改回来」的死循环。2024年的正确解法是:
- ESLint通过
eslint-config-prettier把格式类规则全部关闭 - 格式检查交给Prettier的
--check命令 - 提交前用 lint-staged 先跑Prettier --write,再跑ESLint --fix
四、完整代码实现
4.1 依赖版本和安装
先锁定版本。我用的是2024年8月的稳定版本:
npm i -D \
eslint@9.9.0 \
prettier@3.3.3 \
typescript-eslint@8.3.0 \
eslint-plugin-vue@9.28.0 \
@vue/eslint-config-typescript@14.1.3 \
eslint-config-prettier@9.1.0 \
eslint-plugin-import-x@4.2.1 \
eslint-import-resolver-typescript@3.6.3 \
@eslint/js@9.9.0 \
globals@15.9.0
4.2 ESLint flat config(eslint.config.js)
ESLint 9默认使用flat config,不再读 .eslintrc。根目录创建 eslint.config.js:
// eslint.config.js
const js = require('@eslint/js');
const vue = require('eslint-plugin-vue');
const tseslint = require('typescript-eslint');
const vueParser = require('vue-eslint-parser');
const tsParser = require('@typescript-eslint/parser');
const importX = require('eslint-plugin-import-x');
const prettierConfig = require('eslint-config-prettier');
const globals = require('globals');
module.exports = [
// 全局忽略
{
ignores: [
'dist/**',
'node_modules/**',
'public/**',
'coverage/**',
'*.d.ts',
'auto-imports.d.ts',
'components.d.ts'
]
},
// 基础规则:所有JS/TS/Vue文件
js.configs.recommended,
...tseslint.configs.recommended,
...vue.configs['flat/recommended'],
prettierConfig,
{
files: ['**/*.{js,mjs,cjs,ts,vue}'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.browser,
...globals.node
},
parser: vueParser,
parserOptions: {
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module',
extraFileExtensions: ['.vue']
}
},
plugins: {
'import-x': importX
},
rules: {
// —— 代码质量规则 ——
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrors: 'none'
}],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports',
disallowTypeAnnotations: false
}],
'vue/multi-word-component-names': 'off',
'vue/require-default-prop': 'off',
// —— 关闭所有与Prettier重叠的格式规则 ——
// 缩进交给Prettier
'indent': 'off',
'@typescript-eslint/indent': 'off',
'vue/html-indent': 'off',
// 引号交给Prettier
'quotes': 'off',
'@typescript-eslint/quotes': 'off',
// 分号交给Prettier
'semi': 'off',
'@typescript-eslint/semi': 'off',
// 行宽交给Prettier
'max-len': 'off',
// 逗号交给Prettier
'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': 'off',
'vue/comma-dangle': 'off',
// 空格交给Prettier
'object-curly-spacing': 'off',
'@typescript-eslint/object-curly-spacing': 'off',
'array-bracket-spacing': 'off',
'computed-property-spacing': 'off',
'comma-spacing': 'off',
'space-infix-ops': 'off',
'keyword-spacing': 'off',
'key-spacing': 'off',
'arrow-spacing': 'off',
'block-spacing': 'off',
'func-call-spacing': 'off',
'@typescript-eslint/type-annotation-spacing': 'off',
// 换行交给Prettier
'linebreak-style': 'off',
'eol-last': 'off',
'padded-blocks': 'off',
'lines-around-comment': 'off',
// 运算符换行交给Prettier
'operator-linebreak': 'off',
// 属性换行交给Prettier
'object-property-newline': 'off',
'object-curly-newline': 'off',
'array-element-newline': 'off',
// vue模板格式全部交给Prettier
'vue/html-self-closing': 'off',
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/html-quotes': 'off',
'vue/attributes-order': 'off',
// —— import排序 ——
'import-x/order': ['error', {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true }
}]
}
},
// 针对Vue文件的额外规则
{
files: ['**/*.vue'],
rules: {
'vue/component-tags-order': ['error', {
order: ['script', 'template', 'style']
}]
}
}
];
4.3 Prettier 配置(.prettierrc.json)
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "ignore",
"vueIndentScriptAndStyle": false,
"endOfLine": "auto"
}
4.4 package.json 脚本
{
"scripts": {
"lint": "eslint . --ext .js,.ts,.vue",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix",
"format": "prettier --write \"**/*.{js,ts,vue,json,md,yaml,yml,css,scss}\"",
"format:check": "prettier --check \"**/*.{js,ts,vue,json,md,yaml,yml,css,scss}\""
},
"lint-staged": {
"*.{js,ts,vue}": [
"prettier --write",
"eslint --fix"
],
"*.{json,md,yaml,yml,css,scss}": [
"prettier --write"
]
}
}
4.5 husky + lint-staged 提交钩子
# .husky/pre-commit
npx lint-staged
4.6 CI 流水线 .gitlab-ci.yml
image: node:20.16.0-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
lint:
stage: test
script:
- npm ci --prefer-offline
- npm run format:check
- npm run lint
only:
- merge_requests
- main
五、效果数据
这套配置上线后,我统计了内部项目(120个Vue文件 + 56个TS/JS文件,共176个文件,约2.4万行代码)的实测数据:
5.1 CI耗时对比
| 阶段 | 改造前 | 改造后 | 降幅 |
|---|---|---|---|
| npm install | 6分20秒 | 3分12秒(npm ci缓存) | 49% |
| ESLint 全量 | 2分48秒(旧配置) | 18秒(flat config) | 89% |
| Prettier --check | 0(没跑) | 25秒 | - |
| 单测 | 18分22秒 | 18分22秒 | 0% |
| 构建 | 22分30秒 | 7分16秒(Vite 5.4 + 缓存) | 68% |
| 流水线总计 | 50分00秒 | 29分15秒 | 41.5% |
5.2 代码质量数据
- ESLint报错数:从198个降到0(存量代码中146个是格式问题被Prettier吸收,52个是真实质量问题已修复)
- CI因lint失败被打回次数:从每月15+次降到0(格式问题在pre-commit阶段就被拦截)
- code review耗时:从平均3小时/PR降到40分钟/PR。没有format噪音diff,review只关注逻辑
- 新人上手时间:从2天(研究配置文件)降到20分钟(跑一遍lint:fix)
5.3 性能提升的原因
ESLint 9自身的性能提升是一方面,但更多收益来自配置优化:
- 旧配置用了
eslint-plugin-prettier,每个文件在ESLint内部额外跑一次Prettier格式化。我测试过,单独这条占掉lint耗时60% - 旧配置 extends 链太长,反复加载插件。flat config只加载需要的东西
- PR级别检查:CI只对merge_requests运行format:check和lint,main分支跳过,省掉一半无效CI
- ESLint 9 默认用文件系统缓存,第二次跑lint快了近3倍
六、避坑指南
以下坑我全踩过,每一项都花过一天以上的时间排查。
坑1:eslint-config-prettier 版本错配
ESLint 9 + flat config 下,旧版 eslint-config-prettier@8.x 导出的还是旧式配置对象,直接放进flat config数组里会报 Cannot read properties of undefined。必须用 9.x,它导出的是真正的flat结构。
排查方法:在flat config里打印每一项的 name 和 rules 数量:
// 临时调试代码
for (const item of config) {
console.log(item.name || 'unnamed', Object.keys(item.rules || {}).length);
}
坑2:vue/max-attributes-per-line 旧规则失效
eslint-plugin-vue 9.x 的flat配置下,很多格式规则默认关闭,但如果你像我一样显式开启了 vue/max-attributes-per-line,它依赖 vue/html-indent 内部的解析器状态,两个规则一个关一个开会引发ESLint崩溃。直接全关,交给Prettier。
坑3:prettier.endOfLine 引发的换行风暴
Windows开发 + Linux CI 的组合,如果 endOfLine 设置为 lf,在Windows上git会把LF自动转成CRLF,导致PR里每个文件都显示「全部行已修改」。我设置为 auto 后,又在.gitattributes里强制:
# .gitattributes
* text=auto eol=lf
*.{js,ts,vue,json,md} text eol=lf
这个做法让仓库永远存LF,Windows本地checkout出来是CRLF,但git diff干净。
坑4:typescript-eslint 的类型信息检查卡顿
如果你需要跑 await 相关的类型规则(比如 @typescript-eslint/no-floating-promises),必须在 parserOptions.project 里指定tsconfig路径。但这会让lint速度慢3-5倍。我们做了双轨制:
- pre-commit和CI主流程只跑非类型检查规则,18秒跑完
- 每晚定时任务跑一次完整的类型检查规则,输出报告
别想着全量开启 projectService: true,在Vue + monorepo场景下会爆内存。
坑5:vue/multi-word-component-names 误报
企业项目里到处都是 index.vue、detail.vue 这种单文件名组件。vue3-recommended 默认要求组件名至少两个单词,报错一堆。直接在规则里关掉,不要用注释绕过——注释会污染代码。
其他容易误伤的规则:vue/require-default-prop(很多Boolean默认值就是undefined)、vue/no-v-html(CMS内容渲染必须用)。规则要按业务场景裁剪,不是越严越好。
七、总结
ESLint管代码质量,Prettier管代码格式,两个工具各干各的。别再让ESLint顺便格式化代码了,eslint-plugin-prettier是2024年最该删的依赖。ESLint 9的flat config是趋势,新项目直接从flat开始,别再看老教程写.eslintrc了。
这套配置我已经在三个项目落地,格式类报错归零,CI时间砍半,code review效率翻倍。配置文件的完整代码可以直接从文中复制使用。如果你们团队还在用Vue 2 + ESLint 8,思路完全一样,只是把 eslint-plugin-vue 换成 plugin:vue/vue2-recommended,其余照抄。