跳至主内容区

设计理念

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

Prettier 是一个固执己见的代码格式化工具。本文档将解释其部分设计选择。

Prettier 关注的核心原则

正确性

Prettier 的首要要求是输出格式正确且行为与格式化前完全一致的代码。若发现 Prettier 违反正确性原则的情况,请务必报告——这属于需要修复的缺陷!

字符串处理

双引号还是单引号?Prettier 会选择转义次数最少的形式。例如 "It's gettin' better!"(而非 'It\'s gettin\' better!')。当转义次数相同或字符串不含引号时,默认使用双引号(可通过 singleQuote 选项修改)。

JSX 有独立的引号选项:jsxSingleQuote。 JSX 源于 HTML,其属性主要使用双引号。浏览器开发者工具也遵循此惯例,即使源代码使用单引号仍以双引号显示 HTML。通过此选项可实现在 JS 中使用单引号,在 "HTML"(JSX) 中使用双引号。

Prettier 会保持字符串原有的转义形式。例如 "🙂" 不会被格式化为 "\uD83D\uDE42",反之亦然。

空行处理

空行的自动生成较为复杂。Prettier 的处理策略是保留源代码中的原始空行,并遵循两条附加规则:

  • 将多个连续空行合并为单个空行

  • 移除代码块(及整个文件)首尾的空行(文件末尾始终保留单个换行符)

多行对象处理

默认情况下,Prettier 的打印算法会在空间允许时单行输出表达式。但由于 JavaScript 中对象用途广泛(如对象列表嵌套配置样式表键控方法),多行格式有时更利于可读性。我们尚未找到通用规则,因此默认行为是:若源代码中 { 与首个键之间存在换行,则保持对象多行格式。结果是过长的单行对象会自动展开,而较短的多行对象不会折叠。

可通过 objectWrap 选项禁用此条件行为。

技巧: 若要将多行对象合并为单行:

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

只需删除 { 后的换行符:

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,
};
关于格式可逆性的说明

这种半手动格式化对象字面量的方式实际上是一种变通方案而非功能特性。当时因未能找到合适的启发式规则且急需修复才如此实现。然而作为通用策略,Prettier 会避免采用这种不可逆的格式化方式,因此团队仍在寻找相关启发规则:要么完全移除该行为,要么减少其应用场景。

何为可逆?对象字面量一旦变为多行格式,Prettier 不会将其重新折叠回单行。若在 Prettier 格式化后的代码中添加属性、运行格式化、又反悔删除该属性后再次运行格式化,最终格式可能与初始状态不同。这种无意义的变更甚至可能被提交,而这正是 Prettier 设计初衷要避免的情况。

装饰器

与对象类似,装饰器用途广泛。有时适合将装饰器置于被装饰对象上方,有时则更适合放在同一行。因尚未找到普适规则,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;
}

注意:Prettier 1.14.x 及更早版本曾尝试自动移动装饰器位置。若您的代码曾被旧版格式化过,可能需要手动合并部分装饰器以避免格式不一致:

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

模板字符串

模板字符串可包含插值表达式。判断是否应在插值内换行取决于模板语义内容(如在自然语言句子中间换行通常不理想)。因信息不足,Prettier 采用类似对象处理的启发规则:仅当插值表达式内原本存在换行时才会进行多行分割。

这意味着即使超出版宽限制,如下示例也不会被拆分为多行:

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

若需 Prettier 分割插值表达式,必须确保 ${...} 内存在换行符。否则无论多长都会保持单行。

团队本不希望依赖原始格式,但这是当前最优解。

分号

本节关于 noSemi 选项的使用。

参考以下代码:

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 中也很常见。

注意:若程序中存在分号相关缺陷,Prettier 不会自动修复。请牢记 Prettier 仅重构代码格式而不改变行为。以这段开发者忘记在 ( 前加分号的缺陷代码为例:

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

若将此代码输入 Prettier,它不会改变代码行为,而是以更清晰展示实际执行效果的方式重新格式化。

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

打印宽度

printWidth 选项对 Prettier 而言更像是指导原则而非硬性规定。它并非行长的绝对上限限制,而是向 Prettier 表明你期望的大致行长。Prettier 会同时生成更短或更长的行,但总体上会努力接近指定宽度。

存在某些极端情况,例如超长字符串字面量、正则表达式、注释和变量名无法跨行(在不使用代码转换的前提下,而 Prettier 不进行此类转换)。或者当代码嵌套达 50 层时,行内容自然会被缩进占满。

除此之外,Prettier 在少数情况下会刻意超出打印宽度。

导入语句

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

Prettier 对 JSX 的格式化方式与其他 JavaScript 代码有所不同:

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。普通 JS 中遗留分号不易察觉,而 JSX 中的残留分号可能成为页面上显示的文本内容。

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

注释处理

对于注释内容本身,Prettier 能做的有限。注释可能包含散文、被注释掉的代码或 ASCII 图示等任意内容,因此无法决定其格式或换行方式。唯一例外是 JSDoc 风格注释(每行以 * 开头的块注释),Prettier 可调整其缩进。

注释定位是另一大挑战。Prettier 会尽力保持注释原位置,但因注释可出现在代码任意位置,这并非易事。

通常,将注释置于独立行(而非行尾)可获得最佳效果。建议优先使用 // eslint-disable-next-line 而非 // eslint-disable-line

注意:当 Prettier 将表达式拆分为多行时,eslint-disable-next-line$FlowFixMe 等"魔法注释"可能需要手动调整位置。

假设存在以下代码:

// 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-disableeslint-enable)或语句级别(例如 /* istanbul ignore next */)的注释,它们更为安全。可以通过 eslint-plugin-eslint-comments 禁用 eslint-disable-lineeslint-disable-next-line 类注释。

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

Prettier 通常能够识别和格式化非标准语法,例如 ECMAScript 早期提案以及未在任何规范中定义的 Markdown 语法扩展。对此类语法的支持属于尽力而为的实验性功能。任何版本都可能引入不兼容变更,这类变更不应被视为破坏性变更。

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

某些文件(如 package.jsoncomposer.lock)由机器生成并由包管理器定期更新。如果 Prettier 对这些文件使用与其他文件相同的 JSON 格式化规则,将会频繁与其他工具产生冲突。为避免此问题,Prettier 对此类文件会改用基于 JSON.stringify 的格式化器。您可能会注意到差异(例如垂直空白的移除),但这属于预期行为。

Prettier _不关心_什么

Prettier 仅负责_打印_代码,并不进行代码转换。这旨在限定 Prettier 的职责范围——让我们专注于打印功能并做到极致!

以下是 Prettier 职责范围外的典型示例:

  • 将单引号/双引号字符串转换为模板字面量(反之亦然)

  • 使用 + 运算符将长字符串字面量拆分为符合打印宽度的片段

  • 在可选位置添加/移除 {}return

  • ?: 三元表达式转换为 if-else 语句

  • 对导入语句、对象键、类成员、JSX键、CSS属性等进行排序或移动。除了这属于代码转换而非打印范畴外,排序可能因副作用(如导入语句)而不安全,且会阻碍验证最重要的正确性目标