likes
comments
collection
share

腾讯QQ结合redux的实战:memo、实现模糊搜索等功能「为了清凉一夏我就不手把手教你了」

作者站长头像
站长
· 阅读数 76

「饭前叨叨」

本文是继续学习react的高级知识,一些基础的模块化思想在我的上一篇文章有体现,这次结合了redux,把数据交给redux进行管理(redux一开始学习感觉鸡肋但后知后觉对大项目开发很友好),不要问为什么还在学redux,再问,我就推荐你去学Mobx(防杠)

腾讯QQ结合redux的实战:memo、实现模糊搜索等功能「为了清凉一夏我就不手把手教你了」

先看看什么菜

  • 项目展示:

腾讯QQ结合redux的实战:memo、实现模糊搜索等功能「为了清凉一夏我就不手把手教你了」

下饭菜

  • redux-thunk:redux的中间件
  • react-lazyload:react懒加载库
  • better-scroll:丝般顺滑的滑动体验
  • antd-moblie蚂蚁金融优秀的开源组件库
  • axios:它是一个基于promise 的网络请求库,用于获取后端数据(fastmock可以让你在没有后端程序的情况下能真实地在线模拟ajax请求
  • styled-compenonts:真正的 css in js,增强 CSS 以对 React 组件系统进行样式设置的结果,具有简单的动态样式轻松维护等优点。

厨艺长进

  • redux数据管理:项目的数据状态(除单页面状态和数据外)由redux接管
  • 模糊搜索
  • 移动端页面自适应:相比上次项目通篇px,调整起来很麻烦,布局有时候会混乱,引入页面自适应,让rem代替px
  • 封装数据请求:由于项目数据写在fastmock上,所以每个请求的链接都很长,使得代码页面臃肿,眼花缭乱
  • 路由懒加载:按需加载,没有加载的页面就先不加载
  • 全局样式管理:方便快捷管理css样式,代码通俗易懂
  • 图片懒加载:图片的懒加载提高用户体验感

「开饭开饭」

redux数据管理

先上图

腾讯QQ结合redux的实战:memo、实现模糊搜索等功能「为了清凉一夏我就不手把手教你了」

首先,我第一次看到这个图的时候,我是懵的。所以放入项目中结合加以理解,才发现redux真的取代useState可以这么舒服。

看以前写的项目的每个页面中成堆成堆的useState头都是大的,这样格局就小了,当开发大项目时不可能每个页面都有那么多个数据状态,并且父子组件传值也携家带口的,过于臃肿

redux就 “应运而生”:

  • 通俗来说就是,你收到取件码去菜鸟驿站拿快递(页面数据状态发生改变),你告诉快递员(dispatch)取件码(action对象),然后快递员去仓库(store)找相应的物品,怎么找呢?通过货架上的信息(reducers)拿到物品给你,这样你就获得了你的快递(newState
  • store:保存数据状态的仓库
  • action:包含了数据状态和变化的种类
  • actioncreatorsaction变化的种类
  • dispatch:发出action的唯一方法
  • reducer:接受action当前数据状态,返回一个新的数据状态

在此项目前,我拿了上一篇文章的项目结合redux,可能会容易看懂,毕竟数据小,gitee项目地址在这

使用

  1. src下创建大仓库(store
    ├─ src
        ├─ store                  
           index.js       // redux配置
           reducer.js     // 各个子仓库数据的集合
    
    • index.js中的window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__是chrome浏览器上的一个调试工具———Redux DevTools
       import { createStore, compose, applyMiddleware } from 'redux';
       import thunk from 'redux-thunk' // 异步数据管理  中间件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";
       import { reducer as homeReducer } from '@/pages/Home/store/index'
       ...
       
       export default combineReducers({
           home: homeReducer,  // 引入小仓库
           ...
       })
      
  2. 添加了大仓库,就得使用ta,调用store。在入口文件 mian.js中引入Provider
    ...
    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>
    )
    
  3. 哪里需要数据状态,就在哪个页面模块下创建小仓库(store)。比如 src / pages / Home 下创建 store文件夹,并且创建四个文件:
    ├─ Home
        ├─ store                  
           index.js              
           reducer.js            // 各个子仓库数据的集合
           constants.js          // 设置action请求的常量
           actionCreators.js     // 结合数据请求处理数据状态
    
    • index.js
      import reducer from './reducer'
      import * as actionCreators from './actionCreators'
      
      export { reducer, actionCreators }
      
    • reducer.js
      import * as actionTypes from './constants'
      
      const defaultState = {
          messages:[],   // 数据
      }
      
      export default ( state = defaultState, action ) => {
          switch (action.type) {
              case actionTypes.CHANGE_MESSAGE:
                  return {
                      ...state,
                      messages:action.data
                  }
              default:
                  return state
          }
      }
      
    • constants.js
      export const CHANGE_MESSAGE = 'CHANGE_MESSAGE';
      
    • actionCreators.js
      import { getMessageListRequest } from '@/api/request'
      import * as actionTypes from './constants'
      
      export const changeMessageList = (data) => ({
          type:actionTypes.CHANGE_MESSAGE,
          data
      })
      export const getMessageList = () => {
          return (dispatch) => {
              getMessageListRequest()
                  .then(data => {
                      dispatch(changeMessageList(data))
                  })
          }
      }
      
  4. 在大仓库中引入这个小仓库
  5. Home页面的index.js文件下,把小仓库 connect 一下
    ...
    import { actionCreators } from "./store/index";
    import { connect } from "react-redux";
    
    function Home(props) {
        // 把数据状态解构出来使用
        const { messages } = props;
        const { getMessageDataDispatch } = props;
        // 如果要获取数据就 getMessageDataDispatch()
        useEffect(() => {
            getMessageDataDispatch();
        }, []);
        ...
    }
    const mapStateToProps = (state) => {
      return {
        messages: state.home.messages,
      };
    };
    const mapDispatchToProps = (dispatch) => {
      return {
        getMessageDataDispatch() {
          dispatch(actionCreators.getMessageList());
        },
      };
    };
    export default connect(mapStateToProps, mapDispatchToProps)(Home);
    
    

模糊搜索

  • 搜索框分为两个组件,大的Search组件包着小的SearchBox组件,这样可以在别处复用组件,并且让代码不那么臃肿
  • 这里使用到了memo包住整个小组件,当部分更新时不会整个组件全部重新渲染,只渲染更新部分,性能优化
// SearchBox 组件
export default memo(function SearchBox(props) {
    const queryRef = useRef();
    let navigate = useNavigate();
    const { newQuery } = props
    const { handleQuery } = props;
    const [show,setShow] =useState(false)
    const [query, setQuery] = useState("");
    //声明周期,一进这个页面就focus
    useEffect(() => {
        queryRef.current.focus();
    }, []);

    const handleChange = (e) => {
        // 获取到输入框的值
        let val = e.currentTarget.value;
        setQuery(val);
        handleQuery(val);
    };

    return (
        <SearchWrapper>
                <div className="search_box">
                    <input
                        type="text"
                        className="box"
                        placeholder='&nbsp;&nbsp;搜索'
                        onChange={handleChange}
                        ref={queryRef}
                    ></input>
                </div>
                <div className="button_box">
                    <button className="button_c"
                    style={show? {backgroundColor: 'grey'}:null}
                    onClick={() => {
                        navigate(-1);
                        setShow(true)
                    }}>
                        取消
                    </button>
                </div>
        </SearchWrapper>
    )
})

// search 组件
function Search(props) {
    const navigate = useNavigate();
    const { searchList, searchHistoryList } = props;
    const { getSearchDataDispatch, getSearchHistoryDataDispatch } = props;
    const [query, setQuery] = useState("");
    const [show, setShow] = useState(false);

    useEffect(() => {
        setShow(true);
    }, []);

    useEffect(() => {
        if (searchHistoryList.length == 0) {
            getSearchHistoryDataDispatch();
        }
    }, []);

    useEffect(() => {
        getSearchDataDispatch(query);
    }, [query]);
    // 更新父组件的query
    const handleQuery = (q) => {
        setQuery(q);
    };

    const renderHistoryList = (index) => {
        return (
            <>
                {index.map((item) => {
                    return (
                        <HistoryWrapper key={item.id}>
                            ...
                        </HistoryWrapper>
                    );
                })}
            </>
        );
    };

    const renderSearchList = () => {
        return (
            <>
                {searchList.map((item) => {
                    return (
                        <SearchResWrapper key={item.id}>
                            <div className="avatar_img">
                                <Lazyload placeholder={ <img width="100%"  height="100%" src={OtherImg} /> }>
                                    <Avatar src={item.img} style={{ "--size": "2rem", "--border-radius": "8rem" }} />
                                </Lazyload>
                            </div>
                            <span className="search_history_list_name">
                                {item.name}
                            </span>
                        </SearchResWrapper>
                    )
                })}
            </>
        );
    };

    const renderOther = () => {
        return (
            <OtherWrapper>
               ...
            </OtherWrapper>
        );
    };

    return (
        <>
            <SearchBox newQuery={query} handleQuery={handleQuery} />
            <PublicWrapper show={!query}>
                <Scroll>
                    {!query && (
                        <div>
                            {renderHistoryList(searchHistoryList)}
                            {renderOther()}
                        </div>
                    )}
                </Scroll>
            </PublicWrapper>
            <PublicWrapper show={query}>
                <Scroll>
                    {query && renderSearchList()}
                </Scroll>
            </PublicWrapper>
        </> 
    );
}

const mapStateToProps = (state) => {
    return {
        searchList: state.search.searchList,
        searchHistoryList: state.search.searchHistoryList,
    };
};
const mapDispatchToProps = (dispatch) => {
    return {
        getSearchDataDispatch(query) {
            dispatch(actionCreators.getSearchList(query));
        },
        getSearchHistoryDataDispatch() {
            dispatch(actionCreators.getSearchHistoryList());
        },
    };
};
export default connect(mapStateToProps, mapDispatchToProps)(Search);
  • 真正实现模糊搜索的地方,放在本地的小仓库(store)的actionCreators.js文件中
  • 每当Search组件获取数据时(getSearchDataDispatch())就对传进来的参数(query)进行筛选,从而实现模糊搜索
    export const getSearchList = (query) => {
        return (dispatch) => {
            getSearchListRequest()
                .then(data => {
                    let res = data.filter(item => { return item.name.indexOf(query)!=-1 })
                    dispatch(changeSearchList(res))
                })
        }
    }
    

小问题

  • 防抖函数已经写好,但是没有放入组件中
  • 其实我在写搜索框时,想用的antd-mobile开源库的SearchBox组件,方便快捷好看且功能强大,但是不知道为什么当页面刚开始渲染时,SearchBox组件的传入的query就已经有值了,导致还没搜索就直接显示出数据???

移动端页面自适应

  1. 在根目录下添加 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";
    };

  1. 在根目录下的index.html文件中引入上述文件
    <script src="/public/js/adapter.js"></script>
  • 因为移动端的设计稿的分辨率大多是640px750px,移动端物理像素比一般为2.0所以用375px,以20px1rem替换
  • 这样替换就可以更好的适配移动端屏幕大小

封装数据请求

明显对比:

腾讯QQ结合redux的实战:memo、实现模糊搜索等功能「为了清凉一夏我就不手把手教你了」

  • 以前的写法写起来很舒服,但是阅读起来真要命,而且这页不符合后续前端后端数据请求的写法,毕竟养成好的习惯是很重要的

写法

  1. 添加 src / api / config.js
    // 配置请求对象
    import axios from 'axios'
    export const baseUrl = "https://www.fastmock.site/mock/be16eda287b24f69cf9baefad9651667/qq";
    const axiosInstance = axios.create({
        baseURL: baseUrl
    })
    
    // 请求拦截器
    axiosInstance.interceptors.request.use(
       ...
     )
    // 响应拦截器
    axiosInstance.interceptors.response.use(
       ...
    )
    
    export { axiosInstance }
    
  • 正如上图所示,在baseUrl处写入链接
  • 在此文件下还可以写入请求拦截器响应拦截器等方法
  1. 在 src / api / request.js
    
    import { axiosInstance } from "./config";
    
    export const getXXXRequest = 
            () => axiosInstance.get('/xxxxx')
    

路由懒加载

// 独立配置文件,路由配置
import React, { lazy, Suspense } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import Home from "@/pages/Home";
const Contacts = lazy(() => import('@/pages/Contacts'))
const Dynamic = lazy(() => import('@/pages/Dynamic'))
const Search = lazy(() => import('@/components/common/Search'))

export default function RoutesConfig() {
  return (
    <Suspense fallback={null}>
      <Routes>
       {/* 实现首页路由的默认跳转 */}
        <Route path="/" element={<Navigate to="/home" replace={true} />} />
        <Route path="/home" element={<Home />} />
        <Route path="/contacts" element={<Contacts />} />
        <Route path="/dynamic" element={<Dynamic />} />
        <Route path="/Search" element={<Search />} />
      </Routes>
    </Suspense>
  )
}
  • 使用React的lazy和suspense进行路由的懒加载
  • <Navigate to="/home" replace={true} />这使得项目运行时默认转跳至home页面

全局样式管理

  1. 添加 src / assets / global-style.js
// 全局风格定义是最重要的
export default{
  "theme-color": "rgb(245, 246, 249)",
  "search-box-color":"rgb(245, 245, 245)",
  "font-weight-l":"600",
}
  1. 在css样式中使用:
import styled from "styled-components"
import style from "@/assets/global-style"
export const Wrapper = styled.div`
    ...
    background: ${style["theme-color"]};
    font-weight: ${style["font-weight-l"]};
`

图片懒加载

import Scroll from '@/components/common/scroll'
import { forceCheck } from 'react-lazyload'
import LazyLoad from 'react-lazyload'

...
        ...
        <Scroll onScroll={forceCheck}>
            ...
             <LazyLoad placeholder={<img className='img' src={图片}/>}>
               // 原本要放的图片
             </LazyLoad>
        </Scroll>
        ...
... 

配置vite alias

  • 你还在为页面引入文件而不知道跳多少层而烦恼吗?../../../../
  • 在根目录下的 vite.config.js文件中添加(本项目是使用vite脚手架创建,上一篇中有使用方法):
    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    import path from 'path'
    
    // https://vitejs.dev/config/
    export default defineConfig({
      ...
      alias:{
        "@":path.resolve(__dirname,'src')
      }
    })
    
  • 这样在后续引入文件时直接一个@就跳到了src下。
        import Scroll from '@/components/common/scroll'
    

「饭后甜点」

react 的 redux 的学习不懂还可以看看技术胖的教学 ,只不过写法老旧,但是不妨碍理解原理

项目还存在许多不足,之后的学习会不断完善,有错误请指出,还希望请点点赞,增加我学习的动力,谢谢,😘

转载自:https://juejin.cn/post/7121900436474298399
评论
请登录