这都拿不下你?React + Redux 让你一眼就爱上的哔哩哔哩会员购

语言: CN / TW / HK

theme: cyanosis highlight: vs2015


我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

继上一篇文章React项目开发-仿哔哩哔哩移动端首页之后,我经过一段时间的学习(主要是对Redux的学习)之后,写下了这篇关于 React + Redux 项目的文章,新增了会员购界面,对首页进行了改进,优化了用户体验,使用 Redux 对状态统一进行管理。

前言

1. Redux 概述

Redux 是一个使用叫做 action 的事件来管理和更新应用状态的模式和工具库,它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

2. 为什么要使用 Redux?

Redux 提供的模式和工具让我们更容易理解应用程序中的状态何时、何地、为什么以及如何更新,以及当这些更改发生时应用程序逻辑将如何表现。

3. 我们应该如何使用 Redux?

Redux 的应用场景:

  • 在应用的大量地方,都存在大量的状态
  • 应用状态会随着时间的推移而频繁更新
  • 更新该状态的逻辑可能很复杂
  • 中型和大型代码量的应用,很多人协同开发

Redux 工作流

为了更好的理解,我们把 Redux 工作流比作图书馆借书流程,当我们(Component)向管理员(Store)发出一个借书行为(Action)时,管理员接收到后,对照借书记录本(Reducers)查看,管理员拿到新书(一个新的状态)后交给我们。

image.png

项目预览:Github Pages

项目准备

安装依赖

在使用 redux 之前,我们需要在之前的基础上安装以下依赖(默认安装最新版本):

npm i redux npm i react-redux npm i redux-thunk npm i redux-logger

redux-thunk 主要的功能就是让我们可以 dispatch 一个函数,applyMiddlewareRedux 的一个原生方法,可将所有中间件组成一个数组,依次执行。

```js 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 界面可以看到当前仓库状态:

Snipaste_2022-07-24_20-52-39.png

redux-logger 会在 dispatch 改变仓库状态的时候打印出旧的仓库状态、当前触发的action以及新的仓库状态。

Snipaste_2022-07-24_20-54-54.png

实现功能

用户体验方面

1. 瀑布流布局 + 图片懒加载

multi-column 布局中子元素的排列顺序是先从上往下再从左至右的,让上下相邻的子元素分开使用 margin-bottom 即可。

  • 瀑布流的优点如下:
  1. 节省空间,外表美观,更有艺术性。
  2. 对于触屏设备非常友好,通过向上滑动浏览。
  3. 用户浏览时的观赏和思维不容易被打断,留存更容易。

实现代码:

css /* 父容器 */ .container { column-count: 2; // 两列布局 column-gap: 10px; // 列间距为 10px } /* 子元素 */ .good-box { width: 100%; break-inside: avoid; // 元素不能中断,auto 可以中断 }

实现效果:

Snipaste_2022-07-24_23-58-20.png

当从远程请求过来的图片还没加载出来时,使用默认图片进行占位,优化用户体验。这里我勾选浏览器中的禁用缓存模拟了一下效果:

Snipaste_2022-07-25_00-16-13.png

2. 页面切换

页面切换的效果我使用了 CSSTransition 对子元素进行包裹,它会将过渡类型给到子元素,添加动画效果。

实现代码:

```jsx import { CSSTransition } from 'react-transition-group' ...

<CSSTransition in={show} // 控制动画的开关 timeout={300} // 动画执行时间 appear={true} // 第一次加载该组件时启用相应的动画渲染 classNames="fly" unmountOnExit // 动画效果消失时,该标签会从 dom 树上移除

... ```

```js 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); } ```

效果如下:

chrome-capture-2022-6-25.gif

3. 加载动画

当我们首次进入到首页或会员购页面时,图片资源是不能瞬间得到的,这里我使用了 antd-mobile 的动态骨架屏,让页面更丰富,填补了等待时间段,提升用户体验。

在搜索界面,我仿造了神三元写的 loading 组件,这是一个在等待请求过程中的动画效果。loading 动画主要实现代码如下:

```jsx import React from 'react'; import styled, { keyframes } from 'styled-components';

const loading = keyframes0%, 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 (

); }

export default React.memo(Loading); ```

效果如下:

chrome-capture-2022-6-25.gif

业务方面

1. 商品收藏

chrome-capture-2022-6-24.gif

这里可能会遇到的问题,点击收藏某件商品时,把列表中的所有商品都收藏了,解决方法:

把商品组件进行单独封装作为子组件,父组件将 good 传递给子组件,子组件拿到单独的 id,在进行之后的操作时,就不会对其它的子组件造成影响。

```jsx import React from 'react' import propTypes from "prop-types"; import { Wrapper } from './style' import GoodsItem from '@/components/GoodsItem';

export default function GoodsList({goodsList}) {

return (

{ goodsList && goodsList.map(good => ( )) }
) }

GoodsList.propTypes = { goodsList: propTypes.array.isRequired } ```

子组件 GoodsItem 中收藏效果的实现代码如下:

```jsx import React, { useState } from "react" import classnames from 'classnames'

const GoodsItem = ({good}) => { const [isColl, setIsColl] = useState(false) // 定义收藏状态

const changeColl = () => { // 对状态进行取反 setIsColl(!isColl) }

return (

...
... {/ 当 isColl 为 true 时,使用 classnames 添加相应的样式,否则为默认样式 /} {'icon-aixin1': isColl}, {'active': isColl} )} onClick={() => changeColl()} > {/ isColl 为 true 时,收藏量+1,否则不变 /} {isColl ? good.collection + 1 : good.collection}
) }

// 性能优化 export default React.memo(GoodsItem) ```

2. 防抖搜索功能

首页搜索功能由父组件 HomeSearch 和子组件 SearchBox 实现。

首页搜索

chrome-capture-2022-6-24 (1).gif

会员购搜索功能的功能由父组件 VipSearch 和子组件 SearchBox 实现。

会员购搜索

chrome-capture-2022-6-24 (2).gif

在搜索上我加上了防抖功能,防抖函数和其他函数放到 util 文件夹下的 index.js 下作为工具使用。

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 对状态进行修改。

jsx // useMomo 可以缓存 上一次函数计算的结果 let handleQueryDebounce = useMemo(() => { return debounce(handleQuery, 500) // 每隔 0.5s 执行一次 }, [handleQuery]) // 使用 useEffect 去更新 useEffect(() => { handleQueryDebounce(query) }, [query])

```jsx // 父组件 const VipSearch = (props) => { ... // 输入时每隔 0.5s 执行一次 useEffect(() => { if (query.trim()) { changeEnterLoadingDispatch(true) getGoodsListDispatch(query) } }, [query]) // 对商品标题进行模糊查询,将搜索到的商品进行渲染 const renderGoodsList = () => { return (

商品列表

{ goodsList.filter(good => good.title.indexOf(query) != -1 ).map(good => { return ( ) }) }
) }

return ( navigate(-1)}>取消 ... { enterLoading && } ) }

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 进行封装,当接口数量较多时,能够减少代码量,使页面更简洁:

```js import axios from 'axios' export const baseUrl = "http://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,当网络图片还没加载出来时,使用本地默认图片进行占位,主要代码如下:

```jsx import LazyLoad from 'react-lazyload' import bilibili from '@/assets/images/bilibili.jpeg'

... <LazyLoad placeholder={}

... ```

4. memo性能优化

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。但React.memo 仅检查 props 的变更。

5. 全局样式风格文件

在项目开发过程中,我们在不同的页面中可能会用到相同的样式,例如背景颜色,字体大小,边框等等,将相同的样式抽离出来放到 assets 文件夹下的 global-style.js 中,便于对样式进行统一管理。

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)" }

定义了全局样式风格文件后,我们就可以在其他的样式文件中进行引用,如下:

```js import styled from "styled-components" import style from '@/assets/global-style'

export const Wrapper = styled.divbackground: ${style["background-color"]}; ... ```

最后

在这个项目中,我借鉴了神三元大佬的网易云音乐项目中的 CSSTransition 组件, loading 组件和 debounce 防抖函数等,项目地址如下:github源码地址,如果有兴趣的小伙伴也可以去瞧瞧他写的掘金小册React Hooks 与 Immutable 数据流实战。 本项目在后期仍会继续改进,实现更多功能,谢谢大家!未完待续......

源码地址:bilibili-page

项目预览:GitHub Pages