React-Hooks + Redux项目实战:仿酷狗移动端页面开发

1,390 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

一、 前言

前段时间用react做了个仿酷狗demo,其中的数据是用useState来管理的。最近学习了redux,在上次项目的基础上,将数据管理由useState升级为了redux,也对项目进行了优化,新增了页面。总的来说是一个比较容易上手的react-hooks+redux项目。

1. 技术栈

  • axios:它是一个基于 promise 的 HTTP 库,简单的讲就是可以发送get、post请求来获取后端数据。

  • antd-mobile: 阿里蚂蚁集团基于React技术栈设计构建的的移动端开源组件库,可参照官方文档使用所需的组件。

  • styled-components:  即css in js,更好的实现了React的组件化思想样式。可以使用变量、继承,使用起来更自由,更灵活。

  • font-awesome: 好用的字体图标库。

  • redux 全局数据状态管理工具

  • react-lazyload 延迟加载图片,先用占位符占位,图片在可视区时加载

  • react-transition-group 提供CSS过渡动画

二、 实用业务

1. loading组件

  • 当页面加载请求数据时,会有延迟,在等待数据到达的过程中添加loading效果。
  • 加载动画封装在loading组件中,方便复用。

效果

loading-3.gif 代码

import React from "react";
import styled, { keyframes } from "styled-components";

const loading = keyframes`
    50%{
        transform:rotatez(180deg) scale(1.5);
        border-style:dotted;
    }
    100%{
        transform:rotatez(360deg) scale(0.9);
    }
`
const LoadingWrapper = styled.div`
        position: fixed;
        top: 0; left: 0; right: 0; bottom: 0;
        margin: auto;
        width: 50px;
        height: 50px;
    .loader {
        display: flex;
        text-align: center;
    }
    .loader::before{
        content:"";
        color:white;
        height:25px;
        width:25px;
        background:transparent;
        border-radius:50%;
        border:0.625rem solid blue;
        border-color:#1565C0 #26C6DA;
        animation:${loading} 1.5s infinite;
    }
    .title {
        width: 4rem;
        color: #666;
        margin-top: 0.5rem;
    }
`
function Loading() {
    return (
        <LoadingWrapper>
            <div className="loader"></div>
            <div className="title">加载中...</div>
        </LoadingWrapper>
    )
}

export default Loading

2. CSS动画过渡

  • 页面跳转过渡效果由CSSTransition实现,将跳转后显示的内容包裹起来,进入页面和退出时即可起到渐入和渐出。

csstransition-2.gif

import { CSSTransition } from "react-transition-group"; 

const [show, setShow] = useState(false);
useEffect(() => { // 进入页面触发过渡动画
    setShow(true)
}, [])
...

<CSSTransition
      in={show} // 动画过渡开关
      timeout={300} // 动画执行时间
      appear={true} // 是否第一次加载该组件时启用相应的动画渲染
      unmountOnExit
      classNames="show" // 指定动画类名的前缀,配合下方css实现动画效果
    >
    <Container>
        ...  
        //跳转后页面内容
    </Container>
</CSSTransition>
import styled from "styled-components";

export const Container = styled.div`
    &.show-enter,&.show-appear {
        /* 初始状态 */
        opacity: 0;
        /* 启用GPU加速 */
        transform: translate3d(100%, 0, 0);
    }
    &.show-enter-active, &.show-apply-active {
        /* 进入之后的状态 */
        opacity: 1;
        transition: all .3s;
        transform: translate3d(0, 0, 0);
    }
    &.show-exit {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }
    &.show-exit-active {
        opacity: 0;
        transition: all .3s;
        transform: translate3d(100%, 0, 0);
    }

三、redux接手数据

1. redux工作流程

  • 可理解为图书馆借书事件。学生(Component)对管理员(Store)说要借什么书(Action),管理员对照记录本(Reducer)查看,由此得到新书。 redux.png

2. 主仓库

  • 在src下创建store文件夹,包含以下两个文件。
store.jpg
  1. index.js
import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk' // 中间件
import reducer from './reducer'

const composeEnhancers = 
    // 激活redux devtools插件
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore( // 通过createStor 创建store仓库
    reducer,  
    composeEnhancers(
        // thunk中间件 能够处理异步数据流 
        applyMiddleware(thunk)
    )    
)

export default store;
  1. reducer.js Redux 提供了一个combineReducers方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer。
import { combineReducers } from 'redux'
import { reducer as homeReducer } from '../pages/Home/store'
import { reducer as rankReducer } from '../pages/Rank/store'
import { reducer as playReducer } from '../components/Player/store'

export default combineReducers({ 
    home: homeReducer,
    rank: rankReducer,
    play: playReducer
})

3. 分仓库、redux实现删除歌曲功能

分仓库提供reducer给主仓库,以此形成不同的数据分支。播放列表仓库结构如下

fen.jpg 搭配redux实现删除歌曲功能,介绍分仓库。效果如下: delete.gif

  1. index.js 统一向外输出store仓库中的文件,方便后续调用
import reducer from './reducer'
import * as actionCreators from './actionCreators'

export {
    // 统一向外输出
    reducer,
    actionCreators,
}
  1. constants.js 声明action-type字符串常量,简洁明了,方便调用。以下为删除操作所需的action-type
export const CHANGE_PLAY_LIST = 'CHANGE_PLAY_LIST'
export const DELETE_SONG = 'DELETE_SONG'
export const DELETE_ALL_SONG = 'DELETE_ALL_SONG'
  1. actionCreators.js 提供各种action,action中带有type和异步数据data,传给reducer函数。
import * as actionTypes from './constants'
import { getPlayRequest } from '@/api/request'

export const changePlayList = (data) => ({
    type: actionTypes.CHANGE_PLAY_LIST,
    data
})

export const deleteSong = (data) => ({
    type: actionTypes.DELETE_SONG,
    data
})

export const deleteAllSong = (data) => ({
    type: actionTypes.DELETE_ALL_SONG,
    data
})

// 获取播放列表数据
export const getPlayList = () => { 
    return dispatch => {
        getPlayRequest()
            .then(data => {               
                dispatch(changePlayList(data))
            })
    }
}
  1. reducer.js 定义仓库数据的初始状态,actionCreators传过来action属性(type,data),reducer通过switch匹配action中的type,返回对应的新的state。在redux中,不可修改原来的state,只能返回新的状态
import * as actionTypes from './constants'
import { findIndex } from '@/api/utils';

const defaultState = {
    playList: []
}

// 删除单首歌曲
const handleDeleteSong = (state, song) => {
    const newPlayList = JSON.parse(JSON.stringify(state.playList));
    const fpIndex = findIndex(song, newPlayList)
    newPlayList.splice(fpIndex, 1) // 删除歌曲
    return {
        ...state,
        playList: newPlayList
    }
}

export default (state=defaultState, action) => {
    switch(action.type) {
        case actionTypes.CHANGE_PLAY_LIST: // 歌曲数据
            return {
                ...state,
                playList: action.data
            }
        case actionTypes.DELETE_SONG:
            return handleDeleteSong(state, action.data)
        case actionTypes.DELETE_ALL_SONG:
            return {
                ...state,
                playList: []
            }
        default:
            return state
    }
}

封装在api文件夹下的utils.js文件中的findIndex方法

// 返回当前点击删除歌曲的数组下标
export const findIndex = (song, list) => {
  return list.findIndex(item => {
    return song.id === item.id;
  });
};

4. redux仓库使用

  1. 在入口文件main.jsx中引入Provider,传入store,将App组件包裹起来即可
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import 'font-awesome/css/font-awesome.min.css'
import './assets/style/reset.css'
import 'swiper/dist/css/swiper.css'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import store from './store'

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}> // 提供store仓库
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>
)
  1. 在浏览器中通过 Redux DevTools查看redux中的数据 redux.gif

四、 性能优化

1. 图片懒加载

lazyload3.gif

  • 先用占位符图片占位,只有当图片在视口中可见时,才会加载该图片。
import LazyLoad from 'react-lazyload'
import Music from './default.png' // 占位图片
<LazyLoad 
    placeholder={<img width="100%" height="100%" src={Music} alt="music"/>}
>
    <img src={item.img} width="100%" height="100%" alt="music"/>
</LazyLoad>

2. 路由懒加载

  • 按需去加载路由对应的资源,提高首屏加载速度
  • Suspense:用于在因网络延迟等而导致的组件不能快速的加载到页面时出现的提示,不使用会出现报错。
import React, { lazy, Suspense } from "react";
import { Routes, Route, Navigate, } from "react-router-dom";
import Home from "../pages/Home" // 首页应尽快加载
const Search = lazy(() => import("../pages/Search"));
const Rank = lazy(() => import("../pages/Rank"))

export default function RoutesConfig() {
  return (
    <Suspense fallback={null}> 
      <Routes>
        <Route path="/home" element={<Home />}></Route>
        <Route path="/" element={<Navigate to="/home" replace={true} />}></Route>
        <Route path="/search" element={<Search />}></Route>
        <Route path="/rank" element={<Rank />}></Route>
      </Routes>
    </Suspense>
  );
}

3. React.memo

组件仅在它的 props 发生改变的时候进行重新渲染。通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。但是通过React.memo(),我们可以仅仅让某些组件进行渲染。由于只有需要被渲染的组件被渲染了,所以这是一个性能提升。

// memo包裹组件
export default (React.memo(Rank));

4. 封装网络请求

api中添加config文件中配置请求对象,在request基于其获取数据,代码更加简洁

// 配置请求对象
import axios from 'axios'

export const baseUrl = "https://www.fastmock.site/mock/1c47faebda5122a88aadb1bb5c0bbe51/music";
const axiosInstance = axios.create({
    baseURL: baseUrl
})

axiosInstance.interceptors.response.use(
    res => res.data,
    err => {
        console.log(err, '网络错误~~')
    }
)

export { axiosInstance }

请求数据

import { axiosInstance } from "./config"

// 专属推荐数据
export const getRecommendRequest = 
    () => axiosInstance.get('/commend')
     
// 播放列表数据
export const getPlayRequest = 
    () => axiosInstance.get('/playlist')
    
// 轮播图
export const getBannerRequest = 
    () => axiosInstance.get('/banner')
    
// 排行榜
export const getRankRequest = 
    () => axiosInstance.get('/rank')

五、结语

这次项目主要在上次的基础上添加了redux,是对用redux管理数据的实战。其他页面的介绍在上一篇文章中。如果你redux感兴趣,可以去看下阮一峰老师的文章Redux 入门教程,文章通俗易懂,讲解的很详细。最后大佬们别忘记点点赞哦~

源码 Traveler-101/KuGou2.0 (github.com)