[ 前端框架/React ] React 练习之商品展示小Demo(麻雀虽小五脏俱全)
React Demo实战
运用到的知识点:
React类、React函数、state变量、父子组件传值、兄弟组件传值、组件划分思想等
实战内容:制作一个可筛选、可用复选框控制的产品列表
实战步骤 Step1:划分原型图中的组成部分
可以看做五部分
- 最小单元:产品项如
Football $10.99
- 第二单元:产品类如
Sporting Goods
- 第三单元:产品展示表
Name Price
包含最小单元和第二单元 - 第三单元:搜索栏
search bar
和复选框checkbox
- 第四单元:整个展示页
product list table
注:这里的产品展示表 与 搜索栏是分在同一层级的,故都属于第三单元
实战步骤 Step2:对整体思路做规划(很重要)
我们需要理解这个东西到底是什么?
没错,就是一个产品展示页面!
我们输入关键字能进行筛选,勾选复选框能够将红色的售罄物品进行过滤。
详细阐述:
1. 我们首先需要编写最小单元:
最小单元分为两块 产品名称 productName
和 产品价格 productPrice
。
我们可以模拟一下数据
// productItem
{
name: 'Football',
price: 10.99,
stocked: true,
id: 'football-1099'
}
因此在它的父组件中我们应该这样写:
<>
<ProductItem productItem={productItem} />
</>
// 注:这里的 <ProductItem /> 就是我们的第一单元了
// 其中 productItem={productItem} 是指
将(等号右边的)父组件中的名叫 productItem 的数据 以 props 的方式
传递给我们的(等号左边的)子组件,在子组件中使用 props.productItem 访问
而它本身需要接收父组件传来的 productItem
数据
因此我们可以初步写出 ProductItem
组件
function ProductItem(props) {
return (
<> // 此处从简不新增样式
<span class={`product-name ${props.productItem.stocked ? 'stoked' : ''}`}>
{props.productItem.name}
</span>
<span class="product-price">
${props.productItem.price}
</span>
</>
)
}
// 注:我们可以看到在 <span></span>标签中我们使用 props.productItem 访问到了父组件传过来的数据
2. 由最小单元我们可以写出第二单元:
第二单元也分为两部分:产品类型 productType
和 产品项 productItem
也即是最小单元。
模拟一下数据:
// productGroup
{
productType: 'Sporting Goods',
productItems: [
// productItem...
]
}
因此我们可以再推出它的父级元素写法:
<>
<ProductGroup productGroup={productGroup} />
</>
同样,初步写出第二单元:
function ProductGroup(props) {
return (
<>
<header></header>
<ProductItem />
<ProductItem />
</>
)
}
在这里我们初步写出了第二单元,我们注意到两个问题:
一个是类型需要单独拿出来;
二是我们无法预知每一组的产品到底有多少个。
所以我们需要用到循环来创建 ProductItem
组件:
// 遍历 props 中的 productGroup.productItems 数组来创建 ProductItem 组件
const rProductItems = props.productItems.map((productItem) => {
// 我们需要获取到每一项产品的信息 所以需要遍历 productItems 这个数组,
// 里面的每一项就是一个 productItem
return <ProductItem productItem={productItem} />
})
这样我们就给每一组商品创建了对应的产品个体组件,得到的 rProductItems
就是这个组件组
稍微修改整合一下,得到如下代码:
function ProductGroup(props) {
const rProductItems = props.productGroup.productItems.map((productItem) => {
return <ProductItem productItem={productItem} />
})
return (
<>
<h3>{props.productGroup.productType}</h3>
{rProductItems}
</>
)
}
看起来没什么问题了,下一个。
3. 同上所述,我们也把第三单元分为两部分 展示表头 productListTitle
和列表的内容 productList
。
不难看出我们的 productList
其实就是若干个 商品组 productGroup
构成的。
模拟数据:
// productList
[
// productGroup ...
]
// 这里稍微整合一下,结合上面两个数据
// productList 是一个无名数组
[
// productGroup // 是一个无名对象
{
productType: 'Sporting Goods',
// productItems // 是一个有名数组
productItems: [
// productItem 是一个无名对象
{
name: 'Football',
price: 10.99,
stocked: true,
id: football-1099
},
{
name: 'Baseball',
price: 2.99,
stocked: true,
id: baseball-299
}
]
}
]
看起来也相当有规模了,太激动忘了要干什么了,接下来是什么来着?
没错,依然是用父组件对这个组件进行规划:
<>
<ProductList productList={productList} />
</>
初步拟写一下 ProductList
组件:
function ProductList(props) {
return (
<>
<header></header>
<ProductGroup />
</>
)
}
咦?是不是似曾相识,而且我们很容易想到 ProductGroup
也是数量不定的,所以我们同样要来遍历...
额,遍历什么?
对了,遍历 props.productList
数组,有人会问了:
productList 数组里的数据是怎么来的呢?
问得好,不过到现在才问是不是有一点点迟了呢?
大家请看我们之前编写第一单元第二单元的时候,我们考虑过这个问题吗?
没有,为什么我们没有考虑,因为这个数据我们默认它是从父组件传过来的,本组件位于第三单元,尚且不是最外层组件(不过不要因为这句话被误导了哟),所以我们可以一直追溯这个数据到最上层去,至于我们是不是要追溯到外太空呢,哈哈,我们留一个问题,稍后来解答。
好了,我们用循环来创建 ProductGroup
组件:
const rProductGruop = props.productList.map((item) => {
// item 是 productList 里的每一项 也即是我们的 productGroup
return (
<ProductGroup productGroup={item} />
)
})
同样进行一个整理:
function ProductList(props) {
const rProductGruop = props.productList.map((item) => {
return (
<ProductGroup productGroup={item} />
)
})
return (
<>
<span class="product-list-name">Name</span>
<span class="product-list-price">Price</span>
{rProductGruop}
</>
)
}
看起来也没有什么问题!至此我们的 Demo
已经按照原型图上写出来了下半部分的初步代码。
但是有人肯定会有疑问:
为什么咱们不先写上面的搜索框呢?
这个嘛,我也不知道,我就喜欢从下面开始做,品尝最精髓的...咳咳,再说下去我要被抓起来了。
这个问题需要大家自己去探索,去摸寻关键,我们可以选择从最大的单元开始写,也可以从上往下写,即使你用脚写,都是可行的。或喜欢、或思路畅通、或代码风格,不同的写法才有不同的思想,这是世界缤纷的原因。
扯远了,咱们接着写。
4. 接下来是搜索框和复选框:
——也是第三单元,为什么不是第一单元呢?
——同学,你的问题太多了!
它分为两个部分,搜索框 seachInput
和 复选框 stockCheckbox
。
其实我们可以不用分得这么细,就是一个搜索框一个复选框,但是这么分是有根据的:
搜索框:我们用来搜索相关商品的输入框,我们输入关键字,下面的 ProductList
就会根据关键字做出相应的更新;
复选框:勾选之后我们就只能看到尚有库存的商品,红色的商品(售罄)就不再显示在页面上了。
通过对比,我们发现这两个框都是对另一个第三单元组件产生影响的,而我们再看看这个 Demo
原型图,它们之间有个共同点,都是 第四单元的 子组件,要实现两个同一层级的组件进行数据交互,这时候我们首先想到的肯定是将父组件作为媒介(本例也是如此)。
我们也可以模拟一下子组件在父组件中的写法:
<>
<SearchBar />
<ProductList productList={productList} />
<>
// 下面这个乌漆嘛黑的家伙是谁?
// 别激动,它只是位于与 SearchBar 组件同一层级的 ProductList 组件
所以我们把搜索框和复选框的值分别作为一个 state
:
import React from 'react'
// 这里为什么要用 class 呢?
class SearchBar extends React.Component{
constructor(props) {
super(props)
this.state = {
searchValue: '', // 搜索关键字初始化为空
onlyShowStocked: false // 复选框初始化为不勾选
}
}
render () {
return (
<div class="search-bar">
<input
type="text"
class="search-bar-input"
placeholder="Search..."
/>
<input
type="checkbox"
class="search-bar-checkbox"
/>
</div>
)
}
}
初步写完之后,我们看到我们在名为 constructor
的函数里增加了两个变量:searchValue
和 onlyShowStocked
。
——我们写这些是为了做什么?
——搜索,筛选
SearchBar
组件本身是不参与数据修改的,所以它只有把自己的要求传达给其父组件,让父组件来修改数据。
——爸爸,
ProductList
组件很坏,仗着你把数据
给他,我现在想碰一下都不行!——好了乖乖,等会爸爸就教训他,你尽管放心,你想把
数据
改成什么样子,我抽屉里有几台函数电脑
,你把你的要求输入到函数电脑里,我等会吃完饭就去拿,保证ProductList
这小子给你改!——嘻嘻,谢谢爸爸!
哎,这段对话真中二啊。
这类有一个概念,叫做 函数电脑
,其实就是父组件中的函数,子组件如果想给父组件传值,只能使用父组件中的函数,所以我们需要借用父组件的函数来传值,但是我们父组件还没有写,怎么办?
我们可以假装父组件已经有某个函数,并且已经能够实现相关的功能(当然等会还是要实现的)。
给 render
函数做一下完善:
render () {
return (
<div class="search-bar">
<input
type="text"
class="search-bar-input"
placeholder="Search..."
value={this.state.searchValue}
onChange={this.handleValueChange}
/>
<input
type="checkbox"
class="search-bar-checkbox"
value={this.state.onlyShowStocked}
onChange={this.handleCheckChange}
/>
<label>Only Show Stocked</label>
</div>
)
}
我们新增了数据绑定,把 SearchBar
组件里的 state
分别绑定到输入框与复选框上,并且我们给这两个框分别添加了两个函数:handleValueChange
和 handleCheckChange
。
——为什么要这四行代码呢?
——其实这得从盘古开天辟地开始说...
——停,不想说就别说!
只举一例,当搜索框里的数据发生变化的时候,我们需要及时监听响应给父组件,因此我们需要添加
onChange={this.handleValueChange}
这行代码来监听数据变化,并且在这个函数中调用父组件的函数来告知父组件我(也即是当前的子组件)已经发生了变化,数据绑定是为了让双方的数据一致。
这时候我们需要对这两个监听函数进行完善:
class SearchBar extends React.Component {
constructor(props) {
// ...
// 绑定 this 有兴趣可以了解一下原理,篇幅限制不多赘述
this.handleValueChange = this.handleValueChange.bind(this)
this.handleCheckChange = this.handleValueChange.bind(this)
}
// 搜索框
handleValueChange(event) {
// 改变当前的 state
this.setState({
searchValue: event.target.value
})
// 给父组件传值,使用父组件的函数,我们假装已经写好了
this.props.getSearchValue(event.target.value)
}
// 复选框
handleCheckChange(event) {
this.setState({
onlyShowStocked: event.target.checked
})
this.props.getCheckedValue(event.target.checked)
}
render () {
// ...
}
}
至此,我们的 SearchBar
组件就已经写好了!
但是,由于我们从 props
中使用了父组件的两个函数,所以我们再来拟写一下父组件,看看有什么不同!
<>
<SearchBar
getSearchValue={getSearchValue}
getCheckedValue={getCheckedValue}
/>
<ProductList productList={productList} />
</>
很显然,与 ProductList
组件的传值不同,这一次我们在 SearchBar
组件中传入的是两个函数,而 ProductList
是一个数组,还记得吗?
5. 好了,终于到了最激动人心的时刻了,那就是我们的父组件!
我们通过 1、2、3、4 步能够总结到下面几个点:
a. 数据已经追溯到了父节点;
b. SearchBar
组件需要改变 ProductList
组件里的值;
c. 父组件需要拥有至少两个函数来获取 SearchBar
传过来的值;
d. 父组件拥有两个子组件。
话不多说,直接上代码!
// import React from 'react' // 之前已经在写其他组件时引入过
class ProductShowPage extends React.Component {
constructor(props) {
super(props)
this.getSearchValue = this.getSearchValue.bind(this)
this.getCheckedValue = this.getCheckedValue.bind(this)
this.state = {
searchValue: '',
onlyShowStocked: false
}
}
// 获取子组件中的数据
getSearchValue = (searchValue) {
this.setState({
searchValue: searchValue
})
}
getCheckedValue = (checkedValue) {
this.setState({
checkedValue: checkedValue
})
}
render() {
return (
<>
<SearchBar
getSearchValue={this.getSearchValue}
getCheckedValue={this.getCheckedValue}
/>
<ProductList productList={productList} />
</>
)
}
}
至此,我们上述的 c、d 就已经解决了,还有 a、b 没有得到解决。
首先是 a ...
——为什么不是b?
——我跟你说,我打字打到这里手都快断了,能不能少问点问题!
我们假设从 b 先开始,我们发现如果 SearchBar
要想改变 ProductList
的数据还是得回到 a 上面来,毕竟要改变数据必须得现拥有数据。
这里的数据就随便编造一点,写一个函数作为返回值:
apiRequest(searchValue, onlyShowStocked) {
// 模拟前后端交互,这里给函数传入两个参数,耳熟能详
const sourceData = [
{
productType: 'Sporting Goods',
productItems: [
{
name: 'Football',
price: 10.99,
stocked: false,
id: football-10-99
},
{
name: 'Baseball',
price: 2.99,
stocked: true,
id: baseball-2-99
},
{
name: 'Basketball',
price: 6.99,
stocked: true,
id: basketball-6-99
}
]
},
{
productType: 'Electronics',
productItems: [
{
name: 'ipod Touch',
price: 219.99,
stocked: true,
id: ipod-touch-219-99
},
{
name: 'iPhone 5',
price: 249.99,
stocked: false,
id: iphone-249-99
},
{
name: 'Nexus 7',
price: 199.99,
stocked: true,
id: nexus-7-199-99
}
]
}
]
let res = []
if (onlyShowStocked) {
sourceData.forEach((productGroup) => {
let productType = productGroup.productType
let productItems = []
productGroup.productItems.forEach((productItem) => {
if ((productItem.name.toLowerCase().indexOf(searchValue.trim().toLowerCase()) !== -1) && productItem.stocked) {
productItems.push(productItem)
}
})
let rProductGroup = {
productType: productType,
productItems: productItems
}
res.push(rProductGroup)
})
} else {
sourceData.forEach((productGroup) => {
let productType = productGroup.productType
let productItems = []
productGroup.productItems.forEach((productItem) => {
if (productItem.name.toLowerCase().indexOf(searchValue.trim().toLowerCase()) !== -1) {
productItems.push(productItem)
}
})
let rProductGroup = {
productType: productType,
productItems: productItems
}
res.push(rProductGroup)
})
}
return res
}
详细的就不赘述了,有兴趣的可以看看。
数据就有了,我们可以添加一个 componentDidMount
函数,在组件挂载完成之后加载数据:
class ProductShowPage extends React.Component {
constructor(props) {
// ...
this.state({
productList: []
})
}
componentDidMount() {
this.apiRequest(this.state.searchValue, this.state.onlyShowStocked)
}
apiRequest(searchValue, onlyShowStocked) {
// ...
}
render () {
// ...
<ProductList productList={this.state.productList} />
}
}
至此,到了最后一步,在获取子组件的值函数里我们需要重新获取带有条件的数据:
//获取子组件中的数据
getSearchValue = (searchValue) => {
this.setState({
searchValue: searchValue
})
let newProductList = this.apiRequest(searchValue, this.state.onlyShowStocked)
this.setState({
productList: newProductList
})
}
getCheckedValue = (checkedValue) => {
this.setState({
onlyShowStocked: checkedValue
})
let newProductList = this.apiRequest(this.state.searchValue, checkedValue)
this.setState({
productList: newProductList
})
}
写至终章,祝你成功!
附上最终代码:
// js 本笔记以最终代码为准,拆解过程可能有笔误或叙述问题
import React from 'react' // 引入React
import './index.scss' // 引入样式
// 第一单元
function ProductItem(props) {
return (
<>
<span className={`product-name ${props.productItem.stocked ? '' : 'no-stocked'}`}>
{props.productItem.name}
</span>
<span className="product-price">
${props.productItem.price}
</span>
</>
)
}
// 第二单元
function ProductGroup(props) {
const rProductItems = props.productGroup.productItems.map((productItem) => {
return <ProductItem productItem={productItem} />
})
return (
<>
<h3>{props.productGroup.productType}</h3>
{rProductItems}
</>
)
}
// 第三单元
function ProductList(props) {
const rProductGruop = props.productList.map((item) => {
return (
<ProductGroup productGroup={item} />
)
})
return (
<div className="product-list">
<span className="product-list-name">Name</span>
<span className="product-list-price">Price</span>
{rProductGruop}
</div>
)
}
// 这里为什么要用 class 呢? 第三单元
class SearchBar extends React.Component {
constructor(props) {
super(props)
// 绑定 this
this.handleValueChange = this.handleValueChange.bind(this)
this.handleCheckChange = this.handleCheckChange.bind(this)
this.state = {
searchValue: '', // 搜索关键字初始化为空
onlyShowStocked: false // 复选框初始化为不勾选
}
}
// 搜索框
handleValueChange(event) {
// 改变当前的 state
this.setState({
searchValue: event.target.value
})
// 给父组件传值,使用父组件的函数,我们假装已经写好了
this.props.getSearchValue(event.target.value)
}
// 复选框
handleCheckChange(event) {
this.setState({
onlyShowStocked: event.target.checked
})
this.props.getCheckedValue(event.target.checked)
}
render() {
return (
<div className="search-bar">
<input
type="text"
class="search-bar-input"
placeholder="Search..."
value={this.state.searchValue}
onChange={this.handleValueChange}
/>
<input
type="checkbox"
class="search-bar-checkbox"
value={this.state.onlyShowStocked}
onChange={this.handleCheckChange}
/>
<label>Only Show Stocked</label>
</div>
)
}
}
// 第四单元
// import React from 'react' // 之前已经在写其他组件时引入过
class ProductShowPage extends React.Component {
constructor(props) {
super(props)
this.getSearchValue = this.getSearchValue.bind(this)
this.getCheckedValue = this.getCheckedValue.bind(this)
this.state = {
searchValue: '',
onlyShowStocked: false,
productList: []
}
}
// 挂载时就获取数据
componentDidMount() {
this.setState({
productList: this.apiRequest(this.state.searchValue, this.state.onlyShowStocked)
})
}
// 获取子组件中的数据
getSearchValue = (searchValue) => {
this.setState({
searchValue: searchValue
})
let newProductList = this.apiRequest(searchValue, this.state.onlyShowStocked)
this.setState({
productList: newProductList
})
}
getCheckedValue = (checkedValue) => {
this.setState({
onlyShowStocked: checkedValue
})
let newProductList = this.apiRequest(this.state.searchValue, checkedValue)
this.setState({
productList: newProductList
})
}
// 模拟前后端交互,这里给函数传入两个参数,耳熟能详
apiRequest(searchValue, onlyShowStocked) {
// 模拟数据库中的数据
const sourceData = [
{
productType: 'Sporting Goods',
productItems: [
{
name: 'Football',
price: 10.99,
stocked: false,
id: 'football-10-99'
},
{
name: 'Baseball',
price: 2.99,
stocked: true,
id: 'baseball-2-99'
},
{
name: 'Basketball',
price: 6.99,
stocked: true,
id: 'basketball-6-99'
}
]
},
{
productType: 'Electronics',
productItems: [
{
name: 'ipod Touch',
price: 219.99,
stocked: true,
id: 'ipod-touch-219-99'
},
{
name: 'iPhone 5',
price: 249.99,
stocked: false,
id: 'iphone-249-99'
},
{
name: 'Nexus 7',
price: 199.99,
stocked: true,
id: 'nexus-7-199-99'
}
]
}
]
let res = []
// 讨论复选框被选上的情况
if (onlyShowStocked) {
// 分别讨论搜索值是否为空(原则上我们可以纳入同一情况)
sourceData.forEach((productGroup) => {
let productType = productGroup.productType
let productItems = []
productGroup.productItems.forEach((productItem) => {
if ((productItem.name.toLowerCase().indexOf(searchValue.trim().toLowerCase()) !== -1) && productItem.stocked) {
productItems.push(productItem)
}
})
let rProductGroup = {
productType: productType,
productItems: productItems
}
res.push(rProductGroup)
})
} else {
sourceData.forEach((productGroup) => {
let productType = productGroup.productType
let productItems = []
productGroup.productItems.forEach((productItem) => {
if (productItem.name.toLowerCase().indexOf(searchValue.trim().toLowerCase()) !== -1) {
productItems.push(productItem)
}
})
let rProductGroup = {
productType: productType,
productItems: productItems
}
res.push(rProductGroup)
})
}
return res
}
render() {
return (
<fieldset className="product-show-page">
<legend>Product Price List</legend>
<SearchBar
getSearchValue={this.getSearchValue}
getCheckedValue={this.getCheckedValue}
/>
<ProductList productList={this.state.productList} />
</fieldset>
)
}
}
export default ProductShowPage
// scss 样式文件
.product-show-page {
margin: auto;
width: 220px;
.search-bar {
width: 100%;
.search-bar-input {
width: 95%;
}
}
.product-list {
h3 {
margin: 0 auto;
}
}
.product-name, .product-price {
display: inline-block;
width: 50%;
}
.product-name {
&.no-stocked {
color: #F00;
}
}
.product-list-name, .product-list-price {
padding: 30px 0 10px 0;
display: inline-block;
width: 50%;
font-weight: 700;
}
}
博客为作者原创,版权所有,保留一切权利。仅供学习和参考,转载必须注明博主ID和转载链接。