1. 程式人生 > 其它 >Thinking in React

Thinking in React

本文翻譯自React的官方部落格,詳情請閱讀原文。

React非常適合構建元件化的應用,它注重高效能,因此組建的重用,專案的擴充套件都十分靈活,Facebook和instagram的不少商業專案使用了此框架。

本文主要通過“輸入查詢資料”這個簡單的demo來說明或者學習如何用React來架構。

資料模型

我們需要根據JSON API來顯示並且操作資料,最終的視覺化操作是基於JSON資料的基礎之上。最終的效果圖如下:

以下便是我們模擬的JSON資料:

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

step1 變UI為元件繼承

我們如何確定哪一部分應該為一個元件呢?我們可以遵循“單一職責”原則,也就是說,理想的元件只做一件事情。元件也應該根據資料(model)的結構靈活進行設計,這樣最終的UI匹配提供的資料(model),利於維護和理解。

如上圖所示,我們將這個應用分為5個元件。

  1. FilterableProductTable (orange): 包含所有的子元件,是個容器
  2. SearchBar (blue): 用於使用者輸入互動
  3. ProductTable (green): 呈現資料項並根據使用者輸入過濾資料
  4. ProductCategoryRow (turquoise):
    顯示條目資訊
  5. ProductRow (red): 顯示產品的具體資訊

我們可以看到,tHead部分(Name和Price)並不是一個單獨的元件,在這個例子中,之所以tHead屬於ProductTable元件是因為它並沒有與資料(model)有關聯,考慮這種情況,如果要單擊tHead部分的表頭實現表格內容的排列,我們最好為tHead單獨設計一個元件,並在該元件上繫結事件處理函式。

至此,我們將這五個元件的繼承關係確定下來:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

step2 建立靜態版本

       有了元件的繼承關係,我們首先建立一個靜態版本的應用。我們可以通過元件複用以及父子元件之間的props通訊來完成模型資料的渲染。props是父子元件通訊的一種方式,如果你也瞭解state特性的話,那麼一定不要使用state來構建靜態版本,state用於建立互動版本,也就是說,state中的資料會隨著時間而改變,下面的一節會講解何時將資料放入state中。

      我們可以自頂向下或者自下而上來構建應用,在做測試時我們可以自下而上來進行每個模組的測試,而一般構建應用我們則是採用自頂向下的模式,結合資料的自上而下傳遞,利於開發。

      在這一步,由於我們構建的是靜態版本,因此每個元件只實現了其render方法,用以基本的資料渲染。最頂層的元件(FilterableProductTable)的props中存入要渲染的資料模型,每當模型資料發生改變時,會對應的檢視層的改變,這也正是React所提出的的單向資料流模型(one-way data flow)。

      在React中,元件有兩種型別資料--props和state。它們之間的具體區別可以參考官方文件

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        });
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" placeholder="Search..." />
                <p>
                    <input type="checkbox" />
                    {' '}
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    render: function() {
        return (
            <div>
                <SearchBar />
                <ProductTable products={this.props.products} />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
 
React.render(<FilterableProductTable products={PRODUCTS} />, document.body);

step3 確定元件的state

      為了使應用具有動態互動性,必須將狀態的改變(使用者的輸入或者單擊操作等)反映到我們的UI上,通過React給元件提供的state完成上述需求。

       我們首先要確定應用的可變的狀態集合,遵循DRY原則:don't repeat youself。我們需要考慮應用中的所有的資料,它包括:

  • 基本的產品列表
  • 使用者輸入的過濾條件
  • checkbox的值
  • 過濾後的產品列表

根據下面條件選擇哪些資料可以作為state:

  1. 是否通過父元件通過props傳遞,如果是,則不是state
  2. 是否隨著時間而改變,如果不變,則不是state
  3. 可以通過其他state或者props計算得到,如果可以,則不是state

產品資料列表是通過父元件的props傳遞,因此不是state,使用者輸入和checkbox滿足上述三個條件,可以作為state,二對於過濾的列表,則可以根據產品資料和使用者輸入來獲取到,因此不是state。

故,input輸入值和checkbox的值可以作為state。

step4 確定state所屬的元件

目前確定了state集合,接下來需要確定究竟是哪個元件擁有這個state,或者隨著state而變化。

我們要明確React的單項資料流是沿著元件繼承鏈流動的,這有時很難確定哪一個元件擁有這個state,不過我們可以根據以下原則來大體確定state所屬的元件。

       在每一個狀態期,

  • 確保每個元件都會根據當前狀態來渲染
  • 尋找其共同的祖先元件
  • 在繼承鏈中層級較高的元件擁有state

回到我們的應用中,

  • ProductTable需要根據state來過濾資料,SearchBar需要顯示輸入的文字和選項.
  • 這兩個元件的共同祖先是 FilterableProductTable.
  • 因此state的集合應該所屬FilterableProductTable元件

所以,我們確定了state所屬的元件是FilterableProductTable。我們需要給該元件設定getInitialState方法設定元件的初始狀態,並且通過props將狀態傳遞給ProductTable和SearchBar,最後我們就可以在ProductTable和SearchBar中獲取狀態並根據當前狀態顯示相應的資料。

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
                return;
            }
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        }.bind(this));
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" placeholder="Search..." value={this.props.filterText} />
                <p>
                    <input type="checkbox" checked={this.props.inStockOnly} />
                    {' '}
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },

    render: function() {
        return (
            <div>
                <SearchBar
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
                <ProductTable
                    products={this.props.products}
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.render(<FilterableProductTable products={PRODUCTS} />, document.body);

step5 新增反向資料流

       等等,目前構建的應用並不能通過表單來反向設定state,因此,我們無法再input標籤輸入任何值。這就需要我們手動進行反向資料設定。React預設的單項資料流是從model渲染到UI,而通過UI來設定model則需要手動編寫,主要的操作就是通過獲取元件對應的DOM物件,獲取當前DOM的屬性值並反向設定state來完成。

      當前版本的應用,React會忽略輸入值和選定值,這是理所當然的,因為我們在FilterableProductTable中設定的state初始值為filterText=‘’,inStockOnly=false,所以對於ProductTable和SearchBar而言,也就是針對這兩個值渲染,但是由於通過input和checkbox的輸入並未改變這兩個state的值,因此,這兩個元件其實並沒有被渲染。

      所以我們通過在ProductTable和SearchBar設定事件監聽函式,並且每當函式觸發時setState當前的狀態,促使元件渲染重繪,完成資料的動態呈現。在具體實現中,可以通過refs錨點來獲取具體的具名元件,並通過呼叫元件的getDOMNode方法,獲取對於DOM物件並據此設定新的state。

/** @jsx React.DOM */

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        console.log(this.props);
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
                return;
            }
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        }.bind(this));
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    handleChange: function() {
        this.props.onUserInput(
            this.refs.filterTextInput.getDOMNode().value,
            this.refs.inStockOnlyInput.getDOMNode().checked
        );
    },
    render: function() {
        return (
            <form>
                <input
                    type="text"
                    placeholder="Search..."
                    value={this.props.filterText}
                    ref="filterTextInput"
                    onChange={this.handleChange}
                />
                <p>
                    <input
                        type="checkbox"
                        checked={this.props.inStockOnly}
                        ref="inStockOnlyInput"
                        onChange={this.handleChange}
                    />
                    {' '}
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },

    handleUserInput: function(filterText, inStockOnly) {
        this.setState({
            filterText: filterText,
            inStockOnly: inStockOnly
        });
    },

    render: function() {
        return (
            <div>
                <SearchBar
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                    onUserInput={this.handleUserInput}
                />
                <ProductTable
                    products={this.props.products}
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.render(<FilterableProductTable products={PRODUCTS} />, document.body);

對,就是這樣

       例子雖然非常簡單,但是裡面蘊含的思想確實值得玩味。元件的設計,資料的傳遞,狀態集的確定,雙向資料的傳遞以及事件處理和獲取具名元件等等技術都包含在內,如果真的吃透了這個例子,那麼我想在今後的可重用敏捷開發之路上必定又有新的收穫,具體到我們的實現上就是元件設計的更為優美,程式碼量更為精少。