前端项目里最没意义又最耗精力的争论,大概就是“代码风格”。用两个空格还是四个空格、字符串单引还是双引、尾逗号加不加,这些讨论一旦进入 Code Review,体感就像在泥潭里摔跤。我接手过不少前后端项目,几乎每一个都把大量 review 时间浪费在“这行缩进不对”“这里怎么多了一个逗号”上。后来统一引入 Prettier,这类争论基本归零。Prettier 的定位很简单:它是一个强约定的代码格式化工具,你给它配置,它把仓库里所有代码按同一套规则重新“打印”出来。配置 Prettier 这件事,表面看是写一个 JSON 文件,实际真正值钱的是理解每个配置项背后的取舍,以及如何把它接进编辑器、提交钩子和 CI,让格式化成为一件不需要人肉维护的事。这篇文章我会从它的工作原理讲起,逐项拆解核心配置,再讲配置文件组织、工程化集成和高频踩坑,适合刚接触 Prettier 的开发者,也适合想彻底梳理团队格式化流程的人。
1. 先搞清楚 Prettier 在项目里到底扮演什么角色
1.1 它不只是在“整理代码”,而是在“重新打印代码”
很多人把 Prettier 想成一个“高级格式化工具”,觉得它跟编辑器自带的格式化差不多。这个理解不够准确。Prettier 的工作方式,是把源码解析成 AST(抽象语法树),然后完全忽略你原本的排版,按照内置的规则重新把代码打印出来。也就是说,它不关心你原来怎么换行、怎么缩进、引号怎么用,它只认代码的语法结构,然后输出一份“标准答案”。
这个设计带来的直接好处是:同样的代码,不管来自哪个开发者、不管在什么操作系统上、不管用什么编辑器保存,只要 Prettier 版本和配置一致,输出结果就完全一致。它不是靠匹配字符串做替换,所以不会出现“这种写法能格式化,那种写法格式不干净”的尴尬。
还有一点很重要:Prettier 严格保证格式化过程不改变代码语义。它只调整空白、换行、引号、尾逗号这些表面格式,不会去改逻辑。所以你可以放心地把它放在提交钩子里批量跑,跑完之后功能不该有任何变化。这一点也是我后来敢在几十个文件上直接prettier --write的底气。
1.2 Prettier、ESLint、EditorConfig 三者的分工不能混
我见过不少项目,ESLint 里配了一大堆缩进规则、引号规则,然后又装了 Prettier,两个工具反复打架,最后只能靠eslint-disable压制报错。这属于分工没想清楚。
我的理解很简单,三个工具各管一段:
- EditorConfig 负责“编辑器打开文件时的初始状态”,比如缩进宽度、编码、换行符,它管的是你还没写代码之前的编辑体验。
- ESLint 负责“代码质量和潜在问题”,比如未使用变量、隐式类型转换、危险语法,它更关注逻辑层面的健康度。
- Prettier 负责“代码输出格式”,只关心最终打印出来的样子好不好看、统不统一。
所以在配置层面,推荐的做法是把样式类规则全部交给 Prettier,ESLint 专注做质量检查。两者不是竞争关系,而是把“风格”和“质量”拆成两条独立的流水线。后面我会专门讲怎么用eslint-config-prettier关闭冲突规则,这一步做对了,你才会真正体会到省心。
2. 安装与环境准备:先把地基打好
2.1 本地安装,别用全局
Prettier 的安装非常轻量,前提是你机器上有 Node.js 环境。建议直接装 LTS 版本,Python 和 Java 项目的同学也别觉得和自己没关系,只要前端仓库里有 package.json,Prettier 就是标准的devDependencies成员。
在项目根目录执行:
npm i -D prettier # 或者 pnpm add -D prettier # 或者 yarn add -D prettier装完之后,可以用npx prettier --version验证一下。为什么强调本地安装而不是全局安装?因为 Prettier 的版本升级偶尔会改变默认输出行为,如果团队里有人全局装 2.x,有人全局装 3.x,格式化结果就会漂移,等于又回到了“每人一个风格”的老路。装在项目本地后,版本被package-lock.json或pnpm-lock.yaml锁住,所有人、所有 CI 机器拿到的都是同一版本,这才是配置可复现的基础。
2.2 VSCode 接入:保存即格式化
编辑器接入是使用频率最高的环节。VSCode 里装官方扩展Prettier - Code formatter(发布者 esbenp),然后在设置里做三件事:把默认格式化器设为 Prettier、开启保存时格式化、把跟 Prettier 无关的格式化器排除掉。
{ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } }这里有个很容易踩的坑:如果项目里还装了其他格式化扩展,比如 Beautify、JS-CSS-HTML Formatter,它们会抢任务。保存时看起来好像格式化了,但用的是另一套规则,Prettier 的配置形同虚设。判断方法很简单,格式化后看状态栏右下角显示的是哪个工具,或者干脆把无关格式化扩展禁用掉。另一个细节是,VSCode 的formatOnSave只对当前语言生效,如果你在.vue文件里发现格式化不生效,记得确认有没有给 Vue 文件的默认格式化器也配置成 Prettier。
2.3 命令行格式化:最朴素的兜底方式
编辑器格式化适合日常开发,但总有一些场景需要命令行兜底,比如批量整理历史遗留文件、在 CI 里做检查。Prettier 提供了两个高频命令:
npx prettier --check "src/**/*.{js,ts,vue,json,css,md}" npx prettier --write "src/**/*.{js,ts,vue,json,css,md}"--check只检查不修改,适合 CI;--write直接改写文件,适合本地批量整理。注意 glob 字符串一定要加引号,否则部分 shell 会自动展开,行为会跟预期不一致。命令行用得少没关系,但它是最可靠的兜底手段,编辑器插件偶尔抽风时,一句--write就解决问题了。
3. 核心配置项逐项拆解:每个参数都代表一个团队决策
3.1 基础风格:printWidth、tabWidth、useTabs、semi
先看一份我常用的基础配置,后面逐项解释:
{ "printWidth": 100, "tabWidth": 2, "useTabs": false, "semi": true, "singleQuote": true, "quoteProps": "as-needed", "trailingComma": "all", "bracketSpacing": true, "bracketSameLine": false, "arrowParens": "always", "proseWrap": "preserve", "htmlWhitespaceSensitivity": "css", "endOfLine": "lf", "embeddedLanguageFormatting": "auto", "vueIndentScriptAndStyle": false }printWidth是新手最容易误解的配置。它的默认值是 80,我见过很多人直接改成 200,以为这样所有代码就会排成一行或很少换行。实际上 Prettier 的算法是:当一行代码超过printWidth时,尽可能寻找最佳换行点把它拆开;如果代码没超过阈值,它不会硬凑长度,也不会把短行强制填满。它是一个“触发换行的阈值”,不是“目标行宽”。比如一行字符串字面量太长,Prettier 不会自动拆字符串,所以调高这个值对超长字符串没有意义。团队里我建议先定 100 或 120,再扫一遍实际效果,不要一味求大。
tabWidth和useTabs要放一起说。useTabs: true代表用 tab 字符缩进,tabWidth决定这个 tab 显示成几个空格。现在主流前端项目几乎都是useTabs: false,也就是纯空格缩进。为什么?因为 tab 在不同编辑器、不同终端里的显示宽度不一致,空格的渲染是完全确定的。至于 2 空格还是 4 空格,没有技术上的对错,团队统一就好。但注意不要用 4 空格配printWidth: 120,那样代码会非常宽,嵌套一深就得频繁换行,观感很差。
semi是分号开关。true就是行尾都加分号,false就是能省则省。这个配置表面是一个二元选择,实际牵扯到团队的编码习惯和周边工具。完全取消分号依赖的是 JavaScript 的自动分号插入机制,绝大多数情况下没问题,但碰到[、(、模板字符串开头的下一行时,偶尔需要手动找补。我的经验是,新项目如果从第一天就用semi: false,配合 ESLint 的no-unexpected-multiline规则,没太大负担;老项目或团队里新手多,保守一点用true更省心。这个决定没有绝对正确答案,重要的是别让两个偏好并存于一个仓库。
3.2 引号与尾逗号:singleQuote、quoteProps、trailingComma
singleQuote是单双引号的选择。很多团队用单引号,是因为 JS 生态里字符串拼接和模板字符串使用频率高,单引号输入成本略低。但这里有个隐蔽的大坑:singleQuote只影响 JavaScript、TypeScript 里的字符串,不影响 HTML 属性。Vue 模板里:title="'hello'"这种写法,属性外用双引号、内嵌字符串用单引号,Prettier 不会因为你设置了singleQuote: true就把 HTML 属性改成单引号。所以你不要在.vue文件里看到双引号就以为是配置没生效。
quoteProps管的是对象属性名是否加引号。默认as-needed表示属性名是合法标识符就不加引号,比如{ name: "x" };如果属性名带着连字符或数字开头,比如{ "data-id": 1 },就必须加引号。consistent模式更严格一些:如果同一个对象里有一个属性需要加引号,那其他属性的引号也要统一加上。我建议默认as-needed就够了,它符合大多数人的阅读习惯。
trailingComma在 Prettier 2.0 之后默认就是all,但很多老团队的配置还停留在es5。两者区别就一句话:es5在对象、数组的末尾加逗号,但不在函数参数列表末尾加;“all” 连函数参数尾逗号也加。尾逗号看起来是细节,实际上对 git diff 的影响非常直观。举个例子,一个对象有三个字段,每行一个,没有尾逗号的时候你要新增第四个字段,就得顺手去第三行末补逗号,git diff 会显示“第三行被修改、第四行被新增”。有尾逗号的时候,新增字段只会显示“第四行被新增”,review 时能少看很多噪音。运行环境方面,现代 Node 和浏览器对函数尾逗号的支持没有任何问题,除非你要兼容老到掉牙的 IE 或低版本 Node,否则直接all。
3.3 括号风格:bracketSpacing、bracketSameLine、arrowParens
bracketSpacing控制对象字面量的大括号内侧是否留空格。true会输出{ foo: 1 },false会输出{foo: 1}。这纯粹是审美偏好,但我发现很多后端转前端的同事更习惯{foo: 1},因为 Java 或 Go 里都是这种风格。前端项目倒是更偏向带空格,因为有空格时对象和代码块在视觉上更容易区分。
bracketSameLine是 JSX、Vue、HTML 里的>符号位置。默认false表示标签跨多行时,>单独放一行:
<div className="wrapper" onClick={handleClick} > ... </div>如果设成true,则会把>跟最后一个属性放在同一行:
<div className="wrapper" onClick={handleClick}> ... </div>这个选项没有对错,纯粹看团队习惯。我个人的感受是,属性多时false更清晰,因为你可以很快扫到闭合>的位置;属性少时true更紧凑。关键是选一个全仓库统一的。
arrowParens也容易让人困惑。默认always会输出(x) => x;有人改成avoid之后发现x => x很清爽,但紧接着就遇到另一个困惑:为什么某些单参数箭头函数还是带了括号?答案是 Prettier 在语法不允许省略括号的场景下会强制加括号,比如参数带类型注解(x: number) => x,或者参数有默认值的时候。这不是配置失效,是语法约束。我的建议是新项目直接用always,因为一旦函数后面要加第二个参数,x =>还得手动改成(x, y) =>,不如一开始就统一带括号,减少无意义的 diff。
3.4 特殊文件和换行策略:proseWrap、htmlWhitespaceSensitivity、endOfLine、embeddedLanguageFormatting
proseWrap是专门针对 Markdown 文本的折行策略,默认preserve。很多人在整仓执行prettier --write .之后发现所有.md文件面目全非,多是因为配置里随手写了proseWrap: "never"。never会把段落里的所有内容拼成一行,整个 diff 变成一团乱麻;always会按printWidth重新折行,如果原始文档是手工换行的,同样会产生巨量变动。对文档类仓库,我强烈建议用preserve,让 Prettier 只管 Markdown 的列表、标题、代码块格式,不要干预段落内的换行。
htmlWhitespaceSensitivity是 Vue/HTML 项目里容易暗流涌动的配置。默认css表示按 CSS 的默认white-space行为决定空白是否有意义;strict表示保留所有敏感空格;ignore表示忽略空格差异。你可能遇到过的现象是:Vue 模板里两个组件标签之间如果写了换行和缩进,渲染出来的文本节点会多一个空格。换成strict之后空格会严格保留,但格式化输出会变得很碎;换成ignore之后格式化很干净,但运行时可能丢掉你本意想保留的空格。常规项目用默认css就行,组件库这类要求精确控制的,建议显式strict,并且记得在模板里慎用修饰空白的小技巧。
endOfLine是我在所有跨平台项目里最想先定下来的配置,默认auto会根据现有文件自动推断换行符。Windows 上文件通常是 CRLF,mac/Linux 是 LF,于是经常出现“我只改了一行代码,git diff 却显示整个文件都变了”的经典惨案。解决办法是统一设成lf,同时做两件配套事:在.editorconfig里写end_of_line = lf,在.gitattributes里写*.js text eol=lf。这样无论是 Windows 还是 Mac,拉下来的代码、格式化过的代码、提交上去的代码,换行符始终一致。这一步不做,endOfLine配了也容易被 Git 的 autocrlf 设置反复“修正”。
embeddedLanguageFormatting管的是代码片段嵌套格式化。Prettier 会识别模板字符串里的 HTML/CSS 代码,或者 Markdown 代码块里的 JS 代码,默认auto会一并格式化。这个能力很多时候很贴心,但也有翻车场景:别人写的示例代码可能是故意保持某种样式,或者模板字符串里拼接的是动态 SQL,格式化后反而不好看。如果你不需要这种“递归格式化”,就把embeddedLanguageFormatting设成off,更可控。
4. 配置文件组织术:从零散到可维护
4.1 配置文件格式与查找规则
Prettier 支持多种配置方式:写在package.json的"prettier"字段,或者独立文件.prettierrc、.prettierrc.json、.prettierrc.yaml、.prettierrc.cjs,还有prettier.config.js。查找规则是:从当前源文件所在的目录开始,逐级向上查找最近的配置。也就是说,你在packages/admin里放一个.prettierrc.json,那这个包里单独配置是生效的,根目录的配置对它无效。
我的建议是,一个普通项目只在根目录放一个.prettierrc.json,不要同时摆好几种配置文件。之前接过一个项目,根目录有.prettierrc,包里又有package.json里的 prettier 字段,两个文件内容还不一样,排查格式化结果为什么跟配置不一致就花了大半天。配置文件多了之后,代码格式化行为就像薛定谔的猫,不到保存那一刻永远不知道是哪种规则。
如果项目package.json里声明了"type": "module",用.js做配置文件会遇到 CommonJS 语法报错的问题,这时候用.prettierrc.cjs最稳妥。.cjs还允许写注释,适合把每个选项的决策理由写下来给团队看,这是个容易被人忽略的价值。另外,因为配置文件本身就是 JSON 或 JavaScript 文件,它会被 Prettier 自己格式化,所以这里也能顺便验证一下你的配置是否生效。
4.2 .prettierignore 和 overrides:不是所有文件都该被格式化
我见过很多团队精心设计了.prettierrc.json,却完全没建.prettierignore,结果某天有人执行npx prettier --write .,把dist、node_modules或各类 lock 文件搅得天翻地覆。Prettier 确实有部分默认忽略,比如node_modules是写死的,但它不会自动忽略dist、build、coverage,更不会放过package-lock.json这种“长得很像 JSON 但不能碰”的文件。
.prettierignore的语法和.gitignore几乎一样,一个基础示例:
node_modules dist build coverage public/vendor package-lock.json pnpm-lock.yaml yarn.lock *.min.js *.min.css这里专门强调 lock 文件:package-lock.json虽然扩展名是 json,但它的格式化规则由 npm 自己控制,Prettier 跑一遍之后会引入大量无意义的 diff,严重时还会让后续npm ci认为锁文件不一致。pnpm-lock.yaml同理,老老实实加进 ignore 名单。
overrides适合处理“同一仓库里不同文件类型需要差异化配置”的场景。比如你希望普通代码用printWidth: 100,但 Markdown 文档保持原样不折行;希望 Vue 模板用更严格的空白策略。示例:
{ "overrides": [ { "files": ["*.md", "*.mdx"], "options": { "proseWrap": "preserve" } }, { "files": "*.vue", "options": { "htmlWhitespaceSensitivity": "strict" } }, { "files": ["*.yml", "*.yaml"], "options": { "tabWidth": 2 } } ] }overrides的files支持 glob 通配,规则从上到下匹配。这就解决了“全局一套配置,特殊文件特殊处理”的需求,不需要为每一种文件单开一个配置文件。
5. 工程化整合:让格式化变成自动化流程
5.1 和 ESLint 和平共处:eslint-config-prettier
前文已经说过 Prettier 和 ESLint 的分工。真正落地时,你还需要一个叫eslint-config-prettier的包,它的作用是把 ESLint 里所有跟代码风格重叠的规则一次性关掉。如果不关,ESLint 会报“字符串应该用双引号”“缩进应该是 4 空格”这类错误,和 Prettier 的格式化结果对着干。
安装和接入很简单:
npm i -D eslint-config-prettier如果你的 ESLint 还在用旧的.eslintrc体系,在配置文件的extends数组末尾加一项:
{ "extends": ["some-config", "prettier"] }注意prettier一定要放在最后,因为它要覆盖前面所有规则。如果你的项目已经切换到 ESLint 9 的 flat config,可以在配置数组最后展开这个包:
import eslintConfigPrettier from "eslint-config-prettier"; export default [ // ...其他配置 eslintConfigPrettier, ];这里顺便说一句,网上能搜到eslint-plugin-prettier,它把 Prettier 当作 ESLint 的一条规则来跑,我不太推荐在常规项目里用它。一是性能差,每次 lint 都要跑一遍完整格式化;二是报错信息非常绕,“Delete␍”这种提示新手根本看不懂;三是职责又混回去了。我更推荐“编辑器用 Prettier 插件做格式化、CI 用prettier --check做检查、ESLint 只做质量检查”的三线分离模式。
5.2 Husky + lint-staged:提交前自动格式化
格式化链条里最重要的一环,是在代码进入仓库前把格式处理好。实现方式是:借助 Husky 在pre-commit阶段触发lint-staged,只对暂存区里的文件跑格式化。这样不会因为你跑了一次全仓--write把无关文件都卷进来。
安装:
npm i -D husky lint-staged npx husky initnpx husky init会创建.husky/pre-commit文件,然后在里面写入:
npx lint-staged接着在package.json里加一段配置:
{ "lint-staged": { "*.{js,ts,jsx,tsx,vue,json,md,css,scss,html}": [ "prettier --write", "eslint --fix" ] } }命令顺序我踩过坑。一开始我把eslint --fix放在前面,结果 ESLint 修完问题之后,输出的格式又跟 Prettier 不一致,然后再跑一次 prettier 虽然修好了,但多了一轮写文件、重新加入暂存区的过程。现在的做法是把prettier --write放第一,eslint --fix放第二。因为eslint-config-prettier已经关闭了样式冲突,eslint --fix改的主要是逻辑层面的问题,不会再次破坏格式化结果。如果你遇到eslint --fix之后格式又乱了,那说明你的 ESLint 配置里还有没被关干净的样式规则,优先检查这里。
lint-staged还有一个容易被忽略的细节:它默认只处理暂存区文件,但同一个文件在你有未暂存改动时会怎样?它会临时 stash 已暂存的部分,只对暂存内容做处理,处理完后把剩余改动恢复回来。绝大多数情况是安全的,但记住一点:先git add再提交,不要把所有改动和暂存改动混在一起。
5.3 CI 检查:给格式化上一道保险
提交钩子是保护开发者的第一道防线,但总有人会用--no-verify强行绕过,也总有人直接合并不规范的第三方分支。所以在 CI 里增加一步prettier --check,是最后兜底。
命令极其简单:
npx prettier --check .GitHub Actions 示例:
name: format-check on: [push, pull_request] jobs: check-format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx prettier --check .--check只报告哪些文件不符合格式,不会修改文件,所以 CI 环境里非常安全。如果你在本地想快速看差异,可以先prettier --write格式一遍,看一眼 git diff,再把改动恢复掉。反正有 Git 兜底,随便试。
6. 高频问题与排查技巧实录
6.1 Prettier 不生效的时候,按这个清单来
我总结了一张高频排查表,遇到“格式化失灵”先对照一遍:
| 症状 | 常见原因 | 解决方案 |
|---|---|---|
| 保存文件后样式没变 | 编辑器默认格式化器不是 Prettier | 把editor.defaultFormatter设为esbenp.prettier-vscode |
| 只有部分语言生效 | 该语言被其他格式化器接管 | 检查[typescript]、[vue]等语言段的 defaultFormatter |
| 命令行格式化没反应 | glob 没匹配到文件,或文件被.prettierignore排除 | 用npx prettier --write 具体文件验证;检查 ignore 规则 |
| 配置改了但输出没变 | 项目里存在多个配置文件,最近的配置覆盖了根配置 | 只保留一份配置文件 |
| 单引号没生效,HTML 属性还是双引号 | 误以为singleQuote影响 HTML | 明确singleQuote只作用于 JS/TS 字符串 |
| 跨平台 diff 全是换行符 | endOfLine未统一 | 配置lf+.editorconfig+.gitattributes |
.astro、.svelte文件不支持 | 这些语言需要额外插件 | 安装对应的prettier-plugin-*并注册到plugins |
这些坑我基本都亲自踩过一遍。特别要提醒的是“多个配置文件”这条,排查起来最隐蔽,因为它不报错,只是静默地让你觉得“配置没用”。处理方式是全局搜索项目里所有.prettierrc*、prettier.config.*以及package.json里的 prettier 字段,只保留一份,其余删除。
6.2 版本升级后全仓大 diff 怎么办
Prettier 3.x 相比 2.x 有一些默认行为和插件 API 的变化,升级之后整仓格式化结果发生漂移是正常的。但不代表没有应对办法。
如果你打算从 2.x 升到 3.x,我的建议是不要直接npm i -D prettier@latest然后顺手--write。正确的流程是:先在分支上升级版本、锁好package-lock,然后跑npx prettier --check .收集差异文件列表,评估改动量;接着用--write把全部文件格式化一遍,单独提一个 commit,这个 commit 里只包含格式化,不夹带任何功能修改;最后再让团队 review 时把这个 commit 跳过或快速确认。
这里有一个实用小技巧:升级后如果 diff 巨大且难以区分“格式化变化”和“逻辑变化”,可以用git diff -w忽略所有空白差异,先确认逻辑层面没有意外改动。干净升级之后,后续的格式化 diff 就只来源于新增代码了。
6.3 团队协作中的几个实际建议
配置 Prettier 这件事,技术上不难,难的是让团队形成共识。我最后分享几条实战心得:
第一,配置变更和代码功能变更必须分开。我曾经见过一个 commit 改了.prettierrc.json的singleQuote,同时又修了一个业务 bug,review 时所有人都被几千行引号变化淹没,真正的 bug 反而看漏了。格式化配置的调整应该单独一个 commit,最好在 PR 描述里写清楚“本次只有格式变化,没有逻辑改动”。
第二,不要频繁改动格式化配置。格式化工具的收益是“长期一致”,每一次风格变更都要付出整仓代码变动和 review 成本的代价。除非有很强的理由,否则定下来就别动。
第三,给新成员准备一份极简的团队约定文档。内容不用长,写清楚“编辑器装哪个插件、保存时格式化开没开、commit 钩子会不会自动处理”即可。很多新人遇到的问题不是不会写配置,而是不知道格式化是自动的,于是手动改缩进、手动补引号,反而破坏了格式。
最后分享一点我的使用习惯
我现在接手一个新项目,标准动作就是:装本地 Prettier、根目录放一个.prettierrc.json、写好.prettierignore、配好 VSCode 保存时格式化、装上 Husky 加 lint-staged、CI 里挂一个prettier --check。这套流程走完,格式化这件事基本就从心智负担里消失了。我记得早期某一次,团队里几个人因为半自动分号的问题在群里争论了很久,后来定下semi: false并全部交给 Prettier 处理,再看 git 记录就干净多了。格式化工具存在的意义,不是让某一种风格胜出,而是让所有人停止讨论风格,把精力留给真正的代码逻辑。希望这篇文章能帮你把 Prettier 配置成项目里最省心的基础设施之一。