轻扫整本小红书--(React 挂钩 + Redux 入门级上手项目)
前言
通过一段时间对React的学习,对React Hooks以及Rudex的React全家桶的学习,检验自己学习成果的时候到了,正所谓是骡子是马,拉出来溜溜就知道了。那不如就用小红书出来练练手吧,在一个小项目上对自己学的东西做个总结,正所谓实践出真理。
项目简介
- react全家桶:react+react-router+redux
- redux-thunk:处理异步逻辑的redux中间件
- styled-components:css in js 的工程化工具
- axios:用来请求后端api数据
- react-lazyload:react 懒加载库
- better-scroll:提升移动端滑动体验
- 坚持前端MVVM的设计理念,遵循组件化、模块化的编程思想
直接上成果吧
项目结构
react-redBook/
node_modules/
src/
api/ //网络请求代码和相关配置
assets/ //静态文件
components/ //可复用的UI组件
pages/ //页面
routes/ //路由配置文件
store/ //redux 相关文件
utils/ //工具类函数
App.jsx //根组件
main.jsx //入口文件
style.js //默认样式
index.html
package.json
readme.md
vite.config.js
前端的总体设计
路由配置
本项目使用react-router对路由进行配置
- router/index.js代码如下
import { lazy,Suspense } from 'react'
import { Routes,Route,Navigate } from 'react-router-dom'
const Detail=lazy(()=>import('../components/Detail'))
import Footer from '../components/Footer'
import Home from '../pages/Home'
const ShopCart=lazy(()=>import('../components/ShopCart'))
const Add=lazy(()=>import('../pages/Add'))
const Mine=lazy(()=>import('../pages/Mine'))
const Order=lazy(()=>import('../pages/Order'))
const Shop=lazy(()=>import('../pages/Shop'))
const RouterConfig=()=>{
return(
<Suspense fallback={null}>
<Routes>
<Route path='/' element={<Navigate to='/home' replace={true}/>}></Route>
<Route path="/home" element={<Home/>}/>
<Route path="/shop" element={<Shop/>}/>
<Route path="/add" element={<Add/>}/>
<Route path="/mine" element={<Mine/>}/>
<Route path="/order" element={<Order/>}/>
<Route path="/detail" element={<Detail/>}/>
<Route path="/shopcart" element={<ShopCart/>}/>
</Routes>
<Footer/>
</Suspense>
)
}
export default RouterConfig
这里使用了 React 提供的 Suspense
和 Lazy
实现了动态路由
这里讲一下为啥用动态路由:
按需加载的作用:主要可以减少首页请求的文件的大小。当我们做的项目够大时,一个首当其冲的问题就是所需加载的 JavaScript 的大小。程序应当只加载当前渲染页所需的 JavaScript。在用户浏览过程中按需加载,这样可以提高首屏加载效率
数据流管理Redux
路由已经配置好了,接下来该是页面组件的编写了,但在页面编写之前,我们应该进行数据流的管理,这样才能使页面组件注入灵魂。
在这里对redux的工作原理就不多说了,建议看技术胖的redux(注意跟上车速)。在本项目中我把reducer进行了拆分,每个页面都有自己独立管理的state的reducer函数,然后合并为一个reducer。
- store/reducer.js代码如下
import { combineReducers } from "redux";
import { reducer as FoundReducer } from '../pages/Home/Found/store'
import { reducer as ShopinfoReducer } from "../pages/Shop/store";
import { reducer as CardListReducer } from "../pages/Home/City/store";
import { reducer as SearchReducer } from '../components/Search/store';
import { reducer as ShopcartReducer } from '../components/ShopCart/store'
import { reducer as CartstoreReducer } from '../components/cartstore/store'
export default combineReducers({
Found: FoundReducer,
Shop: ShopinfoReducer,
Card: CardListReducer,
Search: SearchReducer,
Shopcart: ShopcartReducer,
Cartstore: CartstoreReducer
})
然后在store中创建一个reducer仓库进行数据的派发已经对数据操作,每当我们在store上dispatch一个action,store上的数据就会进行发生改变,并进行数据的重新派发。
- store/index.js
import { applyMiddleware, createStore, compose } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))
// 比如在Dispatch一个Action之后,到达reducer之前,进行一些额外的操作,就需要用到middleware(中间件)
export default store
redux-thunk
可以实现redux
处理异步action
。applyMiddleware
就是增强了原始createStore
返回的dispatch
的功能。
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
;作用是让Redux DevTools生效,方便开发人员查看数据流。
总的store创建好了,接下来以购物车页面的store为例进行讲解使用方法。
结构为:
shopcart/
store/
actionCreators.js
constants.js
index.js
reducer.js
- shopcart/store/reducer.js
该文件的创建用来为shopcart这个组件进行服务,reducer
会接收到两个参数,一个为之前的状态(state
),另一个为动作对象(action
)
import * as actionTypes from './constants'
const defaultState = {
shopCart: []
}
export default (state = defaultState, action) => {
switch (action.type) {
case actionTypes.CHANGE_CARTSTORE:
return {
...state,
shopCart: [...state.shopCart, action.data]
}
case actionTypes.CHANGE_ISSHOW:
return {
...state,
shopCart: [...action.data]
}
default:
return state
}
}
- shopcart/store/actionCreators.js
当界面调用这个文件导出的接口时,dispatch
就会修改对应的数据,对页面进行输出展示
import * as actionTypes from './constants'
const changecartstore = (data) => ({
type: actionTypes.CHANGE_CARTSTORE,
data
})
const changeisshow = (data) => ({
type: actionTypes.CHANGE_ISSHOW,
data
})
export const getcartstoreList = (data) => {
return (dispatch) => {
dispatch(changecartstore(data))
}
}
export const getisshowList = (data) => {
return (dispatch) => {
dispatch(changeisshow(data))
}
}
Redux仓库创建好后,要使数据流向我们的界面,就需要用到一个工具了,这时候react-redux
就出来了。
react-redux
提供了两个重要的对象,Provider
和connect
。Provider
使 store通过props传递给子组件,不管层级、(相对于redux,省去了store.subscribe
)。connect
方法 通过mapStateToProps
获取store的对应值,通过mapDispatchToProps
,改写store的值
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
)
然后在Provider
包裹的组件中使用connect
,这样才能使数据流入到页面。
const mapStateToProps=(state)=>{
return{
show:state.Shopcart.isShow,
shopCart:state.Cartstore.shopCart,
quanselect:state.Shopcart.quanselect
}
}
const mapDispatchToProps=(dispatch)=>{
return{
getisshowDispatch(data){
dispatch(getisshowList(data))
},
getquanselectDispatch(data){
dispatch(getquanselect(data))
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(React.memo(GouCart))
到这。我们的数据就算是流通了,也就可以在页面操作数据了,数据拿到了,就开始写页面吧。
页面编写
商品详情页面开发
当在项目首页点击需要查看的商品时,进行路由跳转,跳转到对应商品的页面,那是怎么拿到对应数据的呢?在这里我采用通过路由传参的方式,把一个商品独一无二的id通过路由传过去,在进行接口的请求。把id相等的数据过滤出来进行页面的渲染。
主要的部分代码
const {shopnum,detail}=props
const [detaill,setDetaill]=useState([])
const [visible,setVisible]=useState(false)
const [search] = useSearchParams()
const detailid= search.get('id') || ''
useEffect(()=>{
let detaildataa= detail.filter((item)=>
detailid == item.id
)
setDetaill(detaildataa)
},[detail])
在这里有一个底部选择商品弹出层,点击加入购物车,弹出层弹出,选择商品的类型,点击确认,购物车就有相应的数据,当购物车没有商品时,购物车的数字不显示,有数据底部的购物车的数字发生相应的变化。
主要实现代码
const addCart=(dataa)=> {
let data=dataa[0]
if(cartstore.every(item=>item.id!==data.id)){
let list={
id:data.id,
images:data.img[0],
num:1,
price:data.price,
select:false,
title:data.title,
store:data.store,
quanselect:false
}
getaddstoreDispatch(list)
}
successToast()
}
购物车页面
选择后的商品进入到购物车中,购物车可以勾选是否购买,选择购买的商品可以进行数量的增加,底部价格自动变化。还能对商品进行删除操作,勾选需要删除的商品,点击删除。
购物车的部分操作函数
const removecart=()=>{
success()
shopCart.forEach((a,index)=>{
if(a.id==item.id){
shopCart.splice(index,1)
}
})
getisshowDispatch(shopCart)
}
const reduceNum=()=>{
shopCart.map((a,index)=>{
if(a.id==item.id){
if(a.num==1){
a.num==1
}else{
a.num--
}
}
})
getisshowDispatch(shopCart)
}
const addNum=()=>{
shopCart.map((a,index)=>{
if(a.id==item.id){
a.num++
}
})
getisshowDispatch(shopCart)
}
const isSelect=()=>{
shopCart.forEach(a=>{
if(a.id==item.id){
a.select=!a.select
}
})
getisshowDispatch(shopCart)
if(shopCart.every(a=>a.select==true)){
getquanselectDispatch(true)
}else{
getquanselectDispatch(false)
}
}
const handleSelect=()=>{
getquanselectDispatch(!quanselect)
if(quanselect){
shopCart.map(a=>{
a.select=false
})
getisshowDispatch(shopCart)
}else{
shopCart.map(a=>{
a.select=true
})
getisshowDispatch(shopCart)
}
}
主要是先在页面组件进行判断找到需要操作的数据,然后dispatch
一个action
对整个购物车数据进行更新,然后页面就可以获得新的数据进行页面的渲染。
短视频数据查看页面
这个页面的下拉刷新以及加载状态,安利到了一波神三元大佬的scoll以及loading组件,超级好用,可以看看他写的掘金小册(真的细节)
import React, { useEffect ,useState} from 'react'
import './index.css'
import Loading from '../../../components/Loading'
import CardList from '../../../components/CardList'
import {Tabs} from 'antd-mobile'
import { connect } from 'react-redux'
import { getFoundList } from './store/actionCreators'
function Found(props) {
let tablist=['推荐','视频','直播','美食','学习','体育','旅行','职场','科技数码','摄影','音乐','舞蹈','汽车']
const {listData,show,getFoundlistDispatch}=props
useEffect(()=>{
getFoundlistDispatch()
},[])
return (
show ?<Loading/>:
<Tabs defaultActiveKey='0' className='tab'>
{
tablist.map((item,index)=>(
<Tabs.Tab title={item} key={index} className='tab-s'>
<CardList list={listData} key={index}></CardList>
</Tabs.Tab>
)
)
}
</Tabs>
)
}
const mapStateToProps=(state)=>{
return{
listData:state.Found.dataList,
show:state.Found.show
}
}
const mapDispatchToProps=(dispatch)=>{
return {
getFoundlistDispatch(){
dispatch(getFoundList())
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Found)
此处的下拉刷新,当下拉刷新时对请求的数据进行截取,每次截取四个进行刷新替换。当数据长度到达一定值,不在加载数据,弹出提示框。以及此处的Lazyload
实现图片懒加载功能,当图片在可见视野之内时才开始加载图片
样式组件 styled-components
styled-components
是对react
写的一套css in js
框架,也就是说在js中写css,相当与(sass、less)
,这能加快我们网页的开发速度。
当然他的用法也比较简单,直接在页面中以组件的方式引入就好了。
- shopcart/index.jsx
import {ShopWrapper} from './style'
return (
<ShopWrapper></ShopWrapper>
)
- shopcart/style.js
import styled from "styled-components";
export const ShopWrapper = styled.div `
.....
`
项目的优化部分
路由懒加载
在路由的配置中,我们使用了React的lazy
和Suspense
组合实现路由懒加载效果,可以避免一些初始不必要的加载,优化首屏的加载效率
在React中使用lazy
后,需要使用到Suspense
,Suspense
组件有一个属性为fallback
,在其包裹的组件为渲染出来之前,会调用fallback
函数,可以包含多个懒加载的组件
onst ShopCart=lazy(()=>import('../components/ShopCart'))
const Add=lazy(()=>import('../pages/Add'))
const Mine=lazy(()=>import('../pages/Mine'))
const Order=lazy(()=>import('../pages/Order'))
const Shop=lazy(()=>import('../pages/Shop'))
const RouterConfig=()=>{
return(
<Suspense fallback={null}>
<Routes>
<Route path='/' element={<Navigate to='/home' replace={true}/>}></Route>
<Route path="/home" element={<Home/>}/>
<Route path="/shop" element={<Shop/>}/>
<Route path="/add" element={<Add/>}/>
<Route path="/mine" element={<Mine/>}/>
<Route path="/order" element={<Order/>}/>
<Route path="/detail" element={<Detail/>}/>
<Route path="/shopcart" element={<ShopCart/>}/>
</Routes>
<Footer/>
</Suspense>
)
}
页面渲染优化Memo
组件性能优化,主要用于子组件当中当我们的父组件数据复杂,多项改变状态的地方,当父组件的改变,没有影响到子组件(props未变,没有props), 组件外面都加memo后就会使用上一次加载的数据,不会进行更新。以此通过记忆组件渲染的方式来提升性能的表现
也就是说使用React.memo之后可以避免不必要组件跟随页面更新而重新进行渲染
import memo from 'React';
const shopcart=()=>{}
export default memo(ShopCart)
图片懒加载
引用react-lazyload实现图片懒加载效果
import LazyLoad from 'React-lazyload';
<LazyLoad
placeholder={<img width="100%"
height="100%" src={jiazai}/>}>
<img
width="100%"
height="100%"
src={item.images + "?param=300x300"}/>
</LazyLoad>
总结
这个小项目也是对自己学习react的一个总结吧,功能还有不全的地方,以及一些页面的布局也有不完善的地方。但对于我来说也是从理论到实践的一大步。我还会持续更新,你要是觉的对你有帮助就帮忙点点赞吧!
源码
- 项目地址:github