0%

以React.js的方式思考

以React.js的方式思考。

这是 React 官方文档中的一章,为了加深理解所以翻译出来,原文在这儿


React 很棒的一点是创建应用中引导你思考的过程。这篇文档中,我们将通过运用React创建一个产品搜索列表,来引导你熟悉这个思考过程。

开始

假设我们已经有了一个JSON API和前端工程师设计的界面,如下面这样:
图片.png

我们的JSON API返回的数据是这个样子:

1
2
3
4
5
6
7
8
[
{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部件的名字呢!

但你怎样定义一个部件呢?你日常编程中怎样决定创建一个函数或对象的?道理相同。一个类似的技术是功能单一原则(single responsibility principle), 意思是,一个部件应该只做一件事情。如果它越来越大,那么它应该被分为更小的部件。

由于你常常将JSON数据展示给用户看,你会发现,如果数据模型建得不错,你的UI(与你的部件结构)也相应的不会太差。原因是UI和数据模型往往依赖相同的信息架构,这也意味着把UI分解为部件常常不是太难,不过是根据数据模型来分解罢了。

图片.png

你会看到我们这个简单的示例程序里有5个部件。

  1. FilterableProductTable(橙色):整个示例程序
  2. SearchBar(蓝色):接收所有的用户输入
  3. ProductTable(绿色):根据用户输入显示和过滤数据
  4. ProductCategoryRow(青绿色):显示类别
  5. ProductRow(红色):显示产品行

如果仔细看ProductTable,会发现表头(Name和Price)不是它自己的部件。这是个见仁见智的问题,使用哪种方式还有争论。这个例子中,我们把它作为ProductTable的一部分,因为渲染数据集是ProductTable的责任。然而,如果这个表头过于复杂(如果以后我们增加点击表头排序),当然应该作为一个独立的部件ProductTableHeader来创建。

现在我们在原型中已经明确了部件,接下来把它们按照层次结构组织起来。原型中一个部件在另一个部件中,层次结构中应该为父子层级关系:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

第二步:建立静态版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
}

class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;

return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}

class ProductTable extends React.Component {
render() {
const rows = [];
let lastCategory = null;

this.props.products.forEach((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>
);
}
}

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

class FilterableProductTable extends React.Component {
render() {
return (
<div>
<SearchBar />
<ProductTable products={this.props.products} />
</div>
);
}
}


const 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')
);

现在你有了部件层级,是时候实现应用了。最容易的方法是先建立一个获取数据、渲染UI但没有交互的版本。把这些过程分离出来,是因为建立静态版本需要很多输入操作但不需要过多思考,增加交互功能不需要太多输入但需要很多思考。接下来我们会看到我这么说的原因。

建立渲染数据模型的静态版本,你需要创建使用其他部件的部件并且用props来传递数据。props是从父部件向子部件传递数据的一种方法。如果你对状态state的概念熟悉,在创建应用的静态版本时一定别使用state。状态只保留在交互的时候用。

你可以由底向上或从上到底开始。或者说,你可以首先创建最顶层的部件(例如从FilterableProductTable开始)或首先创建最底层部件(从ProductRow开始)。在简单的应用中,一般采取由上到底的方式;复杂的应用为了便于边创建边测试则相反。

这一步结束的时候,你会有了一个渲染数据模型的可重用部件库。因为这是应用的静态版,部件只包含render()方法。最顶层的部件(FilterableProductTable)或取数据模型为prop。如果数据模型中的数据有改变,重新调用render(),UI会相应的更新。静态版本复杂性不高,会很容易的看到UI如何更新。React单向数据流(one-way data flowone-way-binding)保证了模块化和相应速度。

属性(Props)和状态(State)的插曲

React中有两种模型数据:props和state。理解两者之间的区别非常重要;进一步了解请参考官方文档

第三步:确定最少(但功能齐全)的UI状态

使UI具备交互功能,需要底层数据触发事件。React的状态state让这一点的实现很简单。

为了正确地创建应用,要首先思考应用需要的最小的状态变化。关键是别重复造轮子——DRY: Don’t Repeat Yourself. 找出应用需要的最少的数据,据此在计算其他的。例如,如果要创建TODO列表,只要有个保存TODO项目的数组即可,不需要TODO项目数量的数据。因为数量可以由获取数组长度很容易地得到。

考虑我们这个例子中需要的数据,我们有了:

  • 产品原始列表
  • 用户输入的搜索文本
  • 复选框的值
  • 过滤的产品列表

我们逐一分析,看看哪个是状态。对每一个数据,只要问三个问题:

  1. 它是父部件经由props传递给子部件的吗?如果是,很可能不是状态。
  2. 它的值在应用操作过程中会改变吗?如果不会,很可能不是状态。
  3. 它的值能由其他状态或属性计算得到吗?如果是,很可能不是状态。

原始数据列表经props传入,那它不是状态。搜索文本和复选框的值会在应用操作过程中被改变,而且不能由其他属性或状态计算获得,看起来是状态。最后,过滤的产品列表不是状态,因为它可以经过计算原始数据列表、搜索文本和复选框的值获得。

最后,我们的状态是:

  • 用户输入的搜索文本
  • 复选框的值

第四步:确定状态的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
}

class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;

return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}

class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;

const rows = [];
let lastCategory = null;

this.props.products.forEach((product) => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
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;
});

return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}

class SearchBar extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;

return (
<form>
<input
type="text"
placeholder="Search..."
value={filterText} />
<p>
<input
type="checkbox"
checked={inStockOnly} />
{' '}
Only show products in stock
</p>
</form>
);
}
}

class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
}

render() {
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>
);
}
}

const 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')
);

我们确定了应用中的最少的状态,接下来,我们确定这些状态属于哪个部件。

记住:React的部件中数据是单向由顶向下流动。哪些部件传递这些状态可能不能马上弄清楚。这往往是新手理解起来最难的部分,按照下面的流程确定:

对于应用中每一个状态:

  • 确定依赖这个状态来渲染的每一个部件
  • 寻找共同的父部件(在部件层级中,位于所有需要这个状态的部件之上的父部件)
  • 或者拥有这些状态的层级更高的部件
  • 如果找不到拥有这个状态的部件,创建一个持有这个状态的新部件,加到部件层级中,位置在共同父部件之上。

我们根据上面的原则检视一下:

  • ProductTable需要根据状态过滤产品,SearchBar需要显示搜索文本和复选框状态
  • 它们共同的父部件是FilterableProductTable
  • 过滤文本和复选框值放在FilterableProductTable看起来有意义

酷,那么我们决定把状态放在FilterableProductTable中。首先,在FilterableProductTable构造器constructor中增加this.state = {filterText: '', inStockOnly: false}来设置应用的初始状态。接着,将filterTextinStockOnly作为属性传递到ProductTable和SearchBar中。最后,用这些属性过滤ProductTable的数据,同时显示在SearchBar表单中。

你会开始看到应用如何反应:设置filterText“ball”然后刷新应用。你会看到数据表正确地刷新了。

第五步:添加反向数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
}

class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;

return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}

class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;

const rows = [];
let lastCategory = null;

this.props.products.forEach((product) => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
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;
});

return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}

class SearchBar extends React.Component {
constructor(props) {
super(props);
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}

handleFilterTextChange(e) {
this.props.onFilterTextChange(e.target.value);
}

handleInStockChange(e) {
this.props.onInStockChange(e.target.checked);
}

render() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
onChange={this.handleFilterTextChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
onChange={this.handleInStockChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
}

class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};

this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}

handleFilterTextChange(filterText) {
this.setState({
filterText: filterText
});
}

handleInStockChange(inStockOnly) {
this.setState({
inStockOnly: inStockOnly
})
}

render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onFilterTextChange={this.handleFilterTextChange}
onInStockChange={this.handleInStockChange}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}


const 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忽略了你的输入。这是因为我们已经将输入的值prop设置为始终等于从FilterableProductTable传入的状态。

让我们想想我们希望发生的事。我们希望确保每当用户更改表单时,我们都会更新状态以反映用户的输入。由于组件应该只更新自己的状态,FilterableProductTable会将回调传递给SearchBar,只要状态更新就会触发。我们可以使用输入上的onChange事件来通知它。FilterableProductTable传递的回调将调用setState(),应用将被更新。

虽然这听起来很复杂,实际上只是几行代码。这真的使数据如何在整个应用程序中如何流动一目了然。

结语

希望这可以让你了解如何用React来构建组件和应用。 尽管可能需要会比以前更多地输入内容,但请记住,代码的可读性远远比代码的编写重要,读取模块化的显式代码非常容易。当你开始构建大型组件库时,将会体会到这种明确性和模块性,通过代码重用,你的代码行将开始缩小。