前言
最近学习了
Redux和React Hooks,经过这一段时间的学习,React 全家桶基本上算是熟悉了,,就用Redux和React Hooks续写对腾讯掌上道聚城的热情。这里主要分享一下Redux和React Hooks的使用方法,希望对您有所帮助。上一篇文章
项目实现
项目介绍
- 使用
Redux集中管理数据,fastmock模拟后端数据接口 - 坚守
MVVM、组件化、模块化思想,函数式组件编写页面React-Router v6编写路由 - 使用
react-transition-group动画过渡库,完成游戏搜索列表的渲染效果 - 使用
styled-components样式组件编写样式 - 参考 三元大佬 纯手写的
debouce函数和搜索框组件,实现搜索框功能 - 使用
antd-mobile提供的TabsSideBar组件 完成横向导航切换和侧边竖屏游戏分类列表的组件 - 使用
antd-mobile提供的Popup弹出层组件 完成GameList组件
项目展示
此次项目完成了对Home、LOLHome及GameList等页面级别组件的数据状态集中管理(Redux),并实现了GameList页面和Home主页添加游戏的交互功能。
话不多说,先看项目实现的效果。
首页(即精选页面)效果图
GameList 游戏列表页面效果图
添加和删除游戏功能及与主页的交互效果图
相关游戏搜索及添加功能效果图
具体实现
使用 React-Router v6 编写路由,为了性能优化我们引入 lazy 实现懒加载,只有在此组件被加载时,内部资源才被引用。
// 独立配置路由文件
import { lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
// 推迟加载,运行,按需加载
// 当切换到这个路由后 加载
import Home from '@/pages/Home'
import LOLHome from '@/pages/LOLHome'
const Find = lazy(() => import('@/pages/Find'))
const Mine = lazy(() => import('@/pages/Mine'))
const Judou = lazy(() => import('@/pages/Judou'))
const RoutesConfig = () => {
return (
<Routes>
<Route path='/' element={<Home />}></Route>
<Route path='/lol' element={<LOLHome />}></Route>
<Route path='/cf' element={<Home />}></Route>
<Route path='/home' element={<Home />}></Route>
<Route path="/judou" element={<Judou />}></Route>
<Route path="/find" element={<Find />}></Route>
<Route path="/mine" element={<Mine />}></Route>
</Routes>
)
}
export default RoutesConfig
实现GameList页面(注:具体代码可在文章结尾链接中查看)
1. 实现弹出层
这里用到了antd-mobile的 Popup 组件,弹出GameList页面
const [visible, setVisible] = useState(false)
const onMaskClick = () => {
setVisible(false)
}
<div>
<i className='iconfont icon-jiahao icon-right'
onClick={() => {
setVisible(true)
}}>
</i>
<Space direction='vertical'>
<Popup
visible={visible}
position='bottom'
onMaskClick={onMaskClick}
bodyStyle={{ minHeight: '100%' }}
>
<GameList onMaskClick={onMaskClick} />
</Popup>
</Space>
</div>
效果图如下:
2. 游戏分类部分
使用 `antd-mobile` 提供的 `Tabs` `SideBar`组件 完成横向导航切换和侧边竖屏游戏分类列表的组件
// 侧边竖屏游戏分类
<div className="side">
<SideBar activeKey={activeKey} onChange={setActiveKey}
style={{ '--width': '65px', '--background-color': '#fff' }}>
{
tabs2.map(item => (
<SideBar.Item key={item.key}
title={<span style={{ fontSize: "0.7rem" }}>{item.title}</span>
} />
))
}
</SideBar>
</div>
// 横向导航切换
<Tabs>
<Tabs.Tab title='端游' key='key1'>
{
renderTabs()
}
</Tabs.Tab>
<Tabs.Tab title='手游' key='key2'>
{
renderTabs2()
}
</Tabs.Tab>
</Tabs>
效果图如下:
3. 游戏搜索部分
- 参考 三元大佬 纯手写的
debouce函数,完成搜索框的防抖效果
const debounce = (func, delay) => {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, args);
clearTimeout(timer);
}, delay);
};
};
export { debounce };
- 搜索框完成边输入边搜索(具有
防抖功能),使用React Hooks封装debounce函数,使用CSSTransition页面交互组件,带来页面的动画效果
const [value, setValue] = useState('')
const [query, setQuery] = useState('');
const [show, setShow] = useState(false)
const queryRef = useRef()
const onSetQuery = (query) => {
setQuery(query)
}
// useMemo 可以缓存上一次函数计算的结果
// 封装debounce函数
let onSetQueryDebounce = useMemo(() => {
return debounce(onSetQuery, 500)
}, [onSetQuery])
useEffect(() => {
// query更新
onSetQueryDebounce(query)
}, [value])
// mounted 挂载 聚焦输入框
useEffect(() => {
queryRef.current.focus()
}, [])
useEffect(() => {
if(query.trim()){
getSearchResultDispatch(query)
}
}, [query])
const onAdd = (value) => {
onSetQuery(value)
setShow(true)
}
<div className='search'>
<i className='iconfont icon-guanbi icon-right' onClick={() => {
onMaskClick()
setShow(false)
setValue('')
queryRef.current.focus()
}}></i>
<SearchBar
ref={queryRef}
placeholder='搜索想要添加的游戏'
className='search1'
value={value}
onChange={e => {
setValue(e)
onAdd(e)
console.log(query)
if (e == '' || !e) {
setShow(!show)
queryRef.current.focus()
}
}}
style={{
'--border-radius': '5rem',
'--height': '1.5rem',
'--padding-left': '0.6rem',
}}
/>
</div>
// 搜索后的结果列表
{query && <CSSTransition
in={show} // 控制动画开启关闭
timeout={1000} // 为动画执行时间
appear={true} // 是否第一次加载该组件时启用相应的动画渲染
classNames="fly"
unmountOnExit // 当动画效果为隐藏时,该标签会从dom树上移除,类似js操作
>
<Container>
{searchResult == false ? renderNull() : renderSearchList()}
</Container>
</CSSTransition>
}
效果图如下:
4. 头部header的编辑部分
此部分完成游戏添加后及取消选中该游戏后,展示已选择的游戏列表的功能
const [activekey, setActivekey] = useState(false)
<div className='header'>
<h2>我的游戏</h2>
<span onClick={() => setActivekey(!activekey)}
className={classnames({ span2: true }, { active: activekey === true })}
>
{ activekey ? '完成' : '编辑' }
</span>
</div>
效果图如下:
5. 主页导航栏部分
添加和删除已选择的游戏,主页导航栏相应的变化,完成了页面间的交互。
效果图如下:
至此腾讯掌上道聚城GameList和之前的组件全部完成Redux集中管理数据的改善和优化,接下来将详细介绍Redux,别走,精彩继续!。
数据流管理Redux
Redux是一个用来管理管理数据状态和UI状态的JavaScript应用工具。随着单页应用(SPA)开发日趋复杂,组件间需要管理的state(状态)比任何时候都要多,这时候react的层级传递数据机制是非常繁琐的,这时候就需要一直新的机制改变这种现状,Redux就此诞生。把所有的state集中到组件顶部,能够灵活的将所有state各取所需的分发给所有的组件,这就是Redux管理数据状态的机制。
简介和工作流程
-
Redux的诞生是为了给React应用提供「可预测化的状态管理」机制。 -
Redux会将整个应用状态(其实也就是数据)存储到到一个地方,称为store -
这个
store里面保存一棵状态树(state tree) -
组件改变
state的唯一方法是通过调用store的dispatch方法,触发一个action,这个action被对应的reducer处理,于是state完成更新 -
组件可以派发
(dispatch)行为(action)给store,而不是直接通知其它组件 -
其它组件可以通过获取
store中的状态(state)来更新本身所需的状态(MVVM)
使用方法
1. 创建store总仓库 并合并每个子模块的store
使用 Redux 提供 createStore方法 创建一个store仓库
import { createStore,compose,applyMiddleware } 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)
)
)
export default store
使用 Redux 提供 combineReducers 整合其子模块的store
import { combineReducers } from 'redux'
import { reducer as HomeReducer } from '@/pages/Home/store'
import { reducer as LOLHomeReducer } from '@/pages/LOLHome/store'
import { reducer as GameListReducer } from '@/pages/GameList/store'
export default combineReducers ({
home: HomeReducer, // Home 子仓store
lolhome: LOLHomeReducer, // LOLHome 子仓store
gamelist: GameListReducer // GameList 子仓store
})
2. 顶层组件Provider声明式的添加数据管理功能
把store直接集成到React应用的顶层props里面,这样各个子组件能访问到顶层props了。
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<HashRouter>
<App />
</HashRouter>
</Provider>
)
3. 搭建子模块的store (此处只介绍GameList模块)
index.js这是子模块的store文件 ,旨在将子模块的store暴露给store(主仓库),以便在store(主仓库)中合并供组件使用。
import reducer from "./reducer";
import * as actionCreators from './actionCreators'
export {
reducer,
actionCreators
}
actionCreators.js该文件通过同步的api(如changeGameList())完成对action对象的创建,并通过异步api(如getGameList())完成对数据的请求及dispatch相应的action。
import * as actionTypes from './constants'
import {
getGameListsRequest,
getSelectedGameListsRequest
} from '@/api/request'
const changeGameList = (data) => ({
type: actionTypes.CHANGE_GAMELIST,
data
})
const changeSelectedGameList = (data) => ({
type: actionTypes.CHANGE_SELECTEDGAMELIST,
data
})
const addList = (data) => ({
type: actionTypes.ADD_LIST,
data
})
const deleteList = (data) => ({
type: actionTypes.DETELE_LIST,
data
})
const deleteSearchList = (data) => ({
type: actionTypes.DETELE_SEARCH_LIST,
data
})
const searchResult = (data) => ({
type: actionTypes.GET_SEARCHRESULT,
data
})
const changeLoading = (data) => ({
type: actionTypes.CHANGE_GAMELISTLOADING,
data
})
export const getSelectedGameList = () => {
return (dispatch) => {
getSelectedGameListsRequest()
.then(data => {
dispatch(changeSelectedGameList(data.data))
})
}
}
export const getGameList = () => {
return (dispatch) => {
getGameListsRequest()
.then(data => {
dispatch(changeGameList(data.data))
dispatch(changeLoading(false))
})
}
}
export const AddListData = (data) => {
return (dispatch) => {
dispatch(addList(data))
}
}
export const DeleteListData = (data) => {
return (dispatch) => {
dispatch(deleteList(data))
}
}
export const DeleteSearchListData = (data) => {
return (dispatch) => {
dispatch(deleteSearchList(data))
}
}
export const getSearchResult = (query) => {
return (dispatch) => {
dispatch(searchResult(query))
}
}
reducer.jsdispatch(action) 发出命令后将state放入reucer加工函数中,匹配相应的action的类型完成对应的数据状态的更新,对state进行加工处理,并同步给组件,完成MVVM数据状态更新。
import * as actionTypes from './constants'
const defaultState = {
games: [],
selectedgamelist: [],
searchresult: [],
loading: true
}
export default (state = defaultState,action) => {
switch (action.type) {
case actionTypes.CHANGE_GAMELIST:
return {
...state,
games: action.data
}
case actionTypes.CHANGE_SELECTEDGAMELIST:
return {
...state,
selectedgamelist: action.data
}
case actionTypes.ADD_LIST:
let addedGame = state.games.find(item => item.cid == action.data)
let games = state.games.filter(item => item.cid != action.data)
let newSelectedGameList = [
...state.selectedgamelist,
addedGame
]
return {
...state,
games,
selectedgamelist: newSelectedGameList,
}
case actionTypes.DETELE_LIST:
let deleteGame = state.selectedgamelist.find(item => item.cid == action.data)
let selectedgamelist = state.selectedgamelist.filter(item => item.cid != action.data)
let newGameList = [
...state.games,
deleteGame
]
let newSearchResult = [
...state.searchresult,
deleteGame
]
return {
...state,
games: newGameList,
selectedgamelist,
searchresult: newSearchResult
}
case actionTypes.GET_SEARCHRESULT:
let result = state.games.filter(
todo => todo.desc.includes(action.data) || todo.classify.includes(action.data)
)
return {
...state,
searchresult: result
}
case actionTypes.DETELE_SEARCH_LIST:
let addedGame1 = state.games.find(item => item.cid == action.data)
let deletesearchResult = state.searchresult.filter(item => item.cid != action.data)
let games1 = state.games.filter(item => item.cid != action.data)
let newSelectedGameList1 = [
...state.selectedgamelist,
addedGame1
]
return {
...state,
games: games1,
selectedgamelist: newSelectedGameList1,
searchresult: deletesearchResult
}
case actionTypes.CHANGE_GAMELISTLOADING:
return {
...state,
loading: action.data
}
default:
return state
}
}
constants.js用于存放action的type类型常量,方便后期代码的修改和维护。
export const CHANGE_GAMELIST = 'CHANGE_GAMELIST'
export const CHANGE_GAMELISTLOADING = 'CHANGE_GAMELISTLOADING'
export const CHANGE_SELECTEDGAMELIST = 'CHANGE_SELECTEDGAMELIST'
export const GET_SEARCHRESULT = 'GET_SEARCHRESULT'
export const DETELE_SEARCH_LIST = 'DETELE_SEARCH_LIST'
export const ADD_LIST = 'ADD_LIST'
export const DETELE_LIST = 'DETELE_LIST'
4. GameList页面连接项目的store总仓库
至此 Redux 数据管理功能就完成了,我们只需要在组件通过 react-redux 提供的connect方法连接到store,并通过 mapStateToProps方法 把Redux中的数据映射到React中的props中去 和使用 mapDispatchToProps方法 把各种dispatch也变成了props让你可以直接使用。
const mapStateToProps = (state) => {
return {
games:state.gamelist.games,
selectedgamelist: state.gamelist.selectedgamelist,
searchResult: state.gamelist.searchresult,
loading:state.gamelist.loading
}
}
const mapDispatchToProps = (dispatch) => {
return {
getGameListDispatch() {
dispatch(getGameList())
},
getSelectedGameListDispatch() {
dispatch(getSelectedGameList())
},
getSearchResultDispatch(query) {
dispatch(getSearchResult(query))
},
AddList(data) {
// console.log(data)
dispatch(AddListData(data))
},
DeleteList(data) {
dispatch(DeleteListData(data))
},
DeleteSearchList(data) {
dispatch(DeleteSearchListData(data))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(GameList))
最后
以上就是整个项目的分享啦,如果对你有帮助,感谢点赞🤗。
源码地址: 传送门👀
项目上线地址 点这里立即康康😍