由於近期在涉及到封裝組件的時候遇到了一些問題,於是我認真地了解了一下react封裝組件過程中應該要涉及和思考到的一些問題,寫了下來。(以下主要是針對UI組件,由於水平有限不保證內容正確性,僅僅是一些個人的思考)
一、什么是組件
組件可以將UI切分成一些的獨立的、可復用的部件,這樣就只需專注於構建每一個單獨的部件。
所謂組件,即封裝起來的具有獨立功能的UI部件。
在 React 中,一切皆是組件,因此理解組件的工作流與核心尤為重要。
且react中有多種創建組件的方式和各種各樣的組件概念,因此在設計組件的時候應該使用哪種組件的創建方式且應該設計一個怎樣的組件都值得深入思考。
那么在React里面一個組件應該有什么特征呢?在react中認為組件應該具有如下特征:
- 可組合(Composeable):一個組件易於和其它組件一起使用,或者嵌套在另一個組件內部。如果一個組件內部創建了另一個組件,那么說父組件擁有(own)它創建的子組件,通過這個特性,一個復雜的UI可以拆分成多個簡單的UI組件;
- 可重用(Reusable):每個組件都是具有獨立功能的,它可以被使用在多個UI場景;
- 可維護(Maintainable):每個小的組件僅僅包含自身的邏輯,更容易被理解和維護;
二、一個設計良好的組件應該有什么特性?
(一)高內聚、低耦合
我們經常談一個設計良好的系統應該是高內聚低耦合的,那么其實我認為一個好的組件也應該是具有高內聚低耦合的特性。
那么我們應該要怎么去做到使一個組件實現高內聚低耦合的特點呢?
- 高內聚:將邏輯緊密相關的內容放在一個組件內。
React可以將展示內容的JSX、定義行為的JavaScript代碼、甚至定義樣式的css,
都可以放在一個JavaScript文件中,因此React天生具有高內聚的特點。 - 低耦合:不同組件之間的依賴關系要盡量弱化。
也就是每個組件要盡量獨立,
一個組件不應該掌握着其他組件的細節,
而是要盡量做到對其他組件了解很少,甚至是一無所知。
為什么需要實現低耦合呢?
因為低耦合會帶來以下的好處:
- 在系統中的局部改變不會影響到其他地方
- 任何組件都可以被替代品取代
- 系統之間的組件可以復用
- 可以輕易測試獨立的組件,提高了應用的測試代碼覆蓋率
而高耦合的組件間會很容易出現一個問題,
就是無法或者很艱難去修改一個大量依賴其他組件的組件,
甚至只是改一個用來傳遞數據的字段都會導致大量的修改。
(二)隱藏內部結構
一個封裝良好的組件應該是要隱藏其內部結構的,並通過一組 props來提供控制其行為的途徑。
隱藏內部結構是必須的。內部結構或實現細節不應該能被其他組件知道或關聯。
React 組件可以是函數式的,也可以是基於類的,可以定義實例方法、設置 refs、維護 state或是使用生命周期方法。而這些實現細節被封裝在組件自身中,其他組件不應該窺見其中的任何細節。
基於此特點來設計的組件對其他組件的依賴是極低的,帶來的是低耦合的特點和好處。
(三)職責單一
我認為組件應該要符合單一職責原則,
一個組件應該盡量只負責一件事情,並且把這件事情做好,
因為我覺得一個組件如果負責處理的事情過多,
在修改其中一件事情的時候很有可能也會影響到它負責的其他事情,且不利於維護和復用。
三、在封裝一個組件的時候應該先思考什么?
- 這個組件應該是做什么的
- 這個組件應該至少需要知道那些信息
- 這個組件會反饋什么東西
在設計一個組件的時候我們不應該僅限於實現當前的需求,
設計出一個只適用於單一項目的組件,而是應該是一個可以適應大部分同種需求的通用組件。
所以我們在碰到一個需求的時候應該首先對需求進行抽象,而不是看到設計稿就擼着袖子上。
例如碰到一個輪播圖組件的需求的時候,我們拆分以下這個需求,可以得到:
(1) 這個組件要做什么:
- 可以展示多張圖片
- 可以向左向右翻頁,或者是可以是上下翻頁
- PageControl的狀態會根據圖片的滾動而相應改變 還有可能有一些隱藏的需求,類似於:
- 應該支持左右兩側或者上下無限循環滾動
- 可以選擇的是否自動輪播
- 支持手動滑動切換圖片
- 圖片有點擊事件,可以點擊來進行相關的事件反應
(2)這個組件至少應該知道什么信息
一個好的組件應該是要像存在魔法一樣,只需要極其少數的參數和條件就可以得到期望的效果。就像這個輪播圖組件一樣,組件應該至少知道的信息有:
- 圖片的url地址數組
- 當圖片不存在時候的占位圖
其他可以知道也可以不知道的信息可以有:
-
是否開啟自動輪播,默認是開啟或者不開啟
-
圖片滾動是左右還是上下,默認是左右
等等 ....................................
(3)這個組件會反饋什么
一個可用的輪播圖效果
四、組件的通信
父組件向封裝好的子組件通信通常是通過props
作為組件的輸入,props的值應該最好是js基本類型 (如 string、number、boolean)
但是props可以傳入的不僅僅只是這些,它可是一個神奇的東西,它可以傳入包括:
- js基本類型(如 string、number、boolean)
-
<Message text= "Hello world!" modal={false} />;
-
復制代碼
- 對象
-
<Message
-
data={{
-
thexAxis: thexAxis ,
-
lineData : lineData
-
}}
-
/>
-
復制代碼
- 數組
-
<MoviesList items={[ 'Batman Begins', 'Blade Runner']} />
-
復制代碼
- 作為事件處理和異步操作時,可以指定為函數:
-
<Message type="text" onChange={handleChange} />
-
復制代碼
- prop 甚至可以是一個組件構造器。組件可被用來處理其他組件的實例化:
-
function If({ Component, condition }) {
-
return condition ? <Component /> : null;
-
}
-
<If condition={ false} component={LazyComponent} />
-
復制代碼
為避免破壞封裝,要謹慎對待 props 傳遞的細節。
父組件對子組件設置 props 時,也不應該暴露自身的結構。
比如,把整個組件實例或 refs 當成 props 傳遞之類的神奇操作。
訪問全局變量是另一個對封裝造成負面影響的問題。
我們可以通過 proptypes來對傳入的數據進行類型限制。
五、react中創建組件的方法
react創建組件有三種方法,分別是:
- function式無狀態組件
- es5方式React.createClass組件
- 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呢:
- 可以通過props從父組件中獲取的變量不應該做為組件State。
- 這個變量如果在組件的整個生命周期中都保持不變就不應該作為組件State。
- 通過其他狀態(State)或者屬性(Props)計算得到的變量不應該作為組件State。
- 沒有在組件的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/>
-
)
-
}
-
}
-
復制代碼
八、高階組件
高階組件給我的感覺類似於高階函數,都是接受一個東西的輸入,
然后再給輸入的東西添加新的特性作為一個新的東西輸出,
看起來類似於裝飾器模式的實現。
但是因為目前為止沒有寫過高階組件,所以就不在這里討論了。