1.Context
上个章节我们完成了按钮的增加删除功能,我们完全是通过props一层层传递函数,然后调用函数传递参数的形式。但是这样我们只能一层层传递,
编辑
这里我们传给按钮组件只需要传三层,但其实以及够麻烦了,如果有十层呢,显然不合理,所以我们就需要一个新的概念,Context,我们需要用到这个东西,首先英语翻译是文本上下文,而Context就 为我们提供组件间共享数据的一种方式,类似于全局作用域,我们在根组件设置的Context,所有内层组件都可以使用。那么怎么用呢?
编辑
首先我们要去创建一个文件夹store,名字是store,所有组件都可以使用。创建testContext文件,然后用React.createContext去创造一个Context,
javascript
import React from 'react'
//Context相当与一个公共
/*
Context相当与一个公共的存储空间
我们可以将多个组件都需要访问的数据存储到一个Context中
这样无需props逐层传递,即可使组件访问到这些数据
通过React.createContext()创建context
*/
const TestContext = React.createContext({
name: 'sun',
age: 11
})
export default TestContext
这样我们就创建好了一个context,而且初始值为name:'sun',age:11,创建好其他组件怎么用呢?
javascript
import React from 'react'
import TestContext from '../store/testContext'
/*
使用方式1
1.引入context
2.使用xxx.Consumer组件创建元素
Consumer的标签体需要一个回调函数
它会讲context设置为回调函数的参数,通过参数就可以访问到这些数据
*/
export default function A() {
return (
// <div style={{ fontSize: '0.12rem', backgroundColor: '#fdd' }}>
// </div>
<TestContext.Consumer >
{
(ctx) => {
return (
<div style={{ fontSize: '0.12rem', backgroundColor: '#fdd' }}>{ctx.name}---{ctx.age}</div>
)
}
}
</TestContext.Consumer>
)
}
我们要用肯定要引入这个文件,然后我们用xxx.Consumer这个组件创建元素,消费者然后里面是一个回调函数,ctx就是默认我们初始化的时候Context里面存的数据,然后就可以直接使用了,但这样很明显太麻烦了,我们可以用钩子函数去把Context数据勾出来。
javascript
import React from 'react'
import TestContext from '../store/testContext'
/*
使用方式2
1,导入Context
2.使用钩子函数useContext()获取到context
3.需要一个Context作为参数
会讲Context中的数据作为返回值返回
*/
export default function B() {
//使用钩子函数获取
const ctx = React.useContext(TestContext)
return (
<div style={{ fontSize: '0.14rem' }}>
{ctx.name}---{ctx.age}
</div>
)
}
只有函数组件可以这样用,类组件不可以这种方式。我们React.useContext钩子然后把引入的盒子放进去作为参数。钩子函数把数据勾出来赋值给ctx,然后就可以直接拿来用了,非常简单。
这就是Context的使用方法,现在我们用到项目里面。
javascript
import React from 'react'
const carContext = React.createContext({
items: [],
totalAmount: 0,
totalPrice: 0,
addItem: () => { },
removeItem: () => { }
})
export default carContext
首先初始化,其实可以不初始化,只不过这样我们知道这个盒子里面的数据有什么,以及什么类型,更好维护,那么我们希望所有根组件的内层组件都可以直接用根组件里面的数据和方法,我们就需要用Provider这个组件了。
xml
<carContext.Provider value={{ ...car, addItem, removeItem }}>
<div>
<FilterMeals filter={filterHandle} />
<Meals hbb={hbb} addCar={addItem} delCar={removeItem} />
<Cart />
</div>
</carContext.Provider>
这样我们就把购物车数据,以及函数方法放在盒子里面,所有的内层组件都可以通过钩子函数之间勾出来使用了。不需要一层层传递了。
javascript
import React from 'react'
import classes from './Cart.module.css'
import iconImg from '../../asset/bag.png'
import carContext from '../../store/car-context'
export default function Cart() {
const ctx = React.useContext(carContext)
return (
<div>
<div className={classes.Cart}>
<div className={classes.Icon}>
<img src={iconImg} alt="" />
<span className={classes.TotalAmount}> {ctx.totalAmount}</span>
</div>
<p className={classes.Price}>{ctx.totalPrice}</p>
<button className={classes.button}>去结算</button>
</div>
</div>
)
}
这样Context就完成了他的使命,项目变得更简洁好维护了。
2.搜索框组件
列表相关的完成了,那么就该搜索组件了。
编辑
这里逻辑比较简单,我们只需要在输入框中的value监听到变化然后想办法传给根组件,根组件去数组里面筛选就好了。
首先先写筛选逻辑。
javascript
const filterHandle = (keyword) => {
const newHbb = originHbb.filter((item) => { return (item.title.includes(keyword)) })
// includes() 是否包含里面的字符串返回值truefalse和index Of()返回值 true 或 false 第一个匹配项的索引,找不到返回 -1
setHbb(newHbb)
}
这里我们通过传过来的value值然后筛选,字符串里面是否包含value筛选,然后渲染更新新的列表。也可以用indexOf()去通过遍历数组索引去找,找不到返回-1,这样indexOf!==-1去筛选,然后更新筛选后的数组就可以了。
vbscript
const inputChange = (event) => {
const keyword = event.target.value.trim()
// trim()去掉字符串中的空格trim() 只会去掉字符串开头和结尾的空格;
props.filter(keyword)
}
至于html样式就不说了,只是position flxed固定定位到顶层,这里trim()去掉我们输入的开头结尾空格。这样搜索框就完成了功能。
3.购物车组件
还记得我们之前在列表里面就以及写了购物车展示的列表,以及总数总价格吗?不记得可以去上一篇看一看,有了那些数据,我们只需要去钩子函数Context里面的数据勾出来就好了。
所以我们先写好展示组件,这个也是position flxed固定定位
javascript
import React from 'react'
import classes from './Cart.module.css'
import iconImg from '../../asset/bag.png'
import carContext from '../../store/car-context'
export default function Cart() {
const ctx = React.useContext(carContext)
return (
<div>
<div className={classes.Cart}>
<div className={classes.Icon}>
<img src={iconImg} alt="" />
<span className={classes.TotalAmount}> {ctx.totalAmount}</span>
</div>
<p className={classes.Price}>{ctx.totalPrice}</p>
<button className={classes.button}>去结算</button>
</div>
</div>
)
}
这样我们的购物车样式就实现了,但是功能还是没有添加。现在来添加功能。
编辑
首先我们看到当我们打开购物车的时候有一个遮盖层。我们直接套公式。
编辑
首先单独搞一个drop节点,因为这样可以跳脱三界之外,不会和根节点里面的样式起冲突,因为当前的根节点我们不能最上面添加一层遮罩层,会导致布局混乱,因为根节点里面的组件也会有各种各样的布局。而我们遮盖层需要最上方,不被其他定位干扰,不被父组件的overflow:hidden影响,所以我们跳脱三界之外 。
编辑
然后就渲染到我们设置的drop节点,然后相当于一个壳子,我们的购物车详情就在这个壳子上面作为纹身。。。当然要把纹身样式也传过来。
现在我们完成了这个外壳,那么就需要写纹身,也就是购物车详情了,这很简单,毕竟数据只需要展示就可以了。
编辑
首先肯定是购物车子组件,然后写内容。
javascript
import React from 'react'
import Drop from '../../UI/Drop/Drop'
import classes from './CartDetails.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash'
import Meal from '../../Meal/index'
import carContext from '../../../store/car-context'
export default function CartDetails() {
const ctx = React.useContext(carContext)
return (
<Drop>
<div className={classes.CartDetails}>
<header className={classes.Header}>
<h2 className={classes.Details}>餐品详情</h2>
<div className={classes.Clean}>
<FontAwesomeIcon icon={faTrash} />
<span>清空购物车</span>
</div>
</header>
<div className={classes.MealList}>
{
ctx.items.map((item) => {
return (<Meal noDesc key={item.id} meal={item} />)
})
}
</div>
</div>
</Drop>
)
}
父组件是Drop然后里面就是纹身,也就是我们的购物车列表组件,从钩子勾出数据展示,没什么好说的,只是有一点是我们购物车不需要展示汉堡详情,所以传过去一个noDesc,然后我们的Meal组件多一个判断。
javascript
{props.noDesc ? null : <p className={classes.Desc}>{props.meal.desc}</p>}
有就不展示详情,这里就实现了这个购物车详情,只不过我们只是写好了组件,并没有可以通过按钮去控制展示。我们现在去添加事件控制展示隐藏。
javascript
import React from 'react'
import classes from './Cart.module.css'
import iconImg from '../../asset/bag.png'
import carContext from '../../store/car-context'
import CartDetails from './CartDetails/CartDetails'
import CheckOut from './CheckOut/CheckOut'
export default function Cart() {
//添加一个state来设置详情是否显示
const [show, setShow] = React.useState(false)
//添加一个显示函数
const showHandle = () => {
if (ctx.totalAmount === 0) {
setShow(false)
return
}
setShow((preState) => { return (!preState) })
}
//添加一个state控制结账页的显示隐藏
const [showCheck, setShowCheck] = React.useState(false)
const showCheckHandle = () => {
if (ctx.totalAmount === 0) return
setShowCheck(true)
}
const hideCheck = () => {
setShowCheck(false)
}
const ctx = React.useContext(carContext)
return (
<div className={classes.Cart} onClick={showHandle} >
{showCheck && <CheckOut hideCheck={hideCheck} />}
{show && <CartDetails />}
<div className={classes.Icon}>
<img src={iconImg} alt="" />
{ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}> {ctx.totalAmount}</span>}
</div>
{ctx.totalPrice === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}
<button onClick={showCheckHandle} className={${classes.button} ${ctx.totalAmount === 0 ? classes.Disable : ''}}>去结算</button>
</div>
)
}
现在我们添加的显示详情以及结账页的显示隐藏,依旧设置state去控制,然后添加点击事件,点击购物车组件显示详情,点击按钮显示支付组件。这里我们先去写详情页面。首先详情页面有一个模态框,也就是点击情况购物车需要弹出一个提示框。
javascript
import React from 'react'
import Drop from '../../UI/Drop/Drop'
import classes from './CartDetails.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash'
import Meal from '../../Meal/index'
import carContext from '../../../store/car-context'
import Confirm from '../../UI/Confirm/Confirm'
export default function CartDetails() {
const ctx = React.useContext(carContext)
//设置state控制确认框的显示
const [show, setShow] = React.useState(false)
//添加函数显示确认窗口
const showConfirm = () => {
setShow(true)
}
const cancelHandle = (e) => {
e.stopPropagation()
setShow(false)
}
const okHandle = () => {
ctx.clearCart()
}
return (
<Drop onClick={cancelHandle}>
{show && <Confirm confirmText={'确认清空购物车吗'} onCancel={cancelHandle} Ok={okHandle} />}
<div className={classes.CartDetails} onClick={e => { e.stopPropagation() }}>
<header className={classes.Header}>
<h2 className={classes.Details} >餐品详情</h2>
<div className={classes.Clean} onClick={showConfirm}>
<FontAwesomeIcon icon={faTrash} />
<span>清空购物车</span>
</div>
</header>
<div className={classes.MealList}>
{
ctx.items.map((item) => {
return (<Meal noDesc key={item.id} meal={item} />)
})
}
</div>
</div>
</Drop>
)
}
javascript
import React from 'react'
import Drop from '../Drop/Drop'
import classes from './Confirm.module.css'
export default function Confirm(props) {
return (
<Drop className={classes.ConfirmOuter}>
<div className={classes.Confirm}>
<p className={classes.ConfirmText}>{props.confirmText}</p>
<div className={classes.Box}>
<button onClick={(e) => { props.onCancel(e) }} className={classes.Cancel}>取消</button>
<button onClick={(e) => { props.Ok(e) }} className={classes.Ok}>确认</button>
</div>
</div>
</Drop>
)
}
这里是我们的详情组件和里面的模态框组件,我们只是通过点击事件操作状态去动态的展示以及隐藏。其实并没有什么复杂的逻辑了。
现在我们写好了这个详情组件我们就需要去写支付组件,首先支付页面是占据一整个屏幕。所以和root根节点是兄弟关系。我们也需要
javascript
import React from 'react'
import ReactDOM from 'react-dom'
import classes from './CheckOut.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faXmark } from '@fortawesome/free-solid-svg-icons'
const checkroot = document.getElementById('checkout')
export default function CheckOut(props) {
const handle = () => {
props.hideCheck()
}
return (
ReactDOM.createPortal(
<div className={${classes.CheckOut}}>
<div className={classes.Close} >
<FontAwesomeIcon icon={faXmark} onClick={handle} />
</div>
</div>
, checkroot)
)
}
注意我们需要给所有的点击事件发生的父组件或者子组件添加禁止冒泡。防止出现bug,一个点击事件导致其他点击事件发生,还有一个注意点是当我们清空购物车的时候,我们设置清空函数,也就是更新购物车这个数据的时候,我们需要把amount去覆盖掉全部为空,不然最外层的数据不会改变。
4.支付组件
最后的组件也就是支付组件我们要开始去书写了,其实就是一个列表而已。展示数据。
javascript
import React from 'react'
import ReactDOM from 'react-dom'
import classes from './CheckOut.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faXmark } from '@fortawesome/free-solid-svg-icons'
import CheckItem from './CheckItem/CheckItem'
import Bar from './Bar/Bar'
import carContext from '../../../store/car-context'
const checkroot = document.getElementById('checkout')
export default function CheckOut(props) {
const ctx = React.useContext(carContext)
const handle = () => {
props.hideCheck()
}
return (
ReactDOM.createPortal(
<div className={${classes.CheckOut}}>
<div className={classes.Close} >
<FontAwesomeIcon icon={faXmark} onClick={handle} />
</div>
<div className={classes.Box}>
<header className={classes.Header}>
<h2 className={classes.Title}>餐品详情</h2>
</header>
<div className={classes.Meals}>
{
ctx.items.map((item) => {
return (<CheckItem key={item.id} meal={item} />)
})
}
</div>
<footer className={classes.Footer}>
<p className={classes.Price}>89</p>
</footer>
</div>
<Bar TotalPrice={ctx.totalPrice} />
</div>
, checkroot)
)
}
这就是整个支付页面,我们在Context存好了我们的购物车详情数据,那么我们就只需要去展示在这里就好了,因为展示的内容不同,所有还需要细分组件,比如这里我们还需要一个底部以及列表分三部分。
编辑
首先这个页面就是一个组件,然后子组件就是我们的列表,以及去支付这个Bar组件。
编辑
我们传递参数,然后展示就可以了。
javascript
import React from 'react'
import classes from './Bar.module.css'
export default function Bar(props) {
return (
<div className={classes.Bar}>
<div className={classes.TotalPrice}>{props.TotalPrice}</div>
<div className={classes.Box}><button className={classes.Button}>去支付</button></div>
</div>
)
}
import React from 'react'
import classes from './CheckItem.module.css'
import Counter from '../../../UI/Counter/Counter'
export default function CheckItem(props) {
return (
<div className={classes.CheckItem}>
<div className={classes.ImgBox}>
<img src={props.meal.img} alt="" />
</div>
<div className={classes.Desc}>
<h2 className={classes.Title}>{props.meal.title}</h2>
<div className={classes.PriceOuter}>
<Counter meal={props.meal} />
<div className={classes.Price}>{props.meal.price * props.meal.amount}</div>
</div>
</div>
</div>
)
}
我们在展示之前一定要根据设计图想好要展示的动态数据,这样传参的时候更加清晰。
编辑
到这里就完成了整个移动端app的案例,总结唯一的js逻辑就是最开始拿到数据之后,进行对原数据数组对象的操作。比如我们购物车需要一个新的数组对象包含我们添加的对象。那么我们就需要去通过push去推,而且注意的是对象操作前先定义一个常量,这样我们筛选filter,不会影响原数组。每次保证都在全部数据中筛选,然后操作。
其他的就是样式以及组件间的数据传输,Context以及props。展示就可以了。