编者按
使用React的思想来构建应用对我在实际项目中以及帮助他人解决实际问题时起到了很大作用,所以我翻译此文来向那些正在或即将陷入React或React-Native深坑的同胞们表示慰问。网上已经有人翻译过,我想用更易读的语言翻译一次,这也是我首次如此一本正经的翻译技术文章给大众阅读,权当练习吧。
原文地址:https://facebook.github.io/react/docs/thinking-in-react.html
转载还请注明出处以及原文地址,出于对作者和译者劳动成果的尊重吧,谢谢了我的哥。
Thinking in React
作者:Pete Hunt 译者:Rex Rao (sohobloo)
我认为React是使用JavaScript构建高性能大型Web应用的首选方案,我们已经在Facebook和Instagram中广泛使用,哎哟,效果不错哟。
React的众多优势之一是——且看它如何让你能顺着思路构建应用。在此,我将引领你用React逐步构建出一个可搜索的商品列表应用。
从模型图开始
假设设计师已经为我们提供了API并可以返回模拟的JSON数据。容我小小鄙视一下这位美工,因为原型图长成这个挫样:
我们的API返回的模拟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"}];
第一步:拆分并构建界面的组件层次结构树
你应该做的第一件事是为你原型图所有组件和子组件画个边框、起个名。要是你跟设计师坐一起,找他们喝喝茶,说不定他们的Photoshop图层名恰巧可以用作你React组件的名字!(译者:我只能说,Too young too simple, sometimes naive!)
但你怎么知道如何拆分一个组件呢?这和你平时决定是不是要新建一个函数或者类的道理一样一样的。其中有个叫做单一职责原则的原理,也就是说理想状态下一个组件只做一件事,当他需要做更多,那就应该继续拆拆拆。
如果你经常向用户展示JSON数据,你会发现只要你的数据模型建得好,你的界面乃至你的组件架构也会完美的与之映射。因为界面和数据模型倾向于支持相同的信息架构,这让界面拆分工作变简单了,拆分出的一个组件只对应展示数据模型中的一种数据就行。
你看,咱这简单的应用有5种组件。我用斜体标示出了每个组件要展示的数据。
- FilterableProductTable(橙色): 包含整个示例
- SearchBar(蓝色): 接收用户输入(user input)
- ProductTable(绿色): 显示基于用户输入(user input)过滤的数据集 (data collection)
- ProductCategoryRow(青色): 显示分类( category)头
- ProductRow(红色): 显示每一行商品(product)
看ProductTable你会发现表头(含"Name"和"Price"标签)并没有拆分成组件,这是出于一种存在争议的个人喜好而已啦。这个例子中,既然渲染数据集 (data collection)是ProductTable的职责,那就让它作为此组件的一部分好了。要是它再复杂一点的话(比如排序功能),那就另当别论独立成ProductTableHeader组件咯。
让我们把从原型图中定义的组件组合成层次结构树。如果一个组件出现在另一个组件中,那么这个组件就是它的子组件,so easy:
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
第二步:用React做个静态版
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'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
有了组件层次结构,是时候表演真正的技术了实现你的应用了。最简单的方式是把数据渲染到界面上,但是不带交互功能。最好是分离这些步骤,因为构建一个静态版本更多是需要你敲键盘而增加交互功能就需要你敲脑袋了。
将你的应用构建出一个静态版本来展示数据模型,你也许会需要构建组件来复用其他组件,用属性(props)传入数据。 属性(props)是一种将数据从父组件传入子组件的途径。 即便你对状态(state)模式非常熟悉,在静态版本中也不要使用状态哦。状态是留给交互来处理那些会变化的数据使用的。作为一个静态版请无视之。
你可以用自上而下或自下而上的方式构建应用。既可以从最顶层组件开始(比如从FilterableProductTable开始)也可以从最底层组件开始(如ProductRow)。在简单的示例中,自上而下往往更容易;而在大型项目中,使用自下而上更好,你还能方便的写单元测试呢!
这一步完成之后,你就有了一个可以展示你的数据模型的组件库。作为一个静态版本,每个组件都只有一个 render()方法。顶层组件(FilterableProductTable)通过属性(prop)获得你的数据模型。如果此时你改变你的基础数据模型并再次调用ReactDOM.render(),界面会刷新。界面的刷新和变化了一目了然直接了当。React的单向数据流(又名单向绑定)让所有事情有序且快速。
如果在这一步中遇到问题,你可以参考React文档。
小插曲儿: 属性(props)与状态(state)
React中有两类数据模型:属性和状态,了解他们的区别是很有必要的!还不太清楚?来来来看这里React官方文档咋说的。
第三步:定义最小(完整)的界面状态值
界面想要动起来?数据必须变起来!React使用状态(state)来实现。
若想正确构建你的应用,首先你得考虑你的应用至少需要一组什么样的可变状态值。来跟我念口诀:取其精华,去其糟粕。找出你的应用的那组干货——绝对最小化的界面状态值组,并且其他任何需要都可以通这组值计算得出。比如你要构建一个待办列表,只需要维护一组待办项即可;你不需要再维护这组列表的个数的值,因为在你需要展示待办数时可以直接获取列表长度得到结果。
来看看我们例子里有哪些数据:
- 原始的商品列表
- 用户输入的搜索文本
- 勾选框的值
- 筛选后的商品列表
让我们逐条看看哪些是状态,对每一条数据三省吾身:
- 是否是父组件传入的属性?如果是的,估计不是状态。
- 是否会随时间变化改变?如果不会变,估计不是状态。
- 能否从其他状态或属性计算得到?如果可以,肯定不是状态。
原始商品列表通过属性传进来,因此它不是状态。搜索框的值和勾选框的值可以改变而且其他东西也计算不出来这些值,看上去应该是状态。最后,筛选后的商品列表也不是状态,因为它可以通过原始商品列表、搜索框的值和勾选框的值计算得出。
最后得出我们需要的状态:
- 用户输入的输入框的值
- 勾选框的值
第四步:给状态找个家
最小状态集新鲜出炉,接下来我们需要定义哪些组件会变化,或者说拥有这些状态。
记住了: React数据总是单向且「下流」的——流向组件层次中的底层。可能并不是一开始就看得出哪个组件拥有什么状态。这常常是萌新最难理解的部分,所以就让老司机带带你吧:
- 对于你应用的每一条状态:
- 找出每一个需要基于此状态来渲染界面的组件。
- 找到它们共同的爹(一个在组件层次中需要此状态的所有控件的顶层父组件)。
- 它们共同的父组件或更高层级的组件都可以作为状态的持有者。
- 如果你觉得哪个组件持有这个状态都很别扭,可以为了这个状态创造一个新的组件来持有,并把这个新组件加到它们共同父组件的上层结构中的任何合适位置。
针对我们的应用,让我们根据以上策略捋一捋:
- ProductTable需要根据状态值来过滤商品列表,SearchBar需要显示搜索文本和勾选框状态值。
- FilterableProductTable是它们的共同父组件。
- 看起来搜索文本和勾选框值放在FilterableProductTable挺合适。
就这么愉快的决定了,把这些状态放FilterableProductTable里吧。
首先在FilterableProductTable中增加getInitialState()(译者:ES6中如果用class构建组件,初始化状态的方法将发生改变)方法并返回{filterText: '', inStockOnly: false}来对应应用的初始状态。然后将filterText和inStockOnly作为属性传给ProductTable和SearchBar
。最后就用属性来过滤ProductTable中的商品列表并把搜索文本设置到SearchBar的输入框中。
来看看你应用的表现如何:把filterText设置成"ball"然后刷新。厉害了我的哥,列表正确的更新了!
第五步:增加反向数据流
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')
);
至此,我们已经构建了一个能正确渲染属性和状态从组件层次自上而下传递的应用了。是时候表演真正的技术了支持数据反向传递了:底层组件需要更新FilterableProductTable里的状态。
React明确的数据传递能让你更容易搞清楚你的程序是怎么运作的,但比起传统的双向数据绑定你就需要敲稍微多一点的代码了。 React提供了一个叫ReactLink的插件来让这种模式变得和双向绑定一样方便,但本文的目的在于让一切明晰,暂不使用。
如果你尝试在当前版本的示例中输入或勾选,你会发现React完全无视你的输入。 怎么回事难道有Bug?乖乖我们故意的!因为我们刚才把input的value属性设置成总是等于FilterableProductTable传进来的状态了。
然并卵,我们需要用户的输入立刻更新状态。既然控件只允许更新自己的状态,FilterableProductTable可以传一个每次状态需要发生变化时都会触发的回调函数回传到SearchBar。我们可以用输入框的onChange事件来触发并在FilterableProductTable传入的回调函数中调用setState()来更新状态。
看上去好像很复杂的样子,其实只是多了几行代码而已,但这真真真的让你能看清数据是如何在你应用的身体里流来流去的。
没错就是这样
希望这篇文章能在你用React构建组件或应用时给你点亮一盏明灯。虽然可能比以前要搬更多砖,但请你记住代码写出来是要可以给人阅读的,特别是那些标准统一、逻辑清晰的代码更赏心悦目。当你开始构建大型的控件库的时候,你会感激这种规则化、清晰化的风格,再加上代码的复用,你的代码行数会得到缩减。☺