前阵子接手一个Vue 3 + TypeScript项目的代码审查,功能全是好的,就是代码风格没法看:有人用单引号有人用双引号,缩进从上到下能跳出三四种花样,组件名有的PascalCase有的kebab-case,还有几处没删干净的console.log。整个项目看起来像一个多人各写各的拼盘。要解决这种混乱,靠的不是某一个人的自律,而是给项目装上ESLint + Prettier这套代码格式校验管线。这篇文章就把我自己的完整配置方案摊开讲:从脚手架怎么选、配置文件每一行是什么意思、Prettier和ESLint怎么分工,到编辑器保存、提交钩子、CI检查怎么一气呵成。不管你是刚接触Vue 3的新手,还是被历史代码格式问题折磨的老开发,照着这份方案走,都能在半天内把项目从“能跑”收敛到“整齐”。
1. 这套组合分工在哪:Lint管“错”,Prettier管“丑”
1.1 为什么两个工具缺一不可
很多刚开始配的同学会问:ESLint不是也能管格式吗?为什么还要单独装Prettier?
这个问题确实问到点子上了。ESLint的定位是“代码质量检查器”,它发现的是逻辑层面和潜在错误层面的问题:声明了没用的变量、隐式any、在Vue模板里用了没有注册的组件、switch分支漏掉default等等。它确实也带了一部分风格规则,比如indent、quotes、semi,但ESLint自己的风格规则设计初衷是给团队“够用就行”,真正做排版时它的能力远不如Prettier细致。
Prettier的定位则完全不同:它是一门“代码格式化器”,不关心你的代码逻辑对不对,只关心排版长什么样。引号是单还是双、行宽到多少换行、运算符两边要不要空格、对象最后一项加不加逗号,这些都用一套高度一致的算法处理,而且基本没有复杂的分支配置。
把两者放一起,分工就清晰了:ESLint负责告诉你“这里有BUG风险”“那里写法不规范”;Prettier负责在保存或提交时,把整个文件的外观统一刷一遍。打个比方,ESLint像审稿编辑,关注内容有没有事实错误;Prettier像排版工人,关注字间距、标点、行距是否统一。只请排版工人,错别字没人抓;只请审稿编辑,版式五花八门。Vue 3 + TypeScript这种文件结构天生复杂的项目里,两者缺一个都会出问题。
1.2 Vue 3 + TypeScript让复杂度上了一个台阶
如果是纯JavaScript项目,装上ESLint选个社区风格就基本完事了。但引入TypeScript后,解析器就变了:TS文件用的是专门的TypeScript解析器,它才能读懂类型标注、接口、泛型。引入Vue后更麻烦,.vue文件本身不是标准JS,它是单文件组件(SFC),内部有template、script、style三大块,ESLint默认的解析器压根没法直接解析。
所以就有了“解析器套娃”:外层用vue-eslint-parser去拆解.vue文件的三个块,script块内部再交给TypeScript解析器去理解TS语法。这也是为什么Vue 3 + TS的ESLint配置经常让人一头雾水——你会看到extends一长串,parserOptions里还有一个parser字段。搞清楚这个套娃结构,后面读任何人的配置都不会懵。
这一层复杂度不搞清楚,最常见的后果是:新建的.vue文件在编辑器里满屏红色波浪线,不是代码有错,而是配置里根本没人告诉ESLint该怎么读这个文件。这类问题排查起来最磨人,因为它不像语法错误那样有明确提示,纯粹是配置层面的“语言不通”。
2. 最快的启动方式:用官方脚手架生成一份标准配置
2.1 create-vue交互项里怎么选
如果你是新建项目,我强烈建议直接用官方脚手架起步,不要一上来就手动装依赖。Vue团队维护的create-vue在交互式提问里已经包含了ESLint、Prettier的支持,它生成的配置虽然不一定百分之百符合每个团队的审美,但结构是目前社区公认最干净的范本。
创建项目的命令很简单:
npm create vue@latest按提示填完项目名后,会进入一系列选择。和代码校验相关的几项建议这样选:
- TypeScript:选是;
- JSX:按实际需要;
- Vue Router / Pinia:按业务需要;
- ESLint:选是(核心项);
- Prettier:选是(核心项);
- 单元测试 / E2E测试:按业务需要。
如果项目已经建好了也没关系,往下看第3章手动配的方式,本质就是把脚手架生成的东西自己复刻一遍。理解生成物的来龙去脉,比单纯点Yes更有价值,因为你迟早要往里面加自己团队的规则。
2.2 生成出来的配置文件到底是什么意思
选完ESLint和Prettier后,项目里会多出一串文件:eslint.config.js、.prettierrc.json、.editorconfig,还有.vscode目录下的extensions.json和settings.json。这几个文件各司其职,我逐个说下它们到底是干嘛的。
eslint.config.js是核心。新版本脚手架生成的内容依赖三个东西:
- @vue/eslint-config-typescript:封装了TypeScript的解析与推荐规则;
- eslint-plugin-vue:提供所有Vue专属规则,比如组件命名、模板里v-if/else配对;
- @vue/eslint-config-prettier:本质上就是eslint-config-prettier在Vue生态里的封装,负责关掉所有和Prettier冲突的格式规则。
配置文件里会调用pluginVue.configs['flat/essential']和vueTsConfigs.recommended这两套预设,它们分别是Vue侧和TS侧的规则套餐。用了这套配置之后,新增的.vue文件会被正确解析,模板里的书写规范和TS文件的类型规范都能被检查到。
.prettierrc.json则接管外观风格。脚手架默认生成的是一个基本空壳,通常只有几个字段,但建议你打开它,把singleQuote、semi、printWidth这些团队共识写清楚,不然后面第一次格式化时会突然刷出几百个diff。这块我在第4章详细展开。
.editorconfig大家容易忽略,它管的是换行符和缩进宽度这类“编辑器行为”。它和Prettier配合,能让不同系统的同事打开文件时基础行为一致。脚手架生成的默认内容一般够用,不建议删。
.vscode下的两个文件也很关键:extensions.json会在团队成员打开项目时推荐安装ESLint和Prettier扩展;settings.json则把“保存时自动修复”这个关键体验提前配置好了。如果你的编辑器用的是VS Code,装好推荐的扩展后,新项目基本开箱即用。
3. 手动配置的真正细节:parser、extends、rules如何协作
3.1 新版flat config的eslint.config.js逐行讲解
对于已经跑起来的项目,手动配一次能帮你彻底搞懂前面说的“套娃结构”。ESLint 9已经全面转向flat config,也就是eslint.config.js这种单一导出数组的结构。我基于自己项目里比较顺手的一份配置,逐段讲它为什么这么写。
import { globalIgnores } from 'eslint/config' import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' import pluginVue from 'eslint-plugin-vue' import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' export default defineConfigWithVueTs( pluginVue.configs['flat/essential'], vueTsConfigs.recommended, globalIgnores(['**/dist/**', '**/coverage/**', '**/node_modules/**']), { name: 'app/additional-rules', rules: { 'vue/multi-word-component-names': 'off', '@typescript-eslint/no-explicit-any': 'warn', 'vue/no-v-html': 'off', 'vue/valid-define-props': 'error', }, }, skipFormatting, )先看defineConfigWithVueTs。它来自@vue/eslint-config-typescript,作用是把TypeScript规则配置正确关联到.vue文件的script块上。如果不用它,你手写数组时很容易漏掉.vue和TS文件的关联,导致parser接不上,这正好对应1.2节说的“解析器套娃”。
pluginVue.configs['flat/essential']是eslint-plugin-vue提供的flat配置。essential档位覆盖的是必查的可靠性规则,比如模板语法错误、事件命名错误。更严的类型还有strongly-recommended和recommended,团队想更激进可以换成recommended档。
vueTsConfigs.recommended对应TS侧推荐规则,比如@typescript-eslint/no-explicit-any默认是warn,未使用变量会报error。注意推荐档并不包括所有类型检查规则,它更偏向“不拖累构建速度”的那部分。
globalIgnores很简单,告诉ESLint哪些目录不要管。dist、coverage这类构建产物没必要扫描,写了反而拖慢lint速度。
接着是自定义rules对象。放在这个位置的对象,优先级会覆盖前面extends进来的预设规则。比如多数团队会把vue/multi-word-component-names关掉,因为Vue官方要求组件名至少两个单词,但业务里难免有Index.vue、Home.vue这种单文件组件;vue/no-v-html也常被关掉,因为做内容管理系统时渲染后端HTML文本是刚需。
最后一项skipFormatting,是@vue/eslint-config-prettier导出物。它放在数组末尾的意义是:用它把前面所有可能和Prettier冲突的格式类规则一次性关掉。顺序很关键——必须放在预设之后,否则前面规则又会把冲突项加回来。
3.2 老项目还在eslintrc时该怎么办
如果你的项目锁在ESLint 8,或者历史包袱没法切flat config,也不是不能用。老式.eslintrc.js的写法核心逻辑一样,只是形态更偏向对象:
module.exports = { root: true, env: { node: true, browser: true, es2021: true, 'vue/setup-compiler-macros': true, }, extends: [ 'plugin:vue/vue3-recommended', 'eslint:recommended', '@vue/typescript/recommended', '@vue/prettier', ], parserOptions: { ecmaVersion: 'latest', sourceType: 'module', }, }这里配置里出现了env字段:'vue/setup-compiler-macros': true,这是老配置里专门为script setup宏准备的,否则defineProps、defineEmits会被当成普通函数调用报“未定义”。新版flat config因为@vue/eslint-config-typescript内部已经处理了这个点,就不需要你手动开。
eslintrc写法方便的地方是层层extends的优先级很直觉:后面的 extends 覆盖前面的,你自己的 rules 字段直接写在对象里。不方便的地方是它有很多隐藏的注册名(比如@vue/prettier实际上是某个包在extends里的注册名),新手想搞清楚到底启用了什么规则,得逐个包翻文档。
flat config的哲学是把所有东西“显式化”:每个配置项就是一个对象,你自己决定放哪些规则、以什么顺序应用,没有隐式的extends。这也是我推荐新项目直接上flat config的根本原因,排错时能找到头。如果你升级到ESLint 9但目录里还留着.eslintrc.js,ESLint会直接提示找不到eslint.config.js,所以老项目迁移时要把旧文件清掉。
3.3 规则优先级与自定义rules的放置位置
flat config里的规则优先级遵循一个简单原则:后出现的对象覆盖先出现的。这也是为什么prettier兼容层要放在数组最后——它要把前面的格式规则全部关掉,必须最后执行。
那自定义rules放在哪?我的习惯是放在预设后面、prettier兼容层前面。原因很简单:自定义rules里我只会写语义类规则,比如某个Vue规则关掉、TS的any警告降级,这些不会被prettier兼容层影响。但如果有人把quotes、semi这类格式规则写进自定义rules,又放在skipFormatting后面,就会把prettier刚关掉的冲突项重新打开,格式校验直接失效。
一句话总结放置逻辑:预设放前面,语义类自定义规则放中间,prettier兼容层压轴;格式规则永不手写,交给Prettier和eslint-config-prettier处理。
4. Prettier与ESLint的边界处理:两个长得像的包怎么选
4.1 eslint-config-prettier和eslint-plugin-prettier完全不同
在处理好Lint之后,另一个最常见的困惑是:网上配Prettier时,有的文章让你装eslint-config-prettier,有的让你装eslint-plugin-prettier,还有的让你两个都装。这两个包名字就差一个词,作用天差地别。
eslint-config-prettier做的事是“关闭ESLint里所有和Prettier重叠的格式规则”。它的价值是让两个工具各管各的:ESLint只保留逻辑检查,排版全交给Prettier。这是一个配置层级的包,你只需要把它放进extends或flat数组的末尾,它不执行任何格式化逻辑。
eslint-plugin-prettier做的事是把Prettier作为一条ESLint规则跑起来,也就是说你执行eslint --fix的时候,它会在内部调用Prettier来格式化。看起来方便,一条命令全搞定,但实际体验有两处问题:一是每次lint都要先跑一遍Prettier,文件多了之后明显慢;二是报错信息比较啰嗦,ESLint的error列表里会混入大量prettier/prettier格式错误。把它当代码质量错误看,时间久了容易产生“反正都是样式错误,无视好了”的麻木心态。
我的观点很明确:新项目一律用eslint-config-prettier,不装eslint-plugin-prettier。格式的事交给Prettier自己,IDE里让Prettier扩展负责保存时排版,命令行里用prettier --write负责批量排版,提交钩子里让lint-staged调用Prettier。ESLint只回答一个问题:代码逻辑和规范有没有问题。
4.2 一份实用的.prettierrc和.editorconfig配合
Prettier本身的配置项不多,但恰恰是这些选项决定了团队代码的统一审美。下面是我比较常用的一份,复制过去基本不用改:
{ "$schema": "https://json.schemastore.org/prettierrc", "semi": false, "singleQuote": true, "printWidth": 100, "tabWidth": 2, "trailingComma": "all", "vueIndentScriptAndStyle": true, "endOfLine": "auto" }逐项说下为什么这么定。
semi设false是不加分号。这是JavaScript社区非常主流的风格,配合ASI机制跑得好好的,模板字符串、函数声明里几乎不会碰到陷阱。当然如果团队写惯了分号,也可以保留true,重点是全员完全一致。
singleQuote设true是因为单引号在视觉上更轻,键盘上不用按Shift。很多团队默认模板是双引号,但我个人建议新建项目就用单引号,老项目则看git历史里大多数情况,不要强行翻案。
printWidth设100是因为Vue SFC里template行本身就长,80会让模板属性换行非常频繁,反而更不易读。100是个比较平衡的值。
vueIndentScriptAndStyle是Vue专属坑:很多人在没设这个选项时,发现.vue文件里script和style标签内部的内容格式化后贴着最左对齐,而不是在标签内缩进。设成true之后,script和style里面的代码会整体保持两级缩进,这在视觉上跟template结构更协调。
endOfLine这里写auto,是为了避免Windows和macOS/Linux协作时换行符全被改掉。如果想让团队强制统一LF,也要在.editorconfig里同步配置,别只改Prettier这边。
.editorconfig是另一个容易被忽视的环节。一个典型的配置长这样:
root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false它能保证无论同事用什么系统、什么编辑器,基础缩进和换行行为从源头一致。注意Prettier和editorconfig偶尔会打架,比如editorconfig要求indent_size=4而Prettier里tabWidth=2,就会导致保存时文件在两个规范之间反复横跳。让两者的缩进设置保持一致,是避免“格式化打架”的第一步。
5. 把校验接到日常流:编辑器保存、提交钩子、CI检查
5.1 VS Code里保存就格式化且不打架的设置
配置写好了,剩下的关键是把“检查和格式化”嵌进日常开发动作里,否则靠人手动跑命令很快就会被遗忘。VS Code是Vue生态里用得最多的编辑器,这套设置我建议每个团队都在项目里固化下来。
首先,项目根目录.vscode/extensions.json要声明两个推荐扩展:
{ "recommendations": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode" ] }然后.vscode/settings.json里做三件事:保存时ESLint自动修复、Prettier担当默认格式化器、针对vue文件单独锁定。
{ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } }有几个细节必须注意。第一个,codeActionsOnSave里source.fixAll.eslint的取值,新版VS Code要求写成"explicit"而不是true,写true虽然旧版本兼容,但新版本会提示已废弃。第二个,因为Prettier已经作为默认格式化器工作,这里不要再装eslint-plugin-prettier,避免保存时ESLint和Prettier两个动作抢文件,形成改过来改过去的死循环。第三个,如果是老项目跑eslintrc,codeActionsOnSave的配置同样适用,但defaultFormatter这一项一定要检查是否有多个扩展抢占vue文件的格式化权限,Volar和Prettier都声明支持vue时会弹窗让选,选Prettier就行。
这套设置的核心思路是:ESLint负责修“错”,Prettier负责排“版”,两者在同一保存动作里各干各的,互不干涉。只要不引入插件层面的重叠,打架概率基本为零。
5.2 npm scripts 与 lint-staged 的配合
编辑器体验解决后,还要保证命令行和提交环节也能跑。package.json scripts里我建议至少放四个命令:
{ "scripts": { "lint": "eslint . --fix", "lint:check": "eslint .", "format": "prettier --write \"src/**/*.{js,ts,vue,json,css,scss,md}\"", "format:check": "prettier --check \"src/**/*.{js,ts,vue,json,css,scss,md}\"" } }lint用于本地顺手修复,format用于全量排版。lint:check和format:check是给CI用的,它们不做修改,只报告差异,保证流水线是可重复的。
提交环节最有用的组合是husky加lint-staged。husky负责管理git hooks,lint-staged负责只处理暂存区里的文件,避免把整个项目几万个文件都格式一遍。
安装和初始化命令如下:
npm install -D husky lint-staged npx husky inithusky init之后会在.husky目录生成pre-commit文件,默认内容是npm test,改成:
npx lint-staged然后package.json里加lint-staged配置:
{ "lint-staged": { "*.{js,ts,tsx,vue}": [ "eslint --fix", "prettier --write" ], "*.{json,css,scss,html,md}": [ "prettier --write" ] } }这个配置的逻辑是:JS/TS/Vue类型的文件先让ESLint检查修复,再让Prettier排一下版;其余纯样式、文档类文件直接交给Prettier统一格式。lint-staged会在每个命令执行成功后自动把修改过的文件重新git add,所以提交时暂存的内容永远是格式化后的版本。
5.3 提交前检查在CI的落地
本地有个痛点:同事可能漏装husky,或者改到一半直接提交。所以真正兜底的还是CI。脚本之前已经分好了,这里只需要在流水线里加两步:跑lint:check,再跑format:check。建议把这两步放在单元测试之前,因为格式错误应该最先被发现,成本最低。
另外一点,CI里千万不要用lint和format这两个带修改的命令。大多数CI环境的git checkout是只读工作区,--fix或--write如果想改文件,要么报权限错误要么让工作区变脏,最终导致后续步骤拿到被改动过的源码。固定用check版本,把差异作为失败原因反馈,是最稳的。
6. 实战中的坑与我的处理方式:从缩进到宏命令
6.1 解析器报错与script setup宏命令
我在接手一些历史项目时,最常见到的一类问题是:ESLint配置里只配了TypeScript解析器,没配vue-eslint-parser。结果打开.vue文件,顶上一行红色波浪线“Parsing error: Unexpected token”——ESLint根本读不懂template标签。
解决办法就一句话:.vue文件的外层解析器必须是vue-eslint-parser。flat config下你不需要手动写parser,因为pluginVue.configs['flat/essential']已经内置;eslintrc下你也不需要手动写,因为@vue/typescript/recommended已经内置。容易出问题的恰恰是那些从旧帖子复制配置、自己东拼西凑的人。
第二个常见问题是eslintrc环境下的script setup宏。老配置里经常看到defineProps、defineEmits下面一条“DefineProps is not defined”的红线。要处理它,需要在env里补上'vue/setup-compiler-macros': true,或者干脆升级到新版create-vue生成的flat配置,新方案内部已经处理宏了。如果你在flat config里还遇到类似报错,多半是@vue/eslint-config-typescript版本太旧,升级到最新版就行。
6.2 缩进、引号、换行符引发的大规模diff
配好Prettier后第一次跑format,项目里可能出现成百上千行diff。这个现象几乎每个接入Prettier的团队都会遇到,但往往不是规则写错,而是三个“底层审美”没统一:单双引号、分号有无、多行对象末尾是否加逗号。
如果团队没有既定的强约定,我的建议是接受Prettier默认值,别在配置文件里反复横跳。Prettier的默认风格虽然不一定人人喜欢,但那是被几十万项目测试过的稳定排版。你们可以约定“使用Prettier默认值”,省掉一半讨论时间。如果团队有明确偏好,记住一点:所有偏好都要写进.prettierrc,并且第一次format造成的diff最好单独一个commit,不要让后续功能提交夹带格式化痕迹,这样git blame才能保持干净。
换行符问题也值得单独说。如果团队里有Windows和macOS成员,git的core.autocrlf设置不一致时,明明只改了一行,diff里却是整个文件都被标记。Prettier里endOfLine设成auto只能减少一半问题,真正管用的是.editorconfig里统一end_of_line为lf,并建议git core.autocrlf保持一致。这里没有银弹,关键是全团队统一并写进自己的开发设备设置文档。
6.3 格式化循环与git预检的冲突
“保存时改一下,过两秒又改回去”这种循环,根源通常是两个formatter同时抢文件。最经典的组合是:代码里装了eslint-plugin-prettier,VS Code里又装了Prettier扩展且formatOnSave为true。保存时ESLint先跑,prettier规则把代码排成A风格;紧接着Prettier扩展又跑,排成B风格;下一秒ESLint又发现不符合A……就无限循环。
遇到循环,先自查组件关系:formatOnSave交给谁、codeActionsOnSave交给谁、有没有装冗余插件。按照第5章那套设置,循环基本不会出现。还有一个容易忽视的细节是多个ESLint配置文件的递归:子目录里藏着一个旧版.eslintrc,导致同一文件被两个配置处理,结果建议以globalIgnores把不需要的子目录排除掉。
提交钩子冲突是另一个坑:lint-staged里如果先跑prettier --write再跑eslint --fix,而项目里确实装了eslint-plugin-prettier,那么后面eslint又会把文件格式改一遍,这时候lint-staged判断文件内容变了,会拦截提交,提示“found modifications”。解决办法很简单:要么升级到eslint-config-prettier方案,要么把lint-staged里的命令顺序调整为先eslint后prettier,保证最后一步是排版终结者。我在第5章写的配置顺序,正是基于这个考虑。
最后提醒一个容易被忽略的点:添加Prettier之后,记得让团队成员重启VS Code并加载工作区设置。旧版本VS Code对codeActionsOnSave的值类型比较敏感,如果升级了VS Code还发现保存不触发,看看右下角是否提示插件未激活,手动重启一次就稳定了。
整套方案跑顺之后,我最明显的感受是:ESLint和Prettier存在的意义不是限制自由,而是把有限的脑力从排版琐事里解放出来。代码风格讨论是团队里最消耗感情的话题之一,有了统一工具,评审时看到的是逻辑和设计,而不是引号与缩进。如果你也打算给项目配上这套检查,别一步到位追求所有规则全开,先跑通基础管线,再把团队最在意的几条规则慢慢加进去,效果会比“一把梭”好得多。