zoukankan      html  css  js  c++  java
  • [ 前端框架/React ] React 练习之商品展示小Demo(麻雀虽小五脏俱全)

    React Demo实战

    运用到的知识点:

    React类、React函数、state变量、父子组件传值、兄弟组件传值、组件划分思想等

    实战内容:制作一个可筛选、可用复选框控制的产品列表

    练习原型图

    实战步骤 Step1:划分原型图中的组成部分

    可以看做五部分

    1. 最小单元:产品项如 Football $10.99
    2. 第二单元:产品类如 Sporting Goods
    3. 第三单元:产品展示表 Name Price 包含最小单元和第二单元
    4. 第三单元:搜索栏 search bar 和复选框 checkbox
    5. 第四单元:整个展示页 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 的函数里增加了两个变量:searchValueonlyShowStocked

    ——我们写这些是为了做什么?

    ——搜索,筛选

    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 分别绑定到输入框与复选框上,并且我们给这两个框分别添加了两个函数:handleValueChangehandleCheckChange

    ——为什么要这四行代码呢?

    ——其实这得从盘古开天辟地开始说...

    ——停,不想说就别说!

    只举一例,当搜索框里的数据发生变化的时候,我们需要及时监听响应给父组件,因此我们需要添加
    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;
       220px;
      .search-bar {
         100%;
        .search-bar-input {
           95%;
        }
      }
    
      .product-list {
        h3 {
          margin: 0 auto;
        }
      }
      .product-name, .product-price {
        display: inline-block;
         50%;
      }
    
      .product-name {
        &.no-stocked {
          color: #F00;
        }
      }
    
      .product-list-name, .product-list-price {
        padding: 30px 0 10px 0;
        display: inline-block;
         50%;
        font-weight: 700;
      }
    }
    
    博主水平有限,难免疏漏有误,欢迎交流指正。
    博客为作者原创,版权所有,保留一切权利。仅供学习和参考,转载必须注明博主ID和转载链接。
  • 相关阅读:
    Linux系统下/tmp目录文件重启后自动删除,不重启自动删除10天前的/TMP的文件(转)
    Docker 镜像加速器
    RabbitMQ集群和高可用配置
    k8s如何管理Pod(rc、rs、deployment)
    微信开放平台开发(1) 语义理解
    微信开放平台开发(2) 微信登录
    微信电话
    微信支付开发(1) 微信支付URL配置
    微信支付开发(2) 微信支付账号体系
    微信支付开发(3) JS API支付
  • 原文地址:https://www.cnblogs.com/ExileRiven/p/15682152.html
Copyright © 2011-2022 走看看