打印算法在布局决策完成后,需要把最终的 Doc 结构转换为可输出的文本字符串。这个转换过程包含几个具体任务:生成文本内容、处理缩进、插入换行符、添加或删除分号、调整引号类型,以及附加注释。
这最后一步看似简单,但它是"把 Prettier 的排版意图变成你看到的代码"的必经之路。每个细节——比如缩进用 Tab 还是空格、行尾加不加分号——都由用户配置(或默认值)在这个阶段生效。
缩进是代码格式化中最基本的视觉元素。Prettier 的 Doc IR 中的 indent 命令会在换行时增加一级缩进。打印器维护一个当前的缩进计数器,每进入一个 indent 节点时加一,退出时减一。
输出的缩进字符由 tabWidth 和 useTabs 两个配置项决定:
useTabs: false(默认):使用空格,每个缩进级别重复 tabWidth 个空格。默认是 2 个空格。useTabs: true:使用 Tab 字符,每个缩进级别一个 Tab。// useTabs: false, tabWidth: 2(默认) function hello() { const message = "world"; return message; } // ↑ 缩进用两个空格 // useTabs: true, tabWidth: 4 function hello() { → const message = "world"; → return message; } // ↑ 缩进用一个 Tab(→ 代表 Tab 字符)
注意 tabWidth 在 useTabs: true 时的含义不是"Tab 对应几个空格"——Tab 就是一个 Tab 字符,它没有固定的宽度,宽度取决于你编辑器的设置。tabWidth 在 useTabs: true 时影响的是行宽计算中的"Tab 算多少个字符"。默认是 2,意味着 Prettier 认为一个 Tab 等于 2 个字符的宽度。
Prettier 在 Doc 构建阶段把所有字符串标记为"需要引号",但不指定用哪种引号。具体使用单引号还是双引号,在最后的字符串生成阶段根据 singleQuote 配置决定。
默认 singleQuote: false,使用双引号。改为 true 后,所有字符串字面量使用单引号,除非字符串内容本身包含单引号(这时 Prettier 会智能地选择另一种引号来避免转义)。
// singleQuote: false(默认) const greeting = "hello world"; const quote = "it's a test"; // singleQuote: true const greeting = 'hello world'; const quote = "it's a test"; // 包含单引号,自动用双引号
对于模板字符串(反引号),Prettier 有自己的判断逻辑:如果字符串内容不包含任何 ${} 插值或换行符,Prettier 会把模板字符串转换为普通引号字符串(根据 singleQuote 配置选择单引号或双引号)。如果包含插值或换行,则保留模板字符串。
// 输入 const a = `hello`; const b = `hello ${name}`; const c = `line1\nline2`; // 输出(singleQuote: true) const a = 'hello'; // 无插值无换行,转为普通字符串 const b = `hello ${name}`; // 有插值,保留模板字符串 const c = `line1 line2`; // 有换行,保留模板字符串
semi 配置控制语句末尾是否添加分号。默认 true(加分号),改为 false 后 Prettier 会执行 ASI(Automatic Semicolon Insertion)规则来决定哪些行尾可以安全地省略分号。
// semi: true(默认) const x = 1; const y = 2; function add(a, b) { return a + b; } // semi: false const x = 1 const y = 2 function add(a, b) { return a + b }
注意 semi: false 不是简单地去掉所有分号。在某些 ASI 规则下可能导致歧义的位置,Prettier 仍然会保留分号,或者通过换行来避免歧义。比如以 (、[、`、+、-、/ 开头的语句,ASI 可能不会在前面插入分号,Prettier 会安全地处理这些情况。
// semi: false const x = 1 ;[1, 2, 3].forEach(console.log) // 分号保留,避免 ASI 歧义
trailingComma 控制多行结构末尾是否加逗号。可选值有 "all"(所有地方都加)、"es5"(只在 ES5 兼容的地方加,即函数参数不加)、"none"(不加)。
默认值是 "all"。这在工程实践中是个好选择——尾随逗号让 git diff 更干净(新增一行只显示新增的那一行,不需要修改上一行的末尾逗号),也让排序重构更方便(交换两行不需要改逗号)。
// trailingComma: "all"(默认) const config = { host: "localhost", port: 3000, debug: true, // ← 末尾有逗号 }; // trailingComma: "none" const config = { host: "localhost", port: 3000, debug: true // ← 没有逗号 };
注释在 AST 中不属于任何语法节点,但在最终输出中必须出现在合理的位置。打印器在生成字符串时,会根据注释在原始代码中的位置信息,把注释插入到 Doc 中对应的间隙处。
Prettier 对注释位置的处理规则大致是:
注释的内容 Prettier 从不修改——它不会重排注释里的文字、不会修正注释里的拼写、不会统一注释的格式。注释的唯一"格式化"是位置的微调。
以下是所有在这个阶段生效的配置项及其默认值:
| 配置项 | 默认值 | 作用 |
|---|---|---|
printWidth |
80 | 行宽限制 |
tabWidth |
2 | 缩进宽度(空格数或 Tab 等价字符数) |
useTabs |
false | 是否用 Tab 缩进 |
semi |
true | 是否加行尾分号 |
singleQuote |
false | 是否用单引号 |
trailingComma |
"all" | 尾随逗号策略 |
bracketSpacing |
true | 对象字面量花括号内是否有空格 |
quoteProps |
"as-needed" | 对象属性名何时加引号 |
arrowParens |
"always" | 箭头函数单个参数是否加括号 |
endOfLine |
"lf" | 换行符类型 |
这些配置项是 Prettier 仅有的用户可调选项。它们的数量如此之少,正是"固执"哲学的体现——Prettier 认为除了这些可调参数外,其他的格式决策应该固定下来,不需要用户操心。
理解了这个生成阶段,你就理解了 Prettier 的输出为什么是确定性的:同样的 AST + 同样的 Doc IR + 同样的配置 = 永远相同的输出。没有任何随机因素介入,没有任何启发式猜测,只有纯算法。