[譯]建立 React 組件的10條準則

原文地址: dev.to/selbekk/the…

要建立供多人使用的組件是很難的,組件包含屬性(props),若是這些屬性要做爲公開 API 的一部分,那就必須很是仔細地考慮組件應該接受哪些屬性。前端

本文會簡要介紹 API 設計中的一些最佳實踐,以及幫助你開發出優秀組件的 10 條準則。react

什麼是 API ?

API (Application Programing Interface,應用編程接口)是兩段代碼交互的地方。它是代碼與世界溝通的橋樑,咱們將這個橋樑稱爲接口。能夠經過 API 進行數據與功能的交互。npm

後端和前端之間的接口是一個 API。 能夠經過與這個 API 進行交互來訪問一組數據和功能。編程

一個類和調用這個類的代碼之間的接口一樣也是一個 API。你能夠調用類裏的方法,來檢索數據或觸發封裝在裏面的功能。後端

同理,組件中要接收的 props 一樣也是 API 。 這是調用者與組件交互的方式,當你想要對外暴露什麼的時候,會應用不少相同的規則和注意事項。
api

API 設計的一些最佳實踐

那麼,在設計 API 的時候,須要注意哪些規則和事項呢?咱們在這方面作了一些研究,最後找到了許多很是優秀的資源。咱們選取了其中的 2 篇:Josh Tauberer 的《What Makes a Good API ?》以及 Ron Kurir 的同名文章;而且咱們也提出了4 個可遵循的最佳實踐。數組

版本穩定

當建立一個 API 的時候,須要考慮的最重要的一件事是儘量地保持 API 的穩定。這意味着須要最大限度地減小重大變化的數量。若是 API 真的有較大的變化,也請確保撰寫了詳細的升級指南,並儘量提供一份代碼模塊,可讓用戶自動完成升級過程。bash

若是正在發佈 API,請確保遵循了語義版本規範。這可讓用戶輕鬆地決定所需的版本。ide

提供錯誤描述信息

每當調用 API 發生錯誤時,你須要儘量地去解釋發生了什麼問題,以及如何去修復錯誤。在沒有任何提醒或信息的狀況下,直接拋出一個「錯誤使用」來羞辱調用者貌似不是一種良好的用戶體驗。wordpress

相反,撰寫描述性錯誤信息能夠幫助調用者來修改他們調用 API 的方式。

別讓程序猿犯嘀咕

程序猿是脆弱的,並且你也不但願他們在使用 API 的時候嚇到他們。也就是說,API 應該儘量直觀。能夠經過遵循最佳實踐和現有的命名習慣來實現這一點。

另外還須要注意一點:保持代碼風格的連貫。若是在布爾屬性的名稱前加上了 is 或者 has 做爲前綴,可是接下來卻又不這麼作了,這就會讓人感到費解。

精簡 API 結構

當咱們在討論作減法的時候,一樣也包括減小 API 。功能多了當然很好,可是 API 的結構越簡單,調用者的學習成本就越小。反過來說,這會被認爲是一個簡單易用的 API 。

總有辦法來控制 API 的大小,其中的一個辦法是,從舊的 API 中重構出一個新的API。

建立組件的 10 條準則

上面的 4 條黃金法則在編寫 REST API 以及古老的 Pascal 程序時很管用,那應該如何轉化才能適用於現代世界的 React 呢?

正如咱們前面所提到的,組件有本身的 API。咱們稱其爲 屬性(props),這是咱們提供給組件數據、回調函數以及其餘功能的方式。咱們應該以何種方式構建props對象,纔可以不違反上述任何一條規範呢?咱們一樣應該以何種方式編寫組件,才能讓下一個開發者輕鬆地測試它們呢?

咱們列出了建立組件時須要遵循的 10 條準則,而且咱們但願你能發現它們是行之有效的。

1. 寫文檔

若是沒有文檔來記錄組件是如何使用的,好吧,雖然大多數開發者會隨時查看你的代碼,但這不能說是一種良好的用戶體驗。

寫文檔有不少工具,咱們推薦如下 3 個:

前兩個會在開發組件的時候提供一個演練場,第三個則會讓你使用 MDX 編寫更多自由格式的文檔。

不管選擇哪一個,都請確保在文檔中記錄了 API 的用法 ,以及組件的用法和使用時機。 後者在共享組件庫中尤其重要。

2. 容許上下文語義

HTML 是一種經過語義化的方式來組織信息的語言。大多數組件是使用 <div /> 標籤來構建的。這在某種程度上是有道理的——由於通用組件不清楚它到底應該是一個 <article /> 仍是 <section /> 或者是 <aside /> ,儘管如此,但只用<div/>來構建也並不完美。

相反,咱們建議容許組件接受一個 as 屬性,它將始終覆蓋正在呈現的DOM 元素。下面是一個實現它的例子:

function Grid({ as: Element, ...props }) {
  return <Element className="grid" {...props} />
}
Grid.defaultProps = {
  as: 'div',
};複製代碼

咱們將 as 屬性重命名爲局部變量 Element,並在 JSX 中使用它。當不須要更多語義化的 HTML 標籤時,咱們也提供了普通的默認值來傳給組件。

當使用 <Grid /> 組件的時候,你能夠傳入合適的標籤:

function App() {
  return (
    <Grid as="main">
      <MoreContent />
    </Grid>
  );
}複製代碼

請注意上面的代碼在 React 組件中一樣有效。下面是一個很好的例子,展現了若是想讓一個<Button />組件呈現一個 React Router <Link />。

<Button as={Link} to="/profile">
  Go to Profile
</Button>複製代碼

3.避免布爾屬性

布爾屬性聽起來不錯,你不須要給它賦值就能夠指定一個布爾屬性,因此這讓它們看起來很是優雅:

<Button large>BUY NOW!</Button>複製代碼

儘管看起來很好,可是布爾屬性卻只容許有 2 個可選值:打開或者關閉,展現或者隱藏,1 或者 0。

每當你開始想爲布爾屬性引入些其餘的東西的時候,好比尺寸、變體、顏色,或者其餘可能除了二元選擇以外的任何東西,就有些麻煩了。

<Button large small primary disabled secondary> WHAT AM I?? </Button>複製代碼

換句話說,布爾屬性經常不能隨着需求的改變而進行擴展。相反,嘗試使用相似字符串類型的可枚舉類型來做爲屬性值,能夠得到二元選擇以外更多的選擇。

<Button variant="primary" size="large"> I am primarily a large button </Button>複製代碼

這並不表明布爾屬性就徹底沒有一席之地了,其實布爾屬性是有用的!上面列出的 disable 屬性應當依舊是布爾類型——由於在「可用」與「不可用」的狀態之間,不存在中間狀態,因此這裏用布爾類型是恰當的。

4.使用 props.children

React 中有幾個特殊的屬性,他們的處理方式與其餘屬性不太同樣。其中一個就是key,用來在有序列表中追蹤列表項的,另外一個就是children。

在一個開始標籤和結束標籤之間的任何東西都被放置在props.children屬性中,推薦儘可能多使用這個屬性。

推薦的緣由是使用props.children屬性比起使用content屬性,或者其餘只接受相似文本的簡單值的屬性來講,要簡便的多。

<TableCell content="Some text" /> // vs <TableCell>Some text</TableCell>複製代碼

使用 props.children還有幾個好處。首先,它的寫法和普通的 HTML 是同樣的。第二,你能夠向組件傳遞任何想要的東西,而不是向組件中添加 leftIcon 和 rightIcon 屬性,把他們做爲 props.children 的一部分傳遞給組件便可。

<TableCell> <ImportantIcon /> Some text </TableCell>複製代碼

你可能會說,個人組件只會渲染普通的文本,不須要渲染其餘東西。在某些狀況下多是正確的,至少如今是沒問題的。而在將來需求變化的時候,你就會發現使用 props.children 的好處。

5.讓父組件的鉤子函數進入內部邏輯

有時,咱們會建立一些內部邏輯複雜的組件,好比自動補全的下拉菜單或者可交互圖表。

這種類型的組件一般會有冗長複雜的 API ,其中一個緣由是,須要覆蓋的功能,以及須要支持的特殊用法,這二者的數量都會隨着時間的推移而斷增長。

若是咱們想提供一個簡單且標準化的屬性,來讓調用者去控制或者覆蓋組件的默認行爲,咱們應該怎麼作呢?

Kent C. Dodds 爲此寫過一篇很棒的文章,在文中他將這個問題的解決方案稱爲:state reducers。請參閱這兩篇文章:Post about the concept itselfHow to implement it for React Hooks

簡單總結來講,這種經過傳遞 state reducer 函數到組件中的模式,容許調用者訪問組件內部分派的全部操做。你能夠修改 state ,或者觸發內部事件。這是一種建立無需 prop 的高度自定義組件很好的方式。

示例代碼:

function MyCustomDropdown(props) {
  const stateReducer = (state, action) => {
    if (action.type === Dropdown.actions.CLOSE) {
      buttonRef.current.focus();
    }
  };
  return (
    <>
      <Dropdown stateReducer={stateReducer} {...props} />
      <Button ref={buttonRef}>Open</Button>
    </>
}複製代碼

順便提一下,你固然能夠建立更簡單的方式來響應事件。在上面的例子中,在組件中提供一個 onClose 屬性會產生更好的用戶體驗。

6. 擴散剩餘屬性

每當建立一個新的組件的時候,請確保將剩餘的屬性也擴散到有意義的元素上。

若是有某些屬性僅需傳遞給子組件或子元素(而組件自身並不須要這個屬性),那就沒必要添加到你的組件中,這麼作可讓組件 API 更加穩定,即便當下一個開發者須要新事件監聽器的時候,也無需發佈新版本的組件。

例如:

function ToolTip({ isVisible, ...rest }) {
  return isVisible ? <span role="tooltip" {...rest} /> : null;
}複製代碼

你的組件能夠向底層組件或元素傳遞屬性,好比 className 或者 ·onClick 的監聽函數,必定要確保外部的調用者同樣能夠這樣作。好比在 class 這種狀況中,你可使用 npm 上的 classname 包來方便地添加 class 屬性(或者乾脆直接用簡單的 string 字符串)。

import classNames from 'classnames';
function ToolTip(props) {
  return (
    <span 
      {...props} 
      className={classNames('tooltip', props.tooltip)} 
    />
}複製代碼

在事件監聽回調的狀況下,能夠用一個小工具函數將它們合併成單個函數。 例如:

function combine(...functions) {
  return (...args) =>
    functions
      .filter(func => typeof func === 'function')
      .forEach(func => func(...args));
}複製代碼

如今,咱們建立了一個以函數數組爲參數的函數,它返回一個新的回調函數,該回調函數會向各個函數傳入相同的參數,並依次調用各個函數。

示例代碼:

function ToolTip(props) {
  const [isVisible, setVisible] = React.useState(false);
  return (
    <span 
      {...props}
      className={classNames('tooltip', props.className)}
      onMouseIn={combine(() => setVisible(true), props.onMouseIn)}
      onMouseOut={combine(() => setVisible(false), props.onMouseOut)}
    />
  );
}複製代碼

7. 充分提供默認值

請確保爲屬性提供了充分的默認值,這樣作能夠最大限度地減小必傳值的數量,並且也大大簡化了代碼實現。

以 onClick 處理函數爲例,若是它不是必需的,就能夠提供一個空函數來做爲默認值。這樣,你就能夠在代碼中隨時調用它,就好像組件老是被提供了回調函數同樣。

另外一個例子是自定義輸入。 除非明確提供,不然假設輸入的字符串是空字符串。 這將使你確保始終處理字符串對象,而不是 undefined 或 null 。

8.不要重命名 HTML 屬性

HTML 做爲一門語言擁有本身的屬性,它自己就是 HTML 元素的 API,爲啥不繼續使用這些 API?

正如前面所提到的,精簡 API 數量並使其具備必定的直觀性是改進組件API的兩種很好的方法。因此與其建立本身的自定義標籤屬性,爲何不直接使用現成的原生標籤 API 呢?

所以,不要重命名任何現有 HTML 屬性。你甚至沒有用新的 API 替換現有的 API,你只是在上面添加了本身的 API。其餘人仍然能夠將原生標籤與你自定義標籤的屬性一塊兒傳遞,那麼最終的值應該是什麼呢?

另外,也請確保在組件中沒有將 HTML 屬性進行覆蓋。一個很好的例子就是 <button /> 元素的 type 屬性。它的值能夠是 submit (默認值)、button 和 reset。可是,許多開發者卻傾向於將這個屬性名用於表示按鈕的可視類型(primary,cta 等等)。

經過改變這個屬性的用途,你不得不添加其餘屬性來覆蓋,設置實際的 type 屬性值,這隻會給調用者帶來困惑。

9. 指定屬性的類型

沒有什麼文檔比代碼中的文檔更好了,React 提供了 prop-types 包來聲明組件 API 的類型,必定要用它。

你能夠爲全部的屬性指定明確的類型,也能夠規定屬性是否必傳,甚至可使用 JSDoc 註釋來作進一步改進。

若是忽略了必傳屬性,或者傳遞了無效值和意外值,運行時會在控制檯打印警告。這樣的開發體驗很棒,而且能夠從生產構建中剝離出來。

若是使用 TypeScript 或者 Flow 來編寫 React 應用,你就能夠將此類API文檔做爲語言功能。這會帶來更好的工具支持,以及更棒的開發體驗。

若是你本身沒有使用類型化的 JavaScript,你仍然應該考慮爲那些使用類型化的 JavaScript調用者提供類型定義。經過這種方式,他們可以更輕鬆地使用你的組件。

10. 爲開發者而設計

最後,須要遵循的最重要一條規則就是:保證 API 以及「組件體驗」已經針對它的調用者進行了優化。

提高開發者體驗的一個辦法是在錯誤調用時提供詳細的錯誤信息,以及在開發過程當中的警告信息。

當編寫錯誤和警告時,記得使用連接引用文檔或提供簡單的代碼示例。這能夠幫助開發者更快地發現錯誤並修改,提供更好的開發體驗。

沒必要擔憂過於冗長的錯誤信息會佔用太大空間,何況構建生產環境的時候也不會把這些信息打包進去。

React 自己就是一個很是優秀的類庫,當你忘記使用 key 或者拼錯了生命週期的名字等等,都會在控制檯收到大量詳細的錯誤警告信息。

所以,要爲你將來的用戶設計,在 5 周內爲本身設計,爲那些在你離開後必須維護你代碼的可憐兄 dei 設計,爲開發者設計!

總結

在經典的 API 設計中咱們還能夠學到不少優秀的東西,經過遵循本文中提到的提示和準則,你應該能夠建立出易於使用、便於維護、使用直觀,以及出現問題時能夠進行快速修復的組件。 你還有哪些關於建立組件的點子?

相關文章
相關標籤/搜索