React在构建一个组件应用时所使用的设计模式与传统的前端开发思路不同,在刚使用的时候觉得它很别扭,甚至多此一举,但当你理顺它的设计思路时,就会觉得一切都是那么理所当然了。构建一个react应用思路上分为以下五步:
1、划分UI组件
拿到UI界面后,首先将一个复杂的应用拆分为单个应用功能组件,每个组件只完成一部分UI的渲染或者一项功能职责。将组件按照UI包含层次划分父子组件关系。如图:
我把上面的应用分为最外层FilterProduct组件,上面搜索框SearchBox组件,下面产品表ProductTable组件,以及每条产品列表ProductRow组件。什么时候应该划分为子组件每个人看法不同,但是当你觉得组件变得复杂时,将其细分为独立组件条理将更清晰。
2、构建静态组件
接下来就是编码实现刚才划分的每个UI组件。在这一步只需要实现每个组件的render方法,用于将静态的数据渲染到页面,这个阶段使用的是自上而下传递的props静态数据。但是我的构建顺序是自下而上的,也就是说先实现每个小组件,然后在其上一层组件中引用下层子组件并构建出上一层组件,这种顺序非常适合实现小型React项目。
3、确定要使用的State
props是不可改变的数据,为了实现用户交互,需要为应用分配可变的数据state。由于state修改较为麻烦,需要将state的数量控制在最小,将要用到的数据分别思考以下三个问题:
- 数据是否通过 props(属性) 从父级传入? 如果是这样,它可能不是 state 。
- 是否永远不会发生变化? 如果是这样,它可能不是 state。
- 是否可以由组件中其他的 state(状态) 或 props(属性) 计算得出?如果是这样,则它不是 state。
在经过以上考量之后可以确定商品搜索框中的用户输入框input、”仅显示有货“的checkbox需要设置为state来储存,分别为searchTxt与filterFlag
4、确定state所属的组件
接下来要确定state应该定义到哪个组件中,由于state只能被其定义的组件使用,其他组件只能通过props传值或函数来访问、修改它,因此state放的位置不合适,在使用时会很麻烦,通过以下步骤来确定:
- 确定每个哪些组件中要用到这个state。
- 找出这些组件的最近公共父级组件。
- 让公共父级组件 或者 另一个更高级组件拥有这个 state(状态) 。
- 如果找不出一个拥有该 state 的合适组件,专门创建一个新组件来保存 state ,并将其添加到公共父级组件的上层。
本例中的state变量searchTxt与filterFlag来源于SearchBox组件的输入,并且ProductTable需要根据它的值来筛选输出,两个都要用到两个state,因此应该将其放到SearchBox与ProductTable的公共父组件FilterProduct中。
5、添加反向数据流
现在各组件可以通过自上而下的props、state实现数据的传递与渲染了,但是要实现state的更新,需要子组件自下而上调用父组件的属性方法来修改位于父组件中的state值。相比于传统的双向绑定数据的修改,react中的数据修改实在是很麻烦。不过数据都集中在顶部,数据自顶向下流动结构清晰、便于调试。关于数据的反向流动共享,详细记录在我的另一个例子中。
最后贴上本例的代码:
<script type="text/babel">
let data=[
{category: "运动商品", price: "¥49.99", stocked: true, name: "足球"},
{category: "运动商品", price: "¥9.99", stocked: true, name: "棒球"},
{category: "运动商品", price: "¥29.99", stocked: false, name: "篮球"},
{category: "电子产品", price: "¥99.99", stocked: true, name: "iPod Touch"},
{category: "电子产品", price: "¥399.99", stocked: false, name: "iPhone 5"},
{category: "电子产品", price: "¥199.99", stocked: true, name: "Nexus 7"}
];
//搜索框
class SearchBox extends React.Component{
constructor(props){
super(props);
this.handleChange=this.handleChange.bind(this);
this.handleClick=this.handleClick.bind(this);
}
handleChange(e){ //调用父组件方法更新state值
this.props.updateSearch(e.target.value)
}
handleClick(){
this.props.updateChecked();
}
render(){
return (
<div className="search-box">
<input type="text" placeholder="搜索商品..."
value={this.props.searchTxt} onChange={this.handleChange} />
<label>
<input type="radio" checked={this.props.checked} onClick={this.handleClick} />
仅显示有货
</label>
</div>
)
}
}
//商品列表条组件
function ProductRow(props) {
let name='';
if(props.item.stocked){
name=<td>{props.item.name}</td>
}else { //没货的商品显示红色
name=<td style={{color:'red'}}>{props.item.name}</td>
}
return (
<tr className="product-row">
{name}
<td>{props.item.price}</td>
</tr>
)
}
//商品列表框
function ProductTable(props) {
let sport=[];
let electronic=[];
props.product.forEach((item)=>{
if (item.category==='运动商品'){
sport.push(item);
}else if(item.category==='电子产品'){
electronic.push(item);
}
});
let sportJSX=sport.map((val,index)=> //遍历渲染每条商品组件
<ProductRow key={index} item={val} />
);
let electronicJSX=electronic.map((val,index)=>
<ProductRow key={index} item={val} />
);
return (
<table className="product-table">
<thead>
<tr>
<th>商品名称</th> <th>价格</th>
</tr>
</thead>
<tbody>
<tr><td colSpan="2">运动商品</td></tr>
{sportJSX}
<tr><td colSpan="2">电子产品</td></tr>
{electronicJSX}
</tbody>
</table>
)
}
//整体父组件
class FilterProduct extends React.Component{
constructor(props){
super(props);
this.state={
filterFlag:false,
searchTxt:''
};
this.updateSearch=this.updateSearch.bind(this);
this.updateChecked=this.updateChecked.bind(this);
}
updateSearch(value){ //更新searchTxt
this.setState({
searchTxt:value
})
}
updateChecked(){ //更新filterFlag
this.setState((prevState) =>({
filterFlag: !prevState.filterFlag
}))
}
render(){
let searchProduct=[];
this.props.product.forEach(item =>{ //筛选符合名称的数据
if(item.name.indexOf(this.state.searchTxt)>-1){
searchProduct.push(item)
}
});
let stockProduct=[];
if(this.state.filterFlag){ //筛选仅显示有货
searchProduct.forEach(item=>{
if(item.stocked){
stockProduct.push(item);
}
});
}else{
stockProduct=searchProduct;
}
return (
<div className="filter-product">
<SearchBox searchTxt={this.state.searchTxt} checked={this.state.filterFlag}
updateSearch={this.updateSearch} updateChecked={this.updateChecked}
/>
<ProductTable product={stockProduct} />
</div>
)
}
}
ReactDOM.render(
<FilterProduct product={data} />,
document.getElementById('app')
)
</script>