React 整潔程式碼最佳實踐

咖啡的香味是暗色的發表於2017-12-04

原文: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。

// Dirty
const MyComponent = () => (
  <div>
    <OtherComponent type="a" className="colorful" foo={123} bar={456} />
    <OtherComponent type="b" className="colorful" foo={123} bar={456} />    
  </div>
);
複製程式碼
// Clean
const 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,於是回去修改程式碼。但是,你有沒有改變註釋來體現新的邏輯?也許會,也許不會。下一個看你程式碼的人可能因為注意到這些註釋而掉進一個陷阱。

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

// Dirty
const 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
);
複製程式碼

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

// Clean
const fetchUser = (id) => (
  fetch(buildUri`/users/${id}`)
    .then(snakeToCamelCase)
    .then(validateUser)
);
複製程式碼

命名

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

這裡列舉一下命名原則:

  • 布林變數或返回布林值的函式應該以“is”,“has”或“should”開頭。

    // Dirty
    const done = current >= goal;
    複製程式碼
    // Clean
    const isComplete = current >= goal;
    複製程式碼
  • 函式命名應該體現做了什麼,而不是是怎樣做的。換言之,不要在命名中體現出實現細節。假如有天出現變化,就不需要因此而重構引用該函式的程式碼。比如,今天可能會從 REST API 載入配置,但是可能明天就會將其直接寫入到 JavaScript 中。

    // Dirty
    const loadConfigFromServer = () => {
      ...
    };
    複製程式碼
    // Clean
    const loadConfig = () => {
      ...
    };
    複製程式碼

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

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

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

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

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

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

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

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

槽糕程式碼與整潔程式碼的例項

使用 DRY 原則

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

// Dirty
import 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

// Clean
import 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”,看起來像是上個世紀的人才會寫的程式碼。

// Dirty
const Icon = ({ className, onClick }) => {
  const additionalClasses = className || 'icon-large';
  
  return (
    <span
      className={`icon-hover ${additionalClasses}`}
      onClick={onClick}>
    </span>
  );
};
複製程式碼

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

// Clean
const Icon = ({ className = 'icon-large', onClick }) => (
  <span className={`icon-hover ${className}`} onClick={onClick} />
);
複製程式碼

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

// Cleaner
const 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;
複製程式碼

從渲染分離有狀態的部分

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

在下面的示例中,使用者資料載入和顯示功能放在一個元件中。

// Dirty
class 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 是一個無狀態元件,所以結果是可預測的。

// Clean
import 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。

// Dirty
class TableRowWrapper extends Component {
  render() {
    return (
      <tr>
        {this.props.children}
      </tr>
    );
  }
}
複製程式碼

整潔版本清除了很多可能導致干擾的資訊。通過 React 核心的優化,使用無狀態元件將佔用更少的記憶體,因為沒有建立 Component 例項。

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

複製程式碼

剩餘/擴充套件屬性(rest/spread)

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

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

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

複製程式碼

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

// Clean
const MyComponent = ({ className, ...others }) => (
  <div className={className}>
    <MyOtherComponent {...others} />
  </div>
);
複製程式碼

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

合理使用解構

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

物件解構

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

// Dirty
componentWillReceiveProps(newProps) {
  this.setState({
    active: newProps.active
  });
}
複製程式碼

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

// Clean
componentWillReceiveProps({ active }) {
  this.setState({ active });
}

複製程式碼

陣列解構

一個經常被忽視的 ES6 特性是陣列解構。以下面的程式碼為例,它獲取 locale 的值,比如“en-US”,並將其分成 language(en)和 country(US)。

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

複製程式碼

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

// Clean
const [language, country] = locale.split('-');

複製程式碼

所以結論是

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

最初發表於:zhuanlan.zhihu.com/p/31615844

相關文章