React 開發實戰(一)- Repeat 元件

sarike發表於2017-09-29

前言

最近在寫一個面向 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.cloneElementReact.Children.xxx 這些 api 通常只會在這種公共元件中使用,在大部分場景,儘量少用。

跟 children 有個約定

有些同學可能已經發現了,上面例子中, Repeatchildren 是個 input,那如果是一個其他的元件不就完蛋了嘛。

這是第一個問題,為了解決這個問題呢,Repeat 需要對它的 children 提兩個條件:

  1. 屬性上必須要接收一個 onChange 回撥函式,函式接收一個物件引數,引數結構如下:

    {
      target: {
          value: 'xxxx'
      }
    }複製程式碼

    value 的值為當前項產出的資料,可能是個物件也可能是字串或者數值。沒錯,我就是為了相容 input event 的資料結構。你當然可以用任何你喜歡的且方便處理的資料結構。

  2. 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 這個元件實現為了一個支援受控的元件,但是在目前的業務場景下已經足夠了,在實際情況下,你可以按需調整。

通過這個例子,還希望大家能體會到元件拆分的一個好處。就是,UserFormRepeat 拆分成兩個元件以後,UserForm 的複用性會更強。可以想象一下,當使用者被批量新增以後,是不是有可能在編輯單個使用者的時候,可以繼續使用這個元件。

好啦,關於第三個場景我想就沒有必要再實現一遍了,Repeat 巢狀多少層其實都是可以的。

更進一步

實際上在實際應用中,Repeat 這個元件還需要做進一步完善,其中一個就是樣式,還有可能在不同的場景下,雖然互動都是這樣,但樣式會有所差異。另外預設是“新增”、“移除”兩個文字按鈕,說不定實際業務場景中是兩個 +,- 的圖示按鈕;還有可能“新增”、“移除”的位置為有所變化。

這些問題怎麼處理呢?下面給大家描述下思路,具體程式碼就不寫了,如果有什麼疑問可以給我留言。

  1. 關於樣式,你可以給 Repeat 新增 itemClassNamebuttonsClassName 兩個屬性分別為每一項和按鈕區域的 css class。這樣你就可以在不同的場景下指定不同的樣式了。

  2. 關於如何將文字按鈕改為圖示按鈕,你可以給 Repeat 新增 renderButtons 這樣一個函式屬性,如果未指定則用預設的方式渲染按鈕,如果有則勇氣返回值渲染屬性。

最後

這是本篇文章的程式碼:codepen.io/Sarike/pen/…

好啦,文章就到這吧,如果有什麼疑問可以給我留言。謝謝大家,祝大家國慶、中秋節快樂。


PS:本系列的所有文章將在 segmentfault 和 掘金同步釋出。

本作品保留所有權利。未獲得許可人許可前,不允許他人複製、發行、展覽和表演作品。不允許他人基於該作品創作演繹作品 。

相關文章