Oh mygod! 买它 买它!React+Redux实现简易版购物车和多功能搜索组件

1,712 阅读7分钟

前言

前端时间,笔者在掘金上写了一篇文章了,适合react新手入门,希望真的有帮助到一部分人哦,这段时间学习了神三元大佬的项目,我把redux加入到项目中,所有的数据请求和状态都放在redux里,顺便继续完善了一下项目,期间了碰到了很多问题,redux确实比较难学,同时开始学,我男朋友就比我的学的通透哈~,我就姗姗来迟,但咱不能输了气势!

项目介绍 ( 貌美如花+赚钱养家)

上篇文章就说了这个项目是仿猫眼电影小程序,我写了两个页面,首页和赛事页面,首页做一个简易的购物车,进入电影详情页,还有一个留言板,赛事页面主打搜索功能,可按内容,地点,类型搜索。总而言之就是首页负责貌美如花,赛事负责赚钱养家~

1. 项目展示

项目在线预览 Vite App (gchhcg.github.io)

首页

SC-1658407834626.gif

赛事页面

SC-1658412241709.gif

2. 工具库

  • antd : antd 是基于 Ant Design 设计体系的 React UI 组件库
  • axiso:axios是一个用于发送Ajax请求的http库,本质上时对Ajax的封装,而且支持Promise操作,让我们无需再 使用传统的callback方式进行异步编程
  • style-components :styled-components 是一个常用的 css in js 类库。和所有同类型的类库一样,通过 js 赋能解决了原生 css 所不具备的能力,比如变量、循环、函数等
  • font-awesome —— 图标字体库
  • CSSTransition:过渡动画效果
  • redux 全家桶 —— 不用我说了哈
  • prop-types : 严格控制父子组件传值的类型合理性
  • react-lazyload :懒加载 优化用户第一次进站体验
  • classnames —— 动态添加类名,实现可操控的样式方法
  • memo: React自带 页面渲染性能优化 让你的网页选择性渲染需要更渲染资源的组件

3. 项目 src文件夹

image.png

3.1 配置文件 vite.config.js



import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  base: './',
  resolve: {
    alias: {
      "@":path.resolve(__dirname,'src')
    }
  }
})

详细介绍

1. 首页

  • 背景色渐变,颜色随着轮播图和 tab 切换

chrome-capture-2022-6-21.gif

2. 配置全局主题样式文件

  • golbal-style.js
// 全局风格定义 是最重要的
// 老板 + 设计师  风格是样式的灵魂 
export default {
    "theme-color": "rgb(255,0,0)",
    "theme-color-s":'#5db3d7',
    "theme-color-m":'#d6c193',
    "theme-color-l":''
}

3. 轮播图

chrome-capture-2022-6-21 (1).gif

export default function Banners() {
    let swiper = null;
    useEffect(() => {
        // swiper 不能多次实例化 
        if (swiper) {
            return 
        }
        new Swiper('.btn-banners', {
            loop: true,
            autoplay: {
                delay: 3000
            },
            pagination: {
                el: '.swiper-pagination'
            }
        })
    }, [])
    
<BannersWrapper>
     <div className="btn-banners swiper-container">
         <div className="swiper-wrapper">
             <div className="swiper-slide">
                 <p>
                     <img width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/059513e0f8e54f5288c961b1017a68ee~tplv-k3u1fbpfcp-zoom-1.image" /> 
                 </p>
             </div>
             <div className="swiper-slide">
                 <p>
                     <img width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f9af6f327ed9412e93a25171148235de~tplv-k3u1fbpfcp-zoom-1.image" />
                 </p>
             </div>
             <div className="swiper-slide">
                 <p>
                     <img width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/93b3b74dec1a47babd8c49da14151834~tplv-k3u1fbpfcp-zoom-1.image" /> 
                 </p>
             </div>
         </div>
         <div className="swiper-pagination"></div>
     </div>
 </BannersWrapper> 

1. 首页界面

SC-1658415427021.gif

2. 电影详情页

  • 头部导航栏固定,position:fiexd

chrome-capture-2022-6-21 (4).gif

  • 引入 antd 组件 <PageHeader/>
  <PageHeader
        className="site-page-header"
        onBack={() => setShow(false)}
        subTitle="详情"
            />
  • 点击“ 想看” “已看” 互不影响 阻止默认事件

chrome-capture-2022-6-21 (2).gif

 const [visited1, setVisited1] = useState(false)
 const [visited2, setVisited2] = useState(false)
   const clickdown1 = (e) =>{
        e.preventDefault();
        e.stopPropagation();
        setVisited1(!visited1);
    }
    const clickdown2 = (e) =>{
        e.preventDefault();
        e.stopPropagation();
        setVisited2(!visited2)
    }
    useEffect(()=> {
        setShow(true)
        window.scrollTo(0,0)
    },[])

3. 演员详情页

chrome-capture-2022-6-21 (3).gif

4. 滑动轮播图 + 评论

SC-1658417286762.gif

5. 购票清单

SC-1658417838848.gif

赛事界面

在上篇文章,我已经介绍过了,没有做任何界面上的变化,想看的小伙伴,附上链接哦!

juejin.cn/post/711413…

Redux 来了 !!!!

Redux 是什么

  • Redux 是 JavaScript 状态容器,提供可预测化的状态管理。 (如果你需要一个 WordPress 框架,请查看 Redux Framework。)可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供 超爽的开发体验

Redux 原理

  • redux原理是将整个应用状态存储到一个地方上称为store,里面保存着一个状态树store tree,组件可以派发(dispatch)行为(action)给store,而不是直接通知其他组件,组件内部通过订阅store中的状态state来刷新自己的视图。state是只读的,唯一改变state的方法就是触发action。

创建仓库

分仓库

  1. 数据管理和组件,在有了 redux 后,变成了平级关系 /store /page
  2. 模块化数据管理,每个模块 reducer+action 下放到页面级路由模块中,方便管理
  3. 每个模块都提供 index.js , 方便统一管理 store, 所有的 reducer,action,constans 都一起 export,作为清单文件

主仓库

  1. 用于统一管理各个分仓数据,并给根组件提供Provider功能的store,和state树根

  • index.js
 import {combineReducers} from 'redux'
 //引入为Count组件服务的reducer
 import Events from '@/pages/Events/store/reducer'
 //引入为Person组件服务的reducer
 import  Home from '@/pages/Home/store/reducer'
 
 //汇总所有的reducer变为一个总的reducer
 export default  combineReducers({
 Events,
 Home
 })

  • store.js
import { createStore, compose, applyMiddleware } from 'redux';
// 组件  中间件redux-thunk   数据
import thunk from 'redux-thunk' // 异步数据管理
import reducer from './index'

const composeEnhancers = 
 window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, 
 // 组合函数
 // devtool
 composeEnhancers(
     // 异步 
     applyMiddleware(thunk)
 )    
)

export default store;
  • main.js
ReactDOM.createRoot(document.getElementById('root')).render(
    <Provider store={store}>
    <HashRouter>
    <App />
    </HashRouter>
    </Provider>
   
)

分仓库

home目录下

image.png

home 下store 有一个简易版的购物车

  • 购物车redux设计思路
  1. redux 大点项目 数据管理 财务管理 数 据统计不能出错 算账技巧
  2. redux 核心使命是 数据管好
    1. 计算正确 初始状态 + reducer 重新运算 和页面状态正确对应 MVVM
    2. 所有的状态 留下来 不用被引用式赋值影响 便于 react-dev-tools logger redux 的状态迁移可以被追溯的
      • { ...state, stataA:1 }
      • Object.assign({} ,state ,{list:[...list]})
      • ImmutableJS 优化
  • reducer 设计完成 store 就基本完成了 财务数据管理

    1. 提供商品的默认值 default 商品 添加到购物车 check:true
    2. 选中 不选 case CHECK_GOODS goodId 不选 =》 选中 goodsNum = 1
    3. CHANGE_GOODS_NUM goodId status 不好分析
      • 0 check false
    4. CHANGE_ALL_GOODS
  • actionCreactors.js (负责统一管理数据状态改变的函数执行,给reducer分配相应的action:状态类型,数据)

import * as actionTypes from './constans'
import {getactors,getmoviesRequest} from '@/api/request'

export const changeactorslist = (data) => ({
    type:actionTypes.CHANGE_ACTORS,
    data
})
export const getactorslist = () => {
    return (dispatch) => {
            getactors()
            .then(item => {
                // console.log(item.data, '////')
                const action = changeactorslist(item.data);
                dispatch(action)
            })
    }
}
export const changemovienum = (data) => ({
    type: actionTypes.CHNAGE_MOVIES_NUM,
    data: data
})
export const checkmovie = (id) => ({
    type: actionTypes.CHECK_MOVIE,
    data: id
})
export const changemovie = (data) => ({
    type:actionTypes.CHANGE_MOVIE_LIST,
    data
})
export const getmovies = () => {
    return (dispatch) => {
        // console.log('12')
            getmoviesRequest()
            .then(item => {
                // console.log(item.data, '////')
                const action = changemovie(item.data);
                dispatch(action)
            })
    }
}

  • constants.js 常量

export const CHANGE_ACTORS = 'CHANGE_ACTORS'
export const CHANGE_MOVIE_LIST = 'CHANGE_MOVIE_LIST'
export const CHECK_MOVIE = 'CHECK_MOVIE'
export const CHNAGE_MOVIES_NUM = 'CHNAGE_MOVIES_NUM'

  • reducer.js (负责根据action值,做相应操作,以实现数据流管理)
import * as actionTypes from './constans'
const defaultState = {
    movieslist:[],
    actorslist:[]
}
export default (state=defaultState,action) => {
     switch (action.type) {
        case actionTypes.CHANGE_MOVIE_LIST:
            return {
               ...state,
               movieslist:action.data
            }
        case actionTypes.CHECK_MOVIE:
            // 在reducer 重新计算前的状态 ? 旧状态
            let checkList = state.movieslist; 
            checkList.map(item => {
                if (item.id == action.data) {
                    item.check = !item.check
                    console.log(item.check)
                    // 0  1  - 
                    item.count == '0' ? item.count = '1' : ''
                }
            })
            // 新状态 
            return  Object.assign({}, state, {
                movieslist: [...checkList]
            })
            break;
        case actionTypes.CHNAGE_MOVIES_NUM:
            let changeList = state.movieslist;
            //  + -  指定商品  action type CHNAGE_GOODS_NUM 
            //  data: {id:id, status:'add|minus' }
            changeList.map((item) => {
                if (item.id == action.data.id ) {
                    action.data.status == 'add'? item.count++: item.count--;
                    item.count == '0' ? item.check = false : ''
                    // -1 UI 去做 item.goodsNum> 0 && <button>-</button>
                }
            })
            return Object.assign({}, state, {movieslist: [...changeList]})
            break;
        case actionTypes.CHANGE_ACTORS:
            return {
                ...state,
                actorslist:action.data
            }
        default:
			return state
    }
}

  • events 目录下

image.png

events 下的store 难度比较大是,把搜索,定位城市和激活状态的tab作为对象参数全部传给action,在action里面过滤,得到符合条件的赛事列表。

  • 工具库函数 fetchTodos
export { debounce };

export const fetchTodos = params => {
    // console.log(params)
    let { query, tab, cityName, result} = params;
    // console.log(query, tab);
    console.log(result,'..')
    
    // result 存放获取赛事信息的数据
    if(tab) {
            switch(tab) {
                case "全部":
                    result = result.filter(todo => todo.pos.includes(cityName))
                    // 数组filter方法,过滤,筛选得到符合条件的信息
                    break;
                case "电竞赛事":
                    result = result.filter(todo => todo.type == '电竞赛事' && todo.pos.includes(cityName))
                    break;
                case "体育赛事":
                    result = result.filter(todo => todo.type == '体育赛事' && todo.pos.includes(cityName))

                default:
                    break;
            }
    }
    if(query) {
        result = result.filter(todo => todo.text.includes(query)||todo.pos.includes(query))
    }
    // Promise 类 resolve 静态方法
    // Promise.all 返回一个fullfiled 的 promise 实例
    return result
}
  • 工具库函数 防抖
const debounce = (func, delay) => {
    let timer;
    return function (...args) {
        if(timer) {
        clearTimeout(timer);
        }
        timer = setTimeout(() => {
        func.apply(this, args);
        clearTimeout(timer);
        }, delay);
    };
};
export { debounce };
  • actionCreators.js
import { 
    getCities,
    getEvent,
} from '@/api/request'
import {fetchTodos} from '@/utils/utils'
import * as actionTypes from './constants'

export const changecitiesList = (data) => ({
    type: actionTypes.CHANGECITIES_LIST,
    data: data
})
export const changeEventsList = (data) => ({
    type:actionTypes.CHANGE_EVENTS_LIST,
    data: data
})
// api请求 一定放在action中 
export const getcitieslist = () => {
    return (dispatch) => {
        console.log('|||||||||||||||')
            getCities()
            .then(item => {
                // console.log(item.data, '////')
                const action = changecitiesList(item.data);
                dispatch(action)
            })
    }
}
export const getEventslist = (data) => {
    return (dispatch) => {
            getEvent()
            .then(item => {
                const result = [...item.data]
                //console.log(result,'猪')
                let a = fetchTodos({...data, result})
                // const action = changeEventsList(item.data);
                dispatch(changeEventsList(a))
            })
    }
}
  • constants.js
export const CHANGECITIES_LIST ='CHANGECITIES_LIST'
export const CHANGE_EVENTS_LIST = 'CHANGE_EVENTS_LIST'


  • reducer.js
import * as actionTypes from './constants'

const defaultState = {
    citieslist: [],
    eventslist: [],
}
export default (state = defaultState, action) => {
    switch(action.type) {
        case actionTypes.CHANGECITIES_LIST: 
            return {
                ...state,
                citieslist: action.data
            }
        case actionTypes.CHANGE_EVENTS_LIST: 
            // console.log('----------')
            return {
                ...state,
                eventslist: action.data,
            }  
            
        default:
            return state
    }
}

来看看我的项目初始状态

image.png

让你看看这个项目的store tree,是不是很清晰呢!

image.png

项目优化

  • memo

    • import { memo } from 'react'
    • export default memo(xxxx)
    • 就可以实现减少渲染重复未变数据
  • Lazy 图片占位 懒加载

  <LazyLoad 
      placeholder={
            <img width="100%" height="100%"
                src={waitImg}
             />}>
            <img src = {url}/>
    </LazyLoad>
  • 路由懒加载

    • import { lazy, Suspense } from "react"
    • const XXX = lazy(() => import('@/pages/XXX'))
    • 类似于下方 👇
const Cities = lazy(() => import('@/pages/Cities'))
const Events = lazy(() => import('@/pages/Events'))
const Eventdetail = lazy(() => import('@/pages/Events/Eventdetail'))
const Movie = lazy(() => import('@/pages/Movie'))
const Yanchu = lazy(() => import('@/pages/Yanchu'))
const Mine = lazy(() => import('@/pages/Mine'))

到这里讲解就结束了,小伙伴想看更多的👇

项目在线预览 Vite App (gchhcg.github.io)

源码地址 cat-movie: 仿猫眼电影搜索组件react实战第一个项目 仿猫眼电影搜索组件react实战第一个项目 (gitee.com)

  • 有问题和可优化点,欢迎大佬评论区讨论指正
  • 求 点赞 收藏 评论指正!  不要挥一挥衣袖不带走一片云彩哦,留下你们的赞哦 ~爱你们哦 ~  !!!
  • 持续更新,持续学习,希望对你所有帮助~