React仿知乎移动端第二弹(搜索页,Redux)

1,280 阅读3分钟

Redux

Redux图解

1.webp React Components(组件)通过ActionCreators发送state和action给StoreStore再把state和action给Reducers,Reducers根据action的type(类型)拿到新的state给Store,Store最后把更新后的state(新状态)给React Components(组件)

Redux代码

store

store.png

store/index.js

使用createStore()创建store,createStore第一个参数为reducer;第二个参数为composeEnhancers(),负责合并中间件。

/* src/store/index.js */
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;

store/reducer.js

使用combineReducers将所有组件的reducer合并和向外输出。

/* src/store/reducer.js */
import { combineReducers } from "redux";
import { reducer as ideaReducer } from '@/pages/Home/Idea/store/index'
import { reducer as searchReducer } from '@/pages/Search/store/index'

export default combineReducers({
    idea:ideaReducer,
    search:searchReducer,
})

main.jsx

引入Provider在最外层包裹,并引入store作为参数传递。

/* src/main.jsx */
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
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>
)

Idea组件的store

Idea-store.png

Idea组件/store/constants.js

constants.js 负责定义actionTypes

/* Idea组件/store/constants.js */
export const CHANGE_IDEA_LIST = 'CHANGE_IDEA_LIST'

Idea组件/store/actionCreators.js

  1. actionCreators.js负责将action传给reducer。
  2. changeIdeaList(data)负责生成action(包括action类型和数据)。
  3. getIdeaList()负责进行数据请求和将数据通过changeIdeaList(data)dispatch给reducer。
/* Idea组件/store/actionCreators.js */
import { getIdeaRequest } from "@/api/request"
import * as actionTypes from './constants'

export const changeIdeaList = (data) => ({
    type: actionTypes.CHANGE_IDEA_LIST,
    data: data
})
// api请求一定放在action中
export const getIdeaList = () => {
    return (dispatch) => {
        getIdeaRequest()
            .then(data => {
                const action = changeIdeaList(data);
                dispatch(action)
            })
    }
}

Idea组件/store/reducer.js

组件的reducer.js负责判断action的类型进行对应的操作。

/* Idea组件/store/reducer.js */
import * as actionTypes from './constants'
const defaultState = {
    ideaList: [],
}
export default (state = defaultState, action) => {
    switch (action.type) {
        case actionTypes.CHANGE_IDEA_LIST:
            return {
                ...state,
                ideaList: action.data
            }
        default:
            return state
    }
}

Idea组件/store/index.js

组件store的index.js负责合并reducer.js和actionCreators.js并向外输出。

/* Idea组件/store/index.js */
import reducer from './reducer'
import * as actionCreators from './actionCreators'
export { reducer, actionCreators } 

组件/index.jsx

  1. mapStateToProps(state)负责设置状态;mapDispatchToProps(dispatch)负责设置高阶函数来调用actionCreators里的函数。
  2. connect(mapStateToProps, mapDispatchToProps)(Idea)
  3. state和mapDispatchToProps(dispatch)里的函数可以通过props在组件中结构出来使用。
/* Idea组件/index.jsx */
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { actionCreators } from './store/index'
import IdeaItem from "./IdeaItem";

function Idea(props) {
  const { ideaList } = props
  const { getIdeaDataDispatch } = props
  useEffect(() => {
    getIdeaDataDispatch();
  }, [])
  return (
    <IdeaItem ideaList={ideaList} />
  )
}
const mapStateToProps = (state) => {
  return {
    ideaList: state.idea.ideaList,
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    getIdeaDataDispatch() {
      dispatch(actionCreators.getIdeaList())
    },
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(Idea)

Search页面实现

search.png

Search目录

Search目录包括子组件和store。

Search目录.png

Search子组件

Search子组件.png

SearchInput组件

input.gif

布局实现

  1. 搜索框和取消键通过flex布局display:flex;实现,取消键设置固定宽度,搜索框设置flex:1;自适应获得宽度。
  2. 搜索框通过flex布局display:flex;实现,设置justify-content: space-between;实现搜索键和删除键分布在两边,input表单在中间。
/* SearchInput/style.js */
import styled from 'styled-components'
import style from '@/assets/global-style'

export const SearchBox = styled.div`
    display:flex;
    align-items: center;
    margin-top:0.5rem;
    font-size: ${style["font-size-m"]};
    .search-box{
        flex:1;
        display: flex;
        justify-content: space-between;
        border-radius: 1.5rem;
        padding: 0.5rem 0.5rem 0.5rem 0.5rem;
        background-color: ${style["search-bgcolor"]};
        color:${style["search-color"]} ;
       >input{
            flex:1;
            border:0;
            padding: 0 0.5rem;
            background-color: ${style["search-bgcolor"]};
            font-size: ${style["font-size-l"]};
       }      
    }
    .back{
        margin-left: 1rem;
    }
`

父子组件传值

父组件Search给子组件SearchInput传querysearchBack(修改CSSTransitionshow实现页面切换返回)、handleQuery (调用setQuery修改query的值)。

/* Search/index.jsx */
import { CSSTransition } from 'react-transition-group';

function Search(props) {
const [query, setQuery] = useState('')
const searchBack = () => {
    setShow(false);
}
const handleQuery = (q) => {
    setQuery(q)
}
return (
<CSSTransition
    in={show}
    timeout={300}
    appear={true}
    classNames="fly"
    unmountOnExit
    onExit={() => {
    navigate(-1)
    }}>
......
<SearchInput
    back={searchBack}
    newQuery={query}
    handleQuery={handleQuery}
/>
......
</CSSTransition>
}

/* SearchInput/index.jsx */
const SearchInput = (props) => {
const { newQuery } = props;
const { handleQuery, back } = props;
}

子组件实现

  1. 使用useMomo(缓存上一次函数的计算结果)和debounce(防抖节流)进行性能优化。
  2. 定义handleChange函数实现将SearchInput组件的query更新为表单的输入值并渲染,使用onChange事件使得表单一改变值就修改query的值。
  3. 使用useEffect(() => { handleQueryDebounce(query) }, [query])修改父组件中query的值。
  4. 父组件的query被修改后,传给子组件的query也改变了,此时表单内的值和组件自己的query也通过useEffect实时更新。
/* SearchInput/index.jsx */
import React, { memo, useState, useEffect, useRef, useMemo } from 'react';
import { debounce } from '@/api/utils';
import { SearchBox } from './style'

const SearchInput = (props) => {
  const { newQuery } = props;
  const { handleQuery, back } = props;
  const queryRef = useRef();
  const [query, setQuery] = useState('');
  // 父组件传过来的函数封装一下
  // 优化再升级
  // useMomo 可以缓存 上一次函数计算的结果 
  let handleQueryDebounce = useMemo(() => {
    return debounce(handleQuery, 500)
  }, [handleQuery])
  // mount 
  useEffect(() => {
    // 挂载后
    queryRef.current.focus();
  }, [])
  // 使用useEffect 去更新 
  useEffect(() => {
    handleQueryDebounce(query)
  }, [query])

  useEffect(() => {
    // mount 时候 执行 父组件  newQuery -> input query 
    let curQuery = query;
    if (newQuery !== query) {
      curQuery = newQuery;
      queryRef.current.value = newQuery;
    }
    setQuery(curQuery)
    // newQuery 更新时执行
  }, [newQuery])

  const clearQuery = () => {
    setQuery('');
    queryRef.current.value = "";
    queryRef.current.focus();
  }
  const handleChange = (e) => {
    let val = e.currentTarget.value
    setQuery(val)
  }
  const displayStyle = query ? { display: 'block' } : { display: 'none' };

  return (
    <SearchBox>
      <div className="search-box">
        <i className='iconfont icon-sousuo'></i>
        <input type="text" placeholder='搜索知乎内容' ref={queryRef}
          onChange={handleChange} />
        <i className='iconfont icon-quxiao'
          style={displayStyle}
          onClick={clearQuery}></i>
      </div>
      <div className='back' onClick={() => back()}>取消</div>
    </SearchBox>
  )
}
export default memo(SearchInput)

OldSearch组件

oldsearch.png

布局实现

OldSearch组件分成上下俩个部分:

  1. 上面部分通过flex布局display:flex;实现,设置justify-content: space-between;实现历史搜索和删除键分布在两边;
  2. 下面部分通过flex布局display:flex;实现,设置flex-wrap:wrap;实现溢出换行,对每一项设置overflow: hidden;white-space: nowrap;text-overflow: ellipsis; 实现溢出显示省略号。
/* OldSearch/style.js */
import styled from 'styled-components'
import style from '@/assets/global-style'

export const OldSearchWrapper = styled.div`
    font-size: ${style["font-size-m"]};
    .header{
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin:0.5rem 0;
        .header-left{
            font-weight: 700;
        }
    }
    .old-search-body{
        display: flex;
        flex-wrap:wrap;
        .old-search-item{
            margin:0 0.2rem 0.5rem 0;
            padding: 0.3rem 0.5rem;
            border-radius:1rem;
            background-color:${style["search-bgcolor"]};
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;        
        }
    }
`

SearchFind组件

find.gif

布局实现

SearchFind组件分成上下俩个部分:

  1. 上面部分通过flex布局display:flex;实现,设置justify-content: space-between;实现搜索发现和换一换键分布在两边;
  2. 下面部分通过flex布局display:flex;实现,设置flex-wrap:wrap;实现溢出换行,对每一项设置适当的宽度实现一行排2个。

SearchRecommend组件

recommend.gif

页面实现

引入antd-moblie的Tabs实现组件页面

/* SearchRecommend/index.jsx */
import { Tabs } from 'antd-mobile'
......
<Tabs 
activeLineMode='fixed'
    style={{
        "--content-padding": 0,
        "--active-line-height": "0.1rem",
        "--fixed-active-line-width": "0.7rem",
        '--title-font-size': '0.7rem',
        "--active-title-color": `${style["color-light"]}`,
        "--active-line-color": `${style["theme-color"]}`,
        "color": `${style["search-color"]}`,
    }}>
    <Tabs.Tab title='热搜影视' key='hotSearchVedio'>
        {renderHotsearchCommend(HotsearchVedioList)}
    </Tabs.Tab>
    <Tabs.Tab title='热搜游戏' key='hotSearchGame'>
        {renderHotsearchCommend(HotsearchGameList)}
    </Tabs.Tab>
</Tabs>

换一换功能实现

使用Math.random()生成0~1之间的随机数,并乘以后端数据数组长度并使用Math.floor()向下取整随机生成可匹配数组下标的随机数。对随机数数组进行去重处理if (arr.indexOf(cur) === -1)

/* SearchFind/index.jsx */
    const [data, setData] = useState([])
    const changedata = () => {
        let len = searchFindList.length;
        let arr = [];
        arr.push(Math.floor(Math.random() * len))
        while (arr.length < 8) {
            let cur = Math.floor(Math.random() * len)
            if (arr.indexOf(cur) === -1) {
                arr.push(cur);
            }
        }
        setData(arr);
    }

SearchItem组件

searchitem.gif

搜索组件实现

  1. 用2个SearchShowWrapper分别包裹除搜索框的其他组件和搜索内容组件。
  2. 根据搜索框是否输入值设置相反的boolean值display:${props=>props.isNone?"none":"block"};来使得未搜索时显示其他组件;搜索框有输入时,其他组件消失,搜索内容组件显示。
/* style.js */
import styled from 'styled-components'
import style from '@/assets/global-style'

export const SearchShowWrapper = styled.div`
   display:${props=>props.isNone?"none":"block"};
`
/* index.jsx */
<SearchShowWrapper isNone={query ? false : true}>
  <SearchItem query={query} searchItemList={searchItemList}/>
</SearchShowWrapper>
<SearchShowWrapper isNone={query ? true : false}>
  <OldSearch oldSearchList={oldSearchList} />
  <SearchFind searchFindList={searchFindList} />
  <SearchRecommend
    HotsearchVedioList={HotsearchVedioList}
    HotsearchGameList={HotsearchGameList}
  />
</SearchShowWrapper>

搜索关键字功能实现

通过正则filter实现模糊搜索。

searchItemList.filter((item, index) => {
    return true == new RegExp('^.*' + query + '.*$').test(item)
})

源代码

“我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!