最近在重构项目代码时,发现团队内部对代码风格的争论又开始了------空格还是tab?分号要不要?对象最后那个逗号留着还是删掉?突然想到,Prettier 这个工具几乎解决了所有这些"无休止的讨论",但它为什么这么"固执",配置选项这么少?它内部到底是怎么工作的?
今天就来深入探究一下 Prettier 的原理,从历史动机到核心算法,看看这个"偏执"的格式化工具是如何做到稳定可预期的。
📈 代码格式化工具的发展历程
阶段 | 代表工具 | 核心特征 | 主要痛点 | 对 Prettier 的启发 |
---|---|---|---|---|
手工+Lint Fix (2014 前) | ESLint --fix , JSCS |
规则驱动、基于 AST node 局部替换 | 全局一致性差;规则相互冲突;配置地狱 | 需要"整体打印"替代"局部补丁" |
多风格派系时期 | Standard, Airbnb, Google, JSCS Preset | 风格战、宗教式争议 | 讨论成本高;迁移痛苦 | 需要去配置化的单一风格 |
早期格式化器 | clang-format(for C/C++), gofmt, rustfmt | 语言内生态(Go/Rust)强制统一 | JS 多范式+多语法扩展复杂 | 借鉴:AST -> 结构化文档 -> 决策 |
Prettier (2017 起) | Prettier | 统一、可预测、最小配置、幂等 | 少量无法表达的风格偏好 | 牺牲自由换协作效率 |
💡 Prettier 的设计理念
看到这些发展历程,就能理解 Prettier 为什么会这样设计了:
- 输出稳定:给定同一输入(无语法错误)必定生成同一结果(幂等性)
- 最少配置:减少无休止的风格争论
- 全量重打印:不在原文本上打补丁,而是重新生成代码
- 语言无关抽象:统一打印框架 + 多解析器支持
- 基于显示宽度的智能换行:不是简单的80列截断,而是结构感知的行拆分算法
与 ESLint 的定位差异
维度 | ESLint | Prettier |
---|---|---|
关注点 | 代码质量 + 潜在 bug + 一部分风格 | 纯格式呈现(空格/换行/对齐/引号/括号位置) |
修改方式 | Rule 逐节点局部 fix | 全量 Reprint (Print Doc) |
配置量 | 高 | 极低 |
可扩展性 | 规则、插件、处理器 | 解析器、插件(语言支持 / 内置 hook) |
冲突处理 | 可能规则冲突 | 极少(风格固定) |
风格哲学 | 你说了算(配置) | 我说了算(约定) |
🔄 Prettier 整体工作流程
来看看 Prettier 内部是怎么处理代码的:
less
源代码(Text)
↓ 解析(外部 parser:@babel/parser / espree / typescript-estree / postcss / md parser ...)
AST (ESTree / CSS AST / Markdown AST ...)
↓ AST -> Doc 转换 (Builder:group, indent, line, softline, ifBreak ...)
Doc Tree (抽象"排版语义"结构)
↓ 线宽优化打印算法 (递归尝试 fits / break)
格式化后文本(Output)
↓ 校验幂等 (可二次 parse 比较)
最终结果
关键点:Prettier 的核心不在 AST 解析,而在 AST 到 Doc 的转换 + Doc 到文本的智能换行算法。
🔧 核心数据结构:Doc(打印文档模型)
Doc 是一个很有意思的中间层,它比 AST 更接近"排版"的概念。来看看主要的 Doc 类型:
Doc 片段 | 含义 | 示例 |
---|---|---|
text |
原子字符串 | "function" |
line |
换行(强制) | 换一行 |
softline |
软换行(可变为空格或换行) | 多参数调用里的逗号后 |
hardline |
一定换行且清空队列 | 模块顶层分隔 |
group |
一个可整体尝试单行的块 | 参数列表、对象字面量 |
indent |
缩进 | group 内部块体 |
ifBreak |
基于是否发生换行选择分支 | 三元表达、括号控制 |
lineSuffix |
推迟到行尾打印 | 注释处理 |
join |
序列插入分隔符 | 逗号拼接 |
来看个具体例子:
less
// 源码:foo(bar, baz, qux)
group([
text('foo'),
text('('),
indent([
softline,
join([text(','), line], [text('bar'), text('baz'), text('qux')])
]),
softline,
text(')')
])
如果单行能放下,就输出 foo(bar, baz, qux)
;如果太长,就变成:
scss
foo(
bar,
baz,
qux
)
🏗️ AST 转 Doc 的构建过程
这个转换过程很关键,来看看简化的实现:
scss
function print(node, path, options, print) {
switch(node.type) {
case 'Program':
return join([hardline, hardline], path.map(print, 'body'));
case 'FunctionDeclaration':
return group([
text('function '), node.id.name, text('('),
group([
indent([
softline,
join([text(','), line], node.params.map(p => print(p)))
]),
softline
]),
text(') '), print(node.body)
]);
case 'BlockStatement':
return group([
text('{'),
indent([
hardline,
join(hardline, node.body.map(s => print(s)))
]),
hardline,
text('}')
]);
}
}
注意这里:不直接拼接字符串,而是构造 Doc 结构,把"换行决策"延后到打印阶段。
🧮 行宽决策与打印算法(核心部分)
这部分是 Prettier 最精妙的地方。算法灵感来源于 Philip Wadler 的 Pretty Printing 论文,Prettier 做了工程化的优化。
基本思路:
- 深度优先遍历 Doc 树
- 用"测量函数"尝试将
group
展开为单行,如果超过最大列宽就标记为换行模式 - 在换行模式下,
softline
变成\n
;在单行模式下,softline
变成空格 - 维护输出缓冲区和当前列计数,遇到换行就重置列计数
来看看核心算法的简化版本:
arduino
function fits(doc, width, pos=0) { // 预测单行是否超宽
const queue = [doc];
while(queue.length) {
const cur = queue.pop();
if(typeof cur === 'string') { pos += cur.length; if(pos > width) return false; }
else if(cur.type === 'line') { return true; } // 单行模式遇强 line 提前成功
else if(cur.type === 'softline') { pos += 1; }
else if(cur.type === 'group') { queue.push(...flatten(cur.contents)); }
// ...其他节点展开
}
return true;
}
function printDoc(doc, width, mode='flat') {
// 遇 group 时调用 fits 测试。失败 => 递归用 break 模式打印该 group
}
实际的 Prettier 算法要复杂得多,涉及队列管理、剩余宽度计算、不同的打印模式等。
如果对这个算法的理论基础有兴趣,可以看看 Philip Wadler 在1997年发表的论文《A Prettier Printer》,这是 Prettier 背后核心思想的直接来源。
关键特性
特性 | 说明 | 价值 |
---|---|---|
惰性决策 | group 展开前不定 | 避免过早换行 |
嵌套优化 | 内层 group 先测 | 减少回溯 |
线性复杂度(近似) | 通过局部预测而非全局搜索 | 性能可控 |
幂等 | 第二次格式化不再变化 | CI 稳定 |
注释稳定 | 注释 attach & lineSuffix 机制 | 不丢失语义 |
🤔 为什么 Prettier 的配置这么少?
用过 ESLint 的都知道配置有多复杂,但 Prettier 只有寥寥几个配置项,这是为什么?
Prettier 有意排除了那些容易引发"风格战争"的选项:比如是否对齐链式调用、空行数量、对象最后逗号的具体位置等。设计哲学就是:减少选择 → 减少讨论 → 提升效率。
保留的少量选项示例:
选项 | 作用 | 说明 |
---|---|---|
printWidth |
理想行宽 | 不是硬切分点,算法尽量适配 |
tabWidth |
缩进宽度 | 影响 indent Doc 渲染 |
useTabs |
空格/Tab | 不影响换行策略 |
semi |
语句末分号 | 语义影响(ASI 陷阱) |
singleQuote |
引号风格 | 影响字符串 text 节点 |
trailingComma |
尾逗号策略 | 有助 diff 与多行保持 |
arrowParens |
单参数箭头函数括号 | 易读性 vs 简洁性 |
bracketSpacing |
{ a:1 } vs {a:1} |
局部微调 |
endOfLine |
换行符规范化 | 跨平台一致 |
🔌 多语言支持与插件机制
Prettier 不只是 JavaScript 工具,它通过"解析器 + 打印器"的组合支持多种语言:
语言 | 解析器 | 特殊处理 |
---|---|---|
JS/TS/Flow/JSX | @babel/parser / typescript-estree | JSX 嵌入、TS 类型节点 |
CSS/SCSS/Less | postcss | 嵌套规则、注释 |
HTML | 基于内部/社区维护的 HTML 解析器 | 嵌入脚本/样式区块再次递归格式化 |
Markdown | remark / mdast | 保留换行、代码块内不改 |
GraphQL | graphql-js | 字段列对齐 |
想要扩展支持新语言的话,插件结构大概是这样的:
javascript
module.exports = {
languages: [{
name: 'MyLang',
parsers: ['mylang'],
extensions: ['.mlg']
}],
parsers: {
mylang: {
parse(text, opts) { return parseToAST(text); },
astFormat: 'mylang-ast'
}
},
printers: {
'mylang-ast': {
print(path, opts, print) {
const node = path.getValue();
// 返回 Doc
}
}
}
};
现在推荐使用 ESM 格式(Prettier 3.x):
javascript
// plugin.mjs
export const languages = [{
name: 'MyLang',
parsers: ['mylang'],
extensions: ['.mlg']
}];
export const parsers = {
mylang: {
parse: (text, opts) => parseToAST(text),
astFormat: 'mylang-ast'
},
};
export const printers = {
'mylang-ast': {
print(path, opts, print) {
const node = path.getValue();
// 返回 Doc
},
},
};
Doc Builder 进阶能力与场景
除了基础的 group/indent/line/softline/ifBreak 外,Doc 模型还提供了更多高级构建块:
Doc 构建块 | 用途 | 典型场景 |
---|---|---|
fill |
尽可能多在一行放置内容,智能换行 | 注释文本、数组项、参数列表 |
align(n) |
指定对齐宽度(不同于缩进) | 特定列对齐、链式调用 |
indentIfBreak |
仅在父级断行时缩进 | 条件缩进、特殊格式 |
dedent |
减少缩进级别 | 逆向缩进、特殊对齐 |
breakParent |
强制父级 group 断行 | 确保某元素前必定换行 |
lineSuffixBoundary |
行尾注释与后续内容的边界 | 防止注释"吃掉"后面的换行 |
cursor |
在输出中标记光标位置 | 编辑器集成、光标保持 |
示例:使用 ifBreak 控制尾随逗号
less
group([
text("["),
indent([
softline,
join(
[text(","), line],
items.map(print)
),
// 仅在换行模式下添加尾随逗号
ifBreak([text(",")], [])
]),
softline,
text("]")
])
示例:使用 fill 处理长注释
scss
fill([
text("// This is a very long comment that might need to"),
line,
text("// be wrapped across multiple lines depending on"),
line,
text("// the available width.")
])
🎯 AST 兼容策略
不同语言的 AST 结构差异很大,Prettier 没有强求统一的 ESTree 格式,而是采用了一种很灵活的方式:parser => AST => printer
,每个 printer 只需要处理对应 astFormat 的结构就行。这样避免了跨语言抽象时的信息丢失。
🆕 Prettier 3.x 的重要变化
Prettier 3.0 在 2023 年发布,带来了一些值得注意的变化:
🔧 环境要求变化
- Node.js 最低版本提升到 v14.0.0+(建议用 v16+)
- 全面支持 ESM 插件,新插件建议用 ESM 格式
- Flow 解析器不再内置,需要单独安装
@prettier/plugin-php
新增/变更选项
选项 | 说明 | 默认值 | 使用场景 |
---|---|---|---|
singleAttributePerLine |
HTML/JSX 属性是否每行一个 | false |
提高多属性标签的可读性 |
bracketSameLine |
替代旧的 jsxBracketSameLine |
false |
统一 JSX/HTML 右尖括号换行策略 |
embeddedLanguageFormatting |
嵌入代码块格式化控制 | auto |
控制 Markdown 中代码块是否格式化 |
CLI 性能优化
--cache
:启用缓存,避免重复格式化未修改文件--cache-location
:指定缓存存储位置--cache-strategy
:content
(默认,基于内容哈希)或metadata
(基于文件元数据)
升级风险与排查建议
- 锁定版本 :在
package.json
中使用精确版本号(如"prettier": "3.0.3"
)而非范围 - 插件兼容性:检查所有插件是否兼容 Prettier 3.x
- CI 验证 :在 CI 中使用
prettier --check
观察格式变化 - 分批迁移:大型仓库可考虑按目录/模块分批升级
json
// package.json 示例 - 精确锁定版本
{
"devDependencies": {
"prettier": "3.0.3",
"@prettier/plugin-php": "0.19.6"
}
}
📝 注释处理的巧妙机制
注释处理是 Prettier 最有意思的部分之一。想想看,注释在 AST 里其实是"无家可归"的------它们不属于任何语法结构,但却直接影响代码的可读性。Prettier 是怎么处理的呢?
大致流程是这样的:
- 收集阶段:解析时把所有注释 token 都记录下来(前置/后置/内部)
- 归属判断:根据位置信息,把注释"挂"到最近的节点上
- 智能插入:打印时在合适的位置插入换行或行末注释
来看个具体例子:
scss
// 这样的代码
if (true) {
doSomething(); // trailing comment
}
这里的 // trailing comment
会被识别为 doSomething()
语句的 trailingComments
,然后打印器会在这个语句的 Doc 末尾加上 lineSuffix([' // trailing comment'])
,确保注释始终跟着对应的代码走。
⚡ 性能优化的小秘密
Prettier 为什么能在大型项目中保持相对不错的性能?其实有不少巧思:
优化策略 | 具体做法 | 好处 |
---|---|---|
流式遍历 | 打印过程基本是单遍,不需要回头再看 | 节省内存 |
智能预判 | fits 函数一旦发现超宽就立即停止,不深入了 |
避免无用计算 |
懒加载决策 | group 的展开策略到真正需要时才确定 | 减少过早换行 |
局部缓存 | 某些不变的子结构可以复用(主要在插件里) | 减少重复构建 |
错误容忍 | 语法有问题时尽量保留原文,而不是直接崩溃 | 提升可用性 |
🔄 大文件处理与增量格式化的思考
有人问过为什么 Prettier 不做增量格式化(就像某些编辑器只格式化修改的部分),其实是有原因的:
Prettier 的决策往往是全局的。比如一个长链式调用是否需要换行,可能会影响到整个表达式的缩进;最外层的 printWidth
设置也会波及到内层的格式判断。如果只格式化局部,很可能破坏整体的一致性。
所以现在的做法比较实用:在 CI 中用 --cache
只处理变化的文件,或者用 lint-staged
在提交前只格式化 staged 的文件。这样既保证了一致性,又避免了不必要的处理。
🤝 与 ESLint 的和谐共处
用过 ESLint 和 Prettier 的同学肯定遇到过这种情况:两个工具对同一段代码有不同的"看法",比如缩进、引号、分号等等。典型的冲突规则有:indent
、quotes
、semi
、comma-dangle
。
解决方案很简单:用 eslint-config-prettier
把这些纯格式相关的 ESLint 规则给关了,让 Prettier 专心做格式化,ESLint 专心做代码质量检查。
小贴士 :以前流行过
eslint-plugin-prettier
(把 Prettier 当作 ESLint 规则来跑),但现在不推荐了,因为这会导致性能问题和奇怪的错误定位。现在的最佳实践是让两个工具各司其职。
现代推荐的配置是这样的:
perl
{
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"plugins": ["@typescript-eslint"],
"rules": {"@typescript-eslint/no-unused-vars": "warn"}
}
ESLint Flat Config 时代的新配置
ESLint v8.21.0+ 推出了 Flat Config,配置方式更简洁:
javascript
// eslint.config.js
import js from '@eslint/js';
import ts from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default [
js.configs.recommended,
...ts.configs.recommended,
prettier, // 这里关闭与 Prettier 冲突的规则
{
rules: { '@typescript-eslint/no-unused-vars': 'warn' },
},
];
Git Hooks 中的协作顺序
在 Git Hooks 里面,执行顺序很重要:
json
// package.json 中的 lint-staged 配置
{
"lint-staged": {
"*.{js,jsx,ts,tsx,css,scss,md,json,yml,yaml}": [
"prettier --write", // 先格式化
"eslint --fix" // 再修复代码质量问题
]
}
}
这样确保先让 Prettier 把格式搞定,再让 ESLint 处理逻辑问题,避免两者互相"打架"。
🔧 幂等性检验:确保格式化的稳定性
什么叫幂等性?简单说就是:格式化一次和格式化一万次,结果都一样。这对于团队协作和 CI/CD 来说非常重要。
Prettier 有个很实用的检验方法:格式化后再格式化一次,如果结果不变,就说明是幂等的。工程上的验证策略通常是:
- 哈希对比:格式化后再次格式化,文件哈希应该不变
- AST 对比:解析原 AST,格式化后再解析,结构应该保持一致(除了空格、注释位置等非语义信息)
想要验证的话,可以用 --debug-check
选项,它会自动进行二次解析比较,这对排查格式化问题很有用:
css
prettier --debug-check src/**/*.js
Prettier 对某些复杂的语法转换会保持"保守"态度,宁愿少改一点,也要确保语义完全不变。
🎯 忽略控制:当你需要精细化管理
虽然 Prettier 的理念是"少配置、少选择",但实际项目中总有一些特殊情况需要精细控制。好在 Prettier 提供了多种忽略机制:
代码级别的精准忽略
有时候某段代码就是要保持特定格式(比如手工对齐的数组),可以这样:
javascript
// prettier-ignore
const uglyCode = matrix[0].map((col, c) =>
matrix.map((row, r) => matrix[r][c]));
// prettier-ignore-start
const hardToFormat = [
first,
second,
third, // 我就是要这个缩进!
fourth
];
// prettier-ignore-end
文件级别的批量控制
.prettierignore
文件的语法跟 .gitignore
很像:
csharp
# .prettierignore 示例
dist/
coverage/
node_modules/
*.min.js # 压缩文件就别折腾了
package-lock.json
yarn.lock # 锁定文件交给工具管理
条件格式化的高级用法
- 只格式化带标记的文件 :用
--require-pragma
选项,只处理包含特定注释(如@prettier
)的文件 - 自动添加标记 :用
--insert-pragma
在格式化后自动添加标记注释 - 局部范围格式化 :用
--range-start
和--range-end
只格式化文件的特定部分
python
# 只格式化 1000-2000 字符范围内的代码
prettier --range-start 1000 --range-end 2000 file.js
针对不同文件类型的专项控制
Prettier 对不同文件类型还有专门的选项:
- Markdown :
--prose-wrap
控制文本换行策略(always
/never
/preserve
) - HTML :
--html-whitespace-sensitivity
控制空白敏感度 - 嵌入代码 :
--embedded-language-formatting
控制是否格式化嵌入的代码块
🔍 调试与问题排查
当 Prettier 的行为跟你预期不符时,怎么快速定位问题?Prettier 提供了一系列调试工具:
调试选项 | 能帮你做什么 | 具体用法 |
---|---|---|
--debug-check |
检查格式化是否真的稳定(幂等) | prettier --debug-check file.js |
--debug-print-doc |
看看 Prettier 内部的 Doc 结构 | prettier --debug-print-doc file.js |
--debug-benchmark |
测试格式化性能 | prettier --debug-benchmark file.js |
--log-level debug |
输出详细的处理日志 | prettier --log-level debug file.js |
想深入分析性能问题的话,还可以结合 Node.js 的性能分析工具:
ini
# 生成 CPU 分析文件
node --cpu-prof --cpu-prof-name=prettier-prof.cpuprofile \
./node_modules/.bin/prettier --write large-file.js
# 然后在 Chrome DevTools 的 Performance 面板中加载 .cpuprofile 文件
这对于排查大文件格式化的性能瓶颈很有用。
⚙️ 配置解析与 Monorepo 项目实践
在复杂项目中,配置的解析顺序和插件管理往往是个头疼的问题。先来了解一下 Prettier 的配置优先级:
配置的优先级规则
Prettier 会按这个顺序查找和应用配置:
- CLI 参数 (优先级最高):如
--tab-width=4
- 项目配置文件 :
.prettierrc.*
系列(.prettierrc
,.prettierrc.json
,.prettierrc.js
,.prettierrc.yaml
) - 专用配置文件 :
prettier.config.*
(prettier.config.js
,prettier.config.cjs
) - package.json 字段 :
package.json
中的prettier
字段 - EditorConfig :
.editorconfig
文件(默认读取,可用--no-editorconfig
禁用)
Monorepo 中的插件解析问题
在 Monorepo 中使用 Prettier 插件时,经常会遇到插件找不到的问题,特别是用 pnpm 这种严格依赖管理的工具:
解决方案:
- 用
--plugin-search-dir
指定插件搜索目录 - 在 pnpm workspaces 中,可能需要在根目录的
.npmrc
中设置shamefully-hoist=true
- 或者使用
--plugin-search-dir=.
从当前目录开始搜索
css
# 指定插件搜索目录的例子
prettier --plugin-search-dir=./packages/tools --write "src/**/*.js"
局部配置覆盖的技巧
通常会在 Monorepo 根目录设置统一配置,但某些子包可能需要特殊处理:
java
// 子包中的 .prettierrc.js
module.exports = {
...require('../../.prettierrc'), // 继承根配置
// 局部覆盖
printWidth: 120, // 比如这个子包需要更宽的行宽
};
这样既保持了整体一致性,又允许必要的局部调整。
🤔 设计取舍:那些有意为之的"固执"
Prettier 有些行为可能让人觉得"怎么不能这样配置?",但这些其实都是有意为之的设计决策:
争议场景 | Prettier 的决策 | 为什么这样设计? |
---|---|---|
链式调用换行 | 统一采用点号前置策略 | 可预测性 > 个人偏好,减少配置争议 |
长模板字符串 | 不强制拆分内部内容 | 避免破坏原有语义和空格结构 |
对象属性对齐 | 不做列对齐 | diff 更稳定,不会因最长属性名变化而重排 |
import 语句排序 | 不做自动排序 | 留给专门的工具处理(如 ESLint 的规则) |
连续空行处理 | 最多保留 2 行 | 避免代码中出现"大段留白"影响结构感知 |
一些典型的边界案例
超长链式调用的统一处理
不管你原来是什么风格,Prettier 都会统一成点号前置:
scss
// 输入:各种混合风格
const result = someObject.method1().method2()
.method3().method4()
.method5();
// Prettier 输出:统一的点号前置
const result = someObject
.method1()
.method2()
.method3()
.method4()
.method5();
这种统一性虽然可能不符合某些团队的习惯,但换来的是零争议和完全的一致性。
TypeScript 复杂类型的智能折行
面对超长的联合类型,Prettier 会智能地进行折行:
go
// 输入:一长串联合类型
type Status = "pending" | "approved" | "rejected" | "in_review" | "needs_changes" | "cancelled" | "expired";
// Prettier 输出:整齐的竖直排列
type Status =
| "pending"
| "approved"
| "rejected"
| "in_review"
| "needs_changes"
| "cancelled"
| "expired";
模板字符串的保守处理
Prettier 对模板字符串内容很"保守",不会轻易拆分:
ini
// 输入:很长的模板字符串
const message = `这是一段非常长的模板字符串内容,Prettier 不会强制拆分它,因为这可能会破坏语义`;
// Prettier 输出:保持不变(即使很长)
const message = `这是一段非常长的模板字符串内容,Prettier 不会强制拆分它,因为这可能会破坏语义`;
// 如果真的需要控制换行,可以用数组拼接:
const message = [
"这是一段需要控制换行的长文本,",
"可以用数组拼接的方式,",
"让 Prettier 帮我们格式化每个元素"
].join("");
注释的智能归属
有时注释会出现"悬空"情况,Prettier 会尽力将它们归属到最近的代码节点:
csharp
// 输入:悬空的注释
function example() {
const x = 1;
// 这个注释看起来像是悬空的
return x;
}
// Prettier 输出:注释被合理地附加到最近的语句
function example() {
const x = 1;
// 这个注释看起来像是悬空的
return x;
}
🚀 新时代的格式化工具对比
除了 Prettier,最近几年也出现了一些很有意思的替代工具。虽然 Prettier 仍然是主流选择,但了解一下这些新工具也挺有意思:
Biome:Rust 驱动的全能选手
Biome 是 Rome 项目的后续,用 Rust 重写,不只是格式化工具,还包含 Lint 和 Import 整理功能:
- 性能优势:比 Prettier 快 10-20 倍,特别是在大型代码库中很明显
- 体验差异:格式化风格跟 Prettier 不完全一样,团队可能需要适应期
- 适用场景:如果你的项目对构建速度有很高要求,可以考虑
perl
# Biome 使用示例
npx @biomejs/biome format --write src/
dprint:多语言统一体验
另一个 Rust 编写的格式化工具,设计理念是支持多种语言的统一体验:
- 灵活配置:插件化设计,配置比 Prettier 灵活
- 兼容模式:提供了 Prettier 兼容模式,迁移相对容易
- 多语言支持:对多语言项目来说可能更统一
bash
# dprint 使用示例
dprint fmt
我该选择哪个?
简单说:
- 新项目或稳定性优先:Prettier 仍然是最安全的选择,生态最成熟,踩坑最少
- 性能有瓶颈:考虑 Biome 或 dprint,但要评估风格差异和迁移成本
- 多语言项目:dprint 可能提供更统一的体验
- 团队协作重于个人偏好:继续用 Prettier,它的"固执"正是协作的优势
📝 总结:从争论到协作的格式化之路
回到开头提到的那些团队争论:空格还是 tab?分号要不要?对象最后的逗号怎么处理?
现在我们知道了,Prettier 之所以"固执",正是为了终结这些无休止的讨论。它的核心理念不是"让每个人都满意",而是"让所有人都用同样的标准"。
🛠️ 实用建议
对于新项目:
- 直接采用 Prettier 默认配置,最多调整
printWidth
和semi
- 配合
eslint-config-prettier
避免与 ESLint 冲突 - 在 Git hooks 中集成,确保提交的代码都经过格式化
对于现有项目:
- 分批迁移,先在新模块试用,观察团队适应度
- 使用
--cache
提升大型项目的格式化速度 - 通过
.prettierignore
排除不适合格式化的文件
对于团队协作:
- 把 Prettier 当作"团队约定"而非"个人工具"
- 重点关注代码逻辑和业务实现,把格式问题交给工具
- 定期检查幂等性(
--debug-check
),确保格式化结果稳定
希望这篇深度解析能帮你更好地理解和使用 Prettier。下次再遇到代码风格争论时,不如试试说:"让 Prettier 来决定吧!" 😉