我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!
继上一篇文章React项目开发-仿哔哩哔哩移动端首页之后,我经过一段时间的学习(主要是对Redux的学习)之后,写下了这篇关于 React + Redux 项目的文章,新增了会员购界面,对首页进行了改进,优化了用户体验,使用 Redux 对状态统一进行管理。
前言
1. Redux 概述
Redux 是一个使用叫做 action
的事件来管理和更新应用状态的模式和工具库,它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。
2. 为什么要使用 Redux?
Redux 提供的模式和工具让我们更容易理解应用程序中的状态何时、何地、为什么以及如何更新,以及当这些更改发生时应用程序逻辑将如何表现。
3. 我们应该如何使用 Redux?
Redux 的应用场景:
- 在应用的大量地方,都存在大量的状态
- 应用状态会随着时间的推移而频繁更新
- 更新该状态的逻辑可能很复杂
- 中型和大型代码量的应用,很多人协同开发
Redux 工作流
为了更好的理解,我们把 Redux
工作流比作图书馆借书流程,当我们(Component
)向管理员(Store
)发出一个借书行为(Action
)时,管理员接收到后,对照借书记录本(Reducers
)查看,管理员拿到新书(一个新的状态
)后交给我们。
项目预览:Github Pages
项目准备
安装依赖
在使用 redux 之前,我们需要在之前的基础上安装以下依赖(默认安装最新版本):
npm i redux
npm i react-redux
npm i redux-thunk
npm i redux-logger
redux-thunk
主要的功能就是让我们可以 dispatch
一个函数,applyMiddleware
是 Redux 的一个原生方法,可将所有中间件组成一个数组,依次执行。
import { createStore, compose, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk' // 异步数据管理
import logger from 'redux-logger' // 让 redux 调试更优秀
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose // 激活 redux devtools
const store = createStore(reducer,
// 合并成一个中间件对象
compose(
composeEnhancers(applyMiddleware(thunk)),
applyMiddleware(logger)
)
)
export default store
安装插件
为了能够看到效果,我们还需在浏览器中(建议使用Chrome)安装插件:Redux DevTools,Redux DevTools插件下载地址。
当我们激活了 redux devtools
后,切换到浏览器的 Redux
界面可以看到当前仓库状态:
redux-logger
会在 dispatch
改变仓库状态的时候打印出旧的仓库状态、当前触发的action以及新的仓库状态。
实现功能
用户体验方面
1. 瀑布流布局 + 图片懒加载
multi-column
布局中子元素的排列顺序是先从上往下再从左至右的,让上下相邻的子元素分开使用 margin-bottom
即可。
- 瀑布流的优点如下:
- 节省空间,外表美观,更有艺术性。
- 对于触屏设备非常友好,通过向上滑动浏览。
- 用户浏览时的观赏和思维不容易被打断,留存更容易。
实现代码:
/* 父容器 */
.container {
column-count: 2; // 两列布局
column-gap: 10px; // 列间距为 10px
}
/* 子元素 */
.good-box {
width: 100%;
break-inside: avoid; // 元素不能中断,auto 可以中断
}
实现效果:
当从远程请求过来的图片还没加载出来时,使用默认图片进行占位,优化用户体验。这里我勾选浏览器中的禁用缓存模拟了一下效果:
2. 页面切换
页面切换的效果我使用了 CSSTransition
对子元素进行包裹,它会将过渡类型给到子元素,添加动画效果。
实现代码:
import { CSSTransition } from 'react-transition-group'
...
<CSSTransition
in={show} // 控制动画的开关
timeout={300} // 动画执行时间
appear={true} // 第一次加载该组件时启用相应的动画渲染
classNames="fly"
unmountOnExit // 动画效果消失时,该标签会从 dom 树上移除
>
<Wrapper>
...
</Wrapper>
</CSSTransition>
import styled from "styled-components"
export const Wrapper = styled.div`
...
/* CSSTransition 过度类型给children */
&.fly-enter,&.fly-appear {
opacity: 0;
/* 启用GPU加速 */
transform: translate3d(100%, 0, 0);
}
&.fly-enter-active, &.fly-apply-active {
opacity: 1;
transition: all .3s;
transform: translate3d(0, 0, 0);
}
&.fly-exit {
opacity: 1;
transform: translate3d(0,0,0)
}
&.fly-exit-active {
opacity: 0;
transition: all .3s;
transform: translate3d(100%, 0, 0);
}
`
效果如下:
3. 加载动画
当我们首次进入到首页或会员购页面时,图片资源是不能瞬间得到的,这里我使用了 antd-mobile
的动态骨架屏,让页面更丰富,填补了等待时间段,提升用户体验。
在搜索界面,我仿造了神三元写的 loading
组件,这是一个在等待请求过程中的动画效果。loading
动画主要实现代码如下:
import React from 'react';
import styled, { keyframes } from 'styled-components';
const loading = keyframes`
0%, 100% {
transform: scale(0.0);
}
50% {
transform: scale(1.0);
}
`
const LoadingWrapper = styled.div`
>div {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
margin: auto;
width: 60px;
height: 60px;
opacity: 0.6;
border-radius: 50%;
background-color: rgba(0, 150, 250, 0.8); // 这里可以选择自己喜欢的颜色
animation: ${loading} 1.4s infinite ease-in; // 动画持续时间
}
>div:nth-child(2) {
animation-delay: -0.7s; // 跳过 0.7s 进入动画周期
}
`
function Loading() {
return (
<LoadingWrapper>
<div></div>
<div></div>
</LoadingWrapper>
);
}
export default React.memo(Loading);
效果如下:
业务方面
1. 商品收藏
这里可能会遇到的问题,点击收藏某件商品时,把列表中的所有商品都收藏了,解决方法:
把商品组件进行单独封装作为子组件,父组件将 good
传递给子组件,子组件拿到单独的 id
,在进行之后的操作时,就不会对其它的子组件造成影响。
import React from 'react'
import propTypes from "prop-types";
import { Wrapper } from './style'
import GoodsItem from '@/components/GoodsItem';
export default function GoodsList({goodsList}) {
return (
<Wrapper>
<div className="container">
{
goodsList && goodsList.map(good => (
<GoodsItem key={good.id} good={good} />
))
}
</div>
</Wrapper>
)
}
GoodsList.propTypes = {
goodsList: propTypes.array.isRequired
}
子组件 GoodsItem
中收藏效果的实现代码如下:
import React, { useState } from "react"
import classnames from 'classnames'
const GoodsItem = ({good}) => {
const [isColl, setIsColl] = useState(false) // 定义收藏状态
const changeColl = () => { // 对状态进行取反
setIsColl(!isColl)
}
return (
<div className="good-box" key={good.id}>
...
<div className="price_coll">
...
<span>
{/* 当 isColl 为 true 时,使用 classnames 添加相应的样式,否则为默认样式 */}
<i
className={classnames(
'iconfont',
{'icon-aixin3': !isColl},
{'icon-aixin1': isColl},
{'active': isColl}
)}
onClick={() => changeColl()}
>
</i>
{/* isColl 为 true 时,收藏量+1,否则不变 */}
<span>{isColl ? good.collection + 1 : good.collection}</span>
</span>
</div>
</div>
)
}
// 性能优化
export default React.memo(GoodsItem)
2. 防抖搜索功能
首页搜索功能由父组件 HomeSearch
和子组件 SearchBox
实现。
首页搜索
会员购搜索功能的功能由父组件 VipSearch
和子组件 SearchBox
实现。
会员购搜索
在搜索上我加上了防抖功能,防抖函数和其他函数放到 util
文件夹下的 index.js
下作为工具使用。
// 防抖函数
export const debounce = (func, delay) => {
let timer
return function (...args) {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
func.apply(this, args)
clearTimeout(timer)
}, delay)
}
}
在子组件 SearchBox
中修改 query
,进行防抖处理,每隔500
毫秒执行一次 handleQuery
去更新父组件 VipSearch
中的 query
,并通过 dispatch
对状态进行修改。
// useMomo 可以缓存 上一次函数计算的结果
let handleQueryDebounce = useMemo(() => {
return debounce(handleQuery, 500) // 每隔 0.5s 执行一次
}, [handleQuery])
// 使用 useEffect 去更新
useEffect(() => {
handleQueryDebounce(query)
}, [query])
// 父组件
const VipSearch = (props) => {
...
// 输入时每隔 0.5s 执行一次
useEffect(() => {
if (query.trim()) {
changeEnterLoadingDispatch(true)
getGoodsListDispatch(query)
}
}, [query])
// 对商品标题进行模糊查询,将搜索到的商品进行渲染
const renderGoodsList = () => {
return (
<Wrapper>
<h3 style={{"margin": "5px"}}>商品列表</h3>
<div className="container">
{
goodsList.filter(good =>
good.title.indexOf(query) != -1
).map(good => {
return (
<GoodsItem key={good.id} good={good} />
)
})
}
</div>
</Wrapper>
)
}
return (
<Container>
<HeaderWrapper>
<SearchBox
newQuery={query}
handleQuery={handleQuery}>
</SearchBox>
<span onClick={() => navigate(-1)}>取消</span>
</HeaderWrapper>
...
{ enterLoading && <EnterLoading><Loading></Loading></EnterLoading> }
</Container>
)
}
const mapStateToProps = (state) => {
return {
enterLoading: state.vipsearch.enterLoading,
goodsList: state.vipsearch.goodsList
}
}
const mapDispatchToProps = (dispatch) => {
return {
changeEnterLoadingDispatch(data) {
dispatch(changeEnterLoading(data))
},
getGoodsListDispatch(query) {
dispatch(getGoodsList(query))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(VipSearch))
优化
1. 封装网络请求
在 api
文件夹下,新添加了 config.js
文件,用来对对象 axiosInstance
进行封装,当接口数量较多时,能够减少代码量,使页面更简洁:
import axios from 'axios'
export const baseUrl = "https://www.fastmock.site/mock/059647e88be0d33ef58d6ab4bf009dd9/bilibili"
// 单例设计模式
const axiosInstance = axios.create({
baseURL: baseUrl
})
// 添加响应拦截,拿到数据时对数据做处理,或抛出错误
axiosInstance.interceptors.response.use(
res => res.data,
err => {
console.log(err, '网络错误~')
}
)
export { axiosInstance }
2. 骨架屏占位
因为大多数图片资源是从 fastmock
中请求过来的,受网络影响需一些时间,用户在等待的过程中页面出现空白状态很影响体验,引入骨架屏让页面更丰富,填补了等待时间段,优化了用户体验。此项目中我使用了 antd-mobile
中的动态骨架屏,Skeleton 骨架屏。
3. 图片懒加载
图片懒加载也叫“按需加载”,也就是当图片资源出现在视口区域内,才会被加载,使用懒加载能大大节省网站的流量,对于有大量图片资源的网站来说显得尤为重要。
这里我使用了 LazyLoad
,当网络图片还没加载出来时,使用本地默认图片进行占位,主要代码如下:
import LazyLoad from 'react-lazyload'
import bilibili from '@/assets/images/bilibili.jpeg'
...
<LazyLoad
placeholder={<img width="100%"
height="100%" src={bilibili}/>}
>
<img src={good.img} />
</LazyLoad>
...
4. memo性能优化
如果你的组件在相同 props
的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。但React.memo
仅检查 props
的变更。
5. 全局样式风格文件
在项目开发过程中,我们在不同的页面中可能会用到相同的样式,例如背景颜色,字体大小,边框等等,将相同的样式抽离出来放到 assets
文件夹下的 global-style.js
中,便于对样式进行统一管理。
export default {
"background-color": "rgba(50, 50, 50, 0.06)",
"search_bar-color": "rgba(50, 50, 50, 0.08)",
"border-color": "rgba(50, 50, 50, 0.2)",
"loading-color": "rgba(0, 150, 250, 0.8)"
}
定义了全局样式风格文件后,我们就可以在其他的样式文件中进行引用,如下:
import styled from "styled-components"
import style from '@/assets/global-style'
export const Wrapper = styled.div`
background: ${style["background-color"]};
...
`
最后
在这个项目中,我借鉴了神三元大佬的网易云音乐项目中的 CSSTransition
组件, loading
组件和 debounce
防抖函数等,项目地址如下:github源码地址,如果有兴趣的小伙伴也可以去瞧瞧他写的掘金小册React Hooks 与 Immutable 数据流实战。 本项目在后期仍会继续改进,实现更多功能,谢谢大家!未完待续......
源码地址:bilibili-page
项目预览:GitHub Pages