本文目的:通过React构建一个可搜索的产品数据表格,来更深刻地领会React哲学。
1、从设计稿开始
-
效果图:
- JSON API返回以下数据:
12345678[{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"}];
2、第一步:将设计好的 UI 划分为组件层级
- 首先,在设计稿上圈出每个组件(以及组件的子组件),并以合适的名称命名。
- 如何确定将哪些组件划分到一个组件中?
- 可以将组件当作一种函数或者对象来考虑,根据单一功能原则来判定组件的范围。
- 一个组件原则上只能负责一个功能。
- 上图包含五个组件:
- 1.橙色(FilterableProductTable):整个示例应用的整体
- 2.蓝色(SearchBar):用户输入
- 3.绿色(ProductTable):数据内容
- 4.天蓝色(ProductCategoryRow):产品类别
- 5.红色(ProductRow):产品
- 更加清晰的层级【设计稿中被其他组件包含的子组件,在层级上应该作为其子节点】
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
- FilterableProductTable
3、第二步:用 React 创建一个静态版本
-
先用已有的数据模型,渲染一个不包含交互功能的UI
-
自上而下或者自下而上构建应用
- 自上而下
- 先编写层级较高的组件
- 适用于比较简单的应用
- 自下而上
- 从最基本的组件开始编写
- 便于大型项目为低层组件编写测试
- 自上而下
-
代码:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111<script type="text/babel">// 5.产品class ProductRow extends React.Component {render () {const product = this.props.productconst name = product.stocked ?product.name :<span style={{ color: 'red' }}>{ product.name }</span>return (<tr class="product-row"><td>{ name }</td><td>{ product.price }</td></tr>)}}// 4.产品类别class ProductCategoryRow extends React.Component {render () {const category = this.props.categoryreturn (<tr class="product-category-row"><th colspan="2">{ category }</th></tr>)}}// 3.数据内容class ProductTable extends React.Component {render () {const rows = []let lastCategory = nullthis.props.products.forEach((product) => {if (product.category !== lastCategory) {rows.push(<ProductCategoryRowkey={ product.category }category={ product.category } />)}rows.push(<ProductRowproduct={ product }key={ product.name } />)lastCategory = product.category})return (<table class="product-table"><thead><tr><th>Name</th><th>Price</th></tr></thead><tbody>{ rows }</tbody></table>)}}// 2.用户输入class SearchBar extends React.Component {render () {return (<form class="search-bar"><input type="text" placeholder="Search..." class="keyword" /><p><input type="checkbox" />{''}Only show products in stock</p></form>)}}// 1.示例整体class FilterableProductTable extends React.Component {render () {return (<div class="filterable-product-table"><SearchBar /><ProductTable products={ this.props.products } /></div>)}}// JSON APIconst 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('root'))</script>123456789101112131415161718192021222324252627282930<style>.filterable-product-table {width: 240px;border: 1px solid silver;padding: 20px;margin: 100px;font-size: 14px;}.search-bar {line-height: 30px;}.search-bar .keyword {width: 100%;height: 30px;border: 1px solid #ddd;text-indent: 10px;}.product-table {margin-top: 15px;width: 100%;text-align: left;line-height: 30px;}.product-category-row {color: #555;}.product-row {line-height: 20px;}</style> -
React 单向数据流(也叫单向绑定)的思想使得组件模块化,易于快速开发。
4、第三步:确定UI state的最小(且完整)表示
- 目的:找出应用所需的state的最小表示,并根据需要计算出其他所有数据
- 关键:DRY【Don’t Repeat Yourself】
- 如何检查数据是否属于state?
- 该数据是否是由父组件通过 props 传递而来的?如果是,那它应该不是 state。
- 该数据是否随时间的推移而保持不变?如果是,那它应该也不是 state。
- 你能否根据其他 state 或 props 计算出该数据的值?如果是,那它也不是 state。
- 示例应用数据分析:
- 1.包含所有产品的原始列表
- 不是state:包含所有产品的原始列表是经由 props 传入的
- 2.搜索关键词
- 是state:随时间会发生改变且无法由其他数据计算而来
- 3.复选框是否选中的值
- 是state:随时间会发生改变且无法由其他数据计算而来
- 4.搜索筛选的产品列表
- 不是state:结果可以由产品的原始列表根据搜索词和复选框的选择计算出来
- 1.包含所有产品的原始列表
5、第四步:确定state放置的位置
注意:React 中的数据流是单向的,并顺着组件层级从上往下传递。
- 判断state应该放在哪个组件中?
- 找到根据这个 state 进行渲染的所有组件。
- 找到他们的共同所有者(common owner)组件(在组件层级上高于所有需要该 state 的组件)。
- 该共同所有者组件或者比它层级更高的组件应该拥有该 state。
- 如果你找不到一个合适的位置来存放该 state,就可以直接创建一个新的组件来存放该 state,并将这一新组件置于高于共同所有者组件层级的位置。
- 分析示例应用:
ProductTable
需要根据 state 筛选产品列表。SearchBar
需要展示搜索词和复选框的状态。- 他们的共同所有者是
FilterableProductTable
。 - 因此,搜索词和复选框的值应该很自然地存放在
FilterableProductTable
组件中。
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 |
// step4 // 3.数据内容 class ProductTable2 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 && !prodcut.stocked) { return } if (product.category !== lastCategory) { rows.push( <ProductCategoryRow key={ product.category } category={ product.category } /> ) } rows.push( <ProductRow product={ product } key={ product.name } /> ) lastCategory = product.category }) return ( <table class="product-table"> <thead> <tr> <th>Name</th> <th>Price</th> </tr> </thead> <tbody>{ rows }</tbody> </table> ) } } // 2.用户输入 class SearchBar2 extends React.Component { render () { const filterText = this.props.filterText const inStockOnly = this.props.inStockOnly return ( <form class="search-bar"> <input type="text" placeholder="Search..." class="keyword" value={ filterText } /> <p> <input type="checkbox" checked={ inStockOnly } /> {''} Only show products in stock </p> </form> ) } } // 1.示例整体 class FilterableProductTable2 extends React.Component { constructor (props) { super(props) this.state = { filterText: 'ball', inStockOnly: false } } render () { return ( <div class="filterable-product-table"> <SearchBar2 filterText={ this.state.filterText } inStockOnly={ this.state.inStockOnly } /> <ProductTable2 products={ this.props.products } filterText={ this.state.filterText } inStockOnly={ this.state.inStockOnly } /> </div> ) } } ReactDOM.render( <FilterableProductTable2 products={ PRODUCTS } />, document.getElementById('root2') ) |
6、第五步:添加反向数据流
-
React 通过一种比传统的双向绑定略微繁琐的方法来实现反向数据传递。
-
代码:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990// step5// 2.用户输入class SearchBar3 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 () {const filterText = this.props.filterTextconst inStockOnly = this.props.inStockOnlyreturn (<form class="search-bar"><inputtype="text"placeholder="Search..."class="keyword"value={ filterText }onChange={ this.handleFilterTextChange } /><p><inputtype="checkbox"checked={ inStockOnly }onChange={ this.handleInStockChange } />{''}Only show products in stock</p></form>)}}// 1.示例整体class FilterableProductTable3 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 class="filterable-product-table"><SearchBar3filterText={ this.state.filterText }inStockOnly={ this.state.inStockOnly }onFilterTextChange={ this.handleFilterTextChange }onInStockChange={ this.handleInStockChange } /><ProductTable2products={ this.props.products }filterText={ this.state.filterText }inStockOnly={ this.state.inStockOnly } /></div>)}}ReactDOM.render(<FilterableProductTable3 products={ PRODUCTS } />,document.getElementById('root3'))