對於封裝react元件的一些思考

日明發表於2019-01-15

由於近期在涉及到封裝元件的時候遇到了一些問題,於是我認真地瞭解了一下react封裝元件過程中應該要涉及和思考到的一些問題,寫了下來。(以下主要是針對UI元件,由於水平有限不保證內容正確性,僅僅是一些個人的思考)

一、什麼是元件

元件可以將UI切分成一些的獨立的、可複用的部件,這樣就只需專注於構建每一個單獨的部件。

所謂元件,即封裝起來的具有獨立功能的UI部件。

在 React 中,一切皆是元件,因此理解元件的工作流與核心尤為重要。

且react中有多種建立元件的方式和各種各樣的元件概念,因此在設計元件的時候應該使用哪種元件的建立方式且應該設計一個怎樣的元件都值得深入思考。

那麼在React裡面一個元件應該有什麼特徵呢?在react中認為元件應該具有如下特徵:

  1. 可組合(Composeable):一個元件易於和其它元件一起使用,或者巢狀在另一個元件內部。如果一個元件內部建立了另一個元件,那麼說父元件擁有(own)它建立的子元件,通過這個特性,一個複雜的UI可以拆分成多個簡單的UI元件;
  2. 可重用(Reusable):每個元件都是具有獨立功能的,它可以被使用在多個UI場景;
  3. 可維護(Maintainable):每個小的元件僅僅包含自身的邏輯,更容易被理解和維護;

二、一個設計良好的元件應該有什麼特性?

(一)高內聚、低耦合

我們經常談一個設計良好的系統應該是高內聚低耦合的,那麼其實我認為一個好的元件也應該是具有高內聚低耦合的特性。

那麼我們應該要怎麼去做到使一個元件實現高內聚低耦合的特點呢?

  1. 高內聚:將邏輯緊密相關的內容放在一個元件內。
    React可以將展示內容的JSX、定義行為的JavaScript程式碼、甚至定義樣式的css,
    都可以放在一個JavaScript檔案中,因此React天生具有高內聚的特點。
  2. 低耦合:不同元件之間的依賴關係要儘量弱化。
    也就是每個元件要儘量獨立,
    一個元件不應該掌握著其他元件的細節,
    而是要儘量做到對其他元件瞭解很少,甚至是一無所知。

為什麼需要實現低耦合呢?

因為低耦合會帶來以下的好處:

  1. 在系統中的區域性改變不會影響到其他地方
  2. 任何元件都可以被替代品取代
  3. 系統之間的元件可以複用
  4. 可以輕易測試獨立的元件,提高了應用的測試程式碼覆蓋率

而高耦合的元件間會很容易出現一個問題,
就是無法或者很艱難去修改一個大量依賴其他元件的元件,
甚至只是改一個用來傳遞資料的欄位都會導致大量的修改。

(二)隱藏內部結構

一個封裝良好的元件應該是要隱藏其內部結構的,並通過一組 props來提供控制其行為的途徑。

隱藏內部結構是必須的。內部結構或實現細節不應該能被其他元件知道或關聯。

React 元件可以是函式式的,也可以是基於類的,可以定義例項方法、設定 refs、維護 state或是使用生命週期方法。而這些實現細節被封裝在元件自身中,其他元件不應該窺見其中的任何細節。

基於此特點來設計的元件對其他元件的依賴是極低的,帶來的是低耦合的特點和好處。

(三)職責單一

我認為元件應該要符合單一職責原則,
一個元件應該儘量只負責一件事情,並且把這件事情做好,
因為我覺得一個元件如果負責處理的事情過多,
在修改其中一件事情的時候很有可能也會影響到它負責的其他事情,且不利於維護和複用。

三、在封裝一個元件的時候應該先思考什麼?

  1. 這個元件應該是做什麼的
  2. 這個元件應該至少需要知道那些資訊
  3. 這個元件會反饋什麼東西

在設計一個元件的時候我們不應該僅限於實現當前的需求,
設計出一個只適用於單一專案的元件,而是應該是一個可以適應大部分同種需求的通用元件。
所以我們在碰到一個需求的時候應該首先對需求進行抽象,而不是看到設計稿就擼著袖子上。

例如碰到一個輪播圖元件的需求的時候,我們拆分以下這個需求,可以得到:

(1) 這個元件要做什麼:

  1. 可以展示多張圖片
  2. 可以向左向右翻頁,或者是可以是上下翻頁
  3. PageControl的狀態會根據圖片的滾動而相應改變 還有可能有一些隱藏的需求,類似於:
  4. 應該支援左右兩側或者上下無限迴圈滾動
  5. 可以選擇的是否自動輪播
  6. 支援手動滑動切換圖片
  7. 圖片有點選事件,可以點選來進行相關的事件反應

(2)這個元件至少應該知道什麼資訊

一個好的元件應該是要像存在魔法一樣,只需要極其少數的引數和條件就可以得到期望的效果。就像這個輪播圖元件一樣,元件應該至少知道的資訊有:

  1. 圖片的url地址陣列
  2. 當圖片不存在時候的佔點陣圖

其他可以知道也可以不知道的資訊可以有:

  1. 是否開啟自動輪播,預設是開啟或者不開啟

  2. 圖片滾動是左右還是上下,預設是左右

    等等 ....................................

(3)這個元件會反饋什麼

一個可用的輪播圖效果

四、元件的通訊

父元件向封裝好的子元件通訊通常是通過props

作為元件的輸入,props的值應該最好是js基本型別 (如 string、number、boolean)
但是props可以傳入的不僅僅只是這些,它可是一個神奇的東西,它可以傳入包括:

  1. js基本型別(如 string、number、boolean)
<Message text="Hello world!" modal={false} />;  
複製程式碼
  1. 物件
<Message
  data={{ 
  thexAxis:  thexAxis ,     
  lineData : lineData
   }} 
  />
複製程式碼
  1. 陣列
<MoviesList items={['Batman Begins', 'Blade Runner']} />  
複製程式碼
  1. 作為事件處理和非同步操作時,可以指定為函式:
<Message type="text" onChange={handleChange} />  
複製程式碼
  1. prop 甚至可以是一個元件構造器。元件可被用來處理其他元件的例項化:
function If({ Component, condition }) {  
 return condition ? <Component /> : null;
  }
<If condition={false} component={LazyComponent} />  
複製程式碼

為避免破壞封裝,要謹慎對待 props 傳遞的細節。
父元件對子元件設定 props 時,也不應該暴露自身的結構。
比如,把整個元件例項或 refs 當成 props 傳遞之類的神奇操作。

訪問全域性變數是另一個對封裝造成負面影響的問題。

我們可以通過 proptypes來對傳入的資料進行型別限制。

五、react中建立元件的方法

react建立元件有三種方法,分別是:

  1. function式無狀態元件
  2. es5方式React.createClass元件
  3. es6方式extends React.Component

而目前react推薦ES5方式和ES6方式建立元件的寫法中推薦的是ES6的寫法,所以這裡就不對ES5的寫法進行討論了。

React.Component

React.Component是以ES6的形式來建立React元件,也是現在React官方推薦的建立元件的方式,
其和React.createClass建立的元件一樣,也是建立有狀態的元件。

相比React.createClass方式,React.Component帶來了諸多語法上的改進

1.import

ES6使用import方式替代ES5的require方式來匯入模組,其中import { }可以直接從模組中匯入變數名,此種寫法更加簡潔直觀。

2.初始化 state

在ES6的語法規則中,React的元件使用的類繼承的方式來實現,去掉了ES5的getInitialState的hook函式,state的初始化則放在constructor建構函式中宣告。

引申內容:

如何正確定義State

React把元件看成一個狀態機。通過與使用者的互動,實現不同狀態,然後渲染UI,讓使用者介面和資料保持一致。 元件的任何UI改變,都可以從State的變化中反映出來; State中的所有狀態都用於反映UI的變化,不應有多餘狀態。

那麼什麼樣的變數應該做為元件的State呢:

  1. 可以通過props從父元件中獲取的變數不應該做為元件State。
  2. 這個變數如果在元件的整個生命週期中都保持不變就不應該作為元件State。
  3. 通過其他狀態(State)或者屬性(Props)計算得到的變數不應該作為元件State。
  4. 沒有在元件的render方法中使用的變數不用於UI的渲染,那麼這個變數不應該作為元件的State。這種情況下,這個變數更適合定義為元件的一個普通屬性。

function和class建立元件的區別

React內部是通過呼叫元件的定義來獲取被渲染的節點,而對於不同的元件定義方式,其獲取節點的步驟也不一樣。如下:

//function方式定義
function Example() {
  return <div>this is a div</div>;
}

const node = Example(props);

// 類方式定義
class Example extends React.Component {
  render() {
    return <div>this is a div</div>;
  }
}

const instance = new Example(props);
const node = instance.render();
複製程式碼

在這裡,函式直接呼叫,類則需要先例項化再去呼叫例項化物件上的render方法;

如果將類按照普通函式去呼叫則會報錯

六、Component 和 PureComponent

因為這方面沒有詳細去了解過,所以也只是粗淺總結一下其區別:

PureComponent除了提供了一個具有淺比較的shouldComponentUpdate方法,
PureComponent和Component基本上完全相同。
當元件更新時,如果元件的 props 和 state 都沒發生改變, render 方法就不會觸發,省去 Virtual DOM 的生成和比對過程,達到提升效能的目的。

如果我們想用PureComponent去代替Component的時候不需要去做太多的事情,
僅僅是把Component改成PureComponent即可。 但是我們並非可以在所有地方都用PureComponent去代替Component,
具體還是要按照實際情況來選擇,因為了解不深就不在此處詳談了。

七、有狀態元件和無狀態元件

無狀態元件更多的是用來定義模板,接收來自父元件props傳遞過來的資料,
使用{props.xxx}的表示式把props塞到模板裡面。
無狀態元件應該保持模板的純粹性,以便於元件複用,所以通常UI元件應該是無狀態元件。
類似於:

    var Header = (props) = (
        <div>{props.xxx}</div>
   );
複製程式碼

而有狀態元件通常是用來處理定義互動邏輯和業務資料
(使用{this.state.xxx}的表示式把業務資料掛載到容器元件的例項上(有狀態元件也可以叫做容器元件,無狀態元件也可以叫做展示元件),
然後傳遞props到展示元件,展示元件接收到props,把props塞到模板裡面。
類似於:

class Home extends React.Component {
  constructor(props) {
      super(props);
      };
   render() {
      return (
         <Header/> 
      )
   }
}
複製程式碼

八、高階元件

高階元件給我的感覺類似於高階函式,都是接受一個東西的輸入,
然後再給輸入的東西新增新的特性作為一個新的東西輸出,
看起來類似於裝飾器模式的實現。
但是因為目前為止沒有寫過高階元件,所以就不在這裡討論了。

相關文章