前言
最近在寫一個面向 React 初學者的系列教程玩轉 React,內容對有 React 開發經驗的同學來說可能太過於基礎和囉嗦,不太感興趣。所以我打算同時開始另外一個系列文章《React 開發實戰》。該系列主要面向有 React 開發經驗的同學,更側重 React 實戰,每一篇文章會跟大家一起開發一個 React 元件或者一個簡單有趣的 React 應用,這些元件或者應用往往滿足如下特點:
- 在我的實際專案中用到過的。
- 在常見的開源元件庫中沒有的。
- 有點小眾,但是在特定的業務場景下能很大地提高專案的開發效率。
- 可能還比較有趣。
如果這些元件能直接應用到大家的實際開發中去,那再好不過了;如果不能,能給大家一點啟發,我覺得這件事情也是很有價值的。
另外,每一篇文章後面都會附有本篇文章的完整示例和程式碼。
問題描述
大家應該都見過這種應用場景,頁面上的某一部分,需要能夠讓使用者新增任意多項。
可能是表單中的一個欄位,如下所示。
也可能是表單的一部分,如下所示,使用者可以在一個表單內增加多個使用者資訊,然後將使用者資訊批量進行儲存。
還有更變態的,如下所示,一個表單內使用者資訊部分可以新增多份,每一個使用者資訊中地址也可以新增多份。(Oh, My God. PM,你殺了我吧。)
還好,React 應付這種需求,還是小菜一碟。但是在一個 web 應用中有這麼多的相似場景的話,如果我們挨個實現一遍,那真是太枯燥了,與搬磚無異。遇到這種情況,就需要我們把相同的功能抽象出來,做成元件,這將極大地提升你的開發效率。
基於這個場景,我們今天就開發一個能讓其 children 重複任意多份的元件,我們就稱之為 Repeat 吧。
你期望 Repeat 元件該怎麼用
在開發一個元件的時候,不要著急寫程式碼,先想想你要把這個元件做成什麼樣子,例如這個 Repeat 元件,我希望有如下特性:
- Repeat 元件提供預設的,新增、移除按鈕。
- 點選新增,將 React 的 children 複製一份,點選移除將某一項移除。
- 當只有一項時不能移除。
- Repeat 支援 onChange 回撥函式,當 Repeat 內的表單輸入發生變化時可以即時通知其父元件。
然後在程式碼中我期望可以這樣來用 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 這個元件。
開始實現 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
欄位來儲存每一個項的資料。 - render 時先獲取到唯一的
children
,然後 map 元件state
中的items
,將每一項對映為children
的一個副本。併為這個副本傳入兩個屬性,onChange
接收每一項的資料變化,value
傳遞每一項當前應展示的值。 - 另外
Repeat
為每一項準備了一個“新增”按鈕和一個“移除”按鈕,用來在當前項位置新增一項或者移除當前項。原理就是將this.state.items
中對應下標處的陣列元素刪掉就好了。
到此,Repeat
是不是大致有模有樣了呢。需要提醒大家的是,React.cloneElement
和 React.Children.xxx
這些 api 通常只會在這種公共元件中使用,在大部分場景,儘量少用。
跟 children 有個約定
有些同學可能已經發現了,上面例子中, 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
這樣一個函式屬性,如果未指定則用預設的方式渲染按鈕,如果有則勇氣返回值渲染屬性。
最後
這是本篇文章的程式碼:codepen.io/Sarike/pen/…
好啦,文章就到這吧,如果有什麼疑問可以給我留言。謝謝大家,祝大家國慶、中秋節快樂。
PS:本系列的所有文章將在 segmentfault 和 掘金同步釋出。
本作品保留所有權利。未獲得許可人許可前,不允許他人複製、發行、展覽和表演作品。不允許他人基於該作品創作演繹作品 。