腾讯QQ结合redux的实战:memo、实现模糊搜索等功能「为了清凉一夏我就不手把手教你了」
「饭前叨叨」
本文是继续学习react的高级知识,一些基础的模块化思想在我的上一篇文章有体现,这次结合了redux,把数据交给redux进行管理(redux一开始学习感觉鸡肋但后知后觉对大项目开发很友好),不要问为什么还在学redux,再问,我就推荐你去学Mobx(防杠)
先看看什么菜
- 项目展示:
下饭菜
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数据管理
先上图
首先,我第一次看到这个图的时候,我是懵的。所以放入项目中结合加以理解,才发现redux
真的取代useState
可以这么舒服。
看以前写的项目的每个页面中成堆成堆的useState
头都是大的,这样格局就小了,当开发大项目时不可能每个页面都有那么多个数据状态,并且父子组件传值也携家带口的,过于臃肿。
redux
就 “应运而生”:
- 通俗来说就是,你收到取件码去菜鸟驿站拿快递(
页面数据状态发生改变
),你告诉快递员(dispatch
)取件码(action对象
),然后快递员去仓库(store
)找相应的物品,怎么找呢?通过货架上的信息(reducers
)拿到物品给你,这样你就获得了你的快递(newState
) store
:保存数据状态的仓库action
:包含了数据状态和变化的种类actioncreators
:action变化的种类dispatch
:发出action的唯一方法reducer
:接受action
和当前数据状态
,返回一个新的数据状态
在此项目前,我拿了上一篇文章的项目结合redux
,可能会容易看懂,毕竟数据小,gitee项目地址在这
使用
- 在
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, // 引入小仓库 ... })
- 添加了大仓库,就得使用
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> )
- 哪里需要数据状态,就在哪个页面模块下创建小仓库(
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)) }) } }
- index.js
- 在大仓库中引入这个小仓库
- 在
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=' 搜索'
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就已经有值了,导致还没搜索就直接显示出数据???
移动端页面自适应
- 在根目录下添加 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>
- 因为移动端的设计稿的分辨率大多是640px和750px,移动端物理像素比一般为2.0所以用375px,以20px为1rem替换
- 这样替换就可以更好的适配移动端屏幕大小
封装数据请求
明显对比:
- 以前的写法写起来很舒服,但是阅读起来真要命,而且这页不符合后续前端后端数据请求的写法,毕竟养成好的习惯是很重要的
写法
- 添加 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
处写入链接 - 在此文件下还可以写入请求拦截器、响应拦截器等方法
- 在 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
页面
全局样式管理
- 此习惯养成是从神三元大佬的项目中学习到的
- 添加 src / assets / global-style.js
// 全局风格定义是最重要的
export default{
"theme-color": "rgb(245, 246, 249)",
"search-box-color":"rgb(245, 245, 245)",
"font-weight-l":"600",
}
- 在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