ESLint+Prettier企业级配置:从冲突到提效
发布日期: 2026/08/01 阅读总量: 0

一、问题:配置了规范,代码还是乱

先交代背景。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-typescripteslint-plugin-prettierprettier,三套规则互相打架。更头疼的是CI流水线:push代码后GitLab Runner跑一次lint+test+build,要50分钟,一半时间耗在ESLint上。

我翻了翻 package.json,ESLint配置用的是老式的 .eslintrc.cjs,里面 extends 了四层:eslint:recommended@vue/typescript/recommendedplugin:vue/vue3-recommendedprettier,然后又被 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年的正确解法是:

  1. ESLint通过 eslint-config-prettier 把格式类规则全部关闭
  2. 格式检查交给Prettier的 --check 命令
  3. 提交前用 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 install6分20秒3分12秒(npm ci缓存)49%
ESLint 全量2分48秒(旧配置)18秒(flat config)89%
Prettier --check0(没跑)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里打印每一项的 namerules 数量:

// 临时调试代码
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.vuedetail.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,其余照抄。