Prettier your project

大搜車無線開發中心發表於2019-04-15

更多文章,參見大搜車技術部落格:blog.souche.com/

大搜車無線開發中心持續招聘中,前端,Nodejs,android 均有 HC,簡歷直接發到:sunxinyu@souche.com

簡介

Prettier is an opinionated code formatter.

從官網的介紹中我們可以看到,首先 Prettier 的定位是一個程式碼格式化工具,並且比較任性(opinionated)。它移除了**幾乎所有[1]**原始格式,來確保所有輸出的程式碼符合一致。

就最初的設計目標來說,Prettier 面向的範圍主要還是 Web 端的一些開發語言,如:

  • JSX
  • CSS, Less , and SCSS
  • GraphQL
  • 等... 不過作為一款程式碼格式化工具,Prettier 通過開放了外掛(Beta)的能力,來對不同語言提供支援。

為什麼要用

對於程式碼風格的聖戰,沒有一天真正停止過:

  • Tab V.S. space
  • single V.S. double quote
  • Semicolons
  • ...

就個人角度來說,這些爭論有意義嗎?我覺得是有的,因為不僅體現了一個程式設計師對細節的追求,還鍛鍊了探索、深挖、求真的能力。但是就團隊的角度來說,我覺得這些爭論是沒有意義的,個人程式碼的風格大相徑庭,從 ESlint 的規則數量就可以明顯地看出這一點。如果從個人程式碼風格的角度出發,制定一套適用於團隊層面的規範,討論到最後,很難有一個統一的結果。 所以,為了統一團隊程式碼風格,確實需要任性(opinionated),才能真正落地。

對新人友好

當有新人進入一個團隊時,他不僅需要熟悉業務流程,兼顧遺留程式碼,如果在開發時還被一系列的格式化程式碼規則磕磕絆絆,是非常不友好的一件事。而通過配置 Prettier,按下一個按鍵就可以直接將程式碼格式化,不需要為了適配各種規則手動修改,能將更多精力放在程式碼質量層面。

易於適應

除了一些非常有爭議的風格作為配置項,Prettier 內建的規則經過了多次修改討論,對於大多數開發者來說,是比較容易接受的。

關於未來

我們引入一個新工具,肯定希望它是穩步上升的,如果用到一半就被廢棄,後續的解耦成本就會成為一個新的問題。 所以一個工具的未來如何一個非常重要的考量點:

  • 作為一個 Facebook 出品的工具,其雄厚的技術實力肯定是我們不容小覷的。
  • 在 GitHub 上的 star 數,是老大哥 ESlint 的三倍之多,說明關注這個專案的人非常多。
  • 再看專案的更新進度,基本每 1 - 2 天就會有新的提交,這個迭代速度說明更新以及問題解決的響應非常快。
  • 還有 npm 上的下載量,達到了 400 萬次 / 周,說明真正在使用的人也非常多。

從以上種種看出,這個專案的未來是充滿光明的。

與 Eslint

比較

每當提到 Prettier,ESlint 一定是一個繞不過去坎。甚至有人覺得兩者是衝突的,是包含與被包含的關係。但是在我看來,他們只是存在某些功能上的重合,硬要從這個角度來說,頂多也是存在交集的關係。

要搞清楚兩者的關係,可以先從定義上解讀它們之間的區別:

  • Prettier 的定位是一個程式碼格式化工具(code formatter),其側重點在於格式化(format),其目的是讓你的程式碼變得更好看(prettier),可讀(readable)。
  • ESlint 的定位一個校驗工具(linting utility),其側重點在於校驗,其目的是讓程式碼更一致和避免 bug。

ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code, with the goal of making code more consistent and avoiding bugs.

簡單來看,它們相同的地方主要有以下幾點:

  1. 都期望能夠減少甚至避免產生 bug。
  2. 都期望統一程式碼風格。
  3. 都具備格式化程式碼的功能。

統一程式碼風格

ESlint 規則成千上萬,主要可以分為兩大類:

  1. 格式化程式碼規則(Formatting rules):如 max-len , no-mixed-spaces-and-tabs , keyword-spacing , comma-style ...
  2. 程式碼質量規則(Code-quality rules):如 no-unused-vars , no-extra-bind , no-implicit-globals , prefer-promise-reject-errors ...

理想情況下,如果 ESlint 規則設定的足夠細緻,基本能保證兩個不同的開發者——採用相同的技術方案——在程式碼風格上 90% 的一致性。 但是對於 Prettier 來說,它工作的領域僅限於第一類——格式化程式碼,也就是說即使存在嚴重影響程式碼質量(Code-quality)的問題——比如死迴圈——Prettier 不會做出任何反應。原因將會在下文中做出(Rationale)說明。

格式化程式碼

ESlint 本身是一個程式碼校驗工具,但是也提供了格式化程式碼的功能,通過新增 --fix 引數,可以將程式碼格式成符合自定義規則的樣子。 Prettier 作為專業的格式化工具,內部自建了一套關於程式碼風格的規則,且這些規則無法被修改,除了暴露出的 19 個的配置項。這麼做的目的,就是為了停止對各種個性化風格優劣的爭論,從而將討論的重點轉移到這 19 個配置項中。

整合

在我們的日常開發過程中,已經習慣了使用 ESlint 來保障程式碼質量與統一風格。如果再加上 Prettier 作為程式碼格式化的工具,就可以起到如虎添翼的效果。但是因為 Prettier 內建的規則可能和 ESlint 的規則產生衝突,所以需要通過配置外掛來解決這些問題。

  • Eslint 檢驗自定義規則的同時,還能支援 Prettier 的規則;
  • 我們希望 Prettier 格式化後的程式碼是符合 Eslint 規則的,但是因為 Prettier 內建規則是無法修改的,所以當兩種的規則衝突時,需要以 Prettier 的規則為主。

通過下面的步驟可以達到上述條件所描述的:

  • 安裝依賴
yarn add --dev eslint-config-prettier eslint-plugin-prettier
複製程式碼
  • 修改 ESlint 配置
// .eslintrc.json
{
  "extends": ["plugin:prettier/recommended"]
}
複製程式碼

如果在專案中使用 Prettier,我們當然不希望每次都要手動去執行格式化的命令,通過定製 pre-commit 鉤子,可以達到自動格式化的效果,詳見文件。 我個人最常用的是 pretty-quick,開箱即用。配置好後,每次將檔案提交到 git 暫存區(stage)之前,都會自動對所有檔案做格式化。這樣一來,只要開發者按照規範 [2] 的流程走,就避免了程式碼格式不一致問題。

yarn add pretty-quick husky --dev
複製程式碼
// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "pretty-quick --staged"
    }
  }
}
複製程式碼

除了通過使用 git hooks 的方式來格式化程式碼,也可以在編寫程式碼的過程中,直接對其進行格式化。以 VSCode 為例:

  1. 需要安裝 prettier-vscode 外掛。
  2. 自定義格式化配置
{
  "editor.formatOnSave": true,
  "editor.formatOnType": true,
  "editor.formatOnPaste": true
}
複製程式碼

這一步也可以通過 Formatting Toggle 外掛來實現。

設計原則

正確性(Correctness)

第一原則。作為一個程式碼格式化工具,如果想要讓別人用的放心,在保證格式化程式碼後不會出現 bug 的同時,也必不能對原始碼做任何入侵。這就是上文中提到的,即使出現了 bug,Prettier 也不會做出反應,關注如何更好地程式碼的原因。eslint --fix 則不同,它會去格式化那些違反規則的程式碼:

return '' + value;
// after eslint --fix
return `${value}`;

{ color: color } 
// after eslint --fix
{ color }

let a = {}
// after eslint --fix
const a = {}

複製程式碼

保證正確性的另一個方面,就是能夠提前預知格式化後可能會產生的 bug。看下面一個例子:

// semi.js
if (shouldAddLines) {
  [-1, 1].forEach(delta => addLine(delta * 20))
}
複製程式碼

通過 prettier semi.js --no-semi命令對上述程式碼格式化,最後得到的結果是這樣的:

if (shouldAddLines) {
  ;[-1, 1].forEach(delta => addLine(delta * 20))
}
複製程式碼

看上去似乎不符合規則,其實這麼做事為了避免下述情況:

 if (shouldAddLines) {
+  console.log('Do we even get here??')
   [-1, 1].forEach(delta => addLine(delta * 20))
 }

// after fomate 
if (shouldAddLines) {
  console.log('Do we even get here??')[-1, 1].forEach(delta => addLine(delta * 20))
}
複製程式碼

通過上面的例子,說明了 Prettier 並非通過一刀切的方式對程式碼進行格式化,還會考慮格式化後的程式碼,在具體的應用場景中會如何。

易讀性

在我們日常開發的過程中,經常會遇到一些涉及效能的問題。解法很多,但是我一般會選擇更加容易讓人看懂的那種。也就是說在一般情況下,為了程式碼的可維護性是可以犧牲一部分效能的。 格式化程式碼不是目的,目的是為了讓程式碼更加易讀。可以通過以下幾個列子看出 Prettier 是如何遵守這一規則的:

  • 多行物件

物件的書寫方式一般由以下兩種:短物件可以寫在一行裡,而一些長物件或者類 CSS 物件我們習慣多行書寫。

// single-line
const user = { name: "John Doe", age: 18 };

// multi-line
const css = {
	fontSize: 12,
	color: "red",
	padding: 100
};
複製程式碼

但是如果僅僅通過長短來格式化,就無法針對不同型別的物件做格式化處理,Prettier 也考慮到了這一點,可以通過下面的方式來自己選想要的格式化結果。

// before
const use = { name: "John Doe",
	age: 18
};
// after
const user = { name: "John Doe", age: 18 };

// ---

// before
const use = {
	name: "John Doe",
	age: 18 };
// after
const user = {
  name: "John Doe",
  age: 18
};

複製程式碼
  • 測試方法

在一些測試方法中,可能必有比較長的描述說明,導致長度(Print Width)超過了既定值。但是 Prettier 並不會去做強制換行處理,因為這樣做是沒有意義的,只會降低可讀性。

  • 易加難減

到目前位置,Prettier 的配置規則僅僅只有 19 條,雖然新增一個規則不是什麼難事,但是如果要將規則刪除,就存在很大的問題了。你可以試想下如果將來某一天 Prettier 的一條規則被刪除了,會發生什麼?以 bracket-spacing為例: 在刪除之前,無論我的配置是 true or false,格式化話後的物件都是遵循一定規則的:

  • true - Example: { foo: bar }.
  • false - Example: {foo: bar}.

但是如果規則被刪除,不復存在了,你的程式碼中可能會存在 {foo: bar, } 這樣的結果,這是大家都不願意看到的。就拿 PR 來說,我在程式碼比對的過程中看到的這個屬於格式上的不統一,在我引入了 Prettier 之後,這個問題理應是不會存在的,漸漸大家就會失去對這個工具的信任。

為什麼要舉 bracket-spacing 這個例子哪,因為這是一個連 Prettier 的作者也不知道為什麼會存在的配置規則,但是它現在依舊存在著。

尾聲

最後,借用我偶像 Dan 對 Prettier 的一段描述來結尾:

I just wanted to take a moment to say there are some of us who actually appreciate tools with limited scope that don’t attempt to solve everybody’s use case, and instead do a specific thing well.

說明

  • [1] 不格式化的某些情況下的 empty lines 以及 multi-line object
  • [2] 之所以強調規範一詞,是因為筆者曾經遇到過,有開發者因為程式碼校驗無法通過,通過直接刪除 node_modules,使 git 鉤子無效化的方式強制推送程式碼。

相關文章