news 2026/9/18 23:14:56

读懂 Prettier 的设计哲学:格式选择背后的规则、选项与边界

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
读懂 Prettier 的设计哲学:格式选择背后的规则、选项与边界

读懂 Prettier 的设计哲学:格式选择背后的规则、选项与边界

【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier

Prettier 是一款「有主见的(opinionated)」代码格式化工具,docs/rationale.md 系统解释了它在引号、空行、对象换行、装饰器、模板字符串、分号、行宽、JSX、注释等场景下「为什么这样格式化」以及「它不做什么」。读完本文,你既能理解每个格式决策背后的工程权衡,也能掌握objectWrapsingleQuoteprintWidth等关键选项的准确语义,并看到这些规则在 Prettier 源码中的真实落点。

正确性是第一位的要求

Prettier 的第一原则是正确性:格式化的输出必须是合法代码,且行为与格式化前完全一致。官方文档明确指出,如果某段代码在 Prettier 处理下行为发生变化,那就是一个必须修复的 bug。

这条原则贯穿后文几乎所有设计:Prettier 不自动修复 ASI(自动分号插入)相关的既有 bug,不重排 import,不转换字符串写法——因为任何「顺手优化」都可能引入行为差异,威胁这个最高目标。

字符串与引号选择

Prettier 对双引号还是单引号的选择规则是:选能让转义符最少的那个。例如应输出"It's gettin' better!",而不是'It\'s gettin\' better!'。当两种引号的转义数相同、或字符串内不含引号时,默认使用双引号;这一点可通过singleQuote选项改为单引号。

这一逻辑在源码中体现得很直接。get-preferred-quote.js 中的getPreferredQuote函数会遍历字符串文本,分别统计首选引号与备选引号出现的次数(preferredQuoteCount/alternateQuoteCount),若字符串中备选引号出现次数更少,就改用备选引号作为包裹符:

// src/utilities/get-preferred-quote.js 核心逻辑 if (codePoint === preferred.codePoint) { preferredQuoteCount++; } else if (codePoint === alternate.codePoint) { alternateQuoteCount++; } return (preferredQuoteCount > alternateQuoteCount ? alternate : preferred).character;

选项定义见 common-options.evaluate.js:singleQuote是布尔型、默认false(即默认双引号)。

JSX 有自己独立的引号选项jsxSingleQuote。文档给出的理由是:JSX 源于 HTML,而 HTML 属性中双引号是主流写法;浏览器开发者工具也总是用双引号展示 HTML——即使源码用的是单引号。独立的选项让用户可以对 JS 使用单引号、对「HTML 味」的 JSX 使用双引号,各取所需。

另外,Prettier 会保留字符串原有的转义方式"🙂"不会被格式化成"\uD83D\uDE42",反之亦然。

空行的保留策略

自动生成空行(比如「在函数之间留一个空行」)在实践中非常困难,所以 Prettier 的策略是:按原样保留源码中的空行,并附加两条规则:

  1. 连续的多个空行会合并为一个空行;
  2. 块(以及整个文件)开头和结尾的空行会被删除(但文件始终以单个换行符结尾)。

在 JS 对象打印的源码中可以找到空行保留的佐证。object.js 在逐个打印属性时,用isNextLineEmpty检查原源码中该属性之后是否为空行,是则插入hardline,从而把原有的空行「带进」格式化结果;isNextLineEmpty的实现在 is-next-line-empty.js。

多行对象与 objectWrap 选项

Prettier 的打印算法默认是「能放进一行就放一行」。但对象字面量在 JS 里用途太广——对象列表、嵌套配置、样式表、带键方法等场景下,保持多行往往更利于阅读。文档承认团队没能找到覆盖所有这些场景的通用规则,于是采用了启发式:只要原源码中{和第一个键之间存在换行,对象就保持多行。由此推论:长的单行对象会被自动展开,但短的多行对象永远不会被折叠。

这个条件行为可以由objectWrap选项控制。选项定义在 common-options.evaluate.js:

取值默认行为
preserve若开括号与第一个属性之间有换行,则保持多行
collapse尽可能折进单行

对应的源码在 object.js:当options.objectWrap === "preserve"且通过hasNewLineAfterOpeningBrace(用hasNewlineInRange检查{与第一个属性起点之间的原始文本是否含换行)判定为真时,shouldBreak被置为true,随后group(content, { shouldBreak })强制该分组破折成多行。

合并与展开对象:一个实用技巧

由于「换行即保持多行」,你可以用一种半手动的方式控制对象布局。

想把这个多行对象合并成单行,只需删掉{后面的换行:

const user = { name: "John Doe", age: 30 };

然后运行 Prettier,得到:

const user = { name: "John Doe", age: 30 };

想再变回多行,就在{后加回一个换行:

const user = { name: "John Doe", age: 30 };

再运行 Prettier,得到规范的多行形式:

const user = { name: "John Doe", age: 30, };

关于格式可逆性的说明

文档诚实地指出:上述对对象字面量的半手动控制其实是一个变通方案(workaround),而非特性——当年是急需修复且尚未找到好的启发式才引入的。Prettier 作为整体策略上会避免这类「不可逆(non-reversible)」的格式化。

什么叫不可逆?对象字面量一旦变成多行,Prettier 就不会再折叠它。设想:你在已格式化的代码里给对象加了一个属性,运行 Prettier 后对象变多行;随后你改变主意删掉该属性,再运行 Prettier,结果可能与最初格式不一致。这种无意义的 diff 甚至可能被提交进代码库——而这正是 Prettier 立志要消除的东西。团队仍在寻找能彻底移除(或至少收窄适用场景)这一行为的启发式。

装饰器:保留你的原始写法

和对象类似,装饰器(decorators)也用于非常多样的场景:有时写在被装饰行的上方更合理,有时同行更美观。团队同样找不到通用规则,所以 Prettier 会保留你书写时的位置(前提是能放进一行)。这不是理想方案,却是对难题的务实处理:

@Component({ selector: "hero-button", template: `<button>{{ label }}</button>`, }) class HeroButtonComponent { // These decorators were written inline and fit on the line so they stay // inline. @Output() change = new EventEmitter(); @Input() label: string; // These were written multiline, so they stay multiline. @readonly @nonenumerable NODE_TYPE: 2; }

唯一的例外是类:文档认为类装饰器写成行内没有任何合理场景,因此总是被移到独立行:

// Before running Prettier: @observer class OrderLine { @observable price: number = 0; }
// After running Prettier: @observer class OrderLine { @observable price: number = 0; }

源码印证了这一点:decorators.js 中shouldBreak在节点是ClassExpression/ClassDeclaration时直接为真,否则取决于装饰器之间或之后是否存在换行(hasNewlineBetweenOrAfterDecorators)——即「保留原样」的实现就是检查原始文本中的换行分布。

注意:Prettier 1.14.x 及更早版本曾尝试自动移动装饰器,如果你用过旧版本处理过代码,可能需要手动合并一些装饰器,避免同一代码库内风格不一致:

@observer class OrderLine { @observable price: number = 0; @observable amount: number = 0; }

模板字符串与插值的换行

模板字符串可以包含插值,而「在插值内部换行是否合适」取决于模板的语义内容——例如在一句自然语言中间断开通常是不 desirable 的。Prettier 不具备判断语义的能力,因此沿用了与对象相同的启发式:只有当原始源码的插值内部已经存在换行时,才允许把插值表达式拆成多行

这意味着下面的字面量即使超过打印宽度也不会被拆分:

`this is a long message which contains an interpolation: ${format(data)} <- like this`;

想让 Prettier 拆分插值,就必须先在${...}内部引入一个换行;否则无论多长都会保持单行。文档也坦承:团队不希望如此依赖原始格式,但目前这是最优的启发式。

分号与 ASI 保护

这一节针对noSemi(即semi: false)用法。考虑这段代码:

if (shouldAddLines) { [-1, 1].forEach(delta => addLine(delta * 20)) }

它没有分号也能正常运行,但 Prettier 会把它变成:

if (shouldAddLines) { ;[-1, 1].forEach(delta => addLine(delta * 20)) }

这是为了防止你犯错。设想 Prettier 不插入这个分号,而你在其后加了一行:

if (shouldAddLines) { + console.log('Do we even get here??') [-1, 1].forEach(delta => addLine(delta * 20)) }

糟糕——上面这段实际会被解析成:

if (shouldAddLines) { console.log('Do we even get here??')[-1, 1].forEach(delta => addLine(delta * 20)) }

[前有一个分号,这类问题就永远不会发生。它让该行独立于其他行,你可以随意增删、移动行而不用思考 ASI 规则。这一实践在 Standard 风格这类「无分号」流派中也同样常见。

实现细节可见 semicolon.js:shouldExpressionStatementPrintLeadingSemicolon只在options.semi为假时工作,随后由expressionNeedsAsiProtection判断表达式是否需要 ASI 保护——以[(、模板字符串、正则开头的表达式,前缀+/-的一元表达式等都会命中,从而在行首补上分号。

重要边界:如果你的程序已经存在分号相关的 bug,Prettier 不会替你自动修复——它只重排格式,不改变行为。例如开发者漏写(前分号的这段代码:

console.log('Running a background task') (async () => { await doBackgroundWork() })()

喂给 Prettier 后,它不会改变代码实际运行时的行为,只会把它格式化成能「暴露真实行为」的样子:

console.log("Running a background task")(async () => { await doBackgroundWork(); })();

printWidth 是指南,不是硬上限

printWidth对 Prettier 来说更像一条指南而非硬性行宽上限,它表达的是「我希望行大概有这么长」。Prettier 既会输出更短的行,也会输出更长的行,但总体上会努力贴近指定宽度。

存在一些无法断行的边界情况:超长字符串字面量、正则表达式、注释、变量名都无法跨行拆开(因为拆它们属于代码转换,见下文「Prettier 不关心什么」)。又或者你把代码嵌套 50 层深,行宽自然被缩进吃光——这是正常现象。

除此之外,还有几处 Prettier有意超出打印宽度的场景:

import 语句

Prettier 可以把长的import拆到多行:

import { CollectionDashboard, DashboardPlaceholder, } from "../components/collections/collection-dashboard/main";

但下面这个超出打印宽度的例子,Prettier 仍会保持单行:

import { CollectionDashboard } from "../components/collections/collection-dashboard/main";

这可能让部分人意外,但原因很实际:用户普遍要求「只引入一个名字时保持单行 import」,require调用同理。

测试函数

另一个高频需求是让冗长的测试描述保持单行,因为把参数折到多行对此类场景帮助有限:

describe("NodeRegistry", () => { it("makes no request if there are no nodes to prefetch, even if the cache is stale", async () => { // The above line exceeds the print width but stayed on one line anyway. }); });

Prettier 对describeittest等常见测试框架函数设有专门处理。

JSX 的特殊排版

当代码涉及 JSX 时,Prettier 的打印方式与其他 JS 代码略有不同:

function greet(user) { return user ? `Welcome back, ${user.name}!` : "Greetings, traveler! Sign up today!"; } function Greet({ user }) { return ( <div> {user ? ( <p>Welcome back, {user.name}!</p> ) : ( <p>Greetings, traveler! Sign up today!</p> )} </div> ); }

原因有二。第一,大量开发者本来就把 JSX 包在括号里(尤其是在return语句中),Prettier 顺应了这一主流风格。第二,这种替代表达方式让 JSX 更容易编辑:JSX 中残留的分号不同于普通 JS——它会变成纯文本直接显示在页面上:

<div> <p>Greetings, traveler! Sign up today!</p>; {/* <-- Oops! */} </div>

把 JSX 独立成块并自带括号,恰好降低了这类残留分号造成的困扰。

注释:格式化边界的所在

注释的内容:Prettier 基本无能为力。注释里可能是散文、被注释掉的代码,也可能是 ASCII 图——什么都能装,Prettier 无从知道该如何格式化或换行它们,所以一律原样保留。唯一的例外是JSDoc 风格注释(每行以*开头的块注释),Prettier 会修正它们的缩进。

注释的位置则是一个真正的难题:注释几乎可以出现在任何位置,Prettier 会尽力让注释停留在大致原来的位置,但结果并不总能完美。文档给出的实用建议:

  • 注释独占一行的效果通常优于行尾注释;
  • 优先用// eslint-disable-next-line而不是// eslint-disable-line
  • 「魔法注释」(如eslint-disable-next-line$FlowFixMe)在 Prettier 把一个表达式拆成多行后,可能需要你手动挪动位置。

举个例子:

// eslint-disable-next-line no-eval const result = safeToEval ? eval(input) : fallback(input);

之后你加了一个条件:

// eslint-disable-next-line no-eval const result = safeToEval && settings.allowNativeEval ? eval(input) : fallback(input);

Prettier 会把它折成:

// eslint-disable-next-line no-eval const result = safeToEval && settings.allowNativeEval ? eval(input) : fallback(input);

此时eslint-disable-next-line已经失效了,你需要手动把注释移到表达式内部:

const result = // eslint-disable-next-line no-eval safeToEval && settings.allowNativeEval ? eval(input) : fallback(input);

文档进一步建议:尽可能使用作用于行范围(如eslint-disable/eslint-enable)或语句级别(如/* istanbul ignore next */)的注释,它们更稳妥;也可以通过 ESLint 插件(如eslint-plugin-eslint-comments)直接禁用eslint-disable-lineeslint-disable-next-line的写法。

关于非标准语法的免责声明

Prettier 常常能够识别并格式化非标准语法,例如 ECMAScript 早期提案(early-stage proposals)和任何规范都未定义的 Markdown 语法扩展。对此类语法的支持属于尽力而为且实验性的:任何版本都可能引入不兼容变化,且不应被视为 breaking change。

关于机器生成文件的免责声明

package.jsoncomposer.lock这类文件由包管理器生成并定期更新。如果 Prettier 对其套用与一般 JSON 文件相同的格式规则,就会持续与这些工具产生冲突。为避免这种麻烦,Prettier 对这类文件改用基于JSON.stringify的格式器。你可能会注意到差异——比如垂直空白的消失——但这正是预期行为。

Prettier 不关心什么:它只打印,不转换

Prettier 只做打印(print),不做转换(transform)。这是为了限定 Prettier 的作用域——把打印这件事做到极致。以下都在明确的作用域之外:

  • 单/双引号字符串与模板字符串之间的互相转换;
  • +把超长字符串字面量拆成能放进行宽的片段;
  • 增删语法上可省略的{}return
  • ?:三元表达式改写为if-else语句;
  • 对 import、对象键、类成员、JSX 键、CSS 属性或任何其他内容做排序/移动——除属于「转换」而非打印外,排序还可能因副作用(import 即为典型)而不安全,并让最核心的正确性目标难以验证。

小结

Prettier 的设计哲学可以概括为三层:正确性优先——任何格式化不得改变代码行为;启发式务实——空行、多行对象、装饰器、插值换行等「没有完美规则」的场景,宁可保留原始写法中的换行线索,也绝不猜测语义;边界清晰——只打印、不转换,对实验性语法与机器生成文件明确声明免责。理解这些规则后,遇到 Prettier 的「反直觉」输出时,你既能判断它是否合理,也能通过objectWrapsingleQuotejsxSingleQuotesemiprintWidth等选项精确地表达团队偏好,而不是与格式化工具搏斗。

【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 23:13:05

HHFT:异构层级特征的两级自注意力推荐模型

推荐系统做到一定阶段&#xff0c;多数团队都会撞上一堵墙&#xff1a;模型的离线指标怎么调都涨不动了&#xff0c;特征该加的也加了&#xff0c;样本量也够大&#xff0c;但AUC就是卡在某个数位上。这时候问题往往不在特征的数量&#xff0c;而在特征的组织方式。HHFT&#x…

作者头像 李华
网站建设 2026/9/18 23:06:53

Win10/Win11安装VS2022社区版C++开发环境实操指南

1. 这不是“点下一步就完事”的安装指南&#xff0c;而是一份C开发者在Win10/Win11上亲手踩坑、反复验证的Visual Studio 2022社区版实操手册你搜“Visual Studio 2022 下载”&#xff0c;页面弹出一堆带广告的第三方站点&#xff0c;点进去要么是捆绑软件&#xff0c;要么是过…

作者头像 李华
网站建设 2026/9/18 23:05:11

在 TheAgentCompany 复现 Bash 接口,TaoToken 发 Key 跑 GPT-5.5

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 23:05:06

SSM网上书店系统实战:动态SQL、事务与防超卖下单

简介&#xff1a;这份基于SSM框架的网上书店系统毕业设计文档&#xff0c;面向计算机相关专业学生及Java Web初学者&#xff0c;提供从选题到实现的完整项目参考。全文围绕Spring、SpringMVC与MyBatis的整合开发展开&#xff0c;覆盖用户登录注销、前台商品浏览与搜索、在线购买…

作者头像 李华
网站建设 2026/9/18 23:03:36

YOLO v5到v11选型指南:目标检测版本演进、训练与部署

YOLO 这个名字现在已经被用得有点泛滥了&#xff0c;你在搜索引擎里敲进去&#xff0c;前面几条可能不是算法&#xff0c;而是某款服务器型号、某个软件版本号&#xff0c;甚至某台打印机。真到要干活的时候&#xff0c;问题反而变得很朴素&#xff1a;我手上这个项目&#xff…

作者头像 李华