☰
NuxtJS 项目 Prettier 配置、ESLint 协作与 CI 校验指南
2026/10/26 7:24:50 网站建设 项目流程

.prettierrc这个东西,写出来往往只有十几行 JSON,但它决定了一个 NuxtJS 项目里每一行代码最终长什么样。很多人第一次接触它,是在接手一个别人搭好的 Nuxt 项目时:git clone下来,改了两行.vue,保存,结果整个文件飘红一片,diff 里全是缩进和引号的变化,自己改的那两行被淹没在几百行噪声里。问题不在代码,在于这个项目有.prettierrc,而你的编辑器没有读到它,或者读到了但用的不是同一份。

我见过太多团队在 Nuxt 项目上把代码格式化当成"编辑器的事",结果就是每个人的保存动作都在制造别人的工作量。也有人走了另一个极端,把 Prettier 和 ESLint 的规则搅在一起,npm run lint一会儿报缩进,一会儿报引号,改完这头那头又错,最后干脆把 lint 从脚本里删掉。这两种做法都不可取。

这篇内容想解决的就是这一件事:在 NuxtJS 项目里,.prettierrc该怎么写、写到什么程度、和 ESLint 怎么分工、本地和 CI 怎么串起来、以及那些格式化跑完之后才冒出来的怪问题怎么排查。适合刚刚接手 Nuxt 项目、或者准备给团队统一代码风格的前端同学,也适合已经配了一版但总觉得哪里别扭的老手对照看看。下面所有配置和命令都基于 Prettier 3.x 与 Nuxt 3/4 的常见工程结构,版本差异我会单独点出来。

1. 先搞清楚:NuxtJS 项目为什么非要一份 .prettierrc

Nuxt 项目跟普通的 Vue 单页应用不太一样。一个.vue文件里同时住着三种语言:<template>是 HTML 方言,<script setup>是 TypeScript(还可能带宏),<style>是 CSS 或者 SCSS。再加上nuxt.config.ts、app.vue、pages/、components/、composables/、server/api/这一堆约定式目录,同一个仓库里散落着.ts、.vue、.json、.md、.mjs、.yaml五种以上后缀。这种混杂度下,靠编辑器默认行为去对齐格式,几乎必然失败。

1.1 编辑器各自为政时,diff 里全是无意义的换行

先说清楚失败的具体形态。同样是"保存时自动格式化",不同人的环境差别大到你想不到:有人装了编辑器插件,插件用的是全局安装的 Prettier 3.4;有人靠语言服务内置的格式化,走的是另一套规则;有人压根没开保存格式化,手动按快捷键,而快捷键绑的是内置格式化器;还有人用的编辑器版本较旧,对<script setup>的支持是残缺的,格式化一次能把泛型参数拆成三行。

这些差异在没有.prettierrc的项目里会被无限放大,因为 Prettier 找不到配置文件时会回退到内置默认值:printWidth是 80,semi是 true,singleQuote是 false,trailingComma在 3.x 里是"all"。而编辑器插件往往还有自己的设置项,比如「Prettier: Print Width」写在用户设置里,优先级又盖过项目配置。最终结果就是:同一个文件,A 保存后是 76 列换行、双引号、行尾逗号,B 保存后是 120 列、单引号、无行尾逗号。两个人轮流提交,这个文件就永远在两种形态之间来回横跳。

我印象最深的一次,是某团队里两个人前后脚改同一个 composable 文件,第三次提交的 diff 里 200 多行改动,真正有逻辑变化的只有一行if条件。review 的人看了十分钟才发现那一行,前面全在翻换行。

1.2 Prettier 和 ESLint 到底谁管什么

这是最容易搞混的一层。很多人配.prettierrc的动机是"我想让 ESLint 别再报格式错误了",方向就偏了。这两个工具的定位完全不同,混着用只会互相甩锅。

维度PrettierESLint
关注点代码长什么样(空白、换行、引号、括号位置)代码写得对不对(潜在 bug、约定、可维护性)
能否自动修复全量可自动重排只有部分规则带 fixer
对 AST 的处理打印式,把 AST 重新输出成文本遍历式,匹配模式后报错
典型规则printWidth、semi、singleQuoteno-unused-vars、vue/multi-word-component-names
会互相冲突的规则无(它只输出格式)所有indent、quotes、semi这类布局规则

看到最后一行就明白了:ESLint 里所有和"排版"相关的规则,本质上都在抢 Prettier 的活。@typescript-eslint/indent、quotes、semi、comma-dangle、vue/html-indent、vue/max-attributes-per-line这些,全都属于应该被关掉的那一类。所以一个干净的 Nuxt 项目里,ESLint 管逻辑和约定,Prettier 管长相,两边不重叠,谁也别去改对方的输出。

1.3 一次提交里混进格式化改动,比不格式化更难受

还有一个经常被忽略的动机:可回溯性。假设某天线上出了个诡异问题,你要git blame定位到某一行是谁改的、为什么改。如果这个文件曾经被一次"顺手格式化"整体重排过,blame 的结果会全部指向那次格式化提交,历史全部断掉。这个损失是隐性的,只有在排查线上问题时才会痛。

所以一个健康的做法是:格式化要配置化、可枚举、可批量执行,并且和业务提交分开。第一次给存量项目上 Prettier 时,单独提交一次"全量格式化",在提交信息里写清楚,之后所有人的增量提交里就不该再出现成片的格式改动了。这条规矩比配置本身更重要,后面第 4 章会讲具体怎么落地。

2. .prettierrc 的每个字段,在 Nuxt 语境下其实都在管一件具体的事

配置项本身不复杂,Prettier 官方给的就那么二十来个。真正难的是"选哪套值",以及"哪些选项在 Vue 单文件组件里有特殊表现"。我见过很多项目直接抄一份网上的配置,跑起来能跑,但团队里每个人都觉得别扭,最后谁都不愿意跑格式化。下面按优先级拆开讲。

2.1 semi、singleQuote、printWidth:先定三件事,别一次改十个

这三个是感知最强的,也是团队最容易吵起来的。我的建议是先定这三项,其余保持默认,跑一周再微调,不要一上来就写二十行配置。

printWidth默认 80,这个值在 Nuxt 项目里偏小。原因很直接:Nuxt 的definePageMeta、useFetch、navigateTo这类调用经常带对象参数,<template>里v-if加上@click再加上:class一行很容易超过 80 列,一旦超过就被拆成每个属性一行,一个按钮组件能撑出十几行。我的经验值是 100 到 120 之间,100 比较稳,120 会让部分 template 里的长表达式挤在一行不好读。这个值不用纠结到精确,团队统一就行。

semi和singleQuote属于纯偏好,没有对错。但有个细节值得注意:Nuxt 项目里经常处理 JSON 和.env之外的配置文件,单引号在 TypeScript 里没问题,但如果你有.json文件被 Prettier 处理,Prettier 会强制用双引号(这是 JSON 规范要求的,singleQuote对 JSON 无效)。所以别指望singleQuote: true能让 JSON 也变成单引号,那是做不到的事。

{ "$schema": "https://json.schemastore.org/prettierrc", "semi": false, "singleQuote": true, "printWidth": 100, "tabWidth": 2, "useTabs": false, "trailingComma": "all", "arrowParens": "always", "endOfLine": "lf" }

trailingComma在 Prettier 3.x 默认就是"all",写不写一样;但如果项目是从 Prettier 2.x 升上来的,默认值曾是"es5",升级后会出现一批"函数参数末尾多了逗号"的改动,这是正常的,别以为是配置错了。endOfLine我强烈建议显式写"lf",Windows 机器上如果不写,默认是"auto",会跟着文件现有行尾走,同一个仓库里就会出现混合行尾,git diff会诡异地显示整个文件都改了。

useTabs保持false。不是说 Tab 不好,而是 Nuxt 生态里绝大多数示例代码、第三方库源码都是空格,混用之后复制粘贴进项目,格式化会来回改,很烦。

2.2 专治 Vue 单文件组件:vueIndentScriptAndStyle 与 singleAttributePerLine

这两个是 Vue 项目专属选项,配置里不加,默认行为往往不是人想要的。

vueIndentScriptAndStyle默认false,意思是<script>和<style>标签内的代码不额外缩进,跟<template>处于同一层:

<template> <div class="page"> {{ title }} </div> </template> <script setup lang="ts"> const title = ref('首页') </script>

设成true之后,<script>里的内容会整体缩进两格。选哪个纯粹看团队历史习惯,但要注意:从false改成true会让所有.vue文件产生全文件级别的 diff,改之前最好确认这值得。我个人倾向于保持false,因为<script setup>里的内容往往是文件的主体,多一层缩进反而浪费横向空间。

singleAttributePerLine默认false,效果是:只要整行没超过printWidth,多个属性就并排放在一行:

<MyButton type="primary" size="large" @click="handleSubmit">提交</MyButton>

设成true之后,只要有多个属性,就强制每个属性一行:

<MyButton type="primary" size="large" @click="handleSubmit" > 提交 </MyButton>

对于组件属性特别多、还带条件渲染的页面(Nuxt 项目里很常见,一个卡片组件带七八个 prop 是常态),true会让 review 更清晰,因为每个 prop 的变化都只影响一行 diff;但对属性少、结构简单的项目,true会让文件长度翻倍。这个值我建议按项目模板复杂度来定,页面模板普遍超过 200 行的项目,开true。

还有一个容易被忽略的htmlWhitespaceSensitivity,默认"css",意思是 Prettier 会参考 CSS 的display值来决定空白怎么处理。这个默认值在大多数情况下是对的,不要轻易改成"strict"。"strict"会把所有空白都当敏感处理,<span>之间的换行会被保留成实际空格,页面上莫名其妙多出一个字宽的间隙,排查起来非常痛苦。

2.3 overrides 与 ignore:让 Prettier 认得 Nuxt 的构建目录

Nuxt 项目有一批"不是源码但会被格式化扫描到"的目录,最典型的就是.nuxt/(nuxi prepare或nuxt dev生成的类型和虚拟模块)和.output/(构建产物)。这两个目录里的文件动辄上千个.ts和.mjs,如果不忽略,prettier --write .会把它们全改一遍,而且下次构建又重新生成,纯属浪费。

.prettierignore的写法跟.gitignore基本一致:

# 依赖与产物 node_modules .nuxt .output dist coverage .nitro .cache # 锁文件不要格式化 pnpm-lock.yaml package-lock.json yarn.lock # 压缩文件 *.min.js *.min.css # 自动生成的类型 **/*.d.ts !types/**/*.d.ts

这里有两个坑要提醒。第一个是**/*.d.ts会把你自己手写的全局类型声明也忽略掉,如果项目里有types/目录存放手写类型,记得用!反向排除回来,就像上面最后两行那样。第二个是锁文件,很多人以为格式化锁文件没关系,但这些文件每次装包都会被重写,你格式化后的漂亮格式会在下一次pnpm install之后荡然无存,白折腾。

overrides用来给特定后缀单独设规则,在 Nuxt 项目里最有用的是 Markdown:

{ "overrides": [ { "files": ["*.md"], "options": { "proseWrap": "preserve", "printWidth": 80 } }, { "files": ["*.yaml", "*.yml"], "options": { "singleQuote": false } } ] }

proseWrap: "preserve"的意义在于:Markdown 里的段落长句默认会被 Prettier 按printWidth强行折行,这在中文文档里会把一句话拆成好几行,阅读体验很差,而且一旦有人改了其中一段,diff 会非常难看。设为preserve之后,Prettier 不改动段落内的换行,只处理列表和代码块。

另外,Nuxt 4 默认把应用代码放进了app/目录(srcDir变了)。如果你的项目在 Nuxt 3 和 Nuxt 4 之间迁移过,.prettierignore里那些按旧路径写的规则要跟着检查一遍,否则可能出现"该忽略的没忽略、该格式化的被忽略"的情况。

2.4 配置文件用 JSON 还是 JS:别在 ESM 上栽跟头

Prettier 支持好几种配置文件名:package.json里的prettier字段、.prettierrc(JSON 或 YAML)、.prettierrc.json、.prettierrc.js、.prettierrc.cjs、.prettierrc.mjs、prettier.config.js等等。Nuxt 3 之后创建的项目,package.json里通常有"type": "module",这一条直接把整件事的复杂度拉高了一档。

因为写了"type": "module"之后,.prettierrc.js会被当成 ESM 模块去加载,如果你在里头用了module.exports = {...},就会报module is not defined。反之,如果项目没有"type": "module",你写了export default {...}也会报错。这类报错通常只在 CI 或者别的同事机器上出现,本地因为缓存或者版本差异反而不报,特别难缠。

我的建议很干脆:能写 JSON 就写.prettierrc.json。它是纯数据,不涉及任何模块系统,谁读都一样,也不会有执行时机的问题。需要注释怎么办?加一行$schema指向 schema 文件,编辑器(VS Code 的 JSON 语言服务)会给你补全和悬停提示,比注释还好用:

{ "$schema": "https://json.schemastore.org/prettierrc" }

确实需要动态配置(比如根据环境变量切换,或者跟 ESLint 配置共用一个常量)时才上.prettierrc.mjs。但要注意一个高频事故:同一个项目里同时存在package.json的prettier字段和.prettierrc文件时,只有一个会生效,而你改的很可能不是生效的那个。我遇到过至少两次这种情况,同事在.prettierrc里改了半天没效果,最后发现package.json底部还留着一坨从模板里带过来的prettier配置。统一原则:一个项目只保留一处配置来源,其余全删。

3. 让 ESLint 和 Prettier 不打架:规则冲突的处理顺序

配置好了.prettierrc,下一步通常就是"为什么 ESLint 还在报缩进"。这一章讲的就是怎么把两条流水线并到一条上。

3.1 eslint-config-prettier 和 eslint-plugin-prettier 不是一回事

这两个包名字像,作用完全相反,装错了会浪费半天时间。

eslint-config-prettier是一个配置,作用是关掉 ESLint 里所有与 Prettier 冲突的规则。它本身不报任何错,只是把那些"管排版"的规则设为off。这是推荐用法。

eslint-plugin-prettier是一个插件,作用是把 Prettier 当作一条 ESLint 规则来跑(规则名prettier/prettier)。如果格式化结果不符合配置,ESLint 就会报一条错误。它的问题是:每格式化一次文件就要完整跑一遍 Prettier,在大项目里eslint .会明显变慢;而且它把 Prettier 的问题伪装成 ESLint 的问题,报错信息里出现prettier/prettier这种前缀,新人一看就懵。

我的做法是只装eslint-config-prettier,把 Prettier 作为独立命令跑,两边各司其职:

pnpm add -D prettier eslint-config-prettier

package.json的脚本:

{ "scripts": { "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint .", "lint:fix": "eslint . --fix" } }

format和lint:fix分开,是刻意的。合在一起会让人搞不清到底是谁改了代码,出问题时也很难二分定位。

3.2 Nuxt 的 ESLint 扁平配置里,prettier 该排在哪

Nuxt 生态现在主推@nuxt/eslint模块,它生成的是扁平配置(eslint.config.mjs),不再用.eslintrc。扁平配置的核心规则是后面的覆盖前面的,所以顺序极其重要。

eslint-config-prettier必须放在最后。原因很直白:它的唯一职责就是把布局规则关掉,如果它后面还有别的配置把indent重新打开,那它就白关了。正确写法:

// eslint.config.mjs import withNuxt from './.nuxt/eslint.config.mjs' import eslintConfigPrettier from 'eslint-config-prettier' export default withNuxt( { rules: { 'vue/multi-word-component-names': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }] } }, eslintConfigPrettier )

注意withNuxt()这个包装,它是@nuxt/eslint提供的入口,内部已经包含了一套 Vue + TypeScript 的推荐规则。把eslintConfigPrettier作为最后一个参数传进去,顺序就是对的。

对于还在用旧版@nuxtjs/eslint-config-typescript的项目,.eslintrc里的写法是extends数组最后一项放"prettier":

{ "extends": [ "@nuxtjs/eslint-config-typescript", "plugin:prettier/recommended" ] }

这里注意plugin:prettier/recommended其实同时做了两件事:加载eslint-plugin-prettier并且带上eslint-config-prettier。如果你只想要后者,就写"prettier"。升级到扁平配置时,这一行要手动拆开,别直接照搬。

3.3 规则打架时怎么一步一步缩小范围

哪怕配置顺序对了,偶尔还是会撞上。比如格式化之后 ESLint 报vue/max-attributes-per-line,或者反过来eslint --fix之后 Prettier 又报错。这类问题有一套很有效的排查路径,不用瞎猜。

第一步,先确认到底是谁在改代码。把文件用prettier --write跑一遍,git diff看看改动;再git checkout .还原,用eslint --fix跑一遍,再看 diff。两个 diff 有没有重叠区域?如果重叠在同一处(比如同一个标签的属性布局),那就是典型的规则冲突。

第二步,定位到具体是哪条规则。用eslint --print-config <file>输出这个文件最终生效的完整配置,搜一下indent、quotes、semi、max-attributes-per-line这几个关键词,看它们的值是不是off。如果不是off,说明eslint-config-prettier没生效,或者被后面的配置覆盖了。

npx eslint --print-config pages/index.vue > config.json

第三步,确认冲突的规则是否在eslint-config-prettier的关闭清单里。绝大多数 Vue 相关的布局规则它都管,但极少数新出的规则可能需要等配置包更新。遇到这种情况,直接在项目配置里手动关掉那一条,并在旁边写个注释说明原因,比升级依赖更省事。

这里分享一个经验:冲突绝大多数时候不是配置问题,是版本问题。eslint-plugin-vue从 v9 升到 v10、@typescript-eslint大版本更新,都可能引入新的布局类规则。所以升级这些依赖的时候,记得先跑一次prettier --check .建立基线,升级后再跑一次对比,能第一时间发现新规则带来的干扰。

4. 从本地到 CI:把"我这边格式化过了"变成可验证的流程

配置只解决了"标准是什么",没解决"谁来执行"。人是有惰性的,光靠自觉,三个月后格式又开始漂移。这一章讲怎么用工具把这件事焊死,同时别把开发体验搞得太难受。

4.1 lint-staged 与 pre-commit 钩子的正确写法

最常见的方案是husky+lint-staged:提交前只对暂存区里的文件跑格式化。只跑暂存文件这一点很关键,如果对整个仓库跑,提交一次要等十几秒,人就会开始找--no-verify绕过。

安装:

pnpm add -D husky lint-staged pnpm exec husky init

husky init会生成.husky/pre-commit文件并把prepare脚本写进package.json。然后把 pre-commit 内容改成:

# .husky/pre-commit pnpm exec lint-staged

lint-staged的配置放在package.json或者单独的lint-staged.config.mjs里。我倾向于单独文件,因为要写的规则会越来越多:

// lint-staged.config.mjs export default { '*.{js,ts,mjs,cjs,vue,json,md,yml,yaml}': ['prettier --write'], '*.{js,ts,vue}': ['eslint --fix'] }

这里有几个真实的坑。

第一,两个命令对一个文件的执行顺序。写成数组['prettier --write', 'eslint --fix']是串行的,顺序就是数组顺序,先格式化再 lint。如果反过来,ESLint 先跑完,Prettier 再把格式改掉,ESLint 的检查结果就不准了。顺序是先 Prettier 后 ESLint。

第二,husky的版本差异。v9 之后pre-commit文件里不再需要那段#!/usr/bin/env sh和. "$(dirname -- "$0")/_/husky.sh"的头部,直接写命令就行。很多教程还是老写法,照抄在 v9 上会出现警告或者直接不执行。

第三,lint-staged在多包仓库(monorepo)里的配置位置。如果 Nuxt 项目放在apps/web/下面,package.json在根目录,配置也要跟着分层,否则路径匹配会失效。这种情况建议每个子包各配一份,用--cwd参数区分。

还有一个很容易被忽略的点:钩子失败后暂存区会被修改。Prettier 和 ESLint 的--fix会直接改文件内容,但改动后的文件并没有被重新git add,结果是提交被中断,你需要重新git add再提交一次。这个体验很糟糕,lint-staged其实已经处理了这个问题(它会自动把修改后的文件重新加进暂存区),但前提是你写的命令是它认识的形式。如果你自己写了复杂的 shell 脚本,它可能识别不了,那就变成"文件被改了但没暂存",下次提交时改动还在工作区,很容易漏掉。

4.2 CI 上用 --check 卡住格式漂移

本地钩子可以被绕过,所以 CI 上必须有一道兜底。Prettier 提供的检查命令是--check,它不写文件,只在有文件不符合格式时返回非零退出码:

npx prettier --check .

输出大概是这个形式:

Checking formatting... [warn] pages/index.vue [warn] components/AppHeader.vue [warn] Code style issues found in 2 files. Run Prettier with --write to fix.

把这条命令放进 CI 的流程里,位置放在单元测试之前比较合适,因为它跑得快,能在前 10 秒就反馈问题,不用等测试跑完才发现格式不对。

大项目里还要考虑一个现实问题:几万个文件全量扫描,--check也可能跑十几秒。Prettier 3.0 加了--cache选项,可以把上次的结果缓存起来,只检查变化的文件:

npx prettier --check . --cache

缓存在node_modules/.cache/prettier下面。在 CI 里,需要把这个目录也缓存下来才能生效;如果 CI 每次都从零开始,加--cache意义不大,反而多了一层目录读写。本地开发时加--cache收益明显。

这里有个反直觉的建议:CI 里不要跑prettier --write。有些人为了"自动修复",在 CI 里直接--write再检查git diff是否为空。这个方案在 PR 场景下会很别扭,因为 CI 跑在一个临时环境里,改了文件也不会推回分支,最后只能打印一堆 diff 让人手动复制。老老实实--check就够了,修复是本地的事。

4.3 给存量项目做一次性全量格式化

老项目第一次上 Prettier,最怕的就是把格式化改动和业务改动混在一次提交里。有个稳妥的流程,我用了好几次都没翻车。

第一步,先把.prettierrc和.prettierignore写好,只提交这两个文件,提交信息写"新增 Prettier 配置"。这一步不动任何源码,可以先合进去,让所有人都拉到。

第二步,确认忽略规则是对的。跑一次prettier --check .,把输出里的文件列表看一眼:有没有.nuxt/、.output/下面的东西混进来?有没有不该格式化的第三方脚本?如果有,补进.prettierignore再跑。这一步花五分钟,能省掉后面一小时的回退。

第三步,单独建一个分支做全量格式化,prettier --write .,然后单独一次提交,提交信息写清楚"全量格式化,无逻辑变更"。这个提交不要夹带任何业务改动,review 时可以直接跳过。

第四步,把这次格式化提交合并后,在代码托管平台上配置一个"忽略该提交的 blame"(多数平台都支持在.git-blame-ignore-revs文件里列提交哈希,然后在 blame 视图里跳过)。

# .git-blame-ignore-revs # 全量格式化提交 a1b2c3d4e5f6...

第五步,启用 pre-commit 钩子和 CI 检查。到这一步,格式化的债务就算还清了,后面只需要维护增量。

还有个小提醒:全量格式化之后,如果有人还在用旧分支,合并时会有一大片冲突。所以这个操作最好挑在迭代间隙做,并且提前通知所有参与者,让大家尽快把手上分支合掉。

5. 格式化跑完之后才冒出来的问题:几段真实排查过程

配置都对了,命令也跑通了,问题往往从这一刻才开始。下面三段是我实实在在遇到过的,排查过程都完整写出来,你可以照着复现思路。

5.1 模板里的空白被改写,页面上多出奇怪的空隙

现象:全量格式化之后,某个商品详情页的标签行之间出现了细小的间隙,肉眼可见但 CSS 里找不到任何margin或padding的来源。对比格式化前的截图,就多了这么一点。

排查过程:先在浏览器开发者工具里选中那个间隙,看它归属哪个节点。发现间隙在两个<span>之间,而且不在任何一个元素内部,是纯文本节点。这就说明是 HTML 空白折叠造成的。

回头看 diff,格式化前是:

<span>{{ item.name }}</span><span v-if="item.tag">({{ item.tag }})</span>

同一行紧挨着写的,中间没有空白。格式化后变成:

<span>{{ item.name }}</span> <span v-if="item.tag">({{ item.tag }})</span>

中间多了一个换行。在正常的display: inline元素之间,一个换行会被折叠成一个空格(宽度约为一个字符的四分之一到一半),视觉上就是那个"奇怪的间隙"。

为什么 Prettier 会这么改?因为htmlWhitespaceSensitivity默认"css",它会读取 CSS 来判断空白是否敏感。但这里有个前提:Prettier 只能看到同一个文件内的<style>块里的样式,如果这个display: inline是在外部样式表或者 UI 框架的全局样式里定义的,Prettier 就判断不出来,只能按默认保守策略断行。

解决办法有三个,按推荐程度排:

  1. 给父容器加上display: flex或者font-size: 0+ 子元素恢复字号,从根上消除空白节点的影响。这是最干净的,也顺便解决了以后所有类似问题。
  2. 把这两个<span>用一个<template>或者包裹元素包起来,让 Prettier 没有机会在它们中间插入换行。
  3. 给这个文件单独配置htmlWhitespaceSensitivity: "ignore",或者在 Prettier 里用<!-- prettier-ignore -->注释包住这一段。
<!-- prettier-ignore --> <span>{{ item.name }}</span><span v-if="item.tag">({{ item.tag }})</span>

我个人更推荐第一种。prettier-ignore只是把问题藏起来,下次有人挪动这个区块,注释就跟着丢了。

5.2 忽略规则写漏,构建产物被格式化

现象:CI 突然报某个页面在构建时找不到模块,本地跑却正常。报错指向.nuxt/里的一个虚拟模块文件。

排查过程:先看 CI 的构建日志,发现构建前跑了一次prettier --check,通过了,说明格式没问题。再往上翻,发现 CI 的缓存步骤里有个prettier --write .——这是某次调试时临时加进去的,忘了删。

再看.prettierignore,里面写的是:

.nuxt .output

看起来没问题。但实际执行时,prettier --write .的匹配路径是相对于当前工作目录的,如果 CI 脚本先cd到了子目录再执行,那.nuxt这个相对路径就匹配不上了。另外还有个更隐蔽的情况:.prettierignore在 Prettier 3 里对目录的匹配规则跟.gitignore有细微差别,单纯写.nuxt只能匹配到根目录下的这个目录名,如果是apps/web/.nuxt这种嵌套路径,得写成**/.nuxt。

把配置改成:

**/node_modules **/.nuxt **/.output **/dist **/.nitro

再跑一遍,--check的输出里不再出现构建目录,问题消失。

这段经历给我的教训是:.prettierignore里的每条规则都要能被验证。最简单的验证方式是把--check的输出重定向到文件,然后 grep 一下那几个目录名,能搜到就说明没忽略干净:

npx prettier --check . 2>&1 | grep -E '\.nuxt|\.output|dist'

有输出就是漏了,没有就对了。这个检查我建议写进项目初期的一次性验证清单里,后面就不用反复踩了。

5.3 编辑器保存与命令行结果对不上

现象:某个同事保存.vue文件时格式正常,但一跑prettier --check就报这个文件不合规,--write之后文件又变了。他改完再保存,又变回去,陷入死循环。

这类问题基本都出在"编辑器用的不是项目里的那份配置"上。排查链路可以按下面这个顺序走,几步就能定位。

第一步,确认编辑器插件用的是哪个 Prettier。多数编辑器集成会优先用项目node_modules里的版本,但如果没找到,就会回退到全局安装或者插件自带的版本。插件设置里通常有个「Prettier: Prettier Path」或者「使用工作区版本」之类的选项,检查一下。

第二步,确认配置文件被正确解析。Prettier 提供了一个很实用的命令:

npx prettier --find-config-path pages/index.vue

它会输出这个文件最终使用的是哪个配置文件。如果输出的是空,或者指向了你意料之外的路径(比如~/.prettierrc或者某个父目录的配置),问题就找到了。Prettier 的配置查找是从文件所在目录向上逐级查找的,找到第一个就停。所以如果你在仓库外层的某个目录放了一份.prettierrc,它可能优先于项目里的那份生效。

第三步,确认没有多份配置并存。同时存在package.json的prettier字段、.prettierrc、prettier.config.js时,只有一份生效。用--find-config-path确认后,把多余的删掉。

第四步,确认编辑器没有自己的设置覆盖。VS Code 的prettier.printWidth、prettier.singleQuote这类用户级设置,在某些配置下会覆盖项目配置。最稳妥的做法是在项目根目录放一个.editorconfig+ 工作区设置,把格式化器指定死:

{ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } }

注意最后两行。Vue 文件在装了 Vue 语言服务之后,默认格式化器可能是语言服务自带的那个,而不是 Prettier。如果不显式指定,<template>部分走语言服务、<script>部分走 Prettier,同一个文件两种风格,特别诡异。

还有一个细节值得单独提:.editorconfig和.prettierrc的endOfLine要一致。如果.editorconfig里写了end_of_line = crlf,而.prettierrc写的是"endOfLine": "lf",每次保存都在两种行尾之间切换,git status会一直显示文件被修改但内容看不出区别。这种情况下git diff甚至什么都不显示,只有git diff --stat能看到文件被标记为改动,非常迷惑。统一用lf,两边一起改。

最后再分享一个小技巧:如果团队里有人长期用某款编辑器、又实在不想装插件,可以在项目里提供一个只做检查的脚本,挂到他习惯的快捷键上。格式化的本质是"让机器决定长相",只要最终提交的文件符合规则,中间用什么工具其实无所谓。真正要守住的底线只有两条:.prettierrc是唯一标准,CI 的--check是唯一裁判。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询