假设我们要创建一个应用,如何用react的思维进行创建呢?本例是react官网的例子,主要是进行更详细的叙述和细节补充。
注:本文默认您已经清楚react的基本用法了,不清楚请点击React官网学习哦。
我们要创建一个什么样的应用?
假设我们要创建一个商品列表的应用。
界面如下(一般界面我们可以从设计师处拿到)
而我们从后端获取的数据如下:
[
{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"}
];
Step 1: Break The UI Into A Component Hierarchy
(把UI分成层级组件)
首先你可以根据UI图形为每个component划分区域(一个矩形之类),如果设计稿做得好,很可能每个图层的名称就是我们的组件名称。
如何进行分层呢?原则和我们创建一个函数和对象是一致的,就是“单一职责原则”。一个组件理论上应该只做一件事。如果内能还能在划分,那么它应该继续细化。
由于您经常向用户显示JSON数据模型,您会发现,如果您的模型构建正确,您的UI(以及您的组件结构)将很好地映射。 这是因为UI和数据模型倾向于遵循相同的信息架构,这意味着将UI分成组件的工作往往是微不足道的。 只需将其分解成代表您的数据模型的一部分。
基于以上原则,我们把应用分成以下组件。
- FilterableProductTable (橘色): contains the entirety of the example
- SearchBar (蓝色): receives all user input
- ProductTable (绿色): displays and filters the data collection based on user input
- ProductCategoryRow (蓝绿色): displays a heading for each category
- ProductRow (红色): displays a row for each product
所以我们的组件层级结果如下
- FilterableProductTable
- SearchBar
- ProductTable
- -- ProductCategoryRow
- -- ProductRow
根据我们的结构创建初级代码
代码如下
/**
* Created by zhengzehao on 2017/10/18.
*/
import React, {Component} from 'react'
import './myCss.css'
class FilterableProductTable extends Component {
render() {
return (<div>
<SearchBar/>
<ProductTable/>
</div>)
}
}
class SearchBar extends Component {
render() {
return (<h2>SearchBar</h2>)
}
}
class ProductTable extends Component {
render() {
return (<div>
<h2>
ProductTable
</h2>
<div>
<ProductCategoryRow/>
<ProductRow/>
</div>
</div>)
}
}
class ProductCategoryRow extends Component {
render() {
return (<div>ProductCategoryRow</div>)
}
}
class ProductRow extends Component {
render() {
return (<div>ProductRow</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'}
];
export default FilterableProductTable
我们预览看看结果怎么样。
Step 2: Build A Static Version in React(用创建一个静态版本)
虽然上面的基础版本看起有点丑,但是确定了我们的组件和层级关系,接下来我们不考虑其交互关系,先创建一个静态版本。
因为创建这样的版本可以可以根据各个模块分别创建,而不需要考虑那么多的数据传递方式。
首先需要确定的是,既然是静态版本,我们还不需要state,因为使用state意味着我们有数据的交互,而我们只是创建静态版本。
而我们为什么要用props呢?因为props是父组件传递给子组件的方式,既然我们确定了层级关系,在静态版本我们也需要确定子组件的数据来源。
至于是自上而下(top-down),还是自下而上(bottom-up),取决于我们的结构,一般小应用我们用自上而下是比较快的,而层级比较多而复杂的组件,我们采取自下而上是比较理想的。
FilterableProductTable的修改
对于这个最大的上层组件,我们确定的是关系如下:
<div>
<SearchBar/>
<ProductTable/>
</div>
但既然我们<ProductTable/>
需要展示数据,我们的来源就需要从顶层这里获取。
因此我们的组件修改如下:
(只是添加了products的引用,简单吧...)
class FilterableProductTable extends Component {
render() {
return (<div>
<SearchBar/>
<ProductTable products={PRODUCTS}/>
</div>)
}
}
SearchBar的修改
想想我们的SearchBar长什么样?
一个input框和一个checkbox,然后是一段文字
所以很容易就创建以下结构。
ps:我们先不考虑数据交互,先做静态版本,所以这里先不与
products交互。
class SearchBar extends Component {
render() {
return (
<div>
<input type="text" placeholder="Search"/>
<p>
<input type="checkbox"/>{''}Only show products in stock
</p>
</div>
)
}
}
这是的p标签只是用以区分input和checkbox,或者说换行用的,可以自行替换.
ProductTable的修改
同样,我们根据UI来看html应该怎么写。
首先是一个table,里面包着两个heade(Name & Price),
Sporting Goods和Electronics呢?
它们也是每一个类别的表头,但是横跨两行。所以我们可以轻松写出这样的结构。
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<th cols={2}>{category}</th>
</tr>
<tr>
<td>{name}</td>
<td>{price}</td>
</tr>
</tbody>
</table>
这里,Name和Price是固定的,而后面的内容是根据后台数据变化的,所以可以把这部分设置为一个变量,如下:
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
这里我们的rows要怎么写呢?
考虑一下,我们首先需要一个数组,包含所有的table内容,每个类别有一个表头和内容,
所以应该是这样的
rows=[<Category1/>,<Row1/>,<Row2/>,
<Category2/>,<Row3/>,</Row4/>]
而我们的后台数据是这样的
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'}
];
这么形成rows形式的结构呢?就是对一个数组判断,如果有一个category(比如Sporting Goods),就push这个表头,然后push后面的内容.
如果下一个对象的category还是一样的,我们就忽略这个category,但是继续push其他内容。直到我们遇到下一个category(比如Electronics),我们才push这个表头。
const rows = [];
let lastCategory = null;//作为是否是新category的判断
//首先我们需要从父元素获取这个products的对象列表,然后对每个元素进行遍历(category有新的就push表头,否则就push内容)
this.props.products.forEach((product) => {
//要清楚我们需要的结构rows = [<Category1/>,<Row1/>,<Row2/>,<Category2/>,<Row3/>,</Row4/>]
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category}/>
);
//这里添加cataegory时因为子元素需要获取每个表头的名字,key就不多说了,不清楚可以看官网了解key的作用
}
rows.push(
<ProductRow
product={product}
key={product.name}/>
);
//这里添加product是因为子元素需要获取每个product的内容,包括价格,名称等等
lastCategory = product.category;
});
所以我们创建了一个单项数据流的结构,自上而下,只有一个render()方法,当我们需要确认UI变化的原因,可以很容易地追溯到。
综上,我们的ProductTable代码如下:
class ProductTable extends React.Component {
render() {
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) => {
//要清楚我们需要的结构rows = [<Category1/>,<Row1/>,<Row2/>,<Category2/>,<Row3/>,</Row4/>]
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>
);
}
}
ProductCategoryRow的修改
这里的html结构我们上面已经讨论过了。
由于需要category的字段,我们直接从父元素获取(this.props.category)
class ProductCategoryRow extends Component {
render() {
var category = this.props.category;
return (
<tr>
<th cols={2}>{category}</th>
</tr>
)
}
}
ProductRow的修改
结构之前已经讨论过,而我们需要对是否stocked进行标记,所以采取
三元表示符。(stocked为false的颜色为红)
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;
// 如果是stocked就直接输出,否则用span包裹并设置style的color为red
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
完整的代码
/**
* Created by zhengzehao on 2017/10/18.
*/
import React, {Component} from 'react'
class FilterableProductTable extends Component {
render() {
return (<div>
<SearchBar/>
<ProductTable products={PRODUCTS}/>
</div>)
}
}
class SearchBar extends Component {
render() {
return (
<div>
<input type="text" placeholder="Search"/>
<p>
<input type="checkbox"/>{''}Only show products in stock
</p>
</div>
)
}
}
class ProductTable extends React.Component {
render() {
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) => {
//要清楚我们需要的结构rows = [<Category1/>,<Row1/>,<Row2/>,<Category2/>,<Row3/>,</Row4/>]
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 ProductCategoryRow extends Component {
render() {
var category = this.props.category;
return (<tr>
<th cols={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>;
// 如果是stocked就直接输出,否则用span包裹并设置style的color为red
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
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'}
];
export default FilterableProductTable
预览一下:
Step 3: Identify The Minimal (but complete) Representation Of UI State
要使UI产生交互效果,在REACT中我们是通过state来进行的。
考虑一下我们的的各种数据有:
- 后端提供的products列表。
- input标签的输入框产生的内容
- checkbox的结果
- ProductTable过滤后的结果
考虑是不是state数据有以下原则
- 是否可以从父组件通过props传递过来?
- If so, it probably isn’t state.
- 是否是一个常量?
- If so, it probably isn’t state.
- 是否可以根据其他state或者props计算出来?
- If so, it isn’t state.
接下来逐项分析:
- products列表的内容可以从父元素的props获取,所以不是state。
- 搜索框和checkbox的内容似乎是state,因为它们无法从其他元素获取或者计算得出。
- 最后,过滤的列表也不是state,因为它可以通过搜索框和checkbox的值进行筛选得出。
最终,我们只有两个state需要设置。
- 搜索框里用户输入的值
- checkbox的值
Step 4:Identify Where Your State Should Live
好的,所以我们确定了最小的应用状态是什么。 接下来,我们需要确定哪个组件应该改变或拥有这种状态。
记住:React是一个单向数据流的层级结构,而我们或许一开始并不清楚需要把state设置在哪里。
这通常是对入门者最困扰的地方,但是我们可以根据下面的步骤进行分析。
对每个state进行分析:
- Identify every component that renders something based on that state.
- 找出每个需要根据state进行渲染的组件
- Find a common owner component (a single component above all the components that need the state in the hierarchy).
- 找出一个共通组件(一般是层级组件的上层)
- Either the common owner or another component higher up in the hierarchy should own the state.
- 一个共通组件或者另外的更高阶层的组件应该拥有这些state
- If you can’t find a component where it makes sense to own the state, create a new component simply for holding the state and add it somewhere in the hierarchy above the common owner component.
- 如果没有找到一个组件适合去存放这些state,我们可以创建一个。
我们根据上面的原则进行分析
-
ProductTable需要根据与用户在搜索框的输入和checkbox的勾选进行过滤数据,把过滤后的数据展示在上面。
-
共通的组件便是FilterableProductTable.(ProductTable和SearchBar的共通父级)
Cool, so we’ve decided that our state lives in FilterableProductTable
① 在FilterableProductTable 建立初始statethis.state = {filterText: '', inStockOnly: false}
② 在<ProductTable/> 和 <SearchBar/>传递props filterText 和 inStockOnly=state对应的值,使子组件获取到该值。
<div>
<SearchBar filterText={this.state.filterText}inStockOnly={this.state.inStockOnly}/>
<ProductTable products={PRODUCTS}
filterText={this.state.filterText} inStockOnly={this.state.inStockOnly}/>
</div>
③ 用这些props去过滤ProductTable的内容,并且把SeachBar对应的props设为这些state。
class SearchBar extends Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
return (
<div>
<input type="text" placeholder="Search" value={filterText}/>
<p>
<input type="checkbox" checked={inStockOnly}/>{''}Only show products in stock
</p>
</div>
)
}
}
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) => {
// new
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>
);
}
}
完整的代码
/**
* Created by zhengzehao on 2017/10/18.
*/
import React, {Component} from 'react'
class FilterableProductTable extends Component {
constructor(props) {
super(props)
this.state = {
filterText: '',
inStockOnly: false
};
}
render() {
return (<div>
<SearchBar filterText={this.state.filterText} inStockOnly={this.state.inStockOnly}/>
<ProductTable products={PRODUCTS}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}/>
</div>)
}
}
class SearchBar extends Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
return (
<div>
<input type="text" placeholder="Search" value={filterText}/>
<p>
<input type="checkbox" checked={inStockOnly}/>{''}Only show products in stock
</p>
</div>
)
}
}
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) => {
// new
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 ProductCategoryRow extends Component {
render() {
var category = this.props.category;
return (<tr>
<th cols={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>;
// 如果是stocked就直接输出,否则用span包裹并设置style的color为red
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
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'}
];
export default FilterableProductTable
Step 5: Add Inverse Data Flow
到目前为止,我们已经构建了一个应用程序,可以在层次结构中用function的props或者state进行正确地渲染。 现在是时候来进行数据交互了。
React这样的数据结构使我们能明确地知道各个组件之间的数据流动,但相对一般的双向数据流,我们需要多一步(通过回调函数)来改变state的值。
如果你尝试在上一步建立的app中进行输入或者改变checkbox的状态,你会发现是无效的。原因是我们设置了input的值永远是由FilterableProductTable的state进行传递的。
让我们确认一下发生了什么。我们想要确定无论何时,用户改变表单,我们就根据输入的内容(包括input和checkbox)改变我们的state。
FilterableProductTable 会传递回调函数,只要我们的state需要变更,它便会触发。
我们可以通过onChange作为这个回调函数,一旦用户的表单发生改变,我们就用setState()来改变state。
完整代码
相对上一步,我们就多了两个函数,onChange的时候调用这两个函数进行改变state,注意要绑定this哦😯...
/**
* Created by zhengzehao on 2017/10/18.
*/
import React, {Component} from 'react'
class FilterableProductTable extends 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={PRODUCTS}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}/>
</div>)
}
}
class SearchBar extends 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() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
return (
<div>
<input type="text" placeholder="Search" value={filterText} onChange={this.handleFilterTextChange}/>
<p>
<input type="checkbox" checked={inStockOnly} onChange={this.handleInStockChange}/>{''}Only show products in stock
</p>
</div>
)
}
}
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) => {
// new
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 ProductCategoryRow extends Component {
render() {
var category = this.props.category;
return (
<tr>
<th cols={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>;
// 如果是stocked就直接输出,否则用span包裹并设置style的color为red
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
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'}
];
export default FilterableProductTable
此时就可以进行筛选和搜索了。
作者:Aleph_Zheng
链接:https://www.jianshu.com/p/9ff4eb06b443
来源:简书
著作权归作者所有。