React实现(Web端)网易云音乐项目(二),错过了真的可惜呀

接着上一篇来继续写了,这篇博客主要完成下面这部分
在这里插入图片描述
一.首先先完成这个轮播图了,那肯定需要请求数据了,所以我们先把网络请求部分先写好

在React里面我们也是通过axios来发送网络请求的,先安装

yarn add axios

安装好之后,我们在之前创建的services文件夹里面创建三个js文件,分别叫request.js,recommend.js,config.js

request.js就放我们封装的网络请求

import axios from 'axios';

import { BASE_URL, TIMEOUT } from "./config";

const instance = axios.create({
  baseURL: BASE_URL,
  timeout: TIMEOUT
});

instance.interceptors.request.use(config => {
  // 1.发送网络请求时, 在界面的中间位置显示Loading的组件

  // 2.某一些请求要求用户必须携带token, 如果没有携带, 那么直接跳转到登录页面

  // 3.params/data序列化的操作

  return config;
}, err => {

});

instance.interceptors.response.use(res => {
  return res.data;
}, err => {
  if (err && err.response) {
    switch (err.response.status) {
      case 400:
        console.log("请求错误");
        break;
      case 401:
        console.log("未授权访问");
        break;
      default:
        console.log("其他错误信息");
    }
  }
  return err;
});

export default instance;

config.js放我们的请求地址

const devBaseURL = "http://123.207.32.32:9001";
const proBaseURL = "https://production.org";
export const BASE_URL = process.env.NODE_ENV === 'development' ? devBaseURL: proBaseURL;

export const TIMEOUT = 5000;

说明一下,我们开发项目肯定大多数时候有两个环境,一个开发环境一个正式环境,这就方便之后我们更改网络请求地址

recommend.js里面放请求的数据接口

import request from './request';
//轮播图数据
export function getTopBanners() {
  return request({
    url: "/banner"
  })
}

二.我们这个项目是把网络请求的数据统一的放在redux里面进行存储的哈,这样我感觉更能考验我们的redux能力,因为redux毕竟比较难嘛

在我们的store文件夹里面创建一个index.js,一个reducer.js,另外我们是用redux-thunk来作为中间件的,喜欢用saga的也可以用saga

index.js

import { createStore, applyMiddleware, compose } from 'redux';
//applyMiddleware:中间件
import thunk from 'redux-thunk';
import reducer from './reducer';

//打开谷歌调试工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer, composeEnhancers(
  //合并多个中间件
  applyMiddleware(thunk)
));

export default store;

reducer.js

// combineReducers:合成reducer,因为可能有多个reducer

// 类似:immutable,深拷贝
import { combineReducers } from 'redux-immutable';


//怕命名冲突,as是重命名
import { reducer as recommendReducer } from '../pages/discover/c-pages/recommend/store';

const cReducer = combineReducers({
  recommend: recommendReducer   //防止命名冲突
});

export default cReducer;

这个里面需要安装一个redux-immutable,最大的目的我觉得就是提升性能吧

yarn add redux-immutable

上面那个引入recommend/store,是我在我们的recommend文件夹里面又创建了一个小的store,可以形象称为子redux,我们在子redux里面再定义数据,然后将它导入到我们的主redux里面,让我们的项目更加清晰,正规化

在这里插入图片描述
别忘了去我们的APP.js中去配置一下

import { Provider } from 'react-redux';
import store from './store';

export default memo(function App() {
  return (
    <Provider store={store}>
    <HashRouter>
      <LSHAppHeader/>
      {renderRoutes(routes)}
      <LSHAppFooter/>
    </HashRouter>
    </Provider>
  )
})

三.开始在recommend文件中里面那个store进行一些配置,在store里面创建四个js文件

constants.js,定义我们的常量

export const CHANGE_TOP_BANNERS = "recommend/CHANGE_TOP_BANNERS";     //轮播图常量

actionCreators.js

import * as actionTypes from './constants';

import { 
  getTopBanners    //从我们的网络请求里面导入
} from '@/services/recommend';

const changeTopBannerAction = (res) => ({
  type: actionTypes.CHANGE_TOP_BANNERS,
  topBanners: res.banners
});

export const getTopBannerAction = () => {
  return dispatch => {
    getTopBanners().then(res => {
      dispatch(changeTopBannerAction(res));
      console.log(res)
    })
  }
};

reducer.js

import { Map } from 'immutable';
//导入所有常量
import * as actionTypes from './constants';

const defaultState = Map({
  topBanners: [],
});

function reducer(state = defaultState, action) {
  switch (action.type) {
    case actionTypes.CHANGE_TOP_BANNERS:
      return state.set("topBanners", action.topBanners);
    default:
      return state;
  }
}

export default reducer;

为什么非要用immutable呢?因为react有一个原则就是数据的不可变性,其他的可以去搜索一下哈哈

index.js,导出我们的reducer

import reducer from './reducer';

export {
  reducer
}

至此,我们的网络请求和存储数据全部完成了,接下来可以画页面了

四.轮播图页面

首先在recommend文件夹下面创建一个c-cpns文件夹,c-cpns文件夹里面创建一个top-banner文件夹,然后里面创建一个index.js,一个style.js

index.js

我们先一步一步来,分为三块,注意用到了redux里面的hooks,不熟练的最好去稍微学习一下下哈

第一块,为导入配置

import React, { memo, useEffect, useRef, useCallback, useState } from 'react';

import { useSelector, useDispatch, shallowEqual } from 'react-redux';
//useSelector获取我们的state数据,shallowEqual为一个比较,主要为了优化性能

import { getTopBannerAction } from '../../store/actionCreators';
//从我们的recommend中的store导入

import { Carousel } from 'antd';
import {
  BannerWrapper,
  BannerLeft,
  BannerRight,
  BannerControl
} from './style';

第二块,为逻辑代码

export default memo(function HYTopBanner() {
  // state
  const [currentIndex, setCurrentIndex] = useState(0);

  // 组件和redux关联: 获取数据和进行操作
  const { topBanners } = useSelector(state => ({
    // topBanners: state.get("recommend").get("topBanners")
    //上面一种写法的语法糖
    topBanners: state.getIn(["recommend", "topBanners"])
  }), shallowEqual);
  const dispatch = useDispatch();

  // 其他hooks
  const bannerRef = useRef();
  useEffect(() => {
    dispatch(getTopBannerAction());
  }, [dispatch]);
  //背景图的切换
  const bannerChange = useCallback((from, to) => {
    setCurrentIndex(to);
  }, []);

  // 其他业务逻辑
  const bgImage = topBanners[currentIndex] && (topBanners[currentIndex].imageUrl + "?imageView&blur=40x20")
})

首先获取我们的state,然后通过useDispatch来获取我们store里面的dispatch,之后在useEffect发送网络请求

第三块

 return (
    <BannerWrapper bgImage={bgImage}>
      <div className="banner wrap-v2">
        <BannerLeft>
          <Carousel effect="fade" autoplay ref={bannerRef} beforeChange={bannerChange}>
            {
              topBanners.map((item, index) => {
                return (
                  <div className="banner-item" key={item.imageUrl}>
                    <img className="image" src={item.imageUrl} alt={item.typeTitle} />
                  </div>
                )
              })
            }
          </Carousel>
        </BannerLeft>
        <BannerRight></BannerRight>
        <BannerControl>
          <button className="btn left" onClick={e => bannerRef.current.prev()}></button>
          <button className="btn right" onClick={e => bannerRef.current.next()}></button>
        </BannerControl>
      </div>
    </BannerWrapper>
  )

由于我们用的antd的那个走马灯轮播,useRef是为了我们调用方法的时候,useCallback是因为,我们变换图片的同时,它的背景图也跟着发生了变化,对了,这个地方还用到了给css样式传参,通过props

style.js

import styled from 'styled-components';

export const BannerWrapper = styled.div`
  background: url(${props => props.bgImage}) center center/6000px;

  .banner {
    height: 270px;
    background-color: red;

    display: flex;
    position: relative;
  }
`

export const BannerLeft = styled.div`
  width: 730px;

  .banner-item {
    overflow: hidden;
    height: 270px;
    .image {
      width: 100%;
    }
  }
`

export const BannerRight = styled.a.attrs({
  href: "https://music.163.com/#/download",
  target: "_blank"
})`
  width: 254px;
  height: 270px;
  background: url(${require("@/assets/img/download.png")});
`

export const BannerControl = styled.div`
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);

  .btn {
    position: absolute;
    width: 37px;
    height: 63px;
    background-image: url(${require("@/assets/img/banner_sprite.png")});
    background-color: transparent;
    cursor: pointer;

    &:hover {
      background-color: rgba(0, 0, 0, .1);
    }
  }

  .left {
    left: -68px;
    background-position: 0 -360px;
  }

  .right {
    right: -68px;
    background-position: 0 -508px;
  }
`

至此我们的轮播图就完成啦哈哈

下面我们来做这个页面

在这里插入图片描述
第一步是干啥??肯定是先在网络请求里面封装接口啊

来到我们services文件夹里面的recommend.js

export function getHotRecommends(limit) {
  return request({
    url: "/personalized",
    params: {
      limit
    }
  })
}

这里有个limit参数,设为几,它就显示几条数据,这个地方很明显要设为8

第二步呢?那肯定是去我们recommend里面的store去存储数据喽,对吧,思路没错应该

先去constants.js里面定义常量

export const CHANGE_HOT_RECOMMEND = "recommend/CHANGE_HOT_RECOMMEND";

然后去actionCreators.js

import * as actionTypes from './constants';

import { 
  getHotRecommends
} from '@/services/recommend';

const changeHotRecommendAction = (res) => ({
  type: actionTypes.CHANGE_HOT_RECOMMEND,
  hotRecommends: res.result
})

export const getHotRecommendAction = (limit) => {
  return dispatch => {
    getHotRecommends(limit).then(res => {
      dispatch(changeHotRecommendAction(res));
    })
  }
}

之后去reducer.js中

import { Map } from 'immutable';
//导入所有常量
import * as actionTypes from './constants';
const defaultState = Map({
  hotRecommends: [],
});

function reducer(state = defaultState, action) {
  switch (action.type) {
    case actionTypes.CHANGE_HOT_RECOMMEND:
      return state.set("hotRecommends", action.hotRecommends);
    default:
      return state;
  }
}

export default reducer;

好了,这个页面的数据存储完毕,可以开始撸页面了

在c-cpns文件夹里面创建一个hot-recommend文件夹,然后里面创建一个index.js,一个style.js

还是分三步来好吧

第一步,导入配置

import React, { memo, useEffect } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';

import { HOT_RECOMMEND_LIMIT } from '@/common/contants';

import HYThemeHeaderRCM from '@/components/theme-header-rcm';
import HYSongsCover from '@/components/songs-cover';
import {
  HotRecommendWrapper
} from './style';
import { getHotRecommendAction } from '../../store/actionCreators';

HOT_RECOMMEND_LIMIT这个是我之前定义的那个limit,由于比较喜欢拆分,哈哈

export const HOT_RECOMMEND_LIMIT = 8;

HYThemeHeaderRCM,HYSongsCover这两个我觉得是可以复用的,就给它们放在了components文件夹里面,传数据肯定是在父组件里面获取,然后通过props传递,这个毋庸置疑

components中创建一个theme-header-rcm文件夹,里面创建一个index.js,一个style.js

index.js

import React, { memo } from 'react';
import PropTypes from 'prop-types';

import { HeaderWrapper } from './style';

const HYThemeHeaderRCM = memo(function(props) {
  const { title, keywords } = props;

  return (
    <HeaderWrapper className="sprite_02">
      <div className="left">
        <h3 className="title">{title}</h3>
        <div className="keyword">
          {
            keywords.map((item, index) => {
              return (
                <div className="item" key={item}>
                  <a href="todo">{item}</a>
                  <span className="divider">|</span>
                </div>
              )
            })
          }
        </div>
      </div>
      <div className="right">
        <a href="todo">更多</a>
        <i className="icon sprite_02"></i>
      </div>
    </HeaderWrapper>
  )
})

HYThemeHeaderRCM.propTypes = {
  title: PropTypes.string.isRequired,
  keywords: PropTypes.array
}

HYThemeHeaderRCM.defaultProps = {
  keywords: []
}

export default HYThemeHeaderRCM;

PropTypes进行类型的检测等,我觉得最好还是带上吧,开发规范

style.js

import styled from 'styled-components';

export const HeaderWrapper = styled.div`
  height: 33px;
  border-bottom: 2px solid #C10D0C;
  padding: 0 10px 4px 34px;
  background-position: -225px -156px;

  display: flex;
  justify-content: space-between;
  align-items: center;

  .left {
    display: flex;
    align-items: center;

    .title {
      font-size: 20px;
      font-family: "Microsoft Yahei", Arial, Helvetica, sans-serif;
      margin-right: 20px;
    }

    .keyword {
      display: flex;

      .item {
        .divider {
          margin: 0 15px;
          color: #ccc;
        }
      }
    }
  }

  .right {
    display: flex;
    align-items: center;
    .icon {
      display: inline-block;
      width: 12px;
      height: 12px;
      margin-left: 4px;
      background-position: 0 -240px;
    }
  }
`

然后是另外一个

components中创建一个songs-cover文件夹,里面创建一个index.js,一个style.js

index.js

import React, { memo } from 'react';

import { getCount, getSizeImage } from "@/utils/format-utils";

import { SongsCoverWrapper } from './style';

export default memo(function HYSongsCover(props) {
  const { info } = props;

  return (
    <SongsCoverWrapper>
      <div className="cover-top">
        <img src={getSizeImage(info.picUrl, 140)} alt="" />
        <div className="cover sprite_covor">
          <div className="info sprite_covor">
            <span>
              <i className="sprite_icon erji"></i>
              {getCount(info.playCount)}
            </span>
            <i className="sprite_icon play"></i>
          </div>
        </div>
      </div>
      <div className="cover-bottom text-nowrap">
        {info.name}
      </div>
      <div className="cover-source text-nowrap">
        by {info.copywriter || info.creator.nickname}
      </div>
    </SongsCoverWrapper>
  )
})

getCount, getSizeImage ,这两个方法说明一下,先说getCount

就是相当于一个过滤器,当我们数字过长,比如让它除以1万,然后它后面就跟上个万,依次类推

然后getSizeImage

就是你们做项目的时候,有一些图片它非常的大,然后加载的时候特别的慢,网易云就是在图片的后面拼接了一个参数param,参数就传递宽高,这样我们就可以在请求图片的同时,将它的宽高进行锁定,这样当我们项目加载这个图片的时候,就加载我们定义的宽高的图片,可以很好的节省性能和流量,类似这样

"https://p1.music.126.net/fFua795_abR_N2gXAl--LQ==/109951165222245155.jpg?param=140x140"

好吧,去到我们的utils文件里面创建一个format-utils.js

export function getCount(count) {
  if (count < 0) return;
  if (count < 10000) {
    return count;
  } else if (Math.floor(count / 10000) < 10000) {
    return Math.floor(count / 1000) / 10 + "万";
  } else {
    return Math.floor(count / 10000000) / 10 + "亿";
  }
}

export function getSizeImage(imgUrl, size) {
  return `${imgUrl}?param=${size}x${size}`;
}

style.js

import styled from "styled-components";

export const SongsCoverWrapper = styled.div`
  width: 140px;
  margin: 20px ${props => (props.right || 0)} 20px 0;

  .cover-top {
    position: relative;

    &>img {
      width: 140px;
      height: 140px;
    }
    
    .cover {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-position: 0 0;

      .info {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 0 10px;
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        background-position: 0 -537px;
        color: #ccc;
        height: 27px;

        .erji {
          margin-right: 5px;
          display: inline-block;
          width: 14px;
          height: 11px;
          background-position: 0 -24px;
        }

        .play {
          display: inline-block;
          width: 16px;
          height: 17px;
          background-position: 0 0;
        }
      }
    }
  }

  .cover-bottom {
    font-size: 14px;
    color: #000;
    margin-top: 5px;
  }

  .cover-source {
    color: #666;
  }
`

好了,至此两个复用型组件定义完了,回到我们的hot-recommend中

开始第二步,逻辑代码,隔太远了,我把上面的也复制下来吧…

import React, { memo, useEffect } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';

import { HOT_RECOMMEND_LIMIT } from '@/common/contants';

import HYThemeHeaderRCM from '@/components/theme-header-rcm';
import HYSongsCover from '@/components/songs-cover';
import {
  HotRecommendWrapper
} from './style';
import { getHotRecommendAction } from '../../store/actionCreators';

export default memo(function HYHotRecommend() {
  // state

  // redux hooks
  //shallowEqual,为了浅层比较
  const { hotRecommends } = useSelector(state => ({
    hotRecommends: state.getIn(["recommend", "hotRecommends"])
  }), shallowEqual);
  const dispatch = useDispatch();

  // other hooks
  useEffect(() => {
    dispatch(getHotRecommendAction(HOT_RECOMMEND_LIMIT));
  }, [dispatch]);

该解释的,我前面都解释过了,跳过了哈

 return (
    <HotRecommendWrapper>
      <HYThemeHeaderRCM title="热门推荐" keywords={["华语", "流行", "民谣", "摇滚", "电子"]} />
      <div className="recommend-list">
        {
          hotRecommends.map((item, index) => {
            return <HYSongsCover key={item.id} info={item}/>
          })
        }
      </div>
    </HotRecommendWrapper>
  )

style.js

import styled from "styled-components";

export const HotRecommendWrapper = styled.div`
  .recommend-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
  }
`

好了,完事,这两个页面完成,可能大家都觉得全是静态页面,别慌,后面会写播放,歌词等等一些功能,下一篇就写这两个页面了哈

在这里插入图片描述
github项目地址:https://github.com/lsh555/WYY-Music

React是一个用于构建用户界面的JavaScript库。它可以帮助开发者构建出高效、灵活且易于维护的应用程序。而网易云音乐的轮播图是一个非常经典的功能,我们可以通过React实现类似的效果。 首先,我们可以利用React的组件化思想,将轮播图拆分成多个组件,如"Slider"组件、"Slide"组件等。"Slider"组件负责整体的布局和逻辑,而"Slide"组件负责单个图片的展示和样式; 其次,我们可以使用React的状态管理机制来控制轮播图的切换。可以通过useState来定义当前的图片索引,通过useEffect来监听索引的变化,并根据变化来改变轮播图的显示; 接着,我们需要利用React的生命周期函数,比如componentDidMount和componentWillUnmount,来处理轮播图的自动切换。通过设置定时器,在componentDidMount中启动自动切换功能,并在componentWillUnmount中清除定时器,以防止内存泄漏; 最后,我们可以使用React的事件处理机制,比如onClick来处理用户的操作。当用户点击上下一页或者圆点指示器时,可以通过更新状态来改变轮播图的显示。 综上所述,通过利用React的组件化思想、状态管理机制、生命周期函数以及事件处理机制,我们可以实现一个仿造网易云音乐轮播图的功能。这样我们就可以在应用中展示图片,并实现自动切换、手动切换等功能,提升用户体验。 React的灵活性和易用性使得开发此类功能变得非常简单和高效。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值