基本原理
Prettier 是一款有主见(opinionated)的代码格式化工具。本文档解释了它的一些设计选择。
Prettier 关注什么
正确性
Prettier 的首要要求是输出有效的代码,并且其行为与格式化前完全相同。如果您发现 Prettier 无法遵循这些正确性规则,请报告 – 这是一个需要修复的错误!
字符串
使用双引号还是单引号?Prettier 会选择导致转义字符最少的那个。例如 "It's gettin' better!"
,而不是 'It\'s gettin\' better!'
。如果两者相同或字符串不包含任何引号,Prettier 默认使用双引号(但这可以通过 singleQuote 选项更改)。
JSX 有自己的引号选项:jsxSingleQuote。JSX 源于 HTML,在 HTML 中,属性使用双引号是主要做法。浏览器开发者工具也遵循此约定,始终以双引号显示 HTML,即使源代码使用单引号也是如此。一个单独的选项允许对 JS 使用单引号,对“HTML”(JSX)使用双引号。
Prettier 会保持字符串的转义方式。例如,"🙂"
不会被格式化为 "\uD83D\uDE42"
,反之亦然。
空行
事实证明,自动生成空行非常困难。Prettier 采取的方法是保留空行,使其与原始源代码中的方式相同。还有两个额外的规则
- Prettier 会将多个空行折叠成一个空行。
- 块(和整个文件)开头和结尾的空行将被移除。(但是,文件始终以一个换行符结尾。)
多行对象
默认情况下,Prettier 的打印算法会将表达式打印在一行上(如果它们适合)。但是,对象在 JavaScript 中用于许多不同的用途,有时如果它们保持多行,确实有助于提高可读性。例如,请参阅 对象列表、嵌套配置、样式表 和 键控方法。我们还没有找到适用于所有这些情况的良好规则,因此 Prettier 会在原始源代码中 {
和第一个键之间存在换行符时,保持对象为多行。这样做的一个结果是,长的单行对象会自动展开,但短的多行对象永远不会折叠。
提示:如果您有一个想要合并成单行的多行对象
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,我们最终可能会得到与初始格式不同的格式。这种无用的更改甚至可能被包含在提交中,而这正是 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 及更早版本尝试自动移动您的装饰器,因此如果您在代码上运行过旧版本的 Prettier,则可能需要在此处手动合并一些装饰器以避免不一致。
@observer
class OrderLine {
@observable price: number = 0;
@observable
amount: number = 0;
}
最后一点:TC39 尚未决定装饰器是放在 export
之前还是之后。在此期间,Prettier 支持这两种方式。
@decorator export class Foo {}
export @decorator class Foo {}
模板字面量
模板字面量可以包含插值。不幸的是,决定是否在插值中插入换行符取决于模板的语义内容 - 例如,在自然语言句子中间引入换行符通常是不希望的。由于 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();
})();
打印宽度
对于 Prettier 来说,printWidth 选项更像是一条指导原则,而不是一个硬性规则。它不是允许的最大行长限制。它是一种告诉 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 对常见的测试框架函数(如 describe
、it
和 test
)有特殊情况处理。
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 更易于编辑。很容易留下分号。与普通 JS 不同,JSX 中遗留的分号最终可能会显示在页面上成为纯文本。
<div>
<p>Greetings, traveler! Sign up today!</p>; {/* <-- Oops! */}
</div>
注释
在注释的**内容**方面,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-plugin-eslint-comments
禁止使用 eslint-disable-line
和 eslint-disable-next-line
注释。
关于非标准语法的免责声明
Prettier 通常能够识别和格式化非标准语法,例如 ECMAScript 早期阶段的提案和任何规范中未定义的 Markdown 语法扩展。对这种语法的支持被认为是尽力而为的实验性支持。在任何版本中都可能会引入不兼容性,并且不应将其视为重大更改。
Prettier **不** 关注的内容
Prettier 仅**打印**代码。它不会转换代码。这是为了限制 Prettier 的范围。让我们专注于打印,并将其做得非常好!
以下是一些超出 Prettier 范围的事例
- 将单引号或双引号字符串转换为模板字面量,反之亦然。
- 使用
+
将长字符串字面量拆分为适合打印宽度的部分。 - 在可选的情况下添加/删除
{}
和return
。 - 将
?:
转换为if
-else
语句。 - 对导入、对象键、类成员、JSX 键、CSS 属性或任何其他内容进行排序/移动。除了是**转换**而不是仅仅打印(如上所述)之外,排序也可能不安全,因为它存在副作用(例如,对于导入),并且难以验证最重要的 正确性 目标。