最近在寫一個面向 React 初學者的系列教程玩轉 React,內容對有 React 開發經驗的同窗來講可能太過於基礎和囉嗦,不太感興趣。因此我打算同時開始另一個系列文章《React 開發實戰》。該系列主要面向有 React 開發經驗的同窗,更側重 React 實戰,每一篇文章會跟你們一塊兒開發一個 React 組件或者一個簡單有趣的 React 應用,這些組件或者應用每每知足以下特色:javascript
若是這些組件能直接應用到你們的實際開發中去,那再好不過了;若是不能,能給你們一點啓發,我以爲這件事情也是頗有價值的。css
另外,每一篇文章後面都會附有本篇文章的完整示例和代碼。java
你們應該都見過這種應用場景,頁面上的某一部分,須要可以讓用戶添加任意多項。web
多是表單中的一個字段,以下所示。segmentfault
也多是表單的一部分,以下所示,用戶能夠在一個表單內增長多個用戶信息,而後將用戶信息批量進行保存。api
還有更變態的,以下所示,一個表單內用戶信息部分能夠添加多份,每個用戶信息中地址也能夠添加多份。(Oh, My God. PM,你殺了我吧。)數組
還好,React 應付這種需求,仍是小菜一碟。可是在一個 web 應用中有這麼多的類似場景的話,若是咱們挨個實現一遍,那真是太枯燥了,與搬磚無異。遇到這種狀況,就須要咱們把相同的功能抽象出來,作成組件,這將極大地提高你的開發效率。數據結構
基於這個場景,咱們今天就開發一個能讓其 children 重複任意多份的組件,咱們就稱之爲 Repeat 吧。函數
在開發一個組件的時候,不要着急寫代碼,先想一想你要把這個組件作成什麼樣子,例如這個 Repeat 組件,我但願有以下特性:this
而後在代碼中我指望能夠這樣來用 Repeat 這個組件:
class App extends React.Component { handleChange(items) { console.info(items); } render() { <Repeat onChange={items => this.handleChange(items)}> <input type="text" /> </Repeat> } }
OK,就是這麼簡單,這樣 Input 組件就能夠重複加添多份了。基於這個構想,咱們來實現 Repeat 這個組件。
class Repeat extends React.Component { constructor(props) { super(props); this.state = { items: [''], }; } handleChange(e, index) { const items = [...this.state.items]; items[index] = e.target.value; this.setState({ items }); this.props.onChange(items); } handleAddItem(e, index) { e.preventDefault(); const items = [...this.state.items]; items.splice(index, 0, ''); this.setState({ items }); } handleRemoveItem(e, index) { e.preventDefault(); if (this.state.items.length === 1) return; const items = [...this.state.items]; items.splice(index, 1); this.setState({ items }); } render() { const children = React.Children.only(this.props.children); const elementItems = this.state.items.map((item, index) => ( <div key={index}> { React.cloneElement(children, { onChange: e => this.handleChange(e, index), value: item, }) } <div> <a href="#" onClick={e => this.handleAddItem(e, index)}>添加</a> <a href="#" onClick={e => this.handleRemoveItem(e, index)}>移除</a> </div> </div> )); return <div>{elementItems}</div>; } }
代碼很簡單,簡單解釋一下:
state
中持有 items
字段來保存每個項的數據。children
,而後 map 組件 state
中的 items
,將每一項映射爲 children
的一個副本。併爲這個副本傳入兩個屬性,onChange
接收每一項的數據變化,value
傳遞每一項當前應展現的值。Repeat
爲每一項準備了一個「添加」按鈕和一個「移除」按鈕,用來在當前項位置新增一項或者移除當前項。原理就是將 this.state.items
中對應下標處的數組元素刪掉就行了。到此,Repeat
是否是大體有模有樣了呢。須要提醒你們的是,React.cloneElement
和 React.Children.xxx
這些 api 一般只會在這種公共組件中使用,在大部分場景,儘可能少用。
有些同窗可能已經發現了,上面例子中, Repeat
的 children
是個 input
,那若是是一個其餘的組件不就完蛋了嘛。
這是第一個問題,爲了解決這個問題呢,Repeat 須要對它的 children
提兩個條件:
屬性上必需要接收一個 onChange
回調函數,函數接收一個對象參數,參數結構以下:
{ target: { value: 'xxxx' } }
value
的值爲當前項產出的數據,多是個對象也多是字符串或者數值。沒錯,我就是爲了兼容 input event 的數據結構。你固然能夠用任何你喜歡的且方便處理的數據結構。
children
組件須要接收一個 value 屬性,以展現其擁有的值。也就是說 children
組件應當是一個受控的(controlled)組件。這就是一個協議,你但願某個組件內經過 Repeat
組件方便地添加多份並能獲取到一組數據,那就必需要遵照這個協議。有同窗可能會說爲何不搞的智能一點呢?嗯,這裏我想分享一點我的經驗:有些時候,尤爲是在業務開發過程當中,把公共部分抽取出來複用便可,點到爲止,沒有必要搞得那麼「強大」,剩下的事情讓一個很容易遵照的協議來完成,其實效率會更高,更容易讓人理解。
其實在計算機的世界中到處充滿了協議,例如你想讓 HTTP Server 返回正確的響應,你必需要遵循 http 協議來和它通訊;你生產的顯卡能買的出去,必需要遵照相應的協議,要能插到別人家生產的主板上。
扯遠了!收!
對,有了上面這個約定之後,Repeat
一行代碼未加,是否是感受功能完善了許多?嗯,就是這個目的。如今咱們來實現一下文章開始時候說的第二個場景。
聰明的你必定已經知道該怎麼作了,沒錯,只要咱們實現一個 UserForm
組件,並讓他知足上面的約定便可。請看代碼:
class UserForm extends React.Component { handleFieldChange(e) { const { name, value } = e.target; const formData = { ...this.props.value, [name]: value, } this.props.onChange({ target: { value: formData, } }); } render() { const formData = this.props.value || {}; return ( <div> <div> <label for="">姓名</label> <input type="text" name="name" value={formData.name} onChange={e => this.handleFieldChange(e)} /> </div> <div> <label for="">地址</label> <input type="text" name="addr" value={formData.addr} onChange={e => this.handleFieldChange(e)} /> </div> </div> ) } }
爲了讓代碼更簡潔,我把 UserForm
這個組件實現爲了一個支持受控的組件,可是在目前的業務場景下已經足夠了,在實際狀況下,你能夠按需調整。
經過這個例子,還但願你們能體會到組件拆分的一個好處。就是,UserForm
和 Repeat
拆分紅兩個組件之後,UserForm
的複用性會更強。能夠想象一下,當用戶被批量添加之後,是否是有可能在編輯單個用戶的時候,能夠繼續使用這個組件。
好啦,關於第三個場景我想就沒有必要再實現一遍了,Repeat 嵌套多少層其實都是能夠的。
實際上在實際應用中,Repeat 這個組件還須要作進一步完善,其中一個就是樣式,還有可能在不一樣的場景下,雖然交互都是這樣,但樣式會有所差別。另外默認是「添加」、「移除」兩個文字按鈕,說不定實際業務場景中是兩個 +,- 的圖標按鈕;還有可能「添加」、「移除」的位置爲有所變化。
這些問題怎麼處理呢?下面給你們描述下思路,具體代碼就不寫了,若是有什麼疑問能夠給我留言。
Repeat
添加 itemClassName
和 buttonsClassName
兩個屬性分別爲每一項和按鈕區域的 css class。這樣你就能夠在不一樣的場景下指定不一樣的樣式了。Repeat
添加 renderButtons
這樣一個函數屬性,若是未指定則用默認的方式渲染按鈕,若是有則勇氣返回值渲染屬性。這是本篇文章的代碼:https://codepen.io/Sarike/pen...
好啦,文章就到這吧,若是有什麼疑問能夠給我留言。謝謝你們,祝你們國慶、中秋節快樂。