这都拿不下你?React + Redux 让你一眼就爱上的哔哩哔哩会员购
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
)查看,管理员拿到新书(一个新的状态
)后交给我们。
项目预览:Github Pages
项目准备
安装依赖
在使用 redux 之前,我们需要在之前的基础上安装以下依赖(默认安装最新版本):
npm i redux
npm i react-redux
npm i redux-thunk
npm i redux-logger
redux-thunk
主要的功能就是让我们可以 dispatch
一个函数,applyMiddleware
是 Redux 的一个原生方法,可将所有中间件组成一个数组,依次执行。
```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
界面可以看到当前仓库状态:
redux-logger
会在 dispatch
改变仓库状态的时候打印出旧的仓库状态、当前触发的action以及新的仓库状态。
实现功能
用户体验方面
1. 瀑布流布局 + 图片懒加载
multi-column
布局中子元素的排列顺序是先从上往下再从左至右的,让上下相邻的子元素分开使用 margin-bottom
即可。
- 瀑布流的优点如下:
- 节省空间,外表美观,更有艺术性。
- 对于触屏设备非常友好,通过向上滑动浏览。
- 用户浏览时的观赏和思维不容易被打断,留存更容易。
实现代码:
css
/* 父容器 */
.container {
column-count: 2; // 两列布局
column-gap: 10px; // 列间距为 10px
}
/* 子元素 */
.good-box {
width: 100%;
break-inside: avoid; // 元素不能中断,auto 可以中断
}
实现效果:
当从远程请求过来的图片还没加载出来时,使用默认图片进行占位,优化用户体验。这里我勾选浏览器中的禁用缓存模拟了一下效果:
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);
}
```
效果如下:
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); ```
效果如下:
业务方面
1. 商品收藏
这里可能会遇到的问题,点击收藏某件商品时,把列表中的所有商品都收藏了,解决方法:
把商品组件进行单独封装作为子组件,父组件将 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.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 (
// 性能优化 export default React.memo(GoodsItem) ```
2. 防抖搜索功能
首页搜索功能由父组件 HomeSearch
和子组件 SearchBox
实现。
首页搜索
会员购搜索功能的功能由父组件 VipSearch
和子组件 SearchBox
实现。
会员购搜索
在搜索上我加上了防抖功能,防抖函数和其他函数放到 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 (
商品列表
return (
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