React 整潔代碼最佳實踐

原文:Clean Code vs. Dirty Code: React Best Practices

作者:Donavon West

本文主要介紹了適用於現代 React 軟體開發的整潔代碼實踐,順便談談 ES6/ES2015 帶來的一些好用的「語法糖」。

什麼是整潔代碼,為什麼要在乎?

整潔代碼代表的是一種一致的編碼風格,目的是讓代碼更易於編寫,閱讀和維護。通常情況下,開發者在解決問題的時候,一旦問題解決就發起一個 Pull Request(譯註:合併請求,在 Gitlab 上叫 Merge Request)。但我認為,這時候工作並沒有真正完成,我們不能僅僅滿足於代碼可以工作。

這時候其實就是整理代碼的最好時機,可以通過刪除死代碼(殭屍代碼),重構以及刪除注釋掉的代碼,來保持代碼的可維護性。不妨問問自己,「從現在開始再過六個月,其他人還能理解這些代碼嗎?」簡而言之,對於自己編寫的代碼,你應該保證能很自豪地拿給別人看。

至於為什麼要在乎這點?因為我們常說一個優秀的開發者大都比較」懶「。在遇到需要重複做某些事情的情況下,他們會去找到一個自動化(或更好的)解決方案來完成這些任務。

整潔代碼能夠通過「味道測試」

整潔代碼應該可以通過「味道測試」。什麼意思呢?我們在看代碼的時候,包括我們自己寫的或或是別人的,會說:「這裡不太對勁。」如果感覺不對,那可能就真的是有問題的。如果你覺得你正在試圖把一個方形釘子裝進一個圓形的洞里,那麼就暫停一下,然後休息一下。多次嘗試之後,你會找到一個更好的解決方案。

整潔代碼是符合 DRY 原則的

DRY 是一個縮略詞,意思是「不要重複自己」(Don』t Repeat Yourself)。如果發現多個地方在做同樣的事情,那麼這時候就應該合併重複代碼。如果在代碼中看到了模式,那麼表明需要實行 DRY。

// Dirtyconst MyComponent = () => ( <div> <OtherComponent type="a" className="colorful" foo={123} bar={456} /> <OtherComponent type="b" className="colorful" foo={123} bar={456} /> </div>);

// Cleanconst MyOtherComponent = ({ type }) => ( <OtherComponent type={type} className="colorful" foo={123} bar={456} />);const MyComponent = () => ( <div> <MyOtherComponent type="a" /> <MyOtherComponent type="b" /> </div>);

有時候,比如在上面的例子中,實行 DRY 原則反而可能會增加代碼量。但是,DRY 通常也能夠提高代碼的可維護性。

注意,很容易陷入過分使用 DRY 原則的陷阱,應該學會適可而止。

整潔代碼是可預測和可測試的

編寫單元測試不僅僅只是一個好想法,而且應該是強制性的。不然,怎麼能確保新功能不會在其他地方引起 Bug 呢?

許多 React 開發人員選擇 Jest 作為一個零配置測試運行器,然後生成代碼覆蓋率報告。如果對測試前後對比可視化感興趣,請查看美國運通的 Jest Image snanshot。

整潔代碼是自注釋的

以前發生過這種情況嗎?你寫了一些代碼,並且包含詳細的注釋。後來你發現一個 bug,於是回去修改代碼。但是,你有沒有改變注釋來體現新的邏輯?也許會,也許不會。下一個看你代碼的人可能因為注意到這些注釋而掉進一個陷阱。

注釋只是為了解釋複雜的想法,也就是說,不要對顯而易見的代碼進行注釋。同時,更少的注釋也減少了視覺上的干擾。

// Dirtyconst fetchUser = (id) => ( fetch(buildUri`/users/${id}`) // Get User DTO record from REST API .then(convertFormat) // Convert to snakeCase .then(validateUser) // Make sure the the user is valid);

在整潔代碼的版本中,我們對一些函數進行重命名,以便更好地描述它們的功能,從而消除注釋的必要性,減少視覺干擾。並且避免後續因代碼與注釋不匹配導致的混淆。

// Cleanconst fetchUser = (id) => ( fetch(buildUri`/users/${id}`) .then(snakeToCamelCase) .then(validateUser));

命名

在我之前的文章 將函數作為子組件是一種反模式,強調了命名的重要性。每個開發者都應該認真考慮變數名,函數名,甚至是文件名。

這裡列舉一下命名原則:

  • 布爾變數或返回布爾值的函數應該以「is」,「has」或「should」開頭。

// Dirtyconst done = current >= goal;// Cleanconst isComplete = current >= goal;

  • 函數命名應該體現做了什麼,而不是是怎樣做的。換言之,不要在命名中體現出實現細節。假如有天出現變化,就不需要因此而重構引用該函數的代碼。比如,今天可能會從 REST API 載入配置,但是可能明天就會將其直接寫入到 JavaScript 中。

// Dirtyconst loadConfigFromServer = () => { ...}; // Cleanconst loadConfig = () => { ...};

整潔代碼遵循成熟的設計模式和最佳實踐

計算機已經存在很長一段時間了。多年以來,程序員通過解決某些特定問題,發現了一些固有套路,被稱為設計模式。換言之,有些演算法已經被證明是可以工作的,所以應該站在前人的肩膀上,避免犯同樣的錯誤。

那麼,什麼是最佳實踐,與設計模式類似,但是適用範圍更廣,不僅僅針對編碼演算法。比如,「應該對代碼進行靜態檢查」或者「當編寫一個庫時,應該將 React 作為 peerDependency」,這些都可以稱為最佳實踐。

構建 React 應用程序時,應該遵循以下最佳實踐:

  • 使用小函數,每個函數具備單一功能,即所謂的單一職責原則(Single responsibility principle)。確保每個函數都能完成一項工作,並做得很好。這樣就能將複雜的組件分解成許多較小的組件。同時,將具備更好的可測試性。
  • 小心抽象泄露(leaky abstractions)。換言之,不要強迫消費方去了解內部代碼實現細節。
  • 遵循嚴格的代碼檢查規則。這將有助於編寫整潔,一致的代碼。

整潔代碼不需要花長時間來編寫

總會聽到這樣的說法:編寫整潔代碼會降低生產力。簡直是在胡說八道。是的,可能剛開始需要放慢速度,但最終會隨著編寫更少的代碼而節奏加快。

而且,不要小看代碼評審導致的重寫重構,以及修復問題花費的時間。如果把代碼分解成小的模塊,每個模塊都是單一職責,那麼很可能以後再也不用去碰大多數模塊了。時間就省下來了,也就是說 「write it and forget it」。

槽糕代碼與整潔代碼的實例

使用 DRY 原則

看看下面的代碼示例。如上所述,從你的顯示器退後一步,發現什麼模式了嗎?注意 Thingie 組件與 ThingieWithTitle 組件除了 Title 組件幾乎完全相同,這是實行 DRY 原則的最佳情形。

// Dirtyimport Title from "./Title";export const Thingie = ({ description }) => ( <div class="thingie"> <div class="description-wrapper"> <Description value={description} /> </div> </div>);export const ThingieWithTitle = ({ title, description }) => ( <div> <Title value={title} /> <div class="description-wrapper"> <Description value={description} /> </div> </div>);

在這裡,我們將 children 傳遞給 Thingie。然後創建 ThingieWithTitle,這個組件包含 Thingie,並將 Title 作為其子組件傳給 Thingie

// Cleanimport Title from "./Title";export const Thingie = ({ description, children }) => ( <div class="thingie"> {children} <div class="description-wrapper"> <Description value={description} /> </div> </div>);export const ThingieWithTitle = ({ title, ...others }) => ( <Thingie {...others}> <Title value={title} /> </Thingie>);

默認值

看看下面的代碼。使用邏輯或將 className 的默認值設置成 「icon-large」,看起來像是上個世紀的人才會寫的代碼。

// Dirtyconst Icon = ({ className, onClick }) => { const additionalClasses = className || "icon-large"; return ( <span className={`icon-hover ${additionalClasses}`} onClick={onClick}> </span> );};

這裡我們使用 ES6 的默認語法來替換 undefined 時的值,而且還能使用 ES6 的箭頭函數表達式寫成單一語句形式,從而去除對 return 的依賴。

// Cleanconst Icon = ({ className = "icon-large", onClick }) => ( <span className={`icon-hover ${className}`} onClick={onClick} />);

在下面這個更整潔的版本中,使用 React 中的 API 來設置默認值。

// Cleanerconst Icon = ({ className, onClick }) => ( <span className={`icon-hover ${className}`} onClick={onClick} />);Icon.defaultProps = { className: "icon-large",};

為什麼這樣顯得更加整潔?而且它真的會更好嗎?三個版本不是都在做同樣的事情嗎?某種意義上來說,是對的。讓 React 設置 prop 默認值的好處是,可以產生更高效的代碼,而且在基於 Class 的生命周期組件中允許通過 propTypes 檢查默認值。還有一個優點是:將默認邏輯從組件本身抽離出來。

例如,你可以執行以下操作,將所有默認屬性放到一個地方。當然,並不是建議你這樣做,只是說具有這樣的靈活性。

import defaultProps from "./defaultProps";// ...Icon.defaultProps = defaultProps.Icon;

從渲染分離有狀態的部分

將有狀態的數據載入邏輯與渲染邏輯混合可能增加組件複雜性。更好的方式是,寫一個負責完成數據載入的有狀態的容器組件,然後編寫另一個負責顯示數據的組件。這被稱為 容器模式。

在下面的示例中,用戶數據載入和顯示功能放在一個組件中。

// Dirtyclass User extends Component { state = { loading: true }; render() { const { loading, user } = this.state; return loading ? <div>Loading...</div> : <div> <div> First name: {user.firstName} </div> <div> First name: {user.lastName} </div> ... </div>; } componentDidMount() { fetchUser(this.props.id) .then((user) => { this.setState({ loading: false, user })}) }}

在整潔版本中,載入數據和顯示數據已經分離。這不僅使代碼更容易理解,而且能減少測試的工作量,因為可以獨立測試每個部分。而且由於 RenderUser 是一個無狀態組件,所以結果是可預測的。

// Cleanimport RenderUser from "./RenderUser";class User extends Component { state = { loading: true }; render() { const { loading, user } = this.state; return loading ? <Loading /> : <RenderUser user={user} />; } componentDidMount() { fetchUser(this.props.id) .then(user => { this.setState({ loading: false, user })}) }}

使用無狀態組件

React v0.14.0 中引入了無狀態函數組件(SFC),被簡化成純渲染組件,但有些開發者還在使用過去的方式。例如,以下組件就應該轉換為 SFC。

// Dirtyclass TableRowWrapper extends Component { render() { return ( <tr> {this.props.children} </tr> ); }}

整潔版本清除了很多可能導致干擾的信息。通過 React 核心的優化,使用無狀態組件將佔用更少的內存,因為沒有創建 Component 實例。

// Cleanconst TableRowWrapper = ({ children }) => ( <tr> {children} </tr>);

剩餘/擴展屬性(rest/spread)

大約在一年前,我還推薦大家多用 Object.assign。但時代變化很快,在 ES2016/ES7 中引入新特性 rest/spread。

比如這樣一種場景,當傳遞給一些 props 給一個組件,只希望在組件本身使用 className,但是需要將其他所有 props 傳遞到子組件。這時,你可能會這樣做:

// Dirtyconst MyComponent = (props) => { const others = Object.assign({}, props); delete others.className; return ( <div className={props.className}> {React.createElement(MyOtherComponent, others)} </div> );};

這不是一個非常優雅的解決方案。但是使用 rest/spread,就能輕而易舉地實現,

// Cleanconst MyComponent = ({ className, ...others }) => ( <div className={className}> <MyOtherComponent {...others} /> </div>);

我們將剩餘屬性展開並作為新的 props 傳遞給 MyOtherComponent 組件。

合理使用解構

ES6 引入 解構(destructuring) 的概念,這是一個非常棒的特性,用類似對象或數組字面量的語法獲取一個對象的屬性或一個數組的元素。

對象解構

在這個例子中,componentWillReceiveProps 組件接收 newProps 參數,然後將其 active 屬性設置為新的 state.active

// DirtycomponentWillReceiveProps(newProps) { this.setState({ active: newProps.active });}

在整潔版本中,我們解構 newPropsactive。這樣我們不僅不需要引用 newProps.active,而且也可以使用 ES6 的簡短屬性特性來調用 setState

// CleancomponentWillReceiveProps({ active }) { this.setState({ active });}

數組解構

一個經常被忽視的 ES6 特性是數組解構。以下面的代碼為例,它獲取 locale 的值,比如「en-US」,並將其分成 language(en)和 country(US)。

// Dirtyconst splitLocale = locale.split("-");const language = splitLocale[0];const country = splitLocale[1];

在整潔版本,使用 ES6 的數組解構特性可以自動完成上述過程:

// Cleanconst [language, country] = locale.split("-");

所以結論是

希望這篇文章能有助於你看到編寫整潔代碼的好處,甚至可以直接使用這裡介紹的一些代碼示例。一旦你習慣編寫整潔代碼,將很快就會體會到 「write it and forget it」 的生活方式。

推薦閱讀:

React Conf 2017 不能錯過的大起底——Day 1!
解析 Redux 源碼
我對Flexbox布局模式的理解
從0到1,細說 React 開發環境搭建那點事
Immutable 詳解及 React 中實踐

TAG:React | JavaScript | 前端开发 |