跳至主内容区

Prettier 1.0 正式发布

· 1 分钟阅读
非官方测试版翻译

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

本文由 vjeux 撰写,jlongster 编辑,最初发表于此处

两个多月前,我们正式发布prettier,旨在解决代码格式化浪费时间的难题。最初这只是一个实验性项目,但它显然引起了广大开发者的共鸣——短短两个月内就收获了约 7000 个 GitHub star 和每月超 10 万次的 npm downloads

简单来说,prettier 是一款通过将代码编译为 AST 再进行美化输出的 JavaScript 格式化工具。如同浏览器自动折行文本,prettier 也会根据设定的行宽对代码进行智能折行处理。其产出是风格统一、美观规范的代码,无论原始作者是谁。这彻底解决了开发者手动调整代码格式的耗时问题,也避免了团队间的格式争议(参见演示动图)。

最初我们并不确定能否始终生成既规范又美观的代码,但现在这个疑问已不复存在。众多知名项目(React、Jest、immutable-js、Haul 等)和企业(Oculus、Cloudflare)已采用 prettier 格式化其 JavaScript 代码库,并切实体验到自动化格式化的优势(更多案例详见这条推文)。部分用户甚至在数万行代码的完整项目上运行了 prettier。

过去三周我们梳理了所有未解决问题,尽可能敲定了代码格式规则,并修复了已知的大部分缺陷。虽然尚未达到完美,我们将持续迭代优化,但现在正是发布 1.0 正式版的最佳时机!

这不意味着格式规则将永远冻结,但后续调整会非常谨慎。例如我们正考虑优化三元表达式的格式,不过此类变更将极为有限。若确实需要重大调整,我们会发布新的主版本。

1.0 版本的核心意义在于prettier 已具备生产环境使用安全性。我们修复了大量错误,确保输出结果稳定且语义正确。无论您想在小项目中格式化几个文件,还是对整个代码库进行全面转换,prettier 都已准备就绪。何时放弃特定的手工格式化习惯,完全取决于您的实践节奏。

现在让我们看看 1.0 版为 prettier 带来了哪些新特性!

感谢所有为本次发布贡献力量的开发者,特别鸣谢 Ian Storm Taylor 设计的项目标识!

Adam Stankiewicz, Alex Rattray, Alex Stachowiak, Amjad Masad, Andrey Okonetchnikov, António Nuno Monteiro, Artem Sapegin, Benjamin Tan, Bill Mill, Brandon Mills, Brian Holt, Brian Ng, Charles Pick, ChristianHersevoort, Christopher Chedeau, Christopher Moeller, Damien Lebrun, Dan Harper, Dara Hak, David Ascher, David Hong, Davy Duperron, Eric Clemmons, Erick Romero, Esben Petersen, Filipe Fortes, Gregor Adams, Hampus,hlsson, Hawken Rives, Henry Zhu, Ilya Panasenko, James Henry, James Long, Jamie Webb, Jan Kassens, Jed Watson, Jeffrey Horn, Jeremy Morrell, Joe Fiorini, Jon LaBelle, Joseph Savona, Karl Horky, Karl O'Keeffe, Kent C. Dodds, Kevin Gibbons, Kim Joar Bekkelund, Lucas Bento, Lucas Duailibe, MarekMatejkaKCL, Mateusz Zatorski, Michael Ficarra, Michał Pierzchała, Mikael Brevik, Nathan Friedly, Patrick Camacho, Paul,arduner, Rafael Hengles, Raghuvir Kasturi, Rasmus Eneman, Riley Tomasek, Rogelio Guzman, Royce Townsend, Sean Grove, Shigeaki Okazaki, Simen,ekkhus, Simon Lydell, Sorin Muntean, Spencer Dixon, Thai Pangsakulyanont, Tom McKearney, Travis Jefferson, Umidbek Karimov, Vincent Voyer, Vu Tran, Walter Breakell, Yatharth Khatri, azu, daleroy, kalmanb, skratchdot

配置选项

Prettier 本质上是一个固执己见的代码格式化工具。项目初期,我们曾设想像 gofmt 或 refmt 那样完全不提供配置选项。但随着项目推进,我们意识到这种设计会阻碍 Prettier 的推广——许多本应从中受益的用户会因其格式化风格不符合预期而拒绝使用。

如今,我们对"固执己见"有了新诠释:只为影响代码基础结构的关键语法提供配置选项。这类选项通常具有"如果做不到X功能,无论多优秀我都不会使用"的特性。例如我(@vjeux)永远不会选用 standard,仅因其强制省略分号。这虽非理性判断,却反映了普遍存在的"代码风格之争"现象。

我们依然不会为所有语法类型添加选项,仅聚焦于核心差异点。目前确定的两大关键选项是:制表符与空格分号与无分号。现在,这些选项已正式加入 Prettier!

--no-semi (#1129)

特别感谢 rattrayalex 在此功能实现中的卓越贡献!

// Before
console.log();
[1, 2, 3].map(x => x + 1);

// After
console.log()
;[1, 2, 3].map(x => x + 1)

--use-tabs (#1026)

衷心感谢 rhengles 的代码实现及其背后的原理阐释!

// Before
if (1) {
··console.log(); // Two spaces
}

// After
if (1) {
» console.log(); // One Tab!
}

格式化改进

下文将系统梳理 1.0 版本的所有细节优化。虽然通常不会以博文形式发布更新日志,但本次展示既能体现 Prettier 的稳定性进展,也清晰呈现了我们近期修复的问题类型。

为返回调用的无花括号箭头函数添加换行 (#927)

这可能是当前版本 Prettier 被反馈最多的问题:之前我们在函数调用内部的箭头函数后没有添加换行。现在,这个问题已经修复。值得一提的是,如今反馈的问题都聚焦于这类细节,而不再是"格式完全崩溃"这类严重问题,这令人欣喜。

// Before
const testResults = results.testResults.map(testResult =>
formatResult(testResult, formatter, reporter));

// After
const testResults = results.testResults.map(testResult =>
formatResult(testResult, formatter, reporter)
);

为赋值表达式添加软换行并为二元表达式返回值添加括号 (#871 & #870)

当长链逻辑表达式的首行与变量声明或 return 语句处于同一行时,视觉效果会显得突兀。改进方案是将其移至下一行,并在 return 语句外添加括号以保证语法正确性。

// Before
const computedDescriptionLines = (showConfirm &&
descriptionLinesConfirming) ||
(focused && !loading && descriptionLinesFocused) ||
descriptionLines;

// After
const computedDescriptionLines =
(showConfirm && descriptionLinesConfirming) ||
(focused && !loading && descriptionLinesFocused) ||
descriptionLines;


// Before
return !filePath.includes(coverageDirectory) &&
!filePath.endsWith(`.${SNAPSHOT_EXTENSION}`);

// After
return (
!filePath.includes(coverageDirectory) &&
!filePath.endsWith(`.${SNAPSHOT_EXTENSION}`)
);

内联函数首个参数分组优化 (#947)

Prettier 首次引入的自定义模式是将函数作为最后一个参数内联打印。相对少见的是对首个参数采用相同处理。虽然现代库很少使用这种风格,但像 setTimeout 这类 JavaScript 核心函数仍采用此方式,因此 Prettier 有必要支持该格式。

// Before
setTimeout(
function() {
thing();
},
500
);

// After
setTimeout(function() {
thing();
}, 500);

优化样式组件的 JSX 输出 (#1144)

模板字符串格式化极具挑战性,因为其中的空白字符具有语义意义。Prettier 坚持不改变程序语义的原则,因此保持其原始格式。当前处理方式虽非最优,我们已有通用解决方案的构思。在实现前,针对 JSX 可先进行此类专项修复。

// Before
<style>
{
`
color: red;
`
}
</style>

// After
<style>{`
color: red;
`}</style>

装饰器中支持同行点表示法 (#1029)

MobX 3 现在使用需要内联书写的成员表达式,此前的启发式规则限制过严,现已修复。

class X {
// Before
@action.bound
setPrice(price) {
this.price = price;
}

// After
@action.bound setPrice(price) {
this.price = price;
}
}

单对象解构函数紧凑格式优化 (#1022)

React 无状态函数组件当前广受欢迎,将首个参数的解构紧凑排列可有效节省空间。

// Before
export default function StatelessFunctionalComponent(
{
searchFilters,
title,
items
}
) {
return <div />;
}

// After
export default function StatelessFunctionalComponent({
searchFilters,
title,
items,
}) {
return <div />
}

支持柯里化格式 (#1066)

Redux 强烈推崇柯里化函数写法,即每个参数都是嵌套函数。我们不再采用缩进格式,而是改为内联排列。

// Before
const mw = store =>
next =>
action => {
return next(action)
};

// After
const mw = store => next => action => {
return next(action)
};

保留 JSX 字符串转义规则 (#1056)

针对字符串转义,我们尝试过多种方案:完全不转义、全部转义、白名单转义...但始终无法找到满足所有用例的启发式方案。因此决定保持输入字符串的原始转义状态,此规则现已应用于 JSX 字符串。

// Before
<a href="https://foo.bar?q1=foo&amp;q2=bar" />

// After
<a href="https://foo.bar?q1=foo&q2=bar" />

括号处理

基于 AST 打印既是福音也是挑战——我们必须重新输出所有括号。此前我们的立场是仅输出保证程序有效且执行逻辑不变的最少必需括号。现在,我们愿意添加非必需但能提升代码可读性的括号。

为二元运算符添加括号 (#1153)

// Before
var sizeIndex = index - 1 >>> level & MASK;

// After
var sizeIndex = ((index - 1) >>> level) & MASK;

为无混淆箭头规则添加括号 (#1182)

// Before
var x = a => 1 ? 2 : 3;

// After
var x = a => (1 ? 2 : 3);

链式赋值中移除冗余括号 (#967)

// Before
this.size = (this._origin = (this._capacity = 0));

// After
this.size = this._origin = this._capacity = 0;

Flow 支持

在Prettier早期开发阶段,我们主要聚焦于完善对核心JavaScript语言的支持。现在核心功能已趋稳定,我们有更多精力优化Flow构造的格式化输出。

支持Flow泛型换行 (#1041)

// Before
type _ReactElement<DefaultProps, Props, Config: $Diff<Props, DefaultProps>, C: $React.Component<DefaultProps, Props, any>> = $React.Element<Config>;

// After
type _ReactElement<
DefaultProps,
Props,
Config: $Diff<Props, DefaultProps>,
C: $React.Component<DefaultProps, Props, any>
> = $React.Element<Config>;

改进Flow交集类型打印 (#1018#1155)

// Before
type Props =
& {
focusedChildren?: React.Children,
onClick: () => void,
overlayChildren?: React.Children,
}
& FooterProps;

// After
type Props = {
focusedChildren?: React.Children,
onClick: () => void,
overlayChildren?: React.Children,
} & FooterProps;

支持元组类型标注换行 (#1003)

// Before
export type FileMetaData = [/* id */ string, /* mtime */ number, /* visited */
| 0
| 1, /* dependencies */ Array<string>];

// After
export type FileMetaData = [
/* id */ string,
/* mtime */ number,
/* visited */ 0|1,
/* dependencies */ Array<string>,
];

单参数Flow简写类型省略括号 (#972)

// Before
type T = { method: (a) => void };

// After
type T = { method: a => void };

支持接口定义换行 (#1060)

// Before
export interface Environment1 extends GenericEnvironment<SomeType, AnotherType, YetAnotherType> {
m(): void
}

// After
export interface Environment1
extends GenericEnvironment<SomeType, AnotherType, YetAnotherType> {
m(): void
}

修复Flow数字字面量类型打印 (#1132)

// Before
type Hex = {n: 1};

// After
type Hex = {n: 0x01};

换行点优化

目前仍存在少数场景中,Prettier输出的代码会超出80列限制,而实际上存在更紧凑的写法。解决方案是精准定位可行的换行点,并确保不影响常见用例的格式效果。

支持字符串和成员表达式在等号后换行 (#1142#1188)

// Before
elements[0].innerHTML = '<div></div><div></div><div></div><div></div><div></div><div></div>';
var testExampleOrOrderOfGetterAndSetterReordered = obj.exampleOfOrderOfGetterAndSetterReordered;

// After
elements[0].innerHTML =
'<div></div><div></div><div></div><div></div><div></div><div></div>';
var testExampleOrOrderOfGetterAndSetterReordered =
obj.exampleOfOrderOfGetterAndSetterReordered;

支持顶级成员表达式换行 (#1036)

// Before
expect(
findDOMNode(component.instance()).getElementsByClassName(styles.inner)[0].style.paddingRight
).toBe("1000px");

// After
expect(
findDOMNode(component.instance()).getElementsByClassName(styles.inner)[0]
.style.paddingRight
).toBe("1000px");

其他改进

内联无括号箭头函数的类表达式 (#1023)

细微但重要的改进:现在直接内联箭头函数返回的类表达式。

// Before
jest.mock(
'../SearchSource',
() =>
class {
findMatchingTests(pattern) {
return {paths: []};
}
},
);

// After
jest.mock(
'../SearchSource',
() => class {
findMatchingTests(pattern) {
return {paths: []};
}
},
);

对象解构模式不再保留原始换行 (#981)

针对对象字面量,我们原有特殊处理:当原始代码中存在\n时保持展开格式。此逻辑本应仅适用于对象,但因共享解构处理代码而意外生效。现已修复此问题。

// Before
const Component2 = ({
props
}) => <Text>Test</Text>;

// After
const Component1 = ({ props }) => <Text>Test</Text>;

语言支持

要使Prettier真正可用,必须确保它能处理您编写的所有代码。我们的目标是支持底层解析器(babylon和flow)所能解析的全部语法。

支持带await标志的ForOf语句 (#1073)

async function f() {
for await (const line of readLines(\\"/path/to/file\\")) {
(line: void); // error: string ~> void
}
}

支持Flow类型扩展运算符 (#1064)

type TypeB = { ...TypeA };

正确性保障

Prettier的首要核心要求是输出行为与原始代码一致的合法代码。您应该能毫无顾虑地在整个代码库运行Prettier。为此我们建立了多重保障机制:

  • 建立自动化流程确保输出代码合法性

  • 在大型代码库实战验证(包括Facebook、CDNjs及众多社区项目)

  • 通过eslump进行高强度模糊测试

  • 将正确性问题报告列为最高优先级并立即修复

如果不幸遇到问题,请及时报告以便我们修复,你可以使用 // prettier-ignore 临时绕过格式化。

正确处理 JSXText 中的 \r\n (#1170)

// Before
<div>
{" "}
Text{" "}
</div>;

// After
<div>
Text
</div>;

修复 new 内部调用时的括号处理 (#1169)

// Before
new factory()();

// After
new (factory())();

为箭头函数返回类型添加括号 (#1152)

// Before
const f = (): string => string => {};

// After
const f = (): (string => string) => {};

修正精确对象流类型注解的打印格式 (#1114)

// Before
type Props = {};

// After
type Props = {||};

打印 return 语句后的悬挂注释 (#1178)

// Before
function f() {
return;
}

// After
function f() {
return /* a */;
}

打印对象键后的注释 (#1131)

// Before
let a = {
"a": () => 1
};

// After
let a = {
"a" /* comment */: () => 1
};

修复返回序列表达式内的前导注释 (#1133)

// Before
function sequenceExpressionInside() {
return;
// Reason for a
a, b;
}

// After
function sequenceExpressionInside() {
return ( // Reason for a
a, b
);
}

修正单参数箭头函数可选参数的打印格式 (#1002)

// Before
a = b? => c;

// After
a = (b?) => c;

修复默认导出处理 (#998)

// Before
export { foo, bar } from "./baz";

// After
export foo, {bar} from './baz';

在 JSXEmptyExpression 内部换行打印行注释/混合注释 (#985)

// Before
<div>
{
// single line comment}
</div>;

// After
<div>
{
// single line comment
}
</div>;

注释处理

prettier 通过打印 AST 实现格式化,这使得注释处理尤为复杂——注释可能出现在任意词法标记之间。prettier 的处理机制是将注释关联到 AST 节点(前置或后置)。我们采用通用启发式算法确定位置,多数情况有效,但仍需手动处理大量边界情况。

prettier 无法完美处理所有特殊位置的注释,但我们力求在大多数场景下合理呈现。若发现注释相关异常,请提交 issue 以便我们优化处理。

为 willBreak 添加 breakParent 支持 (#674)

// Before
runtimeAgent.getProperties(objectId, false, false, false, ( // ownProperties // accessorPropertiesOnly // generatePreview
error,
properties,
internalProperties
) => {
return 1;
});

// After
runtimeAgent.getProperties(
objectId,
false, // ownProperties
false, // accessorPropertiesOnly
false, // generatePreview
(error, properties, internalProperties) => {
return 1
},
);

修复 switch case 注释的多余空行 (#936)

// Before
switch (foo) {
case "bar":
doThing()


// no default
}

// After
switch (foo) {
case "bar":
doThing()

// no default
}

修正 import 声明注释处理 (#1030)

// Before
import {
FN1,
FN2,
// FN3,
FN4
} from // FN4,
// FN5
"./module";

// After
import {
FN1,
FN2,
// FN3,
FN4,
// FN4,
// FN5
} from './module';

修复函数末参数的注释位置 (#1176 & #905)

// Before
function f(
a: number
)// some comment here
: number {
return a + 1;
}

// After
function f(
a: number
// some comment here
): number {
return a + 1;
}

解除 flow 联合类型注释限制 (#1040)

// Before
type UploadState<E, EM, D> =
// The upload hasn't begun yet
| A
| // The upload timed out
B
| // Failed somewhere on the line
C;

// After
type UploadState<E, EM, D> =
// The upload hasn't begun yet
| A
// The upload timed out
| B
// Failed somewhere on the line
| C;

修正 ObjectPattern 内部的注释处理 (#1045)

// Before
export default (
{
foo,
bar
// comment
// comment 2
}: {
foo?: Object,
bar?: Object
}
) => {};

// After
export default (
{
foo,
bar
}: {
// comment
foo?: Object,
// comment 2
bar?: Object,
},
) => {}

修复注释排序位置 (#1038)

// Before
let {
// comment
a = b
} = c;

// After
let {
a = b // comment
} = c;

修正二元表达式注释位置 (#1043)

// Before
a = Math.random() * (yRange * (1 - minVerticalFraction)) +
minVerticalFraction * yRange// Comment
-
offset;

// After
a =
// Comment
Math.random() * (yRange * (1 - minVerticalFraction)) +
minVerticalFraction * yRange -
offset;

修复类方法注释处理 (#1074)

// Before
class x {
focus() // do nothing
{
// do nothing
}
}

// After
class x {
focus() {
// do nothing
// do nothing
}
}

支持注释块中使用 "// prettier-ignore" (#1125)

// Before
module.exports = {
// Some comment
// prettier-ignore
m: matrix(1, 0, 0, 0, 1, 0, 0, 0, 1)
};

// After
module.exports = {
// Some comment
// prettier-ignore
m: matrix(
1, 0, 0,
0, 1, 0,
0, 0, 1
)
};

稳定 VariableDeclarator 注释处理 (#1130)

// Before
let obj = [ // Comment
'val'
];

// After
let obj = [
// Comment
'val'
];

修复逗号前的注释检测问题 (#1127)

// Before
const foo = {
a: 'a' /* comment for this line */,

/* Section B */
b: 'b',
};

// After
const foo = {
a: 'a' /* comment for this line */,
/* Section B */
b: 'b',
};

修复 if 测试中的最后一条注释问题 (#1042)

// Before
if (isIdentifierStart(code) || code === 92) {
/* '\' */
}

// After
if (isIdentifierStart(code) || code === 92 /* '\' */) {}