小白学React——浅仿二下炒股软件(雪球)

368 阅读7分钟

雪球.webp

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

前言

👉 在上一篇文章(前端小白学React系列之——浅仿一下炒股软件(雪球) - 掘金 (juejin.cn))中,仅仅熟悉了一下react的基本使用,在这篇文章中,笔者想再结合这一段时间之所学,把数据状态交给redux进行管理,以及更新一些功能。

新功能展示🔥

loading状态

loading状态.gif

下拉刷新

下拉刷新.gif

懒加载

懒加载.gif

搜索功能

搜索功能.gif

功能的实现🔥

redux

先介绍一下redux,因为这次的项目把所有的数据状态统一交给redux进行集中管理。

  • redux是什么?
    1. 是一个专门用于做状态管理的JS库(不是react插件库
    2. 它可以用在react,angular,vue等项目中,但基本与react配合使用
    3. 作用:集中式管理react应用中多个组件共享的状态
  • 什么情况下需要使用redux?
    1. 某个组件的状态需要让其他组件可以随时拿到(共享)
    2. 一个组件需要改变另一个组件的状态
    3. 一般来说,能不用就不用

      这里因为自己想把最近学的redux巩固一下,就只能先杀鸡用牛刀了~

  • 原理图

图片1.png

翻译成人话可以把这个事情想象成咋们平常的下馆子的过程

我们(ReactComponents)来到餐馆点餐时,首先在菜单(ActionCreators)点餐(dispatch),要吃什么,要多辣(action),然后服务员(Store)记录并告诉后厨(Reducers),后厨根据顾客的菜单进行烹饪后(newState)再由服务员把菜送到顾客的手中(getState)

  1. 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

  1. 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
})
  1. 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状态

功能效果

loading状态.gif

代码实现

// 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))

下拉刷新

功能效果

下拉刷新.gif

代码实现

//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>
)

懒加载

功能效果

懒加载.gif

可以看到第三张图片出现了懒加载,但效果不是很明显

代码实现

// 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))

路由切换的动态效果

功能效果

路由切换效果.gif

代码实现


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

搜索功能

功能效果

搜索功能.gif

代码实现

//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)
    
    

小结🔥

这次花的时间比较久,还是有很多令我不满意的细节需要后期反复打磨,好在每一份耕耘就对应的每一份收获~

希望这篇文章对大家有所收获,也希望有任何建议可以在评论区告诉我,期待大佬的指正啦,码字不易,点个赞再走呗~😁😁😁

感谢.webp