跳至主内容区

Prettier 1.5:GraphQL、CSS-in-JS 和 JSON

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

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

此版本为 Prettier 新增了 GraphQL 格式化支持、CSS-in-JS(包括 styled-components)以及 JSON 支持。

这是我期待已久的版本:一个对 JavaScript 只有最小改动的版本!

过去六个月,我们持续改进 JavaScript 打印的各个方面,希望有朝一日能达到稳定状态。没有任何自动化工具能为所有边界情况输出完美代码。我们的目标是找到平衡点:当用户反馈代码格式化异常时,我们无法在不恶化其他代码表现、不引入人类难以理解的行为、不增加代码库过度复杂性的前提下进行优化。

虽然尚未完全达成目标,但已比以往任何时候都更接近!

随着 JavaScript 支持需求趋于平稳,我们得以支持前端开发者需要格式化的其他语言。上一个版本我们引入了 TypeScript 和 CSS,本版本则对它们进行了批量修复。同时新增支持三种新语言:GraphQL 查询、CSS-in-JS 内嵌样式和 JSON 现在都能通过 prettier 格式化!

博客文章:为 Prettier 添加新的布局策略 作者 @karl

Prettier 不仅是实用工具,更是精妙的技术结晶。@karl 投入大量时间改进 JSX 支持,并在此过程中为 prettier 实现了新基础功能:fill。他撰写的博客文章《为 Prettier 添加新的布局策略》深入浅出,强烈推荐给想了解 prettier 内部机制的开发者阅读。

GraphQL

感谢 @stubailo@jnwng@tgriesser@azz 的贡献,prettier 现已支持 GraphQL 查询格式化!

该功能支持 .graphql 文件,同时兼容以 graphqlgraphql.experimentalgql 开头的 JavaScript 模板标签,可无缝配合 RelayApollo 使用。

ReactDOM.render(
<QueryRenderer
query={graphql`
query appQuery {
viewer {
...TodoApp_viewer
}
}
`}
// ...
/>,
mountNode
);

请注意:当前仅支持 GraphQL 开源语法,因此不兼容 Relay Classic,仅适用于 Relay Modern。

CSS-in-JS

若您使用 styled-componentsstyled-jsx,prettier 现在可格式化模板表达式内的 CSS。特别感谢 @pomber 的卓越贡献!

const EqualDivider = styled.div`
margin: 0.5rem;
padding: 1rem;
background: papayawhip;
> * {
flex: 1;
&:not(:first-child) {
${props => (props.vertical ? "margin-top" : "margin-left")}: 1rem;
}
}
`;

JSON

该功能实现简单却非常实用,感谢 @josephfrazier 的贡献 :)

{
"name": "prettier",
"version": "1.5.0",
"description": "Prettier is an opinionated JavaScript formatter",
"bin": {
"prettier": "./bin/prettier.js"
}
}

CSS

CSS 支持功能仅用数天完成初版实现却效果惊艳,这让我倍感振奋。本版本包含多项重要 CSS 改进,均无需大规模重构。

CSS:选择器现支持分行打印 (#2047) 作者 @yuchi

CSS 格式化的最大难点在于多选择器处理。初始方案采用 80 字符换行规则,仅当超长时才拆分。但社区反馈普遍倾向另一种策略:始终在 , 后换行。这种在流行代码库中广泛采用的方式,能清晰展现选择器的层级结构。

// Before
.clusterPlannerDialog input[type="text"], .clusterPlannerDialog .uiTypeahead {
color: #333;
}

// After
.clusterPlannerDialog input[type="text"],
.clusterPlannerDialog .uiTypeahead {
color: #333;
}

CSS:十六进制颜色值小写化 (#2203) by @azz

代码格式化的边界较为模糊。核心在于空格处理,但引号类型、分号等通常也被纳入范畴。Prettier 在 JavaScript 中会轻度重构字符串(如移除多余的 \)和标准化数字。CSS 同样需要界定处理范围:我们决定将颜色值字母统一转为小写,但不涉及 rgb() 转十六进制或六位简写为三位等转换。

// Before
.foo {
color: #AAA;
-o-color: #fabcd3;
-ms-color: #AABBCC;
}

// After
.foo {
color: #aa;
-o-color: #fabcd3;
-ms-color: #aabbcc;
}

CSS:使用 fill 策略处理 CSS 属性值 (#2224)

新的 fill 基础功能在 CSS 场景中大放异彩。对于长属性值,不再机械地在每个元素前添加 \n,而是仅在超限时添加 \n,大幅提升代码美观度。

// Before
foo {
border-left:
1px
solid
mix($warningBackgroundColors, $warningBorderColors, 50%);
}

// After
foo {
border-left: 1px solid
mix($warningBackgroundColors, $warningBorderColors, 50%);
}

CSS:允许长媒体查询规则换行 (#2219)

这是完善新语言支持过程中的又一优化:现在长 @media 规则支持智能换行。

// Before
@media all and (-webkit-min-device-pixel-ratio: 1.5), all and (-o-min-device-pixel-ratio: 3/2), all and (min--moz-device-pixel-ratio: 1.5), all and (min-device-pixel-ratio: 1.5) {
}

// After
@media all and (-webkit-min-device-pixel-ratio: 1.5),
all and (-o-min-device-pixel-ratio: 3/2),
all and (min--moz-device-pixel-ratio: 1.5),
all and (min-device-pixel-ratio: 1.5) {
}

CSS:@else 与 } 保持同行 (#2088) by @azz

Less/SCSS 正逐步成为真正的编程语言 :) 我们正将其语法结构按照 JavaScript 模式重构,本次优化重点是 else 的定位。

// Before
@if $media == phonePortrait {
$k: .15625;
}
@else if $media == tabletPortrait {
$k: .065106;
}

// After
@if $media == phonePortrait {
$k: .15625;
} @else if $media == tabletPortrait {
$k: .065106;
}

CSS:实现 prettier-ignore 指令 (#2089) by @azz

虽然 Prettier 旨在格式化整个代码库,但有时开发者需要手动控制特定片段。prettier-ignore 注释就是为此设计的逃生口。此前在 CSS 中未生效是设计疏漏,现已修复 :)

// Before
foo {
/* prettier-ignore */
thing: foo;
-ms-thing: foo;
}

// After
foo {
/* prettier-ignore */
thing: foo;
-ms-thing: foo;
}

CSS:修复 CSS Modules 长 composes 属性破坏性问题 (#2190) by @tdeekens

为提升性能,许多"打包器"使用简单正则而非完整解析来提取依赖。因此我们禁止长 require() 调用换行,此原则同样适用于 CSS Modules:composes 字段换行会导致识别失败。故即使超长,也不再在此处换行。

// Before
.reference {
composes:
selector
from
"a/long/file/path/exceeding/the/maximum/length/forcing/a/line-wrap/file.css";
}

// After
.reference {
composes: selector from "a/long/file/path/exceeding/the/maximum/length/forcing/a/line-wrap/file.css";
}

CSS:逗号分隔的 @import 语句优先尝试 SCSS 解析 (#2225)

我们采用单一高层"解析器"处理 CSS/SCSS/Less(底层使用 postcss-lesspostcss-scss)。通过正则表达式确定首选解析器,语法错误时回退备用方案。但某些情况下首个(错误)解析器不会报错而是跳过元素,因此需增强正则表达式确保早期检测准确性。

幸运的是,这个技巧在实践中效果很好。如果我们发现更多边缘案例,很可能会选择正确的做法——将它们拆分成两个解析器。

// Before
@import "text-shadow";

// After
@import "rounded-corners", "text-shadow";

TypeScript

TypeScript 支持现已稳定,本次发布的所有更改都是针对小型边缘案例的修复。

TypeScript: 将箭头函数的类型参数与参数保持在同一行 (#2101) by @azz

Prettier 的核心算法是:如果组内元素无法全部容纳,则展开该组。这对大多数 JavaScript 代码效果很好,但在处理并排的两个组时(例如 <Generics>(Arguments))表现欠佳。我们必须谨慎创建分组,确保参数优先展开——这通常符合开发者预期。

// Before
export const forwardS = R.curry(<
V,
T
>(prop: string, reducer: ReducerFunction<V, T>, value: V, state: {[name: string]: T}) =>
R.assoc(prop, reducer(value, state[prop]), state)
);

// After
export const forwardS = R.curry(
<V, T>(
prop: string,
reducer: ReducerFunction<V, T>,
value: V,
state: { [name: string]: T }
) => R.assoc(prop, reducer(value, state[prop]), state)
);

TypeScript: 保留 yield/await 非空断言周围的括号 (#2149) by @azz

无论好坏,我们决定手动处理括号添加。当引入新运算符时,必须确保与其他运算符嵌套时能正确添加括号。这次我们遗漏了 TypeScript ! 中的 await 情况。

// Before
const bar = await foo(false)!;

// After
const bar = (await foo(false))!;

TypeScript: 当源代码存在时保留 import 中的 (#2150) by @azz

我们使用 typescript-eslint-parser 将 TypeScript AST 转换为 estree AST 供 Prettier 打印。偶尔会发现它未处理的边缘案例——这次是无法识别空 {}(这对 TypeScript 很重要)。幸运的是,该团队响应迅速,在我们添加临时解决方案后立即修复了问题。

// Before
import from "@types/googlemaps";

// After
import {} from "@types/googlemaps";

TypeScript: 始终将接口声明拆分为多行 (#2161) by @azz

interface 的实现代码与打印 object 的代码共享同一逻辑,后者包含"当内部存在 \n 时保持展开"的规则。但这并非接口的预期行为——我们期望始终展开(如同类声明),即使内容能容纳在 80 列内。

// Before
interface FooBar { [id: string]: number; }

// After
interface FooBar {
[id: string]: number;
}

TypeScript: 修复环境声明中的多余分号 (#2167) by @azz

开发者常请求 no-semisemi 选项,但 Prettier 团队领先一步为您实现了 two-semi!开个玩笑,这只是个已修复的 bug ;)

// Before
declare module "classnames" {
export default function classnames(
...inputs: (string | number | false | object | undefined)[]
): string;;
}

// After
declare module "classnames" {
export default function classnames(
...inputs: (string | number | false | object | undefined)[]
): string;
}

TypeScript: 对调用/构造签名中的函数参数进行分组 (#2169) by @azz

方法前的注释曾会纳入长度计算,常导致意外展开。幸运的是修复很简单——只需用 group 包装输出。

// Before
interface TimerConstructor {
// Line-splitting comment
new (
interval: number,
callback: (handler: Timer) => void
): Timer;
}

interface TimerConstructor {
// Line-splitting comment
new (interval: number, callback: (handler: Timer) => void): Timer;
}

TypeScript: 升级 tsep 解析器 (#2183) by @azz

这个 bug 非常恼人:每次格式化都会给对象键额外添加一个 _

// Before
obj = {
__: 42
___: 42
};

// After
obj = {
_: 42
__: 42
};

TypeScript: 多接口继承时自动换行 (#2085) by @azz

与 JavaScript 不同,TypeScript 允许同时继承多个类。事实证明开发者会使用此特性,现在 Prettier 能更好地处理这类代码的格式化。

// Before
export interface ThirdVeryLongAndBoringInterfaceName extends AVeryLongAndBoringInterfaceName, AnotherVeryLongAndBoringInterfaceName, AThirdVeryLongAndBoringInterfaceName {}

// After
export interface ThirdVeryLongAndBoringInterfaceName
extends AVeryLongAndBoringInterfaceName,
AnotherVeryLongAndBoringInterfaceName,
AThirdVeryLongAndBoringInterfaceName {}

TypeScript: 在 BinaryExpression 中正确处理 ObjectPattern 而非 ObjectExpression (#2238) by @azz

这个改动不太起眼,主要修复了 TypeScript 转换到 estree 时未妥善处理的边缘情况。

// Before
call(c => { bla: 1 }) || [];

// After
call(c => ({ bla: 1 })) || [];

保留指令后的空行 (#2070)

随着 TypeScript 支持引入,Prettier 现广泛应用于 Angular 代码库,这暴露出一些未妥善处理的边缘情况。本次修复确保函数内部指令后的空行得以保留。

// Before
export default class {
constructor($log, $uibModal) {
"ngInject";
Object.assign(this, { $log, $uibModal });

// After
export default class {
constructor($log, $uibModal) {
"ngInject";

Object.assign(this, { $log, $uibModal });

JavaScript

本次版本更新涉及 JavaScript 的改动很少,这非常理想。我们正逐步接近目标——打造出色的代码格式化工具。虽然永远无法达到 100% 完美的自动格式化,但我们的目标是:针对每个反馈的问题,在确保不破坏其他代码格式的前提下寻找最优解。

允许重新组合 JSX 行 (#1831) by @karl

Prettier 的核心目标是保持代码格式一致性:给定 AST,始终输出相同格式。此前我们仅在 JSX 和对象两处妥协依赖原始格式。本次改动后,我们不再依赖含文本内容的 JSX 原始输入格式,从而实现在 JSX 内部重排文本。

// Before
const Abc = () => {
return (
<div>
Please state your
{" "}
<b>name</b>
{" "}
and
{" "}
<b>occupation</b>
{" "}
for the board of directors.
</div>
);
};

// After
const Abc = () => {
return (
<div>
Please state your <b>name</b> and <b>occupation</b> for the board of
directors.
</div>
);
}

在非字面量计算属性表达式处换行 (#2087) by @azz

成员链的格式化是 Prettier 最复杂的部分,我们持续优化其处理逻辑以提升开发者体验。

// Before
nock(/test/)
.matchHeader("Accept", "application/json")[httpMethodNock(method)]("/foo")
.reply(200, {
foo: "bar",
});

// After
nock(/test/)
.matchHeader("Accept", "application/json")
[httpMethodNock(method)]("/foo")
.reply(200, {
foo: "bar",
});

在单变量声明场景中为首个变量添加缩进 (#2095) by @azz

此前对单行多变量声明的支持有限,因主流实践是每个变量单独声明。对于单变量声明我们无需缩进,但若后续还有其他声明,不缩进会导致视觉不协调——此问题现已修复。

// Before
var templateTagsMapping = {
'%{itemIndex}': 'index',
'%{itemContentMetaTextViews}': 'views'
},
separator = '<span class="item__content__meta__separator">•</span>';

// After
var templateTagsMapping = {
'%{itemIndex}': 'index',
'%{itemContentMetaTextViews}': 'views'
},
separator = '<span class="item__content__meta__separator">•</span>';

允许同时换行处理默认导入和命名导入 (#2096) by @azz

此修复解决了 1.4 版本中的意外退化问题:当 import 语句仅含单个元素时我们采用内联格式,但"单个元素"的定义错误地包含了单类型+单元素组合——现已修正。

// Before
import transformRouterContext, { type TransformedContextRouter } from '../../helpers/transformRouterContext';

// After
import transformRouterContext, {
type TransformedContextRouter
} from '../../helpers/transformRouterContext';

启用 allowImportExportEverywhere 选项 (#2207) by @zimme

Prettier 的目标是格式化实际开发中的代码,因此我们为所有支持的解析器启用宽松/实验模式。Babylon 允许在函数内部使用 import(非标准语法),支持此特性成本较低且符合实际需求。

// Before
ParseError

// After
function f() {
import x from 'x';
}

支持 new 调用中的内联模板 (#2222)

我们持续为函数调用添加特性,并需将其回溯到 new 调用——两者 AST 节点类型不同但实际应统一处理。本次重构让两者通过相同调用路径处理,有望减少未来类似问题的发生。

// Before
new Error(
formatErrorMessage`
This is a really bad error.
Which has more than one line.
`
);

// After
new Error(formatErrorMessage`
This is a really bad error.
Which has more than one line.
`);

修复对象值中 + 号的缩进问题 (#2227)

当我们将对赋值表达式(a = b)的启发式规则应用到对象({a: b})时,忘记修复其缩进问题。现已修复。

// Before
var abc = {
thing:
"asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf" +
"asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf" +
"asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf",
}

// After
var abc = {
thing:
"asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf" +
"asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf" +
"asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf",
}

处理三元表达式中的条件语句 (#2228)

当表达式本身是条件语句时 Prettier 已有特殊处理规则,但当条件语句作为三元表达式的左侧时该规则未生效。现已修正。

// Before
room = room.map((row, rowIndex) =>
row.map(
(col, colIndex) =>
rowIndex === 0 ||
colIndex === 0 ||
rowIndex === height ||
colIndex === width
? 1
: 0
)
);

// After
room = room.map((row, rowIndex) =>
row.map(
(col, colIndex) =>
rowIndex === 0 ||
colIndex === 0 ||
rowIndex === height ||
colIndex === width
? 1
: 0
)
);

为格式化添加缓存机制 (#2259)

在 1.0 版本中我们修复了格式化过程中的一个导致指数级性能问题的错误。虽然通过缓解措施避免了常规代码超时,但问题未彻底解决。通过在关键位置添加缓存层,现已完全解决。

此修复确保在调试模式下用 Prettier 格式化 IR 代码时不再出现超时。

// Before
...times out...

// After
someObject.someFunction().then(function () {
return someObject.someFunction().then(function () {
return someObject.someFunction().then(function () {
return someObject.someFunction().then(function () {
return someObject.someFunction().then(function () {
return someObject.someFunction().then(function () {
return someObject.someFunction().then(function () {
return someObject.someFunction().then(function () {
return someObject.someFunction().then(function () {
anotherFunction();
});
});
});
});
});
});
});
});
});

修复可变性符号位置 (#2261)

在实现 TypeScript 支持时重构了修饰符打印逻辑,意外将可变性符号(+)置于 static 之前,这在 Flow 中无效。现已修正。

// Before
class Route {
+static param: T;
}

// After
class Route {
static +param: T;
}

其他改进

范围与光标跟踪的多项修复 (#2266, #2248, #2250, #2136) 由 @CiGit@josephfrazier 贡献

这两项功能在上个版本引入后,我们在实际生产使用中发现了若干问题。大部分问题已修复,如遇新问题请及时反馈!