作者: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分解成组件树
你要做的第一件事就是,在设计图上,把组件和子组件用线框出来,还要给他们命名。
如何划分组件呢?方法同划分函数或者对象一样。你要遵循单一功能原则,就是说,一个组件只做一件事。如果组件不断膨胀,就要把它再拆解成更小的子组件。
如果你的数据模型构建的好,那么你的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就会更新。