用React的方式思考

2016-01-01  本文已影响581人  孙和David

作者:Pete Hunt

翻译:孙和

原文链接

构建大型、反应迅捷的web app,我首选react。我们在facebook和instagram中都用了它,扩展的不错。

当你构建APP时,React重塑了你的思考方式。我会在这篇文章中展现,用react构建应用的思考过程。

首先,JSON API 返回的数据如此这般:

[ 
{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"}
];

第一步:把UI分解成组件树
你要做的第一件事就是,在设计图上,把组件和子组件用线框出来,还要给他们命名。

thinking-in-react-components.png

如何划分组件呢?方法同划分函数或者对象一样。你要遵循单一功能原则,就是说,一个组件只做一件事。如果组件不断膨胀,就要把它再拆解成更小的子组件。

如果你的数据模型构建的好,那么你的UI和组件结构就会相应的更清晰明了。这是因为UI和数据模型遵循同样的信息架构,这意味着如果数据模型足够清晰,那么把UI分解成组件就是小事一桩。只需要把它拆分成组件,每个组件代表数据模型的一部分。

在这个APP中,我们有5个组件:
1、FilterableProductTable:包含整个APP
2、SearchBar:接收用户输入
3、ProductTable:根据用户输入,显示和筛选数据集合
4、ProductCategoryRow:显示每一个分类的标题
5、ProductRow:每一个产品,显示一行

第二步:静态版本

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:'$49.99',stocked:true,name:'Baseball'},
{category:'Sporting Goods',price:'$49.99',stocked:false,name:'Basketball'},
{category:'Electronics',price:'$49.99',stocked:true,name:'iPod Touch'},
{category:'Electronics',price:'$49.99',stocked:false,name:'iPhone 5'},
{category:'Electronics',price:'$49.99',stocked:true,name:'Nexus 7'}
];
ReactDOM.render(
   <FilterableProductTable products={PRODUCTS} />,
    document.getElementById('container')
);

构建一个可以渲染数据模型的静态APP,你要构建很多组件。这些组件重用了其他组件,并且用props传递数据。可以利用props,从父组件向子组件传递数据。在构建静态版时,一定不能用state。state只用于交互,就是说,表示不断变化的数据。

你可以自底向上或者自顶向下构建APP,如果自底向上,你先构建ProductRow;如果自顶向下,你先构建FilterableProductTable。简单情形下,一般自顶向下。大型项目中,一般自底向上,并在构建过程中,编写测试用例。

完成这一步,你会有一些可重用的组件,这些组件渲染你的数据模型。这些组件只有render方法,因为你的APP是静态的。组件树顶端的FilterableProductTable将你的数据模型作为prop。如果你改变数据模型,重新调用ReacDOM.render()方法,UI会更新。React的单向数据流让一切模块化、迅捷。

第三步:找出UI state的最小完备集
为了给UI添加交互,你需要触发数据模型的变化。React用state轻松实现了这一点。
首先,你要想好可变状态的最小集合。关键是DRY,不要重复自己。找出状态的最小集合,用这个集合计算其他需要的状态。例如,你构建一个TODO List,只要保留TODO items 数组,不要为计数保留一个额外的状态变量。当你需要计数时,只需计算TODO items 数组的长度。

考虑我们所有的数据,我们有:原始产品列表,用户键入的搜索文字,checkbox 的值,筛选过的产品列表。

为了确定哪一个是state,只要问三个问题:
1、它是否通过props从父组件传入?如果是,可能不是state
2、它不断变化吗?如果不是,那么可能不是state
3、它可以用其他state或者props计算出来吗?如果是,那么不是state

原始产品列表,作为props传入,不是state;
搜索文本和checkbox是state,因为他们不断变化,并且不能通过其他东西计算出来;
筛选后的产品列表不是state,因为它可以通过原始产品列表和搜索文字或checkbox的取值计算出来。

最终,state是:
用户输入的搜索文本
checkbox的值

第四步:确定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.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'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);

现在,我们已经确定了state的最小集。下面,我们要确定哪个组件改变或拥有这些state。
记住:react的核心是沿着组件树流动的单向数据。弄清楚哪一个组件拥有哪一个state,是最有挑战的部分。
对于每一个state:
确定每一个根据这个state渲染的组件;
找到总组件(组件树中,所有需要这个state的组件,上面的那个组件);
要么是总组件,要么是组件树中更高层的组件,拥有这个state;
如果你找不到可以合理拥有这个state的组件,创建一个新的组件,这个组件只是为了持有这个state,并把这个组件加到组件树中,在总组件之上

根据这一策略:
ProductTable需要根据state筛选产品列表,SearchBar需要展示搜索文本和选中状态;
总组件是FilterableProductTable;
筛选文本和选择值放在FilterableProductTable组件中很合适

棒!我们已经决定把state放在FilterableProductTable中。首先,在FilterableProductTable中添加getInitialState()方法,返回反映初始状态的对象{filterText:'',inStockOnly:false}。然后,把filterText和inStockOnly作为prop传到ProductTable和SearchBar中。最后,用这些props在ProductTable中筛选rows,在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({
    handleChange:function(){
      this.props.onUserInput(
        this.refs.filterTextInput.value,
        this.refs.inStockOnlyInput.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'}
];
ReactDOM.render(
  <FilterableProductTable products={PRODUCTS} />,
  document.getElementById('container')
);

目前,我们已经构建了一个可以正确渲染的APP,在渲染过程中,props和state函数沿着组件树自上而下流动。现在,我们要支持数据反向流动:组件树底层的form组件更新组件树顶层的FilterableProductTable组件state。

React 让这一数据流显式表现出来,这样,在理解程序如何运转时会更容易。但是,相对于传统的双向数据绑定,你需要书写更多。React提供了一个叫ReactLink的插件,让这种模式像双向数据绑定一样方便,但在本文中,我采用显式的方式。

在当前版本中,如果你键入或者勾选,不会有任何反应。因为我们已经将input中的value prop和从FilterableProductTable中传入的state设置为相等。

我们希望当用户改变表单时,我们能更新state来反应用户的输入。由于组件只能更新自己的state,FilterableProductTable将会传递一个回调函数给SearchBar,每当state更新时,这个回调函数就会触发。我们可以用inputs上的onChange事件,监听这一变化。每当onChange事件被触发,由FilterableProductTable传入的回调函数会调用setState()方法,app就会更新。

上一篇下一篇

猜你喜欢

热点阅读