
1. 一个文件六种语言Prettier的格式化版图我在接手一个中大型前端仓库时常会在根目录看到一个薄薄的.prettierrc但它背后格式化过的文件却横跨JavaScrip、TypeScript、CSS、HTML、Markdown、YAML甚至偶尔还有GraphQL片段和Vue单文件组件。很多人对Prettier的认知停留在格式化JS的工具直到某天他们在项目里加入了一个.ya?ml文件或者一段内联在HTML里的style标签才发现这工具根本不是在美化代码而是在维护一套覆盖极广的语言格式化生态系统。这个认知差异恰恰是本文想聊透的事。Prettier的语言支持不是简单地把每种语言各写一个格式化函数而是一套完整的解析器体系加上统一的输出模型。它敢自称Opinionated Code Formatter底气来源于它对前端生态以及周边配置文件、文档文件的广谱覆盖。你能想象用一个工具同时让package.json、README.md、Dockerfile旁边的一份.gitlab-ci.yml、一个.scss样式文件、一段script langts里的TypeScript代码全都在同一套缩进、引号、换行风格下整整齐齐吗Prettier可以而且它处理这些语言时遵循的是同一套底层审美原则。这篇文章适合刚接触Prettier、想弄明白它到底能格式化哪些语言、每类语言要踩什么坑的前端开发者也适合那些正在做多语言仓库规范化、需要理解Prettier边界和扩展方式的工程效率负责人。我会从语言解析机制讲起逐个过一遍主流语言的支持情况和配置要点再把插件扩展和真实项目落地经验放进去。通篇会采用我实际用过的方式来讲不会只抄文档。先看一张我整理的官方内置语言支持速览表下面这篇文章里所有讨论都会反复提到它语言分类具体语种对应的parser取值JavaScript系JavaScript、ES2015、JSXbabel、espree、meriyah、acornTypeScript系TypeScript、TSXtypescript、babel-ts前端模板/组件Vue、Angular、HTMLvue、angular、html样式家族CSS、Less、SCSScss、less、scss数据/配置格式JSON、JSON5、JSONC、YAML、GraphQLjson、json5、jsonc、yaml、graphql文档格式Markdown、MDXmarkdown、mdx字符串/序列化字符串字面量JS里的字符串babel下的特殊处理这张表看起来平平无奇但里面藏着一个关键细节Prettier对语言支持的理解不是每种语言一个格式化器而是每一种语法都能被解析成AST然后统一交给同一个漂亮打印器Doc Printer输出。这才是它能做到跨语言风格一致的底层原因。你写JS时用的是双引号那写CSS时它也会尽量用双引号你段落缩进是2空格Markdown表格里的对齐也会按某种规则统一。这种一致性单独用一堆语言各自的格式化工具是拼不出来的。2. 解析器体系每种语言背后都有一套专用语法引擎2.1 为什么Prettier不自己造所有解析器Prettier不是一个啥都能解析的编译器。它的聪明之处在于把格式化拆成了两个阶段解析parse和打印print。解析阶段完全交给专门的解析器库打印阶段才由Prettier自己接管。对JavaScript它用babel/parserBabel的解析器处理现代语法用espree、meriyah等处理边缘场景。对TypeScript直接使用TypeScript编译器自带的解析能力而不是Babel转译后的产物。对HTML/Vue它依赖angular/compiler的html解析逻辑衍生出的htmlparser2等方案。CSS家族则基于postcss-scss、postcss-less这样的解析器加上postcss-values-parser处理属性值。这种复用专门解析器的思路保证了语法支持演进速度快。Babel对JavaScript新特性的支持天然就能辐射到PrettierTypeScript官方迭代新语法Prettier也能同步跟进。如果Prettier选择自己维护一套完整的文法定义那它至少要维护七八套词法/语法分析器工作量会直接把团队拖垮。2.2 从源码字符串到规整文本的完整流水线我尝试用最直白的方式描述Prettier的格式化流水线因为理解了这条链路你才能预测它会在哪一步卡住按文件扩展名或parser配置找到对应的解析器。解析器把源码字符串变成AST抽象语法树。AST里不再有缩进、换行这些纯排版信息只剩下结构。Prettier遍历AST用一种叫Doc的数据结构描述这段代码应该怎么排版。Doc是一棵指令树声明了哪些内容需要缩进、哪些位置要换行、哪些地方用引号或括号。根据命令行参数比如--print-width、--tab-width、--prose-wrap和文件本身配置把Doc实例化为最终文本。这里有个非常重要的特性Prettier丢弃原有排版只保留语法内容和代码含义。你在解析阶段结束之后原始的空格、换行、缩进、引号样式全部作废。这也解释了为什么Prettier被称为固执己见的格式化器——你给它一堆乱糟糟的代码它不会试着修补而是直接重写排版。很多人第一次用Prettier后会惊呼它怎么连我手动对齐的列都拆了原因就在这里。2.3 嵌入语言与预处理器HTML里的CSS/JS如何被越界处理真正让Prettier语言支持封神的是它的嵌入语言Embedded Language处理能力。一个HTML文件里大概率包含style和script一个Markdown文件里会有代码块一个Vue单文件组件里则同时存在template、script、style三段。Prettier处理这些混合内容的方式是解析出外层语言的结构后把内嵌语言片段抽出去交给对应的嵌套解析器处理再把结果塞回外层Doc。举个例子div pHello/p style .foo{color:red;margin:0} /style script const a {b: 1,c: 2}; /script /div格式化之后会变成div pHello/p style .foo { color: red; margin: 0; } /style script const a { b: 1, c: 2 }; /script /div注意CSS里的空格、缩进、分号风格和JS里的对象展开风格都是Prettier全局限定的风格。这种递归式的格式化能力在同类工具里非常罕见。其他很多格式化工具要么只能处理单语言要么遇到内嵌代码直接跳过。Prettier选择越界处理让混编文件的风格统一到了令人舒适的程度。嵌入语言的支持范围也是分层的。目前比较成熟的包括HTML和Vue里的CSS/Less/SCSS、JavaScript/TypeScript、GraphQLMarkdown里的几乎所有代码块JS模板字符串里的CSS/GraphQL通过注释声明。这里有个很容易踩的坑我先提前说JS模板字符串里的嵌入语言依赖/* prettier */或/* prettier-ignore */注释来触发不是所有模板字符串都会自动格式化内部语言。比如const styles /* prettier */ .a{ color:red; } ;prettier注释让Prettier把这段模板字符串当作CSS语言去处理。没有这个注释它只会把模板字符串当普通字符串保持内容原样。这个机制我在团队里发现知道的人不多值得专门记住。3. 主流语言支持实测配置项、边界与正确打开方式3.1 JavaScript与TypeScriptparser之争背后的场景差异Prettier对JavaScript的支持最成熟但parser选项如果没理解透照样会碰到奇怪问题。默认情况下Prettier会根据文件扩展名自动推断.js、.jsx、.mjs、.cjs使用babel.ts使用typescript.tsx也使用typescript但如果你把parser显式指定为babel去解析TypeScript文件通常也能工作因为babel/parser天生支持TypeScript语法。那为什么还要有typescript这个解析器区别在于一些极端语法的处理以及类型信息保留的精细度。babel/parser对某些TypeScript类型体操特别是装饰器、const类型参数、模块块等新语法可能滞后半拍而typescript解析器永远紧跟TS编译器自己的节奏。我的建议是TS项目里别乱指定parser老老实实用typescript只有在混编仓库里需要统一JS/TS处理逻辑时才用babel做兜底。实际项目中我更常用overrides来按文件精确匹配。比如一个项目里同时存在flow写法和TypeScript写法虽然罕见但迁移期很常见可以这样配{ semi: true, singleQuote: true, overrides: [ { files: *.ts, options: { parser: typescript } }, { files: *.js, options: { parser: flow } } ] }这里我故意把.js指定为flow解析器就是为了处理Flow类型标注。如果整个仓库没有Flow完全不用管这个字段。3.2 HTML/CSS/SCSS/Less样式语法的兼容边界HTML格式化是Prettier里最容易被吐槽的部分。它强制把所有属性放到一行或每个属性一行对长标签的处理时常让人觉得过于机械。Prettier对HTML的处理加入了大量HTML whitespace sensitivity逻辑默认css模式会保留内联元素之间的空格避免破坏渲染结果。这引出一个重要知识点格式化HTML时Prettier优先保证语义不变其次才追求美观。所以不要指望它能帮你把一段乱成麻的HTML梳理成你想要的手写风格它只会按自己的规则理解空格敏感性。CSS/SCSS/Less方面Prettier的格式化能力比较纯粹。它统一属性顺序吗不统一。它优化选择器嵌套也不做。它只做排版层面的规范化空格、换行、引号、分号、缩进、print-width导致的属性值换行。一个实用技巧是在SCSS里配合use规则Prettier不会把你的mixin调用重排只会规范语法书写。如果遇到它不支持的新CSS语法比如某浏览器刚提出来的提案属性可以用/* prettier-ignore */注释强行跳过。3.3 Vue/JSX/Angular框架模板的格式化策略这一块是日常前端开发最容易碰到怪问题的区域。Vue单文件组件里Prettier使用vue解析器它将整个SFC文件视为一个大HTML结构内部的script langts、style langscss再走各自的子解析器。所以你在Vue文件里看到的缩进是vue解析器管理的而不是typescript解析器。这意味着tabWidth、useTabs这些全局配置对SFC内部的三段代码是一致的但你没法单独给template段设置一个不同的缩进宽度——如果你想这么做只能靠不同文件级别的overrides而SFC是单个文件拆不开。JSX的格式化则走babel或typescript解析器的特殊路径对JSX元素的换行规则有一套独立逻辑。经验是JSX里属性超过printWidth时Prettier会把每个属性拆成一行属性少则保持在单行。如果你希望属性永远多行只能手动把第一个属性后换行让Prettier觉得你的意图就是多行排列。Angular模板使用angular解析器比HTML多了一些*ngIf、(click)、[(ngModel)]这类模板语法的识别格式化时不会拆坏事件绑定。3.4 Markdown/YAML/JSON非代码文件也逃不过统一审美Prettier对非代码文件的支持是我认为它最容易被忽视的杀手锏。比如package.json、.prettierrc.yaml、.github/workflows/*.yml、README.md这些文件在团队协作里频繁被修改但因为不是代码很少有格式化工具愿意管它们。Prettier补齐了这个空白。Markdown它会重排段落换行依据proseWrap配置、统一列表符号、调整标题前后的空行、修正表格对齐。proseWrap: preserve表示不改变段落中已有的换行always则强制按printWidth重排。YAML它能统一缩进、引号风格、锚点换行。一个常见场景是GitHub Actions的配置文件prettier --write .github/workflows/ci.yml能直接让YAML风格与项目其他配置一致。JSON除了普通JSON它还支持JSON5、JSONC带注释的JSON。冷知识是Prettier可以用json-stringify解析器把JSON格式化成类似于JSON.stringify(obj, null, 2)后的纯字符串风格适合生成package.json之外的机器配置文件。使用建议对模板类文件我倾向于在overrides里把它们单独设置proseWrap避免长URL或长表格被强行换行导致排版恶化。特别是README默认的proseWrap: preserve才不会把你的段落搞毁。4. 插件协议一条parser字段如何撬动几十种社区语言4.1 Prettier插件到底做了什么事如果说内置语言支持是Prettier的主场那么插件机制就是它生态扩张的引擎。官方核心仓库只维护约15种语言但社区通过插件让Prettier支持了PHP、Java、Ruby、Swift、Elixir、Rust、Tornado、Nginx配置、Solidity、Astro、Pug、Liquid等几十种语言。一个插件本质上就是一个解析器工厂它向外暴露两个关键能力根据文件内容或扩展名返回一个parser对象该对象包含parse(text)方法负责把源码变成可遍历的AST以及astFormat字段告诉Prettier这个AST应该用什么打印器。有时候也提供自定义printers处理非标准AST节点。Prettier官方和社区约定的一套结构实际是一个扩展协议。我在给内部工具链写格式化插件时最大的体感是你不需要关心Prettier的Doc打印细节只需要把语言解析成AST并把AST规范化成Prettier能理解的格式。换句话说插件把最难的语言解析部分解决掉Prettier把排版美学统一掉。4.2 手写一个最小的Prettier插件从Ruby语言的AST映射说起为了讲清楚插件协议我写一个迷你示例。假设我们想给一个虚构的语言这里以简化Ruby为例写插件核心代码如下// prettier-plugin-ruby-mini/index.js const parser require(./parser); const languages [ { name: RubyMini, parsers: [rubymini], extensions: [.rbmini], vscodeLanguageIds: [ruby], }, ]; const parsers { rubymini: { parse: (text) parser.parse(text), astFormat: rubymini-ast, locStart: (node) node.start, locEnd: (node) node.end, }, }; const printers { rubymini-ast: { print(path, options, print) { const node path.getValue(); if (node.type method) { return [ def , node.name, print(body), end, ]; } return print(); }, }, }; module.exports { languages, parsers, printers, };实际开发中你还需要实现parser.parse来把源码转换成Prettier可识别的AST节点结构。但可以看到插件协议的门槛不在Prettier侧而在语言解析侧。这也是为什么很多Prettier插件直接复用已有的解析器库——比如PHP插件直接基于prettier/plugin-php内置的解析器Java插件则基于JavaParser。4.3 插件解析器与原生命令行格式化工具的取舍当Prettier插件覆盖了一门语言时很多人会纠结是用Prettier插件还是继续用该语言的原生格式化工具这里有一个我建议的取舍原则场景建议理由多语言仓库统一风格优先Prettier插件一套配置、一套CI、一套编辑器设置降低工具链成本单一大语言项目且原生工具极强保留原生工具比如Go的gofmt、Rust的rustfmt它们与语言生态深度绑定原生工具与Prettier风格冲突尽量避免混用混用会导致git diff噪声巨大轻则review痛苦重则格式化规则互相打架语言语法演进极快观察插件更新频率插件如果长期滞后不如等待原生工具或官方集成以Java为例Google Java Format和Prettier插件的格式化结果在细节上有差异。如果你确定整个团队只用Prettier统一格式那没问题但如果部分同事依然在IDE里用Google Java Format自动格式化那每次提交都会出现两套风格的拉锯战。这一点在多语言仓库里尤其致命后面我会专门展开。5. 多语言仓库落地经验从配置到CI的完整方案5.1 一份配置文件管理所有语言风格一个真实的中型仓库我建议从三种文件入手搭Prettier第一是.prettierrc.json全局配置统一风格{ printWidth: 100, tabWidth: 2, useTabs: false, semi: true, singleQuote: false, trailingComma: all, bracketSameLine: false, arrowParens: always, proseWrap: preserve, htmlWhitespaceSensitivity: css, endOfLine: lf }第二是.prettierignore避免格式化不该动的东西。我的经验是要至少包含这些内容node_modules dist build coverage *.min.js *.min.css package-lock.json pnpm-lock.yaml .next .vercel特别注意package-lock.json虽然也是JSON但通常不应该被Prettier重排。因为lock文件的diff已经很大了格式化可能造成无意义的变更。绝大多数项目的lock文件由包管理器生成格式由包管理器全权负责。第三是结合overrides做语言级别的精细化设置{ overrides: [ { files: [*.md, *.mdx], options: { proseWrap: preserve, printWidth: 80 } }, { files: [*.yaml, *.yml], options: { singleQuote: false } }, { files: [*.vue], options: { parser: vue } } ] }这里面的核心思想是全局规则服务于绝大多数文件overrides负责例外。如果团队内部对Markdown是否强制换行有争议就直接用proseWrap做文件级覆盖不要让Markdown的段落风格拖垮全站配置的调整。5.2 VSCode里配置Prettier默认格式化器与语言关联编辑器集成是Prettier使用体验的第一道门。VSCode安装esbenp.prettier-vscode扩展后还要做两件容易被忽略的事。设置默认格式化器在.vscode/settings.json中{ editor.defaultFormatter: esbenp.prettier-vscode, editor.formatOnSave: true, [javascript]: { editor.defaultFormatter: esbenp.prettier-vscode }, [typescript]: { editor.defaultFormatter: esbenp.prettier-vscode }, [vue]: { editor.defaultFormatter: esbenp.prettier-vscode }, [json]: { editor.defaultFormatter: esbenp.prettier-vscode }, [markdown]: { editor.defaultFormatter: esbenp.prettier-vscode } }关键点在于VSCode的默认格式化器是针对语言维度的Prettier扩展自己会识别文件属于它的哪个parser然后调用相应的解析器。很多新手只设置了全局editor.defaultFormatter但某些语言会覆盖它比如VSCode内置的HTML格式化器导致格式化出来的代码不是Prettier风格。所以宁可多写几条[语言]级别的覆盖也别只信任全局设置。另一个实用技巧是设置editor.codeActionsOnSave里的source.fixAll只处理可修复的Lint问题格式化交给formatOnSave二者不要混在一起。否则依赖ESLint的--fix做格式化时可能与Prettier的规则冲突。这里推荐的稳妥组合是ESLint负责代码质量检查Prettier负责格式VSCode里formatOnSave只交给Prettier。5.3 CI流水线里的多语言检查prettier --check与lint-staged团队协作中本地格式化往往靠自觉真正兜底的是CI流水线。Prettier命令行本身提供了两种模式# 检查模式不修改文件适合CI prettier --check **/*.{js,ts,tsx,css,scss,md,yaml,yml,json} # 写入模式适合本地 prettier --write **/*.{js,ts,tsx,css,scss,md,yaml,yml,json}--check在发现未格式化文件时会以非零退出码结束这正好让CI判定失败。但注意glob模式在Windows上可能遇到兼容问题更稳妥的方案是在项目里配合lint-staged使用npx lint-staged配合package.json配置{ lint-staged: { *.{js,ts,tsx,jsx}: [prettier --write, eslint --fix], *.{css,scss,less}: [prettier --write], *.{md,mdx}: [prettier --write], *.{json,yaml,yml}: [prettier --write], *.vue: [prettier --write] } }lint-staged的优势是只处理git暂存区里变化的文件避免在全仓库基础上格式化产生巨大diff。我实际使用中还会在CI里加一道全量检查prettier --check .前提是.prettierignore配置得足够完善。如果忽略列表不够CI往往会因为某个自动生成的json文件报错导致检查失败但没人记得这个文件是哪来的尴尬局面。6. 语言支持边界上的现实问题不该Prettier管的事6.1 新语法与等待期格式化工具的滞后属性Prettier本质上是个跟随者不是先驱。它对一门语言的新语法支持必须等待解析器库更新。比如JavaScript刚出某个stage 3提案时Babel可能几天内就支持了但Prettier需要等到发布新版本才会受益。这中间如果想格式化新语法要么等待要么选择跳过const experimental /* prettier-ignore */ { newSyntax: 想怎么排就怎么排 };这个prettier-ignore注释在任何语言里都好使。它的优先级最高比任何配置都高。我见过一个团队因为某个印刷机代码的新语法导致Prettier解析失败最后就是用prettier-ignore先保住上线节奏再等Prettier更新的。建议在项目文档里明确哪些新语法会导致Prettier报错——用ignore注释临时绕过并在升级Prettier后清理。6.2 Prettier不是Linter它不做代码质量判断语言支持范围广的副作用是大家容易把Prettier当成万能代码工具。记住一个原则Prettier只处理排版不做任何代码质量判断。它不会提醒你某个变量未使用不会管你是否应该用。就算你写了const x 1 1这种油腻代码只要排版正确Prettier照样放行。所以团队落地时的正确分工是ESLint/RuboCop/Checkstyle等工具负责质量问题。Prettier只负责格式问题。不要把ESLint的format类规则和Prettier混在一起开着否则两边可能会互相打架。我在团队里用eslint-config-prettier关闭所有与格式相关的ESLint规则再让prettier单独跑。这样职责清晰也能避免ESLint说这里要加空格Prettier说要去掉空格的无解纷争。6.3 新加入一门语言时团队配置的常规步骤假设你想让仓库里新增的.graphql文件也被Prettier接管常规步骤是确认Prettier内置支持GraphQL是的内置支持。在.prettierignore确认没有排除*.graphql。在package.json的scripts里加入对应glob{ format: prettier --write \**/*.{js,ts,tsx,css,scss,md,yaml,yml,json,graphql}\ }在VSCode里确认[graphql]语言使用Prettier默认格式化器。lint-staged里补一条*.{graphql}: [prettier --write]。在CI的prettier --check命令里加上graphql扩展名。这个流程适用于任何语言内置或插件支持。一个容易忽略的点是glob模式里的引号要保留否则在shell展开时可能被路径里的奇怪字符破坏。我在Zsh上遇到过几次因为没加引号导致prettier --write漏掉文件的状况后来一律用带引号的模式。最后关于新增语言还有一个分歧点有人希望格式化全部文件有人希望只格式化自己改过的文件。我的建议是第一阶段用lint-staged控制增量格式化等CI全绿了再跑一次全量prettier --write把历史债一次性还掉。之后再默认全量检查保持从今往后的每个文件的格式都符合Prettier标准。7. 从实际项目复盘我用Prettier统一过一个六语言仓库去年我把一个老仓库从只有JS格式化升级为六语言全格式化仓库里有JavaScript、TypeScript、Vue SFC、SCSS、Markdown、YAMLGitHub Actions配置。当时的步骤和反思这里给你们一份抄作业清单。第一步根目录添加.prettierrc.json风格对准当时团队大多数文件的现状双引号、分号、尾逗号all、宽度100。这一步的核心是尽量减少首次格式化的diff。第二步添加.prettierignore先排除所有生成文件和锁文件。这一步最花时间因为我不光要排除前端常规的dist还要想想有没有自动生成的CHANGELOG.md、文档站构建目录等。第三步全量跑一次prettier --write .然后把diff提交到一个单独commit里方便review时单独查看。这一步建议配合git blame忽略策略避免以后git blame每条记录都指向格式化commit。第四步配置lint-staged和CI。我用lint-staged在提交前格式化暂存文件CI里用prettier --check .做全量兜底。两者结合后团队提交体验很顺滑不需要每个人手动记命令。复盘中最有价值的一条经验是不要试图让Prettier解决所有格式争论。团队里总有人喜欢长行有人喜欢短行有人坚持tab缩进。Prettier的价值恰恰在于把这类争论从代码评审里移除。当我们讨论配置到底用单引号还是双引号时正确做法是投票决定一个值写进配置文件然后以后再也不讨论。语言支持范围越大这个一刀切的一致性价值就越明显——如果你有六种语言每种语言还各有一套格式工具那光维护配置就足以让人崩溃。另一个值得说的小技巧是在项目里专门放一个CONTRIBUTING.md写清楚Prettier版本和用法。Prettier的格式化结果会随版本变化团队如果不同成员用的版本不一致格式化出来的代码也会不一致进而导致无意义的diff。我的建议是在package.json里固定Prettier版本用prettier的engines字段或直接锁精确版本号。最后我想说Prettier对语言的支持深度本质上决定了它的上限在哪里。理解了解析器体系、嵌入语言机制、插件协议和边界限制之后你才真正能把它的全部功力榨出来。不管是三语言仓库还是六语言仓库配置的核心思路都是统一的一个全局配置管风格overrides管例外ignore管保护CI管兜底插件管扩展。这套框架让我在接手任何一个新仓库时都能用最短时间把格式化基础打牢。你们下次遇到一个什么语言都想格式化的项目也试着从这张版图出发去排查问题而不是东搜一个工具西配一个插件最后什么也没统一起来。