携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
前言
👉 在上一篇文章(前端小白学React系列之——浅仿一下炒股软件(雪球) - 掘金 (juejin.cn))中,仅仅熟悉了一下react的基本使用,在这篇文章中,笔者想再结合这一段时间之所学,把数据状态交给redux进行管理,以及更新一些功能。
新功能展示🔥
loading状态
下拉刷新
懒加载
搜索功能
功能的实现🔥
redux
先介绍一下redux,因为这次的项目把所有的数据状态统一交给redux进行集中管理。
- redux是什么?
- 是一个专门用于做状态管理的JS库(
不是react插件库) - 它可以用在react,angular,vue等项目中,但基本与
react配合使用 - 作用:集中式管理react应用中多个组件共享的状态
- 是一个专门用于做状态管理的JS库(
- 什么情况下需要使用redux?
- 某个组件的状态需要让其他组件可以随时拿到(共享)
- 一个组件需要改变另一个组件的状态
- 一般来说,能不用就不用
这里因为自己想把最近学的redux巩固一下,就只能先杀鸡用牛刀了~
- 原理图
翻译成人话可以把这个事情想象成咋们平常的下馆子的过程
我们(
ReactComponents)来到餐馆点餐时,首先在菜单(ActionCreators)点餐(dispatch),要吃什么,要多辣(action),然后服务员(Store)记录并告诉后厨(Reducers),后厨根据顾客的菜单进行烹饪后(newState)再由服务员把菜送到顾客的手中(getState)
- Store : 集中管理数据状态,把action与reducer联系到一起
//Store/index.js:创建Store
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer,
composeEnhancers(
applyMiddleware(thunk)
)
)
export default store;
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose=> 可以让浏览器插件 Redux DevTools 生效,以便开发人员模式运行应用。
applyMiddleware(thunk)=> 启用中间件thunk,可以实现redux处理异步action
- reducer : 1.根据action进行处理数据 2.对每个组件中的数据状态进行汇总
//reducer.js:根据action进行处理数据返回新的状态
import * as actionTypes from './constants'
const defaultState = {
searchDetail:[],
enterLoading:false
}
export default (state = defaultState,action) => {
switch(action.type){
case actionTypes.SET_SEARCH_LIST:
return {
...state,
searchDetail:action.data
}
case actionTypes.SET_ENTER_LOADING:
return {
...state,
enterLoading: action.data
}
default:
return state;
}
}
// Store/reducer.js : 对每个组件中的数据状态进行汇总
import { combineReducers } from "redux";
// import { reducer as stockReducer } from '../components/Stock/StockCurviews/store/index'
import { reducer as homeReducer } from "../page/HomePage/HomeCare/store/index";
import { reducer as searchReducer } from "../page/Search/store/index"
export default combineReducers({
// stock: stockReducer,
home: homeReducer,
search:searchReducer
})
- ActionCreators : 将派发的action传递给store
//actionCreators.js : 将派发的action传递给store
import * as actionTypes from './constants'
import {
getSearchDetailRequest
} from '@/api/request'
export const changeSearchList = (data) => ({
type:actionTypes.SET_SEARCH_LIST,
data:data
})
export const changeEnterLoading = (data) => ({
type: actionTypes.SET_ENTER_LOADING,
data
})
export const getSearchDetail = (query) => {
return (dispatch) => {
getSearchDetailRequest()
.then(data => {
console.log(data);
let list = data.data.filter(item =>
item.name.indexOf(query)!=-1)
console.log(list);
dispatch(changeSearchList(list))
dispatch(changeEnterLoading(false))
})
}
}
loading状态
功能效果
代码实现
// homecare.jsx
import { EnterLoading } from '../../../components/common/style';
import LoadingV3 from '../../../components/common/loading-v3'
useEffect(()=> {
getHomeDataDispatch() // 获取主页的数据,获取成功后关闭loading状态
// console.log(enterLoading);
},[])
...
return(
<div>
...
// 当enterLoading为true时,显示loading组件
{ enterLoading ? <EnterLoading><LoadingV3></LoadingV3></EnterLoading> : null}
</div>
)
const mapStateToProps = (state) => {
console.log(state.home.homedetail);
return {
homedetail:state.home.homedetail,
enterLoading: state.home.enterLoading
}
}
const mapDispatchToProps = (dispatch) => {
return {
getHomeDataDispatch() {
dispatch(actionCreators.getHomeDetail())
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(HomeCare)
// homecare/store/actionCreators
import * as actionTypes from './constants'
import {
getHomeDetailRequest
} from "@/api/request"
const changeHomeDetail = (data) => ({
type:actionTypes.CHANGE_HOMEDETAIL,
data
})
export const getHomeDetail = () => {
return(dispatch) => {
getHomeDetailRequest()
.then(data => {
let list = data.data;
// console.log(list);
dispatch(changeHomeDetail(list))
dispatch(changeEnterLoading(false)) //当数据请求后把loading状态关掉
})
}
}
export const changeEnterLoading = (data) => ({
type: actionTypes.CHANGE_ENTER_LOADING,
data: data
})
这里的
LoadingV3组件自己挑了一个比较好玩的,还有其他有趣的loading组件状态 👉(纯css实现117个Loading效果(中) - 掘金 (juejin.cn))
下拉刷新
功能效果
代码实现
//antd-mobile的 PullToRefresh 组件
import { PullToRefresh,DotLoading } from 'antd-mobile'
import { sleep } from 'antd-mobile/es/utils/sleep'
async function doRefresh() {
await sleep(1000);
getHomeDataDispatch()
}
return(
<div>
<PullToRefresh
onRefresh={doRefresh}
refreshingText={<DotLoading color='#1677ff'/>}
completeText={ <h2 style={{color:'#1677ff'}} >聪明的投资者都在这里</h2>}>
</PullToRefresh>
</div>
)
懒加载
功能效果
可以看到第三张图片出现了懒加载,但效果不是很明显
代码实现
// homecare/index.jsx
import defaultImg from './defaultImg.jpg'
import Scroll from "@/components/common/Scroll";
import { forceCheck } from 'react-lazyload';
import LazyLoad from 'react-lazyload'
// forceCheck实现图片移动到视口时进行加载
<Scroll className="list" onScroll={forceCheck}>
<div className="detail-mid-img">
<LazyLoad
// defaultImg占位图片
placeholder={<img
// width="100%"
// height="100%"
src={defaultImg} />}>
<img
// width="100%"
// height="100%"
src={item.img} alt="" />
</LazyLoad>
</div>
</Scroll>
利用
Scroll组件和react-lazyload中的Lazyload组件实现图片下滑后的懒加载
Scroll组件来源于抖音大佬神三元云音乐项目👉(React Hooks 与 Immutable 数据流实战 - 神三元 - 掘金课程 (juejin.cn))
路由切换的动态效果
功能效果
代码实现
import { CSSTransition } from 'react-transition-group'
import { useNavigate } from 'react-router-dom'
import SearchBox from '../../components/common/search-box'
import React, { useState, useEffect, useRef, useCallback } from 'react'
...
function Search(props) {
...
const [show, setShow] = useState(false);
return (
<CSSTransition
in={show}
timeout={300}
appear={true}
classNames="fly"
unmountOnExit
onExit={() => {
navigate(-1)
}}
>
</CSSTransition>
)
}
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Search))
CSSTransition简单介绍
1. in: boolean 控制组件显示与隐藏,true 显示,false 隐藏。
2. timeout:number,延迟,涉及到动画状态的持续时间。也可传入一个对象,如{ exit:300, enter:500 } 来分别设置进入和离开的延时。
3. classNames:string,动画进行时给元素添加的类名。一般利用这个属性来设计动画。这里要特别注意是 classNames 而不是className。
4. unmountOnExit:boolean,为 true 时组件将移除处于隐藏状态的元素,为 false 时组件保持动画结束时的状态而不移除元素。一般要设成 true。
搜索功能
功能效果
代码实现
//Seach/index.jsx
import {
Container,
SearchDetail
} from './style'
import { CSSTransition } from 'react-transition-group'
import { useNavigate } from 'react-router-dom'
import SearchBox from '../../components/common/search-box'
import React, { useState, useEffect, useRef, useCallback } from 'react'
import { Collapse } from 'antd-mobile'
import { connect } from 'react-redux';
import { getSearchDetail,
changeEnterLoading } from './store/actionCreators'
import { Link } from 'react-router-dom'
function Search(props) {
const { searchDetail,enterLoading } = props
const { getSearchDetailDispatch, changeEnterLoadingDispatch } = props
const navigate = useNavigate();
const [query, setQuery] = useState('');
const [show, setShow] = useState(false);
// 返回效果
const searchBack = useCallback(() => {
setShow(false)
}, [])
// 防抖效果
const handleQuery = (q) => {
setQuery(q)
}
// 热搜展示
useEffect(() => {
setShow(true)
changeEnterLoadingDispatch(false)
}, [])
useEffect(() => {
// 去除空字符串
if(query.trim()){
changeEnterLoadingDispatch(true)
getSearchDetailDispatch(query)
}
},[query])
// 历史搜索
const renderSearch = () => {
let stock = [
{ id: 1, name:'贵州茅台'},
{ id: 2, name:'爱尔眼科'},
{ id: 3, name:'伊利股份'},
{ id: 4, name:'腾讯控股'},
{ id: 5, name:'双汇发展'},
{ id: 6, name:'阿里巴巴'},
{ id: 7, name:'赣锋锂业'},
{ id: 8, name:'建设银行'},
{ id: 9, name:'宁德时代'}
]
return (
<div className="Search-record-list">
{
stock.map(item => {
return(
<span className='item'
key={item.id}
onClick={() => setQuery(item.name)}>
{item.name}
</span>
)
})
}
</div>
)
}
// 搜索结果展示
const searchResult = () => {
return(
<>
{
searchDetail.map((item,index) => {
return( <Link to={`/search/${item.index}`}
key={index}>
<SearchDetail >
<div className="icon">
<i className="iconfont icon-sousuo"></i>
</div>
<div className="word">
<span>{item.name}</span>
</div>
<div className="logo">
{item.logo}
</div>
<div className="id">
{item.id}
</div>
</SearchDetail>
<div className="line">
</div>
</Link>
)
} )
}
</>
)
}
return (
<Container>
<div className="search_box_wrapper">
{/* 使用antd-mobine里的SearchBox实现搜索框效果 */}
<SearchBox
back={searchBack}
newQuery={query}
handleQuery={handleQuery}>
</SearchBox>
</div>
<div className="Search_records" >
{
// 当搜索框中没有输入值时显示
!query && ( <Collapse defaultActiveKey={['1']} >
<Collapse.Panel key='1' title='历史搜索'>
{renderSearch()}
</Collapse.Panel>
</Collapse>)
}
</div>
<div className="Search_result" >
{
// 当搜索框中没有输入值时显示
query && searchResult()
}
</div>
</Container>
)
}
const mapStateToProps = (state) => {
// console.log(state);
// console.log(state.search);
// console.log(state.search.searchDetail);
return {
searchDetail:state.search.searchDetail,
enterLoading: state.search.enterLoading
}
}
const mapDispatchToProps = (dispatch) => {
return {
getSearchDetailDispatch(query){
dispatch(getSearchDetail(query))
},
changeEnterLoadingDispatch(data) {
dispatch(changeEnterLoading(data));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Search))
// search-box/index.js
import React, { useEffect, useState, memo, useRef, useMemo } from 'react'
import styled from 'styled-components'
import { debounce } from '@/api/utils'
import { SearchBar } from 'antd-mobile'
const SearchBox = (props) => {
// newQuery为父组件的中的query
const { newQuery } = props;
const { handleQuery, back } = props;
const queryRef = useRef();
const [query, setQuery] = useState('');
// console.log(queryRef.current);
// 自动聚焦
useEffect(() => {
queryRef.current.focus();
}, [])
// 1.输入框发生变化就会实时获取搜索框query的值
const handleChange = (e) => {
console.log(e);
let val = e;
setQuery(val)
// handleQuery(val);
}
// 2.搜索框中query的值发生改变就会触发防抖功能
useEffect(() => {
// console.log(queryRef)
console.log(query);
handleQueryDebounce(query)
}, [query])
// 3. 触发防抖功能的同时,还会同步执行父组件handleQuery中的内容
// 改变父组件中query 即该组件的newQuery
let handleQueryDebounce = useMemo(() => {
return debounce(handleQuery, 500)
}, [handleQuery])
// 4. 当newQuery改变,就会把query中的值随时渲染到搜索框
useEffect(() => {
// console.log(newQuery);
let curQuery = query;
if (newQuery !== query) {
curQuery = newQuery;
queryRef.current.value = newQuery;
}
// console.log(newQuery);
console.log(queryRef.current.value);
setQuery(curQuery)
}, [newQuery])
return (
<SearchBoxWrapper>
<SearchBar
type="text"
placeholder='请输入内容'
ref={queryRef}
onChange={handleChange} />
<div className='cancel' onClick={() => back()} >
<span>取消</span>
</div>
</SearchBoxWrapper>
)
}
export default memo(SearchBox)
// actionCreators.js
import * as actionTypes from './constants'
import {
getSearchDetailRequest
} from '@/api/request'
...
export const getSearchDetail = (query) => {
return (dispatch) => {
getSearchDetailRequest()
.then(data => {
console.log(data);
// 实现数据的筛选
let list = data.data.filter(item =>
item.name.indexOf(query)!=-1)
console.log(list);
dispatch(changeSearchList(list))
dispatch(changeEnterLoading(false))
})
}
}
let list = data.data.filter(item => item.name.indexOf(query)!=-1)利用数组的filter()方法实现搜索的内容
queryRef.current.focus()利用useRef实现搜索框自动聚焦
小优化
移动端页面自适应
// public / js / adapter.js
var init = function () {
var clientWidth =
document.documentElement.clientWidth || document.body.clientWidth;
if (clientWidth >= 640) {
clientWidth = 640;
}
var fontSize = (20 / 375) * clientWidth;
document.documentElement.style.fontSize = fontSize + "px";
};
// 在根目录下的index.html文件中引入上述文件
<script src="/public/js/adapter.js"></script>
配置src根目录
// 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()],
resolve:{
alias:{
"@":path.resolve(__dirname,'src')
}
}
})
不会再因为
../../../../为页面引入文件而烦恼
只需一个@
import Scroll from "@/components/common/Scroll"
组件性能优化memo
- 使用原因:
每一次状态改变都会进行所有组件的重新渲染,为了避免一些状态没有改变的组件也进行不必要的渲染,可以给每个组件添加memo
- 实现
const Search = () => {} export default React.memo(Search)
小结🔥
这次花的时间比较久,还是有很多令我不满意的细节需要后期反复打磨,好在每一份耕耘就对应的每一份收获~
希望这篇文章对大家有所收获,也希望有任何建议可以在评论区告诉我,期待大佬的指正啦,码字不易,点个赞再走呗~😁😁😁