React组件设计(二)——Redux快速入门秘籍

2,215 阅读17分钟

想直接看看Redux操作的友友们,可直接点击右侧目录哦

一、升级前的优化操作

在上一篇文章中,已经介绍过今日校园_大学圈是属于移动端的组件,有兴趣的友友们对初期react项目开发有兴趣的话,可以点进这个链接去看看哦React组件设计——今日校园_大学圈(多个小功能组件聚集地),但是在我们自己初次设计移动端项目的时候,往往会遗忘掉移动端适配性的设置,以及其他一些在开发项目带来的便捷设置,比如优化路径全局样式变量复用组件等等,下面就来仔细展示设置它们的方法:

1.1 移动端的适配性

设置移动端的适配性就是为了响应式开发响应式开发是现在前端潮流的开发方式,说到移动端的响应式开发,不得不说到一个单位rem,rem是所有 DOM 节点对于根节点 html 的相对值。

rem与px的换算:在与src同一目录下创建一个新的文件夹可以命名public,在public文件夹下,创建一个js文件夹,然后创建一个命名为adpter.js的文件,在这个文件里,设置了20px为1rem,后续有用到px的地方可以根据想要设计的长度或宽度来换算成rem,代码内容如下:

// adpter.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";
  }
  
  init();
  // resize 是指屏幕翻转触发的事件
  window.addEventListener("resize", init);

在创建完adpter.js文件后,我们还需要在首页index.html的head标签中引入

<script src="/public/js/adapter.js"></script>

才能起作用。引入后,其他地方用到的px都要换成rem,font-size可以继续用px

1.2 优化路径

在开发一个react项目的时候,为了减少编写代码量,提高代码的复用性,我们经常会引入在其他文件夹下的文件,如果没有优化路径,我们就会一直../../下去,有时候还会出错,所以我们可以在项目的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')
    }
  }
})

给大家展示一下优化路径后引入文件的方式,如下面在一个文件中,我引入了自己封装的一个可复用的分享模态框,可复用的组件一般都放在src下的components的common文件夹下:

//未优化路径前
import Share from '../../../../components/common/Share'
//优化路径后,根目录下及src下直接用@代替,
import Share from '@/components/common/Share'

1.3 封装手写弹出窗复用组件

从上面的视频可以看到,点进说说的详情页,有很多方用到了我在上一篇展示今日校园_大学圈项目文章用到的举报弹出窗、分享弹出窗以及小弹窗,所以我把它们都封装成了可复用的组件,等用到的时候在引入就可以了,就跟引用antd-mobile一样方便,但要值得注意的是封装的组件要传入指定的值才可以,就比如自己在引入antd-mobile的Popup弹窗的时候,要传入visible、onMaskClick等等,

这里就展示一下封装的操作弹出框和小弹出框,代码有点多,就不放置代码了,有需要的友友可以点击文章末尾的源码地址,目录是src/components/common/Operation:

1.gif

那我就在这里简短的介绍一下说说详情页用到封装的组件要传入的值,在说说详情页用到了操作弹出窗中,点击举报后出现的弹窗:

//设置小弹窗定时消失
    const setState = () =>{
      setTimeout(()=>{
        setLoading(false)
      },3000)
      }
// 小模态框
    {loading&&<Lmodal text={text}/>}
// 设置操作弹出框中举报弹出框
    <Operation 
        complain_={complain_} //决定举报弹出框出现
        setComplain_={setComplain_}//设置举报弹出框出现或消失
        setState={setState}//设置小弹窗定时消失
        setText={setText}// 设置小弹窗中的内容
        setLoading={setLoading}//决定小弹窗出现
    />

可能在这里就会有友友们有疑惑,为什么引入antd-mobile的,要自己封装,原因就是antd-mobile里的弹出层组件样式有点子难重写,达不到自己的想要效果,所以自己选择封装一个可复用的组件也不失为一个好选择😀!

二、开始升级

2.1 大学圈首页

2.1.1 增加粘性布局

在上一篇分享文章中,没有添加话题这个板块,所以“关注-本校-问答”tab这一栏使用了固定布局,但是在tab上方加了话题板块后,这个已经不再适用,那么为了达到下面视频这种效果应该怎么做呢?

99999999.gif

答案就是使用粘性布局,简单好用又高级!

下面是代码展示:

//只需要在最外层的div加上
   position: sticky;
   top: 3.1rem;
//top是指最终要一直粘的位置

2.2 话题广场页面

2.2.1 增加CSSTransition页面级切换 + useCallback

CSSTransition是来自react-transition-group包的一个组件,也是我从研究神三元大佬的项目里学到的🤭,它可以实现页面级组件移入移出的效果,如下方视频中展示的:

2.gif

使用方法CSSTransition要包裹着整个页面展示内容,即CSSTransiton标签包裹的div会被加上相应的动画,同时使用useCallback对关闭动画函数进行缓存(被外层函数包裹,相当于闭包),组件再次更新时(函数重新执行)会根据依赖是否变化决定选用缓存函数(之前生成的函数)还是新函数(新生成的上下文),但这里没有设置依赖,所以一直会复用上次函数。

function TopicSquare(props) {
     const { topicData } = props
    const { getTopicDataDispatch } = props
    const [show,setShow]=useState(false);
    const navigate =useNavigate()
    //这里使用useCallback 
    const searchBack = useCallback(()=>{
        setShow(false)
    })
     useEffect(()=>{
         setShow(true)
        getTopicDataDispatch()
     },[])
  return (
    <CSSTransition
        in={show} 
            //为控制动画开启关闭的开关”,true为开启false为关闭
        timeout={300}
            //动画执行时间
        appear={true}
           //是否第一次加载该组件时启用相应的动画渲染
        classNames="fly"
           //为对应的样式类名和下面的css内的名字对应
        unmountOnExit
        onExit={()=>{navigate(-1)}}
            //入场动画结束时触发的钩子
    >
        <Container> 
            <Detail topicData={topicData} back={searchBack} />
        </Container>
    </CSSTransition>
  )
}


// Detail组件中 调用back(),触发onExit,回退到大学圈活题板块:
<NavBar
        back=''
        onBack={()=>back()}
        className="navbar"
        right='我关注的'
  >话题广场</NavBar>
 // style.js中的代码: 
import styled from "styled-components";

export const Container =styled.div`
    
        //变形的基点
    transform-origin: right bottom;
        //csstransition 过渡类型给children
    &.fly-enter,&.fly-appear{
        opacity: 0;
        //translate3d的z轴会使用GPU 加速
        transform: translate3d(100%,0,0);
    }
    &.fly-enter-active,&.fly-apply-active{
        opacity: 1;
        transition: all .3s;
        transform: translate3d(0,0,0);
    }
    &.fly-exit{
        opacity: 1;
        transform: translate3d(0,0,0);
    }
    &.fly-exit-active{
        opacity: 0;
        transition: all .3s;
        transform: translate3d(100%,0,0);
    }
`

2.2.2 图片懒加载Lazyload

实不相瞒,原先我设计的话题页面没有考虑到图片懒加载的,还是通过研究三元大佬的项目发现的。图片懒加载可以加快页面加载出来的速度,提高性能。

这里使用的懒加载Lazyload是来自react-lazyload里声明的一个组件,所以调用前记得要npm install它一下,但要值得注意的是Lazyload要结合forceCheck一起使用,具体使用方法是:

   import LazyLoad,{ forceCheck } from 'react-lazyload'
   
   <div onScroll = { forceCheck }>
     // onScroll 滚动触发事件
       .....
      <LazyLoad 
          // placeholder 放置懒加载时的图片
           placeholder={<img src={cartoon}/>}
           scroll={true}
      >
         //滚动到视野区时真正该出现的图片
          <img src={pic} />
      </LazyLoad>
     ...
</div>

2.2.3 单列表联动

今日校园官方app的活题列表是点击分类后,进行相应分类数据拉取后展示,但是在这里我换了一种方法,思路来源于点外卖时的点餐列表,当你点击左侧的话题分类时,右边相应的话题会出现在视野,效果展示:

3.gif

具体思路是,通过点击相应分类,传入相应id值,获取对应的话题标题元素,在调用scrollIntoView(),使元素滚入视野,具体代码如下:

// 展示左侧分类列表
 const renderClassify =()=>{
        return topicData.map((item,index)=>
            <div 
                key={index} 
                className={classnames('topic-list',{active:tab==index})}
             >
                <a onClick={()=>scrollToAnchor(index)}>
                 <span>{item.classify}</span>
                </a>
            </div>
        )
    }

// 点级分类时触发的函数 
 const scrollToAnchor =(classify)=>{
       if(classify!=undefined){
           let classifyElemment = document.getElementById(classify)
           if(classifyElemment){
                classifyElemment.scrollIntoView({
                    //进入视野的位置
                    block:'start',
                    //进入视野时的效果,如德芙般丝滑~
                    behavior: 'smooth'
                })
                setTab(classify)
           }
       }
   }

2.2.3 模糊查询搜索话题

话不多说,先来看效果展示:

4.gif

话题广场的数据是通过redux来dispatch得到的,redux详细使用可以点击右侧目录的redux部分;这里输入框使用的是antd-moblie里的SearchBar组件,该组件有onSearch属性相当于onKeyPress事件,进行搜索时调用传入的函数,根据输入的值来筛选,还有onClear属性,显示取消搜索,可传入函数,触发时显示回到所有话题页面展示,具体代码实现如下:

import { SearchBar } from 'antd-mobile'

// topicData 是redux来dispatch所有话题数据
const [topic,setTopic] = useState(topicData)
const [allTopic,setAllTopic] = useState(topicData)
const [notopic,setNotopic] =useState(false)
// 搜索函数
 const filterTopic =(value)=>{
     setVisible(false)
     let data=[];
       data = allTopic.map(({classify,context})=>context.filter(item=>item.title.includes(value)))
    if(data.length!=0){
      data=data.filter(item=>item.length>0) 
    }
    if(data.length==0){
        setNotopic(true)
    }
    setTopic(data)
 }
   
   
  <SearchBar 
      className='searchbar'
      placeholder='搜索话题'
      // 进行搜索
      onSearch={(value)=>filterTopic(value)}
      //取消搜索
      onClear={()=>{
          //显示所有话题列表
          setVisible(true)
          // 保存最开始从redux中dispatch的所有话题数据
          setTopic(allTopic)
          //隐藏搜索不存在时的图片提示
          setNotopic(false)
     }}
 />
  

2.3 说说详情页

2.3.1 使用useRef,用户体验强上加强

useRef:可以用来用来获取DOM元素对象,并且保存数据

先来看下使用后的效果:

5.gif

在这视频中,可以看到的是点击评论或者是说说内容都能进入到详情页,输入评论的文本框同时被聚焦,如此一系列的动作只需要useRef就能解决,具体代码使用方法如下:

import React, { useState,useRef,useEffect } from 'react'
import { TextArea } from 'antd-mobile'

const commentRef = useRef()
// 当前关联对象为空

useEffect(()=>{
    commentRef.current.focus();
    // 被关联对象的focus(),自动聚焦
},[])
  
<TextArea
    placeholder='发条友善的评论吧...'  
    // 使用ref,关联当前文本框dom元素
    ref={commentRef}
    className='input'
    rows={2}
    onFocus={()=>{
      setText1(true)}
    }
 />

再来说明一下:当我们需要获取元素对象的时候, 首先引入useRef, 其次调用useRef()方法接收它的返回值,我们需要获取那个DOM元素就在那个DOM元素上进行绑定,通过ref属性将useRef的返回值绑定到元素身上,这样通过useRef返回一个对象,对象内部有个current属性,这个属性就指向着我们需要的元素对象

三、Redux来了,它来了!

因为说说详情页后面很多功能设计到redux,所以说说详情页的其他功能放在这个板块讲,理解起来也更加深刻

3.1 Redux的概念

> Redux,是一个应用数据流框架,它最主要是用作应用状态的管理;简言之,Redux用一个单独的常量状态树(对象)保存这一整个应用的状态,这个对象不能直接被改变。当一些数据变化了,一个新的对象就会被创建(使用actions和reducers)。

核心概念就是,Redux是数据的掌控者,剥夺了组件管理数据的功能;如果有组件想要用数据,该组件就要跟Redux申请,不能脱离Redux的获取数据以及改变数据。

这样讲可能会觉得有点复杂,但是用习惯了就会发现,Redux用着真香!尤其是当一个数据发生变化,所有用到这个数据的都会随之改变,再也不用做假操作了/(ㄒoㄒ)/~~

3.2 Redux 准备工作

  1. api文件夹中加一个config.js,配置请求接口的域名,创建axios的请求单例
import axios from 'axios'

export const baseUrl =
            'https://www.fastmock.site/mock/b5c0ca9f1fa672b811e5a28a700073e7/address'
const axiosInstance =axios.create({
    baseURL:baseUrl
})

export { axiosInstance }
  1. 修改api文件下的request.js请求接口文件:
import{ axiosInstance } from './config'
//说说数据
export const getContextData = () => axiosInstance.get('/comments')
// 问题数据
export const getContextQue = () => axiosInstance.get('/questions')
//话题数据
export const getContextTopic = () => axiosInstance.get('/topic')

3.3 Redux实现基本读操作和写操作详细步骤

1.创建数据大仓库:在src下创建store文件夹,在文件夹下创建index.js文件和reducer.js文件。 在index.js文件中,redux-thunk涉及到Redux DevTools谷歌扩展程序的配置,大家可以在百度上搜一下这个插件,直接拖进谷歌的扩展程序就安装好了

//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;
//reducer.js文件,集合各个子数据仓库
import { combineReducers } from "redux";

export default combineReducers({
  
})
  1. 配置大仓库,调用store还需要在main.jsx文件里引入Provider,传入src下的store:
import {BrowserRouter}  from 'react-router-dom'
import { Provider } from 'react-redux'
import store from './store'

ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
 <BrowserRouter>
      <App />
    </BrowserRouter>
</Provider>
)
  1. 创建其中一个数据子仓库:在src/pages/CollegeCircle文件夹下,创建一个store文件夹,在该store文件夹下创建以下四个文件,这里我们同时展示用ajax在redux中请求数据的操作:
  • actionCreators.js :
//ajax接口请求
import {
       getContextData,
       getContextQue,
 } from '@/api/request'
import * as actionTypes from './constants'

// 注意这里的是返回一个action对象,要用()包住:
        //且type属性是一定要有的,其他属性根据页面组件的需要可以自行设定,
       //但一定要写成key:value的形式
//这里除了type属性外,还闯入了data
const changeFocus = (data) =>({
   type:actionTypes.VIEW_FOCUS,
   data
})
export const getComments = () =>{
   return (dispatch) => {
      getContextData().then(data => {
          let newdata=data.data.filter(item=>item.Focus=='yes')
          dispatch(changeFocus(newdata))
     })    
  }
}
  • constants.js :用于设置动作请求的type常量
export const VIEW_FOCUS = 'VIEW_FOCUS'
  • index.js:负责向外输出数据子仓库和动作请求
import reducer from "./reducer";
import * as actionCreators from './actionCreators'
export {
   reducer,
   actionCreators
}
  • reducer.js:创建数据子仓库,包含默认数据
import * as actionTypes from './constants'
//最初状态子仓库
const defaultState={
//关注的数据
   focus:[],
   //本校的数据
   school:[],
   //提问数据
   que:[],
   //根据id得到的对应说说详情数据
   detailDataById:[],
   //评论框里的输入值
   inputValue:'发条友善的评论吧...',
   //添加问题时,获取输入框的值
   queInputValue:'init',
   //提交问题状态
  submit:false
}
// 没有动作请求时,返回默认仓库中设置的数据
export default (state=defaultState)=>{
  switch(action.type){
      case actionTypes.VIEW_FOCUS:
           return{
               ...state,
               focus:action.data
           }
        default:
             return state
}

4.创建完collegCircle文件夹下的store,我们要记得在在大仓库中引入

// 在src/store/reducer.js:
import { combineReducers } from "redux";
import { reducer as CollegeReducer } from  '@/pages/collegeCircle/store/index'

export default combineReducers({
   college:CollegeReducer,
})

5.在页面级别的组件(这里展示的是collegeCircle文件夹下的index.jsx),与数据大仓库connect连接起来,大家要记得自己在调用该react-redux包里的声明时,要用npm下载:

import React, { useEffect} from "react";
import { connect } from 'react-redux'
//引入的是自己子仓库的动作请求集合
import { actionCreators } from "./store/index";

function CollegeCircle(props) {
// 解构出页面组件需要用到的数据和动作请求
  const {   focusData  } = props
  const { getCommentsDataDispatch } = props

// 减少触发action次数,提高性能
useEffect (()=>{
  if(!focusData.length){
     getCommentsDataDispatch(
  }
},[])

  return (
      <CircleWrapper>
        <Header/>
        <PullToRefresh  
            onRefresh={()=>{
              getCommentsDataDispatch()
            }}
            headHeight={40}
        >
          <List focusData={focusData}  />
      </PullToRefresh>
        <Footer/>
    </CircleWrapper>

  )
}
// 读操作,如果没有ajax请求数据,那就读取自己在reducer.js文件里自己定义的defaultState最初数据
// state 是大仓库,college是其下小仓库
const mapStateToProps = (state) =>{
  return{
   focusData:state.college.focus,
  }
}
// 写操作
// dispatch是一个redux中已经定义好的回调函数,用来触发动作请求,
// 这里dispatch了一个在动作请求文件里的getCommoents()函数,
    // -> 在getComments()里可以使用ajax请求到数据data 
    // -> 再dispatch同样写在actionCreator.js里的changeFocus(data),
    // -> 触发写在对应子仓库中的ruducer.js的action,进行switch(action.type)匹配
    // -> 匹配成功,重写state中页面需要改变的数据
    // -> 右键浏览器审查,点击菜单栏已经安装好的Redux,可以看到相应动作对象的type和数据
const mapDispatchToProps = (dispatch) =>{
  return{
    getCommentsDataDispatch(){
      dispatch(actionCreators.getComments())
    },
  }
}
export default connect(mapStateToProps,mapDispatchToProps)(React.memo(CollegeCircle))

Redux显示面板: image.png 6. 需要进一步加深印象的概念:

  • Redux中的state是只读的,唯一改变state的方法是触发action,action是一个用于描述已发生事件的普通对象,修改都被集中处理(如这里的reducer.js,switch下的case匹配成功后的操作),而且严格按顺序一个一个执行。
  • reducer.js里的是一个纯函数,一定要有返回数据,并且返回的数据必须从传入的参数即state或action来获取值。

四、Redux 一条龙服务

-点赞+评论+关注+发布问题-

4.1 说说点赞

效果展示:

7.gif

思路:用户浏览页面,点赞状态(dianzanState)为false,第一次点击时,点赞状态为ture,点赞数加一;第二次点击时,将点赞状态变为false,点赞数减一。这里有两点需要注意一下:1. 每一个说说数据都需要包含着一个对应的点赞状态属性,如果是全部说说才只包含一个点赞状态,点击时会出现所有的说说都被点赞的bug。2. 点赞时要传入对应说说的id,根据id筛选出对应的说说,再进行点赞数的处理。

代码展示

  1. actionCreator.js:
const changeZan = (id) =>({
    type:actionTypes.CHANGE_DIANZAN,
    id

})

export const getZan = (id) =>{
    return(dispatch) =>{
         dispatch(changeZan(id))
     }
}
  1. reducer.js
// 下面的数据操作可以用函数封装起来,但是这里为了友友们对代码的理解,就这样展示出来了
case actionTypes.CHANGE_DIANZAN:
            return{
                ...state,
                school:state.school.map(item=>{
                    if(!item.DianzanState){
                        if(item.id==action.id){
                            item.Dianzan=item.Dianzan+1
                            item.DianzanState=true;
                          } 
                    }
                    else{
                        if(item.id==action.id){
                          item.Dianzan=item.Dianzan-1
                          item.DianzanState=false
                        }
                     }
                     return item
                    })
  1. 在页面组件中调用
//在collegeCircle文件夹下的index.jsx中的写操作添加相应的函数,并且要记得解构
 const { getCommentsDataDispatch,getChangeZanDispatch } = props
 
 //将getChangeZanDispatch传入说说展示的子组件中:
    <List 
        getAnswerCountOpposeDispatch = { getAnswerCountOpposeDispatch }  
    />


 // 写操作
 const mapDispatchToProps = (dispatch) =>{
  return{
    getCommentsDataDispatch(){
      dispatch(actionCreators.getComments())
    },
      getChangeZanDispatch(id){
      dispatch(actionCreators.getZan(id))
    },
  }
  ......

4.2 回答点赞

效果展示:

111.gif

思路:回答点赞的操作比说说点赞多了一个反对操作,所以我在这里设置了两个action对象;并且对于复杂的css样式改变,我用了从神三元大佬项目学来的样式组件传递的props技巧

代码展示

  1. 同样是在actionCreator.js:
// 赞同
const changeAnswerCountAgree = (ID) =>({
    type:actionTypes.CHANGE_ANSWER_COUNT_AGREE,
    ID
})
// 反对
const changeAnswerCountOppose = (ID) =>({
    type:actionTypes.CHANGE_ANSWER_COUNT_OPPOSE,
    ID
})
export const getAnswerCountAgree = (ID) =>{
    return(dispatch) =>{
        console.log(ID,'----------------')
        dispatch(changeAnswerCountAgree(ID))
    }
}
export const getAnswerCountOppose = (ID) =>{
    return(dispatch) =>{
        dispatch(changeAnswerCountOppose(ID))
    }
}
  1. reducer.js
 case actionTypes.CHANGE_ANSWER_COUNT_AGREE:
            return{
                ...state,
              que:state.que.map(item=>{
                    if(item.id==action.ID){
                      if(!item.answerState){
                            if(!item.agree){
                                item.agreeCount = item.agreeCount + 1
                            }   
                       }
                    else{
                           item.agreeCount = item.agreeCount - 1
                        } 
                    item.answerState = !item.answerState
                    item.agree = !item.agree
                      }   
                      return item
                  }
              ),
            }
        case actionTypes.CHANGE_ANSWER_COUNT_OPPOSE:
            return{
                ...state,
                que:state.que.map(item=>{
                  if(item.id==action.ID){
                     item.answerState=!item.answerState
                     item.oppose=!item.oppose
                  }
                  return item
                })
            }

3.进行调用

//同样在父组件写操作函数中引入两个Diapatch函数,并解构出来传递给子组件,
//这里就不展示相似代码了,就展示一下子组件是如何调用的
   <LittleBox 
                    answerState={answerState} 
                    index={index}
                    agree={agree}
                    oppose={oppose}
     >
                     <div 
                        className={classnames({agree:!agree},{done:agree})}
                        onClick={()=>getAnswerCountAgreeDispatch(id)}
                     >
                        <i className='fa fa-caret-up'/>
                         <span >赞同{agreeCount}</span>
                     </div>
                     <div 
                        className={classnames({oposition:!oppose},{done:oppose})}
                        onClick={()=>getAnswerCountOpposeDispatch(id)}
                    >
                         <i className='fa fa-caret-down'/>
                         <span >{oppose?"已反对":'反对'}</span>
                     </div>
              </LittleBox>
// style.js中的代码:

export const LittleBox =styled.div`
    color:#ff4044;
    font-size: 12px;
    width: 6rem;
    letter-spacing: 0.06rem;
    border-radius: 999em;
    /* 椭圆!!! */
    line-height: 0.05rem;
    padding:${props=>props.answerState?0:'0.3rem'};
    background-color:${props=>props.answerState?'#fff':'#feeeec'};
    display: flex;
    justify-content: space-between;
    .agree
        position: relative;
        display:${props=>props.answerState?'none':'block'};
        ::after{
            content: '';
            background-color:#ff4044 ;
            height: 0.5rem;
            width: 0.05rem;
            position: absolute;
            left: 3.4rem;
        }
    }
    .oposition{
        display:${props=>props.answerState?'none':'block'};
    }
    .done{
        background-color: #ff4044;
        color:white;
        height:1.2rem;
        width: 3.2rem;
        text-align: center;
        line-height: 1.2rem;
        border-radius: 999em;  
    }
`

4.3 关注

效果展示:

6.gif

点击本校列表中说说的关注后,该条说说数据会进入关注列表的展示;该实现在点击关注的地方触发相应的action,代码如下:

// 1. actionCreator.js:
const changeAddFocus = (ID) =>({
   type:actionTypes.ADD_FOCUS,
   ID
})
export const getAddFocus = (ID) =>{
   return(dispatch) =>{
       dispatch(changeAddFocus(ID))
   }
}

// 2. reducer.js:
   case actionTypes.ADD_FOCUS:
           let item_ = 
                   state.school.map(item =>{
                       if(item.id==action.ID){
                          item.Focus='yes'
                      }
                      return item 
                   })
         return{
               ...state,
             school:item_,
             focus:item_.filter(item => item.Focus == 'yes')
        }

4.4 评论与发布问题

效果展示

10.gif

这两种的操作的思路是一样的:首先使用onChange获取输入框的改变时候的值,但要记得加个工具库lodash的防抖函数debounce,在state.college里设置一个initValue变量存储这个值,最后点击事件时,执行说说里的comments的push操作就成功了

这里展示的是增加评论的代码操作:

// 1.actionCreators.js
const changeInputValue =(value)=>({
    type:actionTypes.CHANGE_INPUT_VALUE,
    value
})
export const changeComments = (ID) => ({
    type:actionTypes.ADD_COMMENTS,
    ID
})
export const getInputValue = (value) =>{
    return(dispatch) =>{
        dispatch(changeInputValue(value))
    }
}
// 2.reducer.js
  case actionTypes.CHANGE_INPUT_VALUE:
            return{
                ...state,
                inputValue:action.value
            }
   case actionTypes.ADD_COMMENTS:
            return{
                ...state,
               detailDataById:state.detailDataById.map(item=>{
                                           item.Comment.push(state.inputValue)
                                           return item
                                        }),
               inputValue:'发条友善的评论吧...' 
            }
 // 3. 解构传递给子组件并调用:
  import _ from 'lodash' // 工具函数库
  import { Toast } from 'antd-mobile'
 
    <TextArea
                placeholder={inputValue}
                className='input'
                rows="4"
                off='true'
                autoFocus
                onChange={
                  _.debounce(getInputValueDispatch,500)}
   />
    <span onClick={()=>{
                    setText1(false)
                  getAddCommentDispatch(id)
                    Toast.show({
                      icon:'success',
                      content: '发送成功'
                    })
                    }}>发送</span>

五、待更新

5.1 结束语

本项目,即今日校园里的大学圈模块,在开发过程中借鉴了一下神三元大佬的项目用到的知识点,比如useMemo、useCallback、useRef、CSSTransition、样式组件props传递用法等,进一步加深了自己对开发项目的理解,如果有兴趣的友友们如果还没看过三元大佬的项目,可以点击这个,去瞧瞧他写的掘金小册React Hooks 与 Immutable 数据流实战

同时在项目中还使用redux了对数据的一系列操作,不得不说redux真的很nice,习惯使用以后发现很多数据处理变得简单起来!如果有友友们对redux还是有点不熟悉的地方,大家可以去百度搜索技术胖的个人博客里的redux教程,虽然里面的redux是用老版本react写的,但是redux的写法是很不错的,有很多可以学习到的redux写法技巧!

最后,本项目在后期仍然会继续改进,实现更多功能,谢谢大家!未完待续......

5.2 项目展示

源码地址 项目展示