# react-dewu **Repository Path**: feiyu_link/react-dewu ## Basic Information - **Project Name**: react-dewu - **Description**: No description available - **Primary Language**: JavaScript - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 8 - **Created**: 2022-07-02 - **Last Updated**: 2022-07-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 实战 ### 创建应用 首先使用的脚手架是 vite,使用 vite 的原因就一个字,快! ### 项目技术栈 - react 全家桶: react + react-router + redux - redux-thunk: 处理异步逻辑的redux中间件 - immer: 轻量级 immutable,进行持久性数据结构处理 - react-lazyload: react 懒加载库 - better-scroll: 提升移动端滑动体验 - stlyed-components:css in js 的工程化工具 - axios:用来请求后端 api 数据 - moment:处理时间和日期的库 - lokijs: js内存数据库 > 每个组件都应用 `memo` 包裹,使得 React 在更新组件之前进行 props 的比对,若 props 不变则不对组件更新,减少不必要的重渲染 ### 项目的架构如下 ``` react-dewu/ node_modules/ src/ api/ 网络请求代码和相关配置 assets/ 静态文件 baseUI/ 基础UI轮子 components/ 可复用的UI组件 database/ 数据库 layouts/ 整体布局 mock/ mock假数据模拟后端 pages/ 页面 routes/ 路由配置文件 store/ redux 相关文件 utils/ 工具类函数 App.jsx 根组件 main.jsx 入口文件 style.js 默认样式 index.html package.json readme.md vite.config.js ``` ### 首先分析页面的整体布局  可以发现只有底部的 tabbar 部分属于共享布局,那就开始编写路由配置 ```js import React, { lazy, Suspense } from 'react'; import HomeLayout from '../layouts/HomeLayout'; import NotFound from '../layouts/NotFound'; import { Navigate } from 'react-router-dom'; const IdentifyComponent = lazy(() => import("../pages/identify")); const ShopListComponent = lazy(() => import("../pages/shopping")); const WashComponent = lazy(() => import("../pages/wash")); const MyComponent = lazy(() => import("../pages/my")); export default [ { path: "/", element: , // 一级路由,对应公共组件,放置 tabbar 的布局 // 二级路由 配置四个 tab 栏 children: [ { path: "/", element: // 默认跳转到 shop 商品页面 }, { path: "/shop", element: , }, { path: "/identify", element: }, { path: "/wash", element: }, { path: "/my", element: }, { path: "*", element: } ] } ]; ``` 这里使用了 React 提供的 `Suspense` 和 `Lazy` 实现了动态路由 > 简单说一下这里为什么要使用动态路由: 对于大型应用来说,一个首当其冲的问题就是所需加载的 JavaScript 的大小。程序应当只加载当前渲染页所需的 JavaScript。有些开发者将这种方式称之为 "代码分拆(code-splitting)" — 将所有的代码分拆成多个小包,在用户浏览过程中按需加载,这样可以提高首屏加载效率 为了让路由文件生效,必须在 App 根组件下面导入路由配置,现在在 App.jsx 中: ```js import React from 'react'; import { useRoutes } from 'react-router';// useRoutes 读取路由配置转换为 Route 标签 import ALLRoutes from './routes/index'; import { IconStyle } from './assets/iconfont/iconfont'; import { Provider } from 'react-redux'; import store from './store'; import { GlobalStyle } from './style'; const App = () => { const routes = useRoutes(ALLRoutes); return ( { routes } ); } export default App; ``` 使用react-router V6 的新特性 `useRoutes` 方法取代了 react-router-config 的 `renderRoutes` 方法,太方便了 然后进行公共页面组件 `HomeLayout` 的开发 ```js import React, { useEffect } from 'react'; import { NavLink, Outlet } from 'react-router-dom'; import { Tab, TabItem } from './HomeLayout.style'; function Home(props) { return ( <> isActive ? "selected" : null} > {changeDispatchIndex(0)}}> 购买 isActive ? "selected" : null} > {changeDispatchIndex(1)}}> 鉴别 isActive ? "selected" : null} > {changeDispatchIndex(2)}}> 洗护 isActive ? "selected" : null} > {changeDispatchIndex(3)}}> 我 /*渲染下一层子路由*/ > ); } export default React.memo(Home); ```  以上是部分代码,[完整代码看这](https://gitee.com/onepiece1205/react-dewu/blob/master/react-dewu/src/layouts/HomeLayout.jsx)。通过 NavLink 中 `className` 提供的 `isActive`属性在选中Tab时激活 selected 属性,给字体加粗并且有下划线的效果, 现在就可以体验在 `Tab` 上自由切换的感觉了 ### 进行第一个页面级(shop页面)组件的开发 分析上图片,要开发的组件有: - 商品列表 - 横向分类列表 - 搜索框 #### 商品列表的开发 ```js import React from 'react'; import LazyLoad from 'react-lazyload'; import { useNavigate } from 'react-router-dom'; import { getCount } from '../../utils/shop'; import { ListItem, List } from './style'; import shopImg from './shop.png'; function RecommendList(props) { let navigate = useNavigate(); const enterDetail = (id) => { navigate(`/shop/${id}`); } return ( { props.recommendList.map(item => { return ( enterDetail(item.purchaseNum)}> }> {item.price} {getCount(item.purchaseNum)}+人付款 {item.title} ) }) } ); } export default React.memo(RecommendList); ``` 实现效果如下 对应的 style.js 在[这里](https://gitee.com/onepiece1205/react-dewu/blob/master/react-dewu/src/components/list/style.js)。`getCount` 是一个工具类函数,与业务功能关系不大,我把它专门放在了`utils`文件夹下 完整列表组件代码看[这里](https://gitee.com/onepiece1205/react-dewu/tree/master/react-dewu/src/components/list) **相关优化** - 引入react-lazyload 实现图片懒加载 ```js }> ``` #### 横向分类列表的开发 在 baseUI 文件夹下新建 horizen-item 目录,接着新建 index.jsx 首先分析这个基础组件接受哪些参数 ```js import { PropTypes } from 'prop-types'; // 引入 prop-types 库进行类型检查 Horizen.defaultProps = { list: [], // 为接受的列表数据 handleClick: null, // 为点击不同的 item 执行方法 title: '', // 为列表左边的标题 oldVal: '', // 为当前的 item 值 }; Horizen.propTypes = { list: PropTypes.array, handleClick: PropTypes.func, title: PropTypes.string, oldVal: PropTypes.string }; ``` 进行 redux 层的开发,在 Shopping 目录下,新建 store 文件夹,然后新建以下文件 ``` actionCreators.js //放不同action的地方 constants.js //常量集合,存放不同action的type值 index.js //用来导出reducer action reducer.js //存放initialState和reducer函数 ``` 然后把对象解构出来 ```js const { list, oldVal, title } = props; const { handleClick } = props; ``` 返回的jsx为 ```js return ( {title} { list.map((item) => { return ( clickHandle(item)}> {item.name} ) }) } ) ``` 完整的 horizen-item 代码看[这里](https://gitee.com/onepiece1205/react-dewu/tree/master/react-dewu/src/baseUI/horizen-item)  现在就可以滑动了,对于引入的`Scroll`组件是我的小伙伴负责开发,[他的文章](https://juejin.cn/post/7053682198100049956)详细介绍了`Scroll`组件的详细开发,`Scroll`组件也是这个项目的灵魂 #### 搜索框  搜索框的制作比较简单,[直接上地址](https://gitee.com/onepiece1205/react-dewu/blob/master/react-dewu/src/baseUI/search-input/index.jsx) #### Redux 层的开发 **申明初始化state** ``` const defaultState = { shopList:[], // 商品列表总数 enterloading: true, // 加载时 loading 状态 pullUpLoading: false, // 上拉刷新时 loading 状态 pullDownLoading: false, // 下拉刷新更多数据时 loading 状态 category: "1001", // 横向分类列表参数 pageCount: 0, // 当前页数,用于实现分页功能 listOffset: 0, // 请求列表的偏移 index: 0 } ``` **定义constants** ```js export const CHANGE_SHOP_LIST = "shop/CHANGE_SHOP_LIST"; export const CHANGE_ENTERLOADING = "shop/CHANGE_ENTER_LOADING"; export const CHANGE_PULLUP_LOADING = 'shop/CHANGE_PULLUP_LOADING'; export const CHANGE_PULLDOWN_LOADING = 'shop/CHANGE_PULLDOWN_LOADING'; export const CHANGE_CATOGORY = 'shop/CHANGE_CATEGORY'; export const CHANGE_LIST_OFFSET = 'shop/CHANGE_LIST_OFFSET'; export const CHANGE_PAGE_COUNT = 'shop/CHANGE_PAGE_COUNT'; export const CHANGE_INDEX = 'shop/CHANGE_INDEX'; export const REFRESH_MORE_SHOP_LIST = 'shop/REFRESH_MORE_SHOP_LIST'; ``` **定义rudecer函数** ```js import { produce } from 'immer'; import * as actionTypes from './constants'; export const shopReducer = produce((state, action) => { switch(action.type) { case actionTypes.CHANGE_ENTERLOADING: state.enterloading = action.data; break; case actionTypes.CHANGE_SHOP_LIST: state.shopList = action.data; break; case actionTypes.CHANGE_CATOGORY: state.category = action.data; break; case actionTypes.CHANGE_PULLUP_LOADING: state.pullUpLoading = action.data; break; case actionTypes.CHANGE_PULLDOWN_LOADING: state.pullDownLoading = action.data; break; case actionTypes.CHANGE_PAGE_COUNT: state.pageCount = action.data; break; case actionTypes.REFRESH_MORE_SHOP_LIST: state.shopList = _.shuffle(action.data); break; case actionTypes.CHANGE_LIST_OFFSET: state.listOffset = action.data; break; case actionTypes.CHANGE_INDEX: state.index = action.data; break; } }, defaultState); ``` **编写具体的 action** ```js import * as actionTypes from './constants'; import { getShopListRequest, getMoreShopListRequest } from '../../../api/request'; export const changeRecommendList = (data) => ({ type: actionTypes.CHANGE_SHOP_LIST, data }); export const updateShopList = (data) => ({ type: actionTypes.REFRESH_MORE_SHOP_LIST, data }); export const changeListOffset = (data) => ({ type: actionTypes.CHANGE_LIST_OFFSET, data }); //进场 loading export const changeEnterLoading = (data) => ({ type: actionTypes.CHANGE_ENTERLOADING, data }); //滑动最底部loading export const changePullUpLoading = (data) => ({ type: actionTypes.CHANGE_PULLUP_LOADING, data }); //顶部下拉刷新loading export const changePullDownLoading = (data) => ({ type: actionTypes.CHANGE_PULLDOWN_LOADING, data }); export const changePageCount = (data) => ({ type: actionTypes.CHANGE_PAGE_COUNT, data }); export const changeCategory = (data) => ({ type: actionTypes.CHANGE_CATOGORY, data }) export const changeIndex = (data) => ({ type: actionTypes.CHANGE_INDEX, data }) export const refreshMoreShopList = (offset, category, shopList) => { return (dispatch) => { getMoreShopListRequest(category, offset).then(res => { const data = [...shopList, ...res.data.data.items]; const length = data.length; setTimeout(() => { dispatch(changeRecommendList(data)); dispatch(changePullUpLoading(false)); dispatch(changeListOffset(length)); }, 1000) }).catch(() => { console.log('获取更多数据失败'); }) } } export const refreshShopList = (category) => { return (dispatch) => { getShopListRequest(category).then(data => { setTimeout(() => { dispatch(updateShopList(data.data.data.items)); dispatch(changePullDownLoading(false)); }, 1000) }).catch(() => { console.log('顶部下拉刷新请求数据失败') }) } } export const updateCategoryData = (category) => { return (dispatch) => { getShopListRequest(category).then(data => { setTimeout(() => { dispatch(updateShopList(data.data.data.items)) dispatch(changeEnterLoading(false)); },1000) }).catch(() => { console.log('分类横向列表更新数据失败'); }) } } export const getShopList = (category) => { return (dispatch) => { getShopListRequest(category).then(data => { setTimeout(() => { dispatch(changeRecommendList(data.data.data.items)) dispatch(changeEnterLoading(false)); },1000) }).catch(() => { console.log('数据传输错误'); }) } } ``` **将相关变量导出** ```js import { shopReducer } from './reducer' import * as actionCreators from './actionCreators' import * as constants from './constants' export { shopReducer, actionCreators, constants }; ``` **组件连接Redux** ```js import { shopReducer } from '../pages/shopping/store'; export default combineReducers({ shopping: shopReducer }); ``` #### 上拉刷新/下拉加载更多实现 在这里 `Scroll` 基础组件的作用就展现出来了。之前我们封装了 `Scroll 组件`,监听上拉 / 下拉刷新的功能已编写完成,核心代码如下 ```js // 顶部下拉刷新 const handlePullDown = () => { pullDownRefresh(category, pageCount); } const pullDownRefresh = () => { dispatch(actionTypes.changePullDownLoading(true)); dispatch(actionTypes.changeListOffset(0)); dispatch(actionTypes.refreshShopList(category)); } // 滑到最底部刷新部分的处理 const handlePullUp = () => { pullUpRefresh(category, pageCount); } const pullUpRefresh = (category, count) => { dispatch(actionTypes.changePullUpLoading(true)); dispatch(actionTypes.refreshMoreShopList(listOffset, category, shopList)); dispatch(actionTypes.changePageCount(() => count + 1)); } //pullUp 上拉加载逻辑 //pullDown 下拉加载逻辑 //pullUpLoading 是否显示上拉 loading 动画 //pullDownLoading 是否显示下拉 loading 动画 //onScroll 滑动触发的回调函数 // scrollRef 操作DOM return ( ) ``` [完整代码在这里](https://gitee.com/onepiece1205/react-dewu/tree/master/react-dewu/src/pages/shopping) 最后来看下实现效果  ### 商品详情页的开发 #### 难点 **详情页的难点有两个** - 详情页是以路由跳转的方式实现的,怎么从拿到外面传过来的商品信息 最好的做法是外部点击时根据商品独一无二的id值去请求接口,返回数据 这里我偷了个懒,不想写接口呀😥,这里我使用里 `useContext` 包裹上一层路由组件,实现了数据的传输,当然也可以用 `redux` 拿到数据,使用 `useContext` 也算是熟悉一下用法吧 ```js // 在 /shopping/index.jsx 下 // recommendShopList 推荐更多商品 // shopList 商品列表 export const ShopsContext = createContext(); return ( ) ``` ```js // 在 /shop/index.jsx 下 import { ShopsContext } from '../shopping'; import _ from 'lodash'; const ShopDetail = () => { // 解构出数据 const { recommendShopList, shopList } = useContext(ShopsContext); // 拿到页面跳转时的查询参数,这里我给的参数是商品的购买数量 const { id } = useParams(); // 引入lodash库的findIndex方法根据拿到的id值找在shoplist中到对应的商品信息的下标 const [index] = useState(_.findIndex(shopList, function(o) {return o.purchaseNum == id})); let { loading, shopListDetail } = useSelector((state) => ({ loading: state.shopDetail.loading, shopListDetail: state.shopDetail.shopListDetail })); useEffect(() => { dispatch(actionTypes.goToDetail(shopList[index].imageArr)); },[]) } ``` 这样就拿到商品的详情信息啦 - 实现评论功能  - 使用 `css @keyframes` 规则实现弹出层 ```js export const CommentsContainer = styled.div` position: fixed; height: 70vh; width: 100vw; bottom: 0; left: 0; right: 0; z-index: 2000; background: ${style["default-color"]}; animation: Popup .4s; padding: 60px 0; @keyframes Popup { 0% { transform: translate3d(0, 100%, 0); } 100% { transform: none; } } ... ` ``` ### Moment.js + lokijs 实现评论 - 新建 `database` 文件夹,添加创建数据库、集合 ```js // index.js import Loki from 'lokijs' export const db = new Loki('goods', { autoload: true, autoloadCallback: databaseInitialize, autosave: true, autosaveInterval: 3000, persistenceMethod: "localStorage" }) // 创建集合 function databaseInitialize() { const comments = db.getCollection('comments') if (comments == null) { db.addCollection('comments') } } export function loadCollection(collection) { return new Promise(resolve => { db.loadDatabase({}, () => { const _collection = db.getCollection(collection) resolve(_collection) }) }) } ``` - 数据库和集合有了,评论不是右手就行? ```js import moment from 'moment' // 需引入moment.locale()文件才能转换日期,完整代码就不列出 const [data, setData] = useState([]) const [query, setQuery] = useState('') const queryRef = useRef() // 获取输入框的值 const handleChange = () => { let newValue = queryRef.current.value if (newValue.trim() != '') { setQuery(newValue); } } ``` - 初始化评论列表数据 ```js useEffect(() => { loadCollection('comments') .then((collection) => { const entities = collection.chain() .find() .simplesort('$loki', 'isdesc') .data() setData(entities) }) }, []) ``` - 添加评论插入集合 ```js const createComment = (query) => { if (query.trim() != '') { setData([...data, { body: query, meta: { created: Date.now() }}]) loadCollection('comments') .then((collection) => { collection.insert([ { body: query } ]) }) .then(setQuery('')) } } ``` - 评论列表展示 ```js { data.map((item, index) => {item.body} {moment(item.meta.created).fromNow()} ) } ``` 详情页的静态页面开发并不难,轮播图的制作可以看 `swiper` 的[官网](https://swiperjs.com/react), 完整商品详情页代码[看这里](https://gitee.com/onepiece1205/react-dewu/tree/master/react-dewu/src/pages/shop) ## 分类部分  (注:因为数据只传了服装的,所以点击一级菜单的其他项没有正常显示属正常操作,文尾有在线地址) - 使用 `better-scroll` 打造基础滑动组件,并衍生出横向导航切换和竖屏商品列表的组件。 - 定义 `Scroll` 所需参数 ```js import PropTypes from "prop-types" Scroll.defaultProps = { direction: "vertical", click: true, refresh: true, onScroll:null, pullUpLoading: false, pullDownLoading: false, pullUp: null, pullDown: null, bounceTop: true, bounceBottom: true }; Scroll.propTypes = { direction: PropTypes.oneOf(['vertical', 'horizental']), // 滚动的方向 refresh: PropTypes.bool, // 是否刷新 onScroll: PropTypes.func, // 滑动触发的回调函数 pullUp: PropTypes.func, // 上拉加载逻辑 pullDown: PropTypes.func, // 下拉加载逻辑 pullUpLoading: PropTypes.bool, // 是否显示上拉 loading 动画 pullDownLoading: PropTypes.bool, // 是否显示下拉 loading 动画 bounceTop: PropTypes.bool, //是否支持向上吸顶 bounceBottom: PropTypes.bool //是否支持向下吸顶 }; ``` - 处理封装 `Scroll` 组件 ```js import React, { forwardRef, useState,useEffect, useRef, useImperativeHandle, useMemo } from "react" import BScroll from "better-scroll" import { debounce } from "../../utils/uiOptimization"; const [bScroll, setBScroll] = useState() const scrollContaninerRef = useRef() const { direction, click, refresh, pullUpLoading, pullDownLoading, bounceTop, bounceBottom } = props const { pullUp, pullDown, onScroll } = props // 防抖 let pullUpDebounce = useMemo(() => { return debounce(pullUp, 500) }, [pullUp]) let pullDownDebounce = useMemo(() => { return debounce(pullDown, 500) }, [pullDown]) // 创建 better-scroll 实例 useEffect(() => { const scroll = new BScroll(scrollContaninerRef.current, { scrollX: direction === "horizental", scrollY: direction === "vertical", probeType: 3, click: click, bounce:{ top: bounceTop, bottom: bounceBottom } }); setBScroll(scroll) return () => { setBScroll(null) } }, []) // 实例绑定 scroll 事件 useEffect(() => { if(!bScroll || !onScroll) return bScroll.on('scroll', onScroll) return () => { bScroll.off('scroll', onScroll) } }, [onScroll, bScroll]) // 上拉判断 useEffect(() => { if(!bScroll || !pullUp) return; const handlePullUp = () => { //判断是否滑动到了底部 if(bScroll.y <= bScroll.maxScrollY + 100){ pullUpDebounce() } }; bScroll.on('scrollEnd', handlePullUp) return () => { bScroll.off('scrollEnd', handlePullUp) } }, [pullUp, pullUpDebounce, bScroll]) // 下拉判断 useEffect(() => { if(!bScroll || !pullDown) return; const handlePullDown = (pos) => { //判断用户的下拉动作 if(pos.y > 50) { pullDownDebounce() } }; bScroll.on('touchEnd', handlePullDown) return () => { bScroll.off('touchEnd', handlePullDown) } }, [pullDown, pullDownDebounce, bScroll]) // 刷新实例 防止无法滑动 useEffect(() => { if(refresh && bScroll){ bScroll.refresh() } }) // 刷新组件 useImperativeHandle(ref, () => ({ // 暴露 refresh 方法 refresh() { if(bScroll) { bScroll.refresh(); bScroll.scrollTo(0, 0); } }, // 暴露 getBScroll 方法,提供 bs 实例 getBScroll() { if(bScroll) { return bScroll; } } })); ``` - 分类页看起来是一个简单的三级菜单?没错,简单的三级菜单而已。 - 路由搭建 ```js import React, { lazy, Suspense } from 'react' const KindComponent = lazy(() => import("../pages/kind")) export default [ { path: "/", element: , children: [ ... { path: "/kind", element: }, ] } ] ``` - 样式组件 `Lcontent` 和 `NavContainer` 以及 UI 组件 `Column` 组成一级菜单。 ```js ``` - 横向二级菜单的 UI 组件,这两个滑块组件具体可[查看对应源码](https://gitee.com/onepiece1205/react-dewu/tree/master/react-dewu/src/baseUI) ```js ``` - 使用 `useRef` 操作 Dom ,获取每一项的高度或者宽度进行累加初始化父元素的高或宽。 ```js useEffect(() => { let categoryDOM = Category.current let tagElems = categoryDOM.querySelectorAll("span") let totalWidth = 0 Array.from(tagElems).forEach(ele => { totalWidth += ele.offsetWidth }); totalWidth += 120 categoryDOM.style.width = `${totalWidth}px` }, []) ``` ## 搜索部分  - 路由搭建 ```js import React, { lazy, Suspense } from 'react' const SearchComponent = lazy(() => import("../pages/search")) export default [ { path: "/", element: , children: [ ... { path: "/search", element: }, ] } ] ``` - 搜索盒子 UI 组件 `SearchBox` ```js // useRef 监听输入 const queryRef = useRef() // 光标聚焦 useEffect(() => { queryRef.current.focus() }, []) // 防抖 缓存 import { debounce } from '../../utils/uiOptimization' let handleQueryDebounce = useMemo(() => { return debounce(handleQuery, 500) }, [handleQuery]); useEffect(() => { handleQueryDebounce(query) }, [query]) ``` ### 分解 Search - `axios` 请求准备 ```js export const getHotKeyWordsRequest = () => { return axiosInstance.get(`/search/hot`); } export const getResultList = (query) => { return axiosInstance.get(`/search/keywords=${query}`) } ``` - `mock` 拦截请求 ```js import Mock from "mockjs"; import { hotKeyWords } from './hot'; import shopAPI from './shop'; // 匹配接口 api 拦截请求 Mock.mock(/\/search\/hot/, "get", hotKeyWords); Mock.mock(/\/search\/keywords=.+/, "get", (options) => { let keyword = options.url.split('=')[1] return shopAPI.shops().data.items.filter((item) => { return item.title.indexOf(keyword) > 0 }) }) ``` ### redux 层开发 - 初始化 `state` ```js const defaultState = ({ hotKeyWords: [], // 热门关键词列表 enterLoading: false, resultList: [] }) ``` - 定义 `constants` ```js export const SET_HOT_KEYWRODS = "search/SET_HOT_KEYWRODS" export const SET_ENTER_LOADING = 'search/SET_ENTER_LOADING' export const SET_RESULT_LIST = 'search/SET_RESULT_LIST' ``` - 定义 `reducer` 函数 ```js import { produce } from 'immer' export const searchReducer = produce((state, action) => { switch(action.type) { case actionTypes.SET_HOT_KEYWRODS: state.hotKeyWords = action.data break; case actionTypes.SET_ENTER_LOADING: state.enterLoading = action.data break; case actionTypes.SET_RESULT_LIST: state.resultList = action.data break; } }, defaultState) ``` - 读者可在 `actionCreators.js` 文件查看具体的 `action` 函数,这里就不列出。 - 导出相关变量 ```js import { searchReducer } from './reducer' import * as actionCreators from './actionCreators' import * as constants from './constants' export { searchReducer, actionCreators, constants } ``` - `reducer` 注册全局 `store` ```js import { combineReducers } from "redux"; import { searchReducer } from '../pages/search/store'; export default combineReducers({ search: searchReducer, }); ``` - 做好这些准备工作,接下来就可以正式连接 `redux` 啦! ```js // index.jsx import { useDispatch, useSelector } from 'react-redux' const { hotKeyWords, enterLoading, resultList } = useSelector((state) => ({ hotKeyWords: state.search.hotKeyWords, enterLoading: state.search.enterLoading, resultList: state.search.resultList })) const dispatch = useDispatch() const getHotKeyWordsDataDispatch = () => { dispatch(actionTypes.getHotKeyWords()) } const getResultListDispatch = (q) => { dispatch(actionTypes.getResultGoodList(q)) } ``` - 搜索框为空时,展示热词列表 ```js useEffect (() => { setShow (true) if (!hotKeyWords.length) { getHotKeyWordsDataDispatch() } }, []) ``` - 点击热词,发送请求展示搜索结果 ```js const handleQuery = (q) => { setQuery (q); if(!q) return; dispatch(actionTypes.changeEnterLoading(true)); getResultListDispatch(q); } ``` - 这里就不具体展示 UI 搭建了,感兴趣的小伙伴可以[查看对应源码](https://gitee.com/onepiece1205/react-dewu/tree/master/react-dewu/src/pages/search) ## 项目的亮点和难点 - 此次项目路由使用了 `react-router` V6 全新版本,前期看官方英文文档花费了一番功夫 - 每个组件都应用 `memo` 包裹,使得 React 在更新组件之前进行 props 的比对,若 props 不变则不对组件更新,减少不必要的重渲染,使用 `useCallback` 优化父子组件函数的传递,使用 `useMemo` 优化父子组件对象/数组的传递 - 坚守前端 MVVM 的设计思想,组件化,模块化思想 - 解决刷新页面时 Tab 图片未选中问题 - 评论实时更新显示 `MVVM` ```js const createComment = (query) => { if (query.trim() != '') { // 前端数据更新 setData([...data, { body: query, meta: { created: Date.now() }}]) // 插入数据库 数据更新 loadCollection('comments') .then((collection) => { collection.insert([ { body: query } ]) }) .then(setQuery('')) } } ``` - `better-scroll` 原理是父级宽或高定死,子元素超过一屏长则滚动,并且其实例只对第一个子元素生效 - 页面跳转时使用 `react-transition-group` 制作动画衔接  分析问题,原因在于图片的状态丢失了,react-router 只保存的 `NavLink` 中 `className` 的 `isActive` 值,导致文字状态没变,但选中图片的状态回到了初始值,我的解决方法是通过引入`redux` 数据流 和 `localstorage` 让 Tab 标签的图片共享一个状态 `index`,部分代码如下:  由于篇幅,完[整代码看这里](https://gitee.com/onepiece1205/react-dewu/blob/master/react-dewu/src/layouts/HomeLayout.jsx) ## 总结 回过头梳理一下,可以说是实打实的项目经验,更重要的是,我们将性能优化由理论展开了实践,并在大大小小的组件封装过程中潜移默化地让大家体会react hooks的各种应用场景,可以说对React技术栈的同学是一个很好的巩固,对于之前掌握其他技术栈的同学也是一次新鲜的经历。 - 欢迎大家的[`star`](https://gitee.com/onepiece1205/react-dewu) - 项目还在更行维护,未来会加入购物车以及登录功能 - [线上地址看这里](http://47.99.137.223/)