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

今天就写一下歌曲播放这个功能,进度条拉伸以及歌曲时间的变化,当我们改变一个状态的时候,其他几个都相应改变,这个功能还是有一点复杂的…

在这里插入图片描述
就是这个,当我们可以播放,暂停,当我们播放的时候,进度条改变以及歌曲时间改变,当我们拉动进度条的时候,歌曲时间暂停,当我们松开的时候,直接跳到这个地方歌词的位置…

话不多说了,开整

一.在我们pages文件夹下,建立一个player文件夹,里面创建一个app-player-bar和一个store文件夹

在这里插入图片描述
二.那肯定是封装我们的网络请求了

在services文件夹下,创建一个player.js

在这里插入图片描述

import request from './request';
export function getSongDetail(ids) {
  return request({
    url: "/song/detail",
    params: {
      ids
    }
  })
}

三.我们需要在我们创建的player文件夹下的store里面去,把数据存到我们的redux里面,这里说一下,我是把所有网络请求的数据都放在我们的redux里面进行管理,而且每一个页面我都分别创建一个子redux,最后再把它们合并到主redux里面,通过redux-thunk

来到我们的store

首先是constants.js,定义常量啊

export const CHANGE_CURRENT_SONG = "player/CHANGE_CURRENT_SONG";

再是actionCreators.js,写方法

import { getSongDetail } from '@/services/player';
//引入我们player中封装的网络请求
import * as actionTypes from './constants';

const changeCurrentSongAction = (currentSong) => ({
  type: actionTypes.CHANGE_CURRENT_SONG,
  currentSong
})

export const getSongDetailAction = (ids) => {
  return dispatch => {
    getSongDetail(ids).then(res => {
      dispatch(changeCurrentSongAction(res.songs[0]));
    })
  }
}

再来到我们的reducer.js

import { Map } from 'immutable';
//提升性能,深拷贝,不清楚的可以百度一下
import * as actionTypes from './constants';

const defaultState = Map({
  currentSong: {}
});

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

export default reducer;

之后就是index.js了,导出reducer

import reducer from './reducer';

export {
  reducer
}

四.数据存到redux之后,我们需要到我们的主redux文件里面进行合并

reducer.js

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

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


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

const cReducer = combineReducers({
  recommend: recommendReducer,   //这是我们之前demo中定义的,之前的博客
  player: playerReducer   //这是我们播放的子redux
});

export default cReducer;

五.OK啦,数据全部弄好了,接下来可以去组件里面了

来到我们app-player-bar文件夹下,创建一个index.js,一个style.js

index.js

老规矩,还是分三步走

第一步,导入我们的配置

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

import { getSizeImage, formatDate, getPlaySong } from '@/utils/format-utils';

import { Slider } from 'antd';

import { getSongDetailAction } from '../store/actionCreators';
import {
  PlaybarWrapper,
  Control,
  PlayInfo,
  Operator
} from './style';

getSizeImage是我们改变图片大小的,formatDate是格式化时间戳的,getPlaySong是我们要获取音乐的mp3,网易云搞的坏事…Slider就是我们那个进度条,在antdui库里面获取的

format-utils.js

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

export function formatDate(time, fmt) {
  let date = new Date(time);

  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  }
  let o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds()
  };
  for (let k in o) {
    if (new RegExp(`(${k})`).test(fmt)) {
      let str = o[k] + '';
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
    }
  }
  return fmt;
};

function padLeftZero(str) {
  return ('00' + str).substr(str.length);
};

export function formatMonthDay(time) {
  return formatDate(time, "MM月dd日");
}

export function formatMinuteSecond(time) {
  return formatDate(time, "mm:ss");
}

export function getPlaySong(id) {
  return `https://music.163.com/song/media/outer/url?id=${id}.mp3`;
}

第二步,我们的逻辑代码,这个有点复杂,我们也分几步好吧

useState

   const [currentTime, setCurrentTime] = useState(0);
  
   const [progress, setProgress] = useState(0);

   const [isChanging, setIsChanging] = useState(false);
 
   const [isPlaying, setIsPlaying] = useState(false);

解析

currentTime:我们歌曲播放的时间
progress:进度条的位置
isChanging: 因为我们滑动进度条的时候和我们歌曲播放的过程中,都会改变我们进度条的位置,这样它们两个就冲突了,啥意思呢,就是当我们把进度条滑到50,但是歌曲刚刚播放,这个时候进度条它就会马上回到我们播放的位置,两者冲突,所以必须有个判断
isPlaying:就是我们播放暂停,来回切换

useDispatch, useSelector, shallowEqual

const { currentSong } = useSelector(state => ({
    currentSong: state.getIn(["player", "currentSong"])
  }), shallowEqual);
  const dispatch = useDispatch();

解析

useSelector:获取我们redux中的state
shallowEqual:优化性能,有一个浅层比较
useDispatch:获取我们的dispatch

useRef,audioRef

 const audioRef = useRef();
  useEffect(() => {
    dispatch(getSongDetailAction(167876));
  }, [dispatch]);
  useEffect(() =>  {
    audioRef.current.src = getPlaySong(currentSong.id);
  }, [currentSong]);

解析

useRef:这是给我们audio标签绑定的一个ref
 <audio ref={audioRef}/>
useEffect:发送网络请求,获取我们redux里面的数据,通过dispatch,下面那个获取歌曲的src地址

处理一些我们的数据,比如转换时间戳啊,地址等等

  const picUrl = (currentSong.al && currentSong.al.picUrl) || "";
  const singerName = (currentSong.ar && currentSong.ar[0].name) || "未知歌手";
  const duration = currentSong.dt || 0;
  const showDuration = formatDate(duration, "mm:ss");
  const showCurrentTime = formatDate(currentTime, "mm:ss");

再来看下面的逻辑

1.歌曲的暂停与播放

   const playMusic = () => {
      isPlaying ? audioRef.current.pause(): audioRef.current.play();
      setIsPlaying(!isPlaying);
    }

2.当我们歌曲播放的时候,进度条和我们歌曲时间的改变

 const timeUpdate = (e) => {
      if (!isChanging) {
        setCurrentTime(e.target.currentTime * 1000);
        setProgress(currentTime / duration * 100);
      }
    }

3.当我们拉动进度条的时候,当 Slider 的值发生改变时,把改变后的值作为参数传入

 const sliderChange = useCallback((value) => {
      setIsChanging(true);
      const currentTime = value / 100 * duration;
      setCurrentTime(currentTime);
      setProgress(value);
    }, [duration]);

至于为什么用useCallback,useCallback主要用在,当把一个回调函数传到一个自定义组件内部时候用

4.把当前值作为参数传入,这个地方要注意我上面说的那种情况,播放时候和我们拉动进度条的时候冲突了,进度条的位置,不过我已经解决了在这

 const sliderAfterChange = useCallback((value) => {
      const currentTime = value / 100 * duration / 1000;
      audioRef.current.currentTime = currentTime;
      setCurrentTime(currentTime * 1000);
      setIsChanging(false);
  
      if (!isPlaying) {
        playMusic();
      }
    }, [duration,isPlaying, playMusic]);

好了,逻辑代码完毕

第三步,接下来是页面布局

return (
    <PlaybarWrapper className="sprite_player">
      <div className="content wrap-v2">
     <Control isPlaying={isPlaying}>
     <button className="sprite_player prev"></button>
     <button className="sprite_player play" onClick={e => playMusic()}></button>
     <button className="sprite_player next"></button>
     </Control>
     <PlayInfo>
     <div className="image">
            <a href="/#">
              <img src={getSizeImage(picUrl, 35)} alt="" />
            </a>
          </div>
      <div className="info">
      <div className="song">
          <span className="song-name">{currentSong.name}</span>
          <a href="#/" className="singer-name">{singerName}</a>
       </div>
       <div className="progress">
              <Slider defaultValue={30} 
                      value={progress}
                      onChange={sliderChange}
                      onAfterChange={sliderAfterChange}/>
              <div className="time">
                <span className="now-time">{showCurrentTime}</span>
                <span className="divider">/</span>
                <span className="duration">{showDuration}</span>
                </div>
              </div>
      </div>
    </PlayInfo>
    <Operator>
          <div className="left">
            <button className="sprite_player btn favor"></button>
            <button className="sprite_player btn share"></button>
          </div>
          <div className="right sprite_player">
            <button className="sprite_player btn volume"></button>
            <button className="sprite_player btn loop"></button>
            <button className="sprite_player btn playlist"></button>
          </div>
        </Operator>
      </div>
      <audio ref={audioRef} onTimeUpdate={timeUpdate}/>
    </PlaybarWrapper>
  )

这是这个页面所有代码

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

import { getSizeImage, formatDate, getPlaySong } from '@/utils/format-utils';

import { Slider } from 'antd';

import { getSongDetailAction } from '../store/actionCreators';
import {
  PlaybarWrapper,
  Control,
  PlayInfo,
  Operator
} from './style';

export default memo(function HYAppPlayerBar() {
  
   // props and state
   const [currentTime, setCurrentTime] = useState(0);
   const [progress, setProgress] = useState(0);
   const [isChanging, setIsChanging] = useState(false);
   const [isPlaying, setIsPlaying] = useState(false);
 
  // redux hook
  const { currentSong } = useSelector(state => ({
    currentSong: state.getIn(["player", "currentSong"])
  }), shallowEqual);
  const dispatch = useDispatch();
  
  // other hooks
  const audioRef = useRef();
  useEffect(() => {
    dispatch(getSongDetailAction(167876));
  }, [dispatch]);
  useEffect(() =>  {
    audioRef.current.src = getPlaySong(currentSong.id);
  }, [currentSong]);
  
  // other handle
  const picUrl = (currentSong.al && currentSong.al.picUrl) || "";
  const singerName = (currentSong.ar && currentSong.ar[0].name) || "未知歌手";
  const duration = currentSong.dt || 0;
  const showDuration = formatDate(duration, "mm:ss");
  const showCurrentTime = formatDate(currentTime, "mm:ss");

    // handle function
    const playMusic = () => {
      isPlaying ? audioRef.current.pause(): audioRef.current.play();
      setIsPlaying(!isPlaying);
    }

    const timeUpdate = (e) => {
      if (!isChanging) {
        setCurrentTime(e.target.currentTime * 1000);
        setProgress(currentTime / duration * 100);
      }
    }
    //useCallback:当把一个回调函数传到一个自定义组件内部时候用
    const sliderChange = useCallback((value) => {
      setIsChanging(true);
      const currentTime = value / 100 * duration;
      setCurrentTime(currentTime);
      setProgress(value);
    }, [duration]);

    const sliderAfterChange = useCallback((value) => {
      const currentTime = value / 100 * duration / 1000;
      audioRef.current.currentTime = currentTime;
      setCurrentTime(currentTime * 1000);
      setIsChanging(false);
  
      if (!isPlaying) {
        playMusic();
      }
    }, [duration,isPlaying, playMusic]);

  return (
    <PlaybarWrapper className="sprite_player">
      <div className="content wrap-v2">
     <Control isPlaying={isPlaying}>
     <button className="sprite_player prev"></button>
     <button className="sprite_player play" onClick={e => playMusic()}></button>
     <button className="sprite_player next"></button>
     </Control>
     <PlayInfo>
     <div className="image">
            <a href="/#">
              <img src={getSizeImage(picUrl, 35)} alt="" />
            </a>
          </div>
      <div className="info">
      <div className="song">
          <span className="song-name">{currentSong.name}</span>
          <a href="#/" className="singer-name">{singerName}</a>
       </div>
       <div className="progress">
              <Slider defaultValue={30} 
                      value={progress}
                      onChange={sliderChange}
                      onAfterChange={sliderAfterChange}/>
              <div className="time">
                <span className="now-time">{showCurrentTime}</span>
                <span className="divider">/</span>
                <span className="duration">{showDuration}</span>
                </div>
              </div>
      </div>
    </PlayInfo>
    <Operator>
          <div className="left">
            <button className="sprite_player btn favor"></button>
            <button className="sprite_player btn share"></button>
          </div>
          <div className="right sprite_player">
            <button className="sprite_player btn volume"></button>
            <button className="sprite_player btn loop"></button>
            <button className="sprite_player btn playlist"></button>
          </div>
        </Operator>
      </div>
      <audio ref={audioRef} onTimeUpdate={timeUpdate}/>
    </PlaybarWrapper>
  )
})

style.js

import styled from 'styled-components';

export const PlaybarWrapper = styled.div`
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 52px;
  background-position: 0 0;
  background-repeat: repeat;

  .content {
    display: flex;
    align-items: center;
    justify-content: space-between;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: 0;
    height: 47px;
  }
`

export const Control = styled.div`
  display: flex;
  align-items: center;

  .prev, .next {
    width: 28px;
    height: 28px;
  }

  .prev {
    background-position: 0 -130px;
  }

  .play {
    width: 36px;
    height: 36px;
    margin: 0 8px;
    background-position: 0 ${props => props.isPlaying ? "-165px": "-204px"};
  }

  .next {
    background-position: -80px -130px;
  }
`

export const PlayInfo = styled.div`
  display: flex;
  width: 642px;
  align-items: center;

  .image {
    width: 34px;
    height: 34px;
    border-radius: 5px;
  }

  .info {
    flex: 1;
    color: #a1a1a1;
    margin-left: 10px;

    .song {
      color: #e1e1e1;
      position: relative;
      top: 8px;
      left: 8px;

      .singer-name {
        color: #a1a1a1;
        margin-left: 10px;
      }
    }

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

      .ant-slider {
        width: 493px;
        margin-right: 10px;

        .ant-slider-rail {
          height: 9px;
          background: url(${require("@/assets/img/progress_bar.png")}) right 0;
        }

        .ant-slider-track {
          height: 9px;
          background: url(${require("@/assets/img/progress_bar.png")}) left -66px;
        }

        .ant-slider-handle {
          width: 22px;
          height: 24px;
          border: none;
          margin-top: -7px;
          background: url(${require("@/assets/img/sprite_icon.png")}) 0 -250px;
        }
      }

      .time {
        .now-time {
          color: #e1e1e1;
        }
        .divider {
          margin: 0 3px;
        }
      }
    }
  }
`

export const Operator = styled.div`
  display: flex;
  position: relative;
  top: 5px;

  .btn {
    width: 25px;
    height: 25px;
    cursor: pointer;
  }

  .favor {
    background-position: -88px -163px;
  }

  .share {
    background-position: -114px -163px;
  }

  .right {
    width: 126px;
    padding-left: 13px;
    background-position: -147px -248px;
    
    .volume {
      background-position: -2px -248px;
    }

    .loop {
      background-position: -3px -344px;
    }

    .playlist {
      width: 59px;
      background-position: -42px -68px;
    }
  }
`

好啦,最后一步,到我们的app.js中,将其引入

import React, { memo } from 'react'
//共享redux
import { Provider } from 'react-redux';

// 可以将路由划分到一个文件里面
import {renderRoutes} from 'react-router-config'
import { HashRouter } from 'react-router-dom';
import routes from './router'
import store from './store';

import LSHAppHeader from '@/components/app-header'
import LSHAppFooter from '@/components/app-footer'

import HYAppPlayerBar from './pages/player/app-player-bar';

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

注意:有些是之前的demo

好啦,完成哈哈

如果有不清楚的话,可以去我的github,有源码,下一篇博客,就更新我们歌词显示这部分内容了,歌词滚动哦,激动吧

github项目地址:https://github.com/lsh555/WYY-Music

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值