原理
Prettier 是一款有自己主張的程式碼格式化工具。本文檔說明了它的一些選擇。
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` 的*之前*還是*之後* (https://github.com/tc39/proposal-decorators/issues/69)。同時,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();
})();
列印寬度
printWidth 選項對 Prettier 來說更像是一個指導方針,而不是一個硬性規則。它不是允許的行長上限。它是一種告訴 Prettier 你大致希望行有多長的方式。Prettier 會產生更短和更長的程式碼行,但通常會努力滿足指定的列印寬度。
有一些邊緣情況,例如非常長的字串字面值、正則表達式、註釋和變數名稱,它們不能跨行拆分(不使用 Prettier 不會做 的程式碼轉換)。或者,如果你將程式碼巢狀 50 層深,你的程式碼行當然會以縮排為主 :)
除此之外,還有一些情況下 Prettier 會故意超過列印寬度。
引入 (Imports)
Prettier 可以將過長的 import
陳述式斷成多行。
import {
CollectionDashboard,
DashboardPlaceholder,
} from "../components/collections/collection-dashboard/main";
以下範例雖然超過了列印寬度,但 Prettier 仍然會將其印在單行中。
import { CollectionDashboard } from "../components/collections/collection-dashboard/main";
有些人可能會覺得這樣很意外,但我們這樣做的原因是,許多使用者要求將只包含單一元素的 import
保持在單行。這同樣適用於 require
呼叫。
測試函式 (Testing functions)
另一個常見的要求是將冗長的測試描述保持在一行,即使它過長。在這種情況下,將參數換行到新行並沒有太大幫助。
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>
註解 (Comments)
關於註解的*內容*,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 屬性或其他任何東西。 除了是*轉換*而不是僅僅列印(如上所述)之外,排序還可能不安全,因為它會產生副作用(例如,對於引入),並且難以驗證最重要的正確性目標。