[譯] 單元素組件模式簡介:使用 React 或其它組件庫建立可靠組件的規則和實踐

單元素組件模式簡介

使用 React 或其它基於組件的庫建立可靠構建模塊的規則和最佳實踐。

在 2002 年 — 當我開始構建網頁的時候 — 包括我在內的大多數開發者都使用 <table> 標籤來構建網頁佈局。css

直到 2005 年,我纔開始遵循網頁標準html

若是有網站或網頁宣稱遵循網頁標準,一般就表示他們的網頁符合 HTML、CSS、JavaScript 等標準。HTML 的部分也要知足無障礙性以及 HTML 語義的要求。前端

我瞭解了語義化和無障礙性,而後開始使用正確的 HTML 標籤和外部 CSS。我很自豪地將 W3C 認證徽章添加到我製做的每一個網站。react

咱們編寫的 HTML 代碼和輸出到瀏覽器中的真實代碼很是類似。這意味着使用 W3C 驗證器 和其它工具來驗證輸出代碼的規範性也能夠告訴咱們如何寫出更好的代碼。android

時光流逝。爲了分離前端中可重用部分,我使用過 PHP、模版系統、jQuery、Polymer、Angular 和 React。尤爲是後幾個,最近三年我一直在使用它們。ios

隨着時間的推移,咱們編寫的代碼和用戶實際使用的代碼愈來愈不一樣了。如今,咱們使用不一樣方式(例如 Babel 和 TypeScript)來編譯代碼。咱們使用 ES2015+JSX 規範編寫,但最終的輸出代碼就只是 HTML 和 JavaScript。git

現在,雖然咱們還會使用 W3C 的工具來驗證咱們的網站,但對於編寫代碼沒有太大幫助。咱們仍在追求代碼穩定和可維護的最佳實踐。並且,若是你正在讀這篇文章,我想你也有一樣的訴求。es6

我爲你準備了一些東西。github

單元素組件模式(Singel 源碼

我已經不知道寫過多少個組件了。但若是把 Polymer、Angular 和 React 的項目都加起來,我敢說這個數字確定超過一千了。npm

除公司項目外,我還維護了一個包含 40 多個示例組件的 React 模版庫。另外,我正在和 Raphael Thomazella 維護一套交互式 UI 組件庫,他爲這個項目貢獻了不少。

許多開發者都有一個誤解:若是以一個完美的文件結構來開始一個項目,那麼他們就不會遇到任何問題。事實上,文件結構的一致性沒那麼重要。若是你的組件沒有遵循明肯定義的規則,這最終會使你的項目很難維護。

在建立和維護了那麼多組件以後,我發現了一些使它們更加一致和可靠的特性,這樣用起來會更加愉快。一個組件越像一個 HTML 元素,它就會變得越可靠

沒有什麼比一個 <div> 標籤更可靠了。

使用組件時,你能夠問問本身下面的問題:

  • 問題 #1:若是我須要將 props 傳遞給嵌套元素會怎麼樣?
  • 問題 #2:因爲某種緣由,這個組件會使應用中斷嗎?
  • 問題 #3:若是我想傳遞 id 或其它 HTML 屬性會怎麼樣?
  • 問題 #4:我能夠經過傳遞 classNamestyle 屬性來自定義組件樣式嗎?
  • 問題 #5:事件是如何處理的呢?

可靠性意味着,在這種狀況下,不須要打開文件查看源碼來了解它的工做原理。若是你在使用一個 <div>,你立刻就會知道答案,以下:

我把這一組規則稱爲 Singel

重構驅動開發

先讓它工做,而後再去優化。

固然,不可能讓全部組件都遵循 Singel 所有規則。在某狀況下 — 實際上不少狀況下 — 你不得不至少打破第一條規則。

應該遵循這些規則的組件是應用中最重要的部分:原子、原始、構建塊、元素或任何稱爲基礎的組件。這篇文章中,我將統稱它們爲單個元素

其中一些很容易抽象出來,好比:ButtonImageInput。也能夠說是那些和 HTML 元素有直接關係的組件。在其它狀況下,只有在重複代碼時纔會識別出它們。那也不要緊。

一般,不管什麼時候你須要更改某個組件時,不論是添加新功能,仍是修復問題,你可能會看到 — 或者開始編寫重複的樣式和行爲。這就是須要將它抽象爲一個新的單元素信號。

單元素組件與其它組件的比值越高,應用就會越穩定、越方便維護。

將它們放到單獨的文件夾中 — 好比:elements, atoms, primitives,所以,不管什麼時候你須要導入這些組件時,你都會確信它們遵循了規則。

一個實例

在本文中,我重點放在 React 上。一樣的規則也適用於其它任何基於組件的庫。

這就是說,咱們有一個 Card 組件。它由 Card.jsCard.css 組成,在 Card.css 文件中咱們爲 .card.top-bar.avatar 和其它類選擇器配置了樣式規則。

const Card = ({ profile, imageUrl, imageAlt, title, description }) => (
  <div className="card">
    <div className="top-bar">
      <img className="avatar" src={profile.photoUrl} alt={profile.photoAlt} />
      <div className="username">{profile.username}</div>
    </div>
    <img className="image" src={imageUrl} alt={imageAlt} />
    <div className="content">
      <h2 className="title">{title}</h2>
      <p className="description">{description}</p>
    </div>
  </div>
);
複製代碼

在某些時候,應用中的其它位置也有可能使用頭像。爲了避免重複 HTML 和 CSS 代碼,咱們要建立一個新的 Avatar 單元素組件,而後就能複用它了。

規則 #1:每次只渲染一個元素

它由 Avatar.jsAvatar.css 組成,後者配置了咱們從 Card.css 中取出用於 .avatar 的樣式,最終返回一個 <img> 元素:

const Avatar = ({ profile, ...props }) => (
  <img
    className="avatar" 
    src={profile.photoSrc} 
    alt={profile.photoAlt} 
    {...props} 
  />
);
複製代碼

下面是咱們如何在 Card 和應用中其它位置使用它:

<Avatar profile={profile} />
複製代碼

規則 #2:從不中斷應用

一個 <img> 元素,雖然 src 屬性是必須的,若是你不傳遞它,也不會中斷應用。可是,對於咱們的應用,若是不傳遞 profile,那麼這個組件就會中斷應用。

React 16 版本中提供了一個名爲 componentDidCatch新的生命週期方法,能夠用來優雅地處理組件內部錯誤。雖然在應用中實現邊界錯誤處理是一種很好的作法,但這也會掩蓋單元素組件中的錯誤。

咱們必須確保 Avatar 組件自己是可靠的,並考慮到所須要的屬性父組件可能不會傳遞的狀況。在這種狀況下,除了在使用 profile 以前檢查它是否存在以外,還要使用 FlowTypeScriptPropTypes 對這種狀況給出警告,以下:

const Avatar = ({ profile, ...props }) => (
  <img 
    className="avatar" 
    src={profile && profile.photoUrl} 
    alt={profile && profile.photoAlt} 
    {...props}
  />
);

Avatar.propTypes = {
  profile: PropTypes.shape({
    photoUrl: PropTypes.string.isRequired,
    photoAlt: PropTypes.string.isRequired
  }).isRequired
};
複製代碼

如今咱們不傳遞任何屬性來使用 <Avatar /> 組件,來看看控制檯會給出什麼警告:

一般,咱們會忽略這些警告並在控制檯中累積幾個。由於當新警告出現時,咱們永遠不會在乎,因此 PropTypes 沒法發揮做用。所以,在這些警告累積以前,請務必解決。

規則 #3:應用全部做爲屬性傳遞的 HTML 屬性

目前爲止,咱們的單元素組件使用了名爲 profile 的自定義屬性。咱們應該避免使用自定義屬性,特別是當它們直接映射爲 HTML 屬性時。查看下面的建議 #1: 避免使用自定義屬性瞭解更多。

經過將全部屬性傳遞給底層元素,就能夠在單元素組件中輕鬆實現應用全部 HTML 屬性。咱們能夠經過傳遞相應的 HTML 屬性來解決自定義屬性問題:

const Avatar = props => <img className="avatar" {...props} />;

Avatar.propTypes = {
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired
};
複製代碼

如今 Avatar 使用起來更像一個 HTML 元素了:

<Avatar src={profile.photoUrl} alt={profile.photoAlt} />
複製代碼

若是底層 HTML 元素接受 children 屬性,這條規則也一樣適用。

規則 #4:應用做爲屬性傳遞的樣式規則

在應用中的某個地方,你可能但願單元素組件有一個稍微不一樣的樣式。你應該能夠經過 classNamestyle 屬性來自定義它。

單元素組件內部樣式等同於瀏覽器應用到原生 HTML 元素的樣式。也就是說,當咱們的 Avatar 組件收到一個 className 屬性時,不該該用來替換內部值 — 而是追加進去。

const Avatar = ({ className, ...props }) => (
  <img className={`avatar ${className}`} {...props} />
);

Avatar.propTypes = {
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired,
  className: PropTypes.string
};
複製代碼

若是咱們將 style 屬性應用於 Avatar 組件,可使用對象擴展 輕鬆完成應用:

const Avatar = ({ className, style, ...props }) => (
  <img 
    className={`avatar ${className}`}
    style={{ borderRadius: "50%", ...style }}
    {...props} 
  />
);

Avatar.propTypes = {
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired,
  className: PropTypes.string,
  style: PropTypes.object
};
複製代碼

如今咱們就能夠像下面這樣將新樣式應用到單元素組件:

<Avatar
  className="my-avatar"
  style={{ borderWidth: 1 }}
/>
複製代碼

若是你發現本身須要複製新樣式,請堅決果斷地建立另外一個組成 Avatar 的單元素組件。建立一個包含另外一個單元素組件沒問題 — 一般也是必須的。

規則 #5:應用全部做爲屬性傳遞的事件處理方法

因爲咱們將全部屬性向下傳遞,單元素組件已經準備好接收任何事件處理屬性。可是,若是組件內部已經應用了這個事件的處理,咱們該怎麼辦?

這種狀況下,咱們有兩個選擇:使用傳遞的處理方法替換掉內部處理方法,或者兩個都調用。這取決於你。只要確保始終應用來自屬性傳遞的事件處理方法。

const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args));

const internalOnLoad = () => console.log("loaded");

const Avatar = ({ className, style, onLoad, ...props }) => (
  <img 
    className={`avatar ${className}`}
    style={{ borderRadius: "50%", ...style }}
    onLoad={callAll(internalOnLoad, onLoad)}
    {...props} 
  />
);

Avatar.propTypes = {
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired,
  className: PropTypes.string,
  style: PropTypes.object,
  onLoad: PropTypes.func
};
複製代碼

建議

建議 #1: 避免使用自定義屬性

在建立單元素組件 — 特別是在應用中開發新功能時 — 很容易去添加自定義屬性來知足不一樣的使用。

使用 Avatar 組件舉個例子,設計師建議有些地方頭像是方形的,而其它地方應該是圓形。你也許認爲給組件添加一個 rounded 屬性是一個好主意。

除非你正在建立一個文檔良好的開源庫,不然,千萬不要那樣。除了文檔須要,那樣還會致使不可擴展和代碼的不可維護。老是建立一個新的單元素組件 — 好比 AvatarRounded — 它會渲染 Avatar 並作一些修改,而不是去添加自定義屬性。

若是你堅持使用獨特的描述性命名、建立可靠的組件,你將會建立成百上千個組件。它們依然是高度可維護的。組件名就能夠做爲文檔。

建議 #2:接收做爲屬性傳遞的 HTML 元素

並非每一個自定義屬性都是很差的。有時你想要改變單元素組件中包裹的 HTML 元素。經過添加一個自定義屬性來達到這個目的多是惟一方法。

const Button = ({ as: T, ...props }) => <T {...props} />;

Button.propTypes = {
  as: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};

Button.defaultProps = {
  as: "button"
};
複製代碼

一個常見的例子是將 Button 組件渲染爲 <a> 元素,以下:

<Button as="a" href="https://google.com">
  Go To Google
</Button>
複製代碼

或者做爲另外一個元素的使用:

<Button as={Link} to="/posts">
  Posts
</Button>
複製代碼

若是你對這個功能感興趣,我建議你看一下 Reas 項目,這是一個使用 Singel 理念構建的 React UI 工具包。

使用 Singel CLI 來驗證你的單元素組件

最後,在閱讀完全部內容以後,你可能想知道是否有工具能夠根據此模式自動驗證元素。我開發了這樣一個工具,叫作 Singel CLI

若是你想在正在進行的項目中使用它,我建議你建立一個新的文件夾並把你的單元素組件放在裏面。

若是你正在使用 React,你能夠經過 npm 安裝 singel 並運行它,以下:

$ npm install --global singel
$ singel components/*.js
複製代碼

輸出結果相似於下面這樣:

另外一種方法是在項目中做爲開發依賴安裝,並在 package.json 文件中添加腳本:

$ npm install --dev singel

{  
  "scripts": {  
    "singel": "singel components/*.js"  
  }  
}
複製代碼

而後,運行 npm 腳本吧:

$ npm run singel
複製代碼

感謝閱讀!

若是你喜歡這篇文章並發現它頗有用,你能夠經過如下方式來表達你的支持:

感謝 Raphael Thomazella

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索