今年夏天,通过 Faux Little Red Book 实践 React + Redux
我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!
前言
前段时间使用 React 框架及其他技术 简单地仿了一下小红书首页,然后经过近期对 Redux、React 的进一步学习后,我将项目的数据使用 Redux 进行了统一管理,对首页进行了优化和部分修改,加入了更好的用户体验,并新增了商城界面,让此项目的业务逻辑更加丰满。
在这篇文章中,我将会介绍 Redux 管理数据 以及页面的优化,并且分享在项目中遇到的问题及解决方法。如果对上个仿首页项目感兴趣的朋友可以看这篇文章 —— 手摸手教你:入门级React仿小红书首页 。看完上篇再来看这篇,将会更好地发现我在最初的基础上做了哪些修改以及优化。项目源码附在最后,注意查收。
技术栈简介
react(react-router)全家桶
:用于构建用户的MVVM框架redux
:状态管理容器redux-thunk
:处理异步逻辑的 redux 中间件react-lazyload
: react 懒加载库axios
: 用来请求后端api的数据styled-components
: 处理样式,体现css in js的前端工程化antd-mobile
:来自阿里的开源组件库fastmock
:免费的后端数据接口
此外,为了更好地将 Redux 产生的效果可视化,可以在浏览器中安装插件:Redux DevTools。然后就可以在浏览器的 Redux 界面看到每次的仓库状态以及效果。
项目实现
效果预览
项目结构
src/
api/ //网络请求代码和相关配置
assets/ //静态文件
font/ //图标文件
components/ //可复用的UI组件
common/ //通用组件
Footer/ //底部组件
pages/ //页面
Home/ //首页组件
Search/ //搜索组件
Shop/ //商城组件
routes/ //路由配置文件
store/ //redux 仓库
utils/ //工具类函数
App.jsx //根组件
main.jsx //入口文件
路由配置
使用 react-router
对路由进行配置,并放到单独文件夹中,使其更好管理。在配置中,采用 懒加载 Lazy 实现路由懒加载效果,可以优化路由加载效率,减少一些初始化的加载过程。在实现懒加载时,需要结合 Suspense 组件 一起使用。代码如下:
import React,{ lazy,Suspense }from 'react'
import { Route,Routes } from 'react-router-dom'
import Home from '../pages/Home'
const Mine = lazy(() => import('../pages/Mine'))
const Message = lazy(() => import('../pages/Message'))
const Shop = lazy(() => import('../pages/Shop'))
const Choose = lazy(() => import('../pages/Choose'))
const Search = lazy(() => import('../pages/Search'))
const RouteConfigs = () => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home/>}></Route>
<Route path="/home" element={<Home/>}></Route>
<Route path="/mine" element={<Mine/>}></Route>
<Route path="/message" element={<Message/>}></Route>
<Route path="/shop" element={<Shop/>}></Route>
<Route path='/choose' element={<Choose />}></Route>
<Route path='/search' element={<Search />}></Route>
</Routes>
</Suspense>
</div>
)
}
export default RouteConfigs
封装数据请求
使用 fastmock
模拟数据,在原先 api 文件夹中只有 request.js 文件的基础上新增了 config.js 文件,好处是对数据请求进行封装,让代码页面简洁,使其可读性更高。
config.js:
import axios from 'axios'
export const baseUrl =
"https://www.fastmock.site/mock/33e7fec4e60b54344eaa2c59a55b379d/red_book/red_book";
const axiosInstance = axios.create({
baseURL: baseUrl
})
axiosInstance.interceptors.response.use(
res => res.data,
err => {
console.log(err, '网络错误~~')
}
)
export { axiosInstance }
request.js:
import { axiosInstance } from "./config";
export const getListRequest = () => axiosInstance.get('/list')
export const getFoodRequest = () => axiosInstance.get('/food')
export const getShopRequest = () => axiosInstance.get('/shop')
export const getSportRequest = () => axiosInstance.get('/sport')
export const getSearchRequest = () => axiosInstance.get('/search')
Redux 配置
Redux 主要由三部分组成:store,reducer,action。store 和 action 都是对象,reducer 是函数。store 的作用是将 action 和 reducer 联系起来并改变 state。在项目复杂,数据繁多的情况下可以使用 Redux 来进行数据管理。
创建主仓库 store
主仓库用于管理各个分仓库的数据。store 是 Redux 中最重要的部分,所有的数据都在 store 中管理,所以可以优先编写 store。
index.js
import { createStore, compose, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk' // 异步数据管理
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 recommendReducer } from '../pages/Shop/Recommend/store/index'
import { reducer as sportlistReducer } from "../pages/Shop/Sport/store/index";
import { reducer as likelistReducer } from "../pages/Home/Like/store/index";
import { reducer as foodlistReducer } from "../pages/Home/Food/store/index";
import { reducer as searchlistReducer } from "../pages/Search/store/index";
export default combineReducers({ // 引入并合并分仓库
recommend: recommendReducer,
sportlist: sportlistReducer,
likelist: likelistReducer,
foodlist: foodlistReducer,
searchlist: searchlistReducer
})
注意,还需要在入口文件 main.jsx
中加入 Provider
声明式开发
main.jsx
import {Provider} from 'react-redux'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)
创建分仓库 store
在需要的页面下创建分仓库,来管理每个页面的状态和数据,下面以 Search
搜索页面为例,来展示 Search
分仓库的建立。
actionCreators.js 管理相应函数来更新数据状态
import * as actionTypes from './constants'
import { getSearchRequest } from '../../../api/request'
export const changeSearch = (data) => ({
type: actionTypes.CHANGE_SEARCH,
data
})
export const changeEnterLoading = (data) => ({
type: actionTypes.ENTER_LOADING,
data
})
export const getSearch= () => {
return (dispatch) => {
getSearchRequest().then(data => {
dispatch(changeSearch(data))
dispatch(changeEnterLoading(false))
})
}
}
reducer.js 操作数据
import * as actionTypes from './constants'
const defaultState = {
search: [],
enterLoading:false
}
const reducer = (state=defaultState, action) => {
switch(action.type) {
case actionTypes.CHANGE_SEARCH:
return {
...state,
search: action.data
}
case actionTypes.ENTER_LOADING:
return {
...state,
enterLoading: action.data,
}
default:
return state;
}
}
export default reducer
index.js 向外输出 actionCreators、reducer 和 constants
import reducer from './reducer'
import * as actionCreators from './actionCreators'
import * as constants from './constants';
export {
reducer,
actionCreators,
constants
}
constants.js 配置文件给 type 取别名
export const CHANGE_SEARCH = 'CHANGE_SEARCH'
export const ENTER_LOADING = 'ENTER_LOADING'
页面连接仓库
该仓库是为 Search
搜索页面服务的,因此需将 Search
的 index.jsx 与仓库连接,代码如下:
index.jsx
import { connect } from "react-redux";
import { getSearch,changeEnterLoading } from "./store/actionCreators";
....
const mapStateToProps = (state) => {
return {
search: state.searchlist.search,
enterLoading:state.searchlist.enterLoading
}
}
const mapDispatchToProps = (dispatch) => {
return {
getSearchDispatch(query) {
dispatch(getSearch(query))
},
changeEnterLoadingDispatch(data) {
dispatch(changeEnterLoading(data))
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(React.memo(Search))
然后在需要的页面做同样的操作,项目的 Redux 配置就这样搭建完毕。启动项目后打开 Redux 便可以清晰的看到所有数据。
页面的变化
这次的页面在布局方式、用户体验以及业务逻辑等方面做出了相应改变,比如瀑布流布局、增加了点赞收藏等,并实现了之前没有实现的其他细节,比如吸顶式导航。
瀑布流布局
页面一改最初的布局风格,将其换成更具特色的瀑布流布局。瀑布流不是什么狂拽酷炫吊炸天的特效,而是一种 限宽不限高 的页面布局方式。它能让界面看起来更加美观清爽。看起来很高级,实现起来却很简单,只需在样式中加入以下几行代码:
.container{ // 父容器
column-count: 2;
column-gap: 10px;
.list{ // 子容器
width: 100%;
break-inside: avoid;
}
注意,整个项目采取的是 styled-components
处理样式,所以此处样式的格式会和传统的格式看起来有些许不一样。
实现瀑布流式布局的关键点在于 column-count
属性,使用 column-count
属性将一个盒子分为多列展示数据。
下拉刷新
下拉刷新使用的是 antd-mobile
中的 PullToRefresh
组件,可以自定义下拉或者下拉后的显示内容。在页面中加入下拉刷新,可以增加用户体验。
import { PullToRefresh } from 'antd-mobile'
import { sleep } from 'antd-mobile/es/utils/sleep'
const XXX = (props) =>{
async function doRefresh() {
await sleep(1000);
}
return(
<PullToRefresh
onRefresh={doRefresh}
canReleaseText={<h4>用力拉</h4>}
completeText={ <h4>好啦</h4>}
refreshingText={<h4>玩命加载中</h4>}>
<ListWrapper>
......
</ListWrapper>
</PullToRefresh>
)
}
关于它的更多用法可以参考 antd-mobile官方文档 ,里面会介绍地更加详细。
点赞收藏
看过上篇项目文章的朋友都知道,原来的首页中未能实现点赞收藏,因此页面十分单调。这次将点赞收藏加入首页,让页面的业务逻辑更加丰富,并给用户带来更好的交互感。具体实现如下:
import React,{useState} from 'react'
import classnames from 'classnames'
const ListThree=({source}) =>{
const [isLike, SetIsLike] = useState(false) // 定义状态
const changeLike = () => { // 设置状态
SetIsLike(!isLike)
}
return (
<div>
<div className="list">
......
<i className={classnames("iconfont",
{"icon-aixin1": !isLike},
{"icon-aixin2": isLike},
{"active": isLike}
)} onClick={()=>changeLike()}>
</i>
......
</div>
)
}
export default ListThree
定义状态,设置状态取反函数,用 classname
动态类名控制最初样式。当点击图标时触发取反函数,让图标发生改变。
图片懒加载
图片懒加载的加入,能让首屏的加载速度提升。从远程请求过来的图片资源在需要的时候加载出来,不需要的时候使用占位图片,进而优化用户体验。具体实现如下:
import LazyLoad from 'react-lazyload'
import red from '../../assets/red.jpg'
const ListOne=({info}) =>{
return (
<div>
<div className="list">
<LazyLoad
placeholder={<img width="100%"
height="100%" src={red} className="list-img"/>}>
<img src={info.img} className="list-img" />
</LazyLoad>
......
</div>
</div>
)
}
export default ListOne
其他细节
在其他方面,我将 css 样式部分进行了微调,美化了页面,并保留了页面上原有的细节。将导航栏实现吸顶,让页面在往下滑动的过程中使其一直保持在上方。
新增的商城页面
这次新增了商城页面模块,整体设计和布局与首页一样,都采用了瀑布流式布局、下拉刷新、懒加载等。在商城页面中加入了一个弹出框,Grid 布局 以及购物车。
弹出框以及 Grid 布局
弹出框使用的是 antd-mobile
中的 Popup
组件,从屏幕中弹出或滑出一块自定义内容区。可以用 position 指定弹出的位置,bodyStyle 设置内容区域样式。实现如下:
import { Popup } from "antd-mobile";
const Shop = () =>{
const [visible2, setVisible2] = useState(false)
<Popup
visible={visible2}
onMaskClick={() => { setVisible2(false) }}
position='top'
bodyStyle={{ height: '215px' }}>
......
</Popup>
}
export default Shop
弹出框里面的内容采用强大的 Grid 网格布局 。grid-template-columns
属性声明每一列宽度。grid-template-rows
属性声明每一行的高度。grid-gap
属性声明行间距和列间距。repeat()函数 用来简化重复的值。实现如下:
.grid{
padding: 15px;
display: grid;
grid-template-columns: repeat(4, 75px);
grid-gap: 15px;
grid-template-rows: 60px 60px;
}
购物车
在商城页面上有个购物车小图标,在页面滑动的过程中始终保持不动,同吸顶导航栏原理一样,添加如下代码即可:
position: fixed;
bottom: 80px;
right: 30px;
z-index: 9999;
比较遗憾的是未能实现购物车的相关功能。但是后面会继续将此项目继续完善,完成购物车功能。
搜索功能
虽然未能实现购物车,但是实现了搜索功能。这次页面中最大的改变就是通过 Redux 完成了简单的模糊搜索功能。
搜索分为两部分,由 Search
组件和 SearchBox
组件完成,并在搜索上加入了防抖函数,具体实现如下:
export const debounce = (func, delay) => {
let timer;
return function (...args) {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, args);
clearTimeout(timer);
}, delay);
};
};
用户输入数据后,对数据进行防抖处理,然后将输入的值传入 Search
组件,Search
组件根据传过来的值 dispatch
并对返回的搜索结果进行渲染。在项目中的数据里,有 title
关键字和 content
内容两种标签,当不论触发那种标签时,都可以显示出结果列表。如下所示:
search.filter
(item => item.title.indexOf(query) != -1||item.content.indexOf(query)!=-1)
具体实现方式及代码可以通过文章最后的项目源码地址查看。
性能优化
懒加载
懒加载简单来说就是延迟加载或按需加载,加上了它,就会有加载速度上的优势,能带来更好的用户体验。
路由懒加载
结合lazy
和Suspense
实现懒加载,具体实现可以看前面的路由配置部分。
图片懒加载
可以减少http
的请求,在用户需要的时候加载出图片,提高页面加载速度,具体实现可以看前面的图片懒加载部分。
memo
memo 的作用是可以实现减少渲染重复未变数据。如果你的组件在相同 props
的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。
总结
以上就是我对这整个项目的分享了。在这个项目中,我借鉴了来自 神三元大佬 的 loading 等组件以及 debounce 防抖函数。本项目后期还会继续优化和完善相应功能,如果觉得还不错的话,欢迎点赞收藏和评论。另外,如果有什么问题或者建议欢迎在评论区讨论。这个夏天,让我们一起动起来 o( ̄▽ ̄)
项目源码地址:red_book