如何寫出漂亮的 React 元件

王下邀月熊_Chevalier發表於2016-11-24

Walmart Labs的產品開發中,我們進行了大量的Code Review工作,這也保證了我有機會從很多優秀的工程師的程式碼中學習他們的程式碼風格與樣式。在這篇博文裡我會分享出我最欣賞的五種元件模式與程式碼片。不過我首先還是要談談為什麼我們需要執著於提高程式碼的閱讀體驗。就好像你有很多種方式去裝扮一隻貓,如果你把你的愛貓裝扮成了如下這樣子:

你或許可以認為蘿蔔青菜各有所愛,但是程式碼本身是應當保證其可讀性,特別是在一個團隊中,你的程式碼是註定要被其他人閱讀的。電腦是不會在意這些的,不管你朝它們扔過去什麼,它們都會老老實實的解釋,但是你的隊友們可不會這樣,他們會把醜陋的程式碼扔回到你的臉上。而所謂的Pretty Components,應該包含如下的特性:

  • 即使沒有任何註釋的情況下也易於理解
  • 比亂麻般的程式碼有更好的效能表現
  • 更易於進行Bug追溯
  • 簡潔明瞭,一句頂一萬句

SFC:Stateless Functional Component

我覺得我們在開發中經常忽略掉的一個模式就是所謂的Stateless Functional Component,不過這是我個人最愛的React元件優化模式,沒有之一。我喜愛這種模式不僅僅因為它們能夠減少大量的模板程式碼,而且因為它們能夠有效地提高元件的效能表現。總而言之,SFC能夠讓你的應用跑的更快,長的更帥。

直觀來看,SFC就是指那些僅有一個渲染函式的元件,不過這簡單的改變就可以避免很多的無意義的檢測與記憶體分配。下面我們來看一個實踐的例子來看下SFC的具體作用,譬如:

如果我們用正統的React元件的寫法,可以得出如下程式碼:

export default class RelatedSearch extends React.Component {
  constructor(props) {
    super(props);
    this._handleClick = this._handleClick.bind(this);
  }
  _handleClick(suggestedUrl, event) {
    event.preventDefault();
    this.props.onClick(suggestedUrl);
  }
  render() {
    return (
      <section className="related-search-container">
        <h1 className="related-search-title">Related Searches:</h1>
        <Layout x-small={2} small={3} medium={4} padded={true}>
          {this.props.relatedQueries.map((query, index) =>
            <Link
              className="related-search-link"
              onClick={(event) =>
                this._handleClick(query.searchQuery, event)}
              key={index}>
              {query.searchText}
            </Link>
          )}
        </Layout>
      </section>
    );
  }
}

而使用SFC模式的話,大概可以省下29%的程式碼:

const _handleClick(suggestedUrl, onClick, event) => {
  event.preventDefault();
  onClick(suggestedUrl);
};
const RelatedSearch = ({ relatedQueries, onClick }) =>
  <section className="related-search-container">
    <h1 className="related-search-title">Related Searches:</h1>
    <Layout x-small={2} small={3} medium={4} padded={true}>
      {relatedQueries.map((query, index) =>
        <Link
          className="related-search-link"
          onClick={(event) =>
            _handleClick(query.searchQuery, onClick, event)}
          key={index}>
          {query.searchText}
        </Link>
      )}
    </Layout>
  </section>
export default RelatedSearch;

程式碼量的減少主要來源兩個方面:

  • 沒有建構函式(5行)
  • 以Arrow Function的方式替代Render語句(4行)

實際上,SFC最迷人的地方不僅僅是其程式碼量的減少,還有就是對於可讀性的提高。SFC模式本身就是所謂純元件的一種最佳實踐正規化,而移除了建構函式並且將_handleClick()這個點選事件回撥函式提取出元件外,可以使JSX程式碼變得更加純粹。另一個不錯的地方就是SFC以Arrow Function的方式來定義了輸入的Props變數,即以Object Destructring語法來宣告元件所依賴的Props:

const RelatedSearch = ({ relatedQueries, onClick }) =>

這樣不僅能夠使元件的Props更加清晰明確,還能夠避免冗餘的this.props表示式,從而使程式碼的可讀性更好。

最後,我還想要強調下雖然我很推崇SFC,不過也不能濫用它。最合適使用SFC的地方就是之前你用純元件的地方。在Walmart Labs中,我們使用Redux來管理應用的狀態,也就意味著我們絕大部分的元件都是純元件,也就給了SFC廣闊的應用空間。一般來說,有以下特徵的元件式絕對不適合使用SFC的:

  • 需要自定義整個元件的生命週期管理
  • 需要使用到refs

Conditional Components

JSX本身不支援if表示式,不過我們可以使用邏輯表示式的方式來避免將程式碼切分到不同的子模組中,大概是如下樣子:

render() {
  <div class="search-results-container">
    {this.props.isGrid
      ? <SearchResultsGrid />
      : <SearchResultsList />}
  </div>
}

這種表示式在二選一渲染的時候很有效果,不過對於選擇性渲染一個的情況很不友好,譬如如下的情況:

render() {
  <div class="search-results-list">
    {this.props.isSoftSort
      ? <SoftSortBanner />
      : null
    }
  </div>
}

這樣子確實能起作用,不過看上去感覺怪怪的。我們可以選用另一種更加語義化與友好的方式來實現這個功能,即使用邏輯與表示式然後返回元件:

render() {
  <div class="search-results-list">
    {!!this.props.isSoftSort && <SoftSortBanner />}
  </div>
}

不過這一點也是見仁見智,每個人按照自己的喜好來就行了。

Arrow Syntax In React And Redux

ES2015裡包含了不少可口的語法糖,我最愛的就是那個Arrow Notation。這個特性在編寫元件時很有作用:

const SoftSort = ({ hardSortUrl, sortByName, onClick }) => {
  return (
    <div className="SearchInfoMessage">
      Showing results sorted by both Relevance and {sortByName}.
      <Link
        href={`?${hardSortUrl}`}
        onClick={(ev) => onClick(ev, hardSortUrl)}>
        Sort results by {sortByName} only
      </Link>
    </div>
  );
};

該函式的功能就是返回JSX物件,我們也可以忽略return語句:

const SoftSort = ({ hardSortUrl, sortByName, onClick }) =>
  <div className="SearchInfoMessage">
    Showing results sorted by both Relevance and {sortByName}.
    <Link
      href={`?${hardSortUrl}`}
      onClick={(ev) => onClick(ev, hardSortUrl)}>
      Sort results by {sortByName} only
    </Link>
  </div>

程式碼行數又少了不少咯!

另一塊我覺得非常適用Arrow Function的地方就是Redux的mapStateToProps函式:

const mapStateToProps = ({isLoading}) => {
  return ({
    loading: isLoading,
  });
};

需要注意的是,如果你返回的是Object,你需要包裹在大括號內:

const mapStateToProps = ({isLoading}) => ({
  loading: isLoading
});

使用Arrow Function優化的核心點在於其能夠通過專注於函式的重要部分而提升程式碼的整體可讀性,並且避免過多的模板程式碼帶來的噪音。

合理使用Object Destructing與Spread Attributes

大的元件往往受困於this.props過長的窘境,典型的如下所示:

render() {
  return (
    <ProductPrice
      hidePriceFulfillmentDisplay=
       {this.props.hidePriceFulfillmentDisplay}
      primaryOffer={this.props.primaryOffer}
      productType={this.props.productType}
      productPageUrl={this.props.productPageUrl}
      inventory={this.props.inventory}
      submapType={this.props.submapType}
      ppu={this.props.ppu}
      isLoggedIn={this.props.isLoggedIn}
      gridView={this.props.isGridView}
    />
  );
}

這麼多的Props估計看著都頭疼,如果我們要將這些Props繼續傳入下一層,大概就要變成下面這個樣子了:

render() {
  const {
    hidePriceFulfillmentDisplay,
    primaryOffer,
    productType,
    productPageUrl,
    inventory,
    submapType,
    ppu,
    isLoggedIn,
    gridView
  } = this.props;
  return (
    <ProductPrice
      hidePriceFulfillmentDisplay={hidePriceFulfillmentDisplay}
      primaryOffer={primaryOffer}
      productType={productType}
      productPageUrl={productPageUrl}
      inventory={inventory}
      submapType={submapType}
      ppu={ppu}
      isLoggedIn={isLoggedIn}
      gridView={isGridView}
    />
  );
}

暫時不考慮unKnown Props,我們可以使用解構賦值來實現這個功能:

render() {
  const props = this.props;
  return <ProductPrice {...props} />
}

Method Definition Shorthand

最後這個方法不一定多有用,不過還是能讓你的程式碼變得更加漂亮。如果你希望在Object中新增函式,你可以使用ES2015 Method Definition Shorthand來代替傳統的ES5的表示式,譬如:

Link.defaultProps = {
  onClick(event) {
    event.preventDefault();
    Logger.log(event);
  }
};

如果你想設定一個預設的空方法,也可以利用這種方式:

ProductRating.defaultProps = {
  onStarsClick() {}
};

相關文章