React Hooks实现网易云音乐PC版

559 阅读4分钟

前言:辛苦了大半个月,终于我的网易云音热要上线了。话不多说直接上链接。感谢高老哥帮忙配置nginx,感谢 Binaryify大佬的网易接口支持,如果大家觉得项目不错请给一个 star 吧 !

Gitee源码地址
Hooks项目地址

首先 看看实现的功能

  1. 首页UI
  2. 歌曲详情页面
  3. 歌单详情页面
  4. 专辑详情页面
  5. 详情页的播放和添加歌单
  6. 音乐播放器列表
  7. 歌曲单曲循环

一张项目截图

以下是一些核心代码

react-router-dom 中的嵌套路由

// 一级路由
import React from "react";
import { Route, BrowserRouter as Router, Switch, withRouter, Redirect } from "react-router-dom";
import Container from "../Content/MainContent";
import Discover from "../Content/Discover";
import Mine from "../Content/Mine";
import Friend from "../Content/Friend";
import Shop from "../Content/Shop";
import Musician from "../Content/Musician";
import Download from "../Content/Download";
import NotFound from "../Content/NotFound";
import SongDetail from '../Content/Discover/SongDetail'
import DiscoverRoutes from "../Routes/discoverRouter";

export default withRouter(function Routes(props) {

  return (
    // <Router>
    <Switch>
      <Route
        exact
        path="/"
        render={() => (
          <Discover>
            <DiscoverRoutes />
          </Discover>
        )}
      ></Route>
      <Route
        path="/discover"
        render={() => (
          <Discover>
            <DiscoverRoutes />
          </Discover>
        )}
      ></Route>
      <Route path="/mine" component={Mine} />
      <Route path="/friend" component={Friend} />
      <Route path="/shop" component={Shop} />
      <Route path="/musician" component={Musician} />
      <Route path="/download" component={Download} />
      <Route path="/404" component={NotFound} />
      <Route exact path="/song/:id/:type" component={SongDetail}></Route>
      <Redirect from="*" to="/404"></Redirect> //最末尾的重定向操作,后面不能插入其他路由
    </Switch>
    // </Router>
  );
})

// 二级路由
import React from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import Recommend from '../Content/Discover/Recommend'
import Rank from '../Content/Discover/Rank'
import Musiclist from '../Content/Discover/Musiclist'
import DiscoverDj from '../Content/Discover/DiscoverDj'
import DiscoverMusician from '../Content/Discover/DiscoverMusician'
import Newsong from '../Content/Discover/Newsong'


const RankTest = ({}) => {
  return <div>RankTest{Date.now()}</div>
}

const DisCoverRouter = () => {
  return <Switch>
    <Route exact path="/" component={Recommend}></Route>
    <Route exact path="/discover" component={Recommend}></Route>
    <Route path="/discover/recommend" component={Recommend}></Route>
    <Route path="/discover/rank" component={Rank}>
        <Route path="/discover/rank" component={Rank}></Route>
        <Route path="/discover/rank/one" component={RankTest}></Route>
        <Route path="/discover/rank/two" component={RankTest}></Route>
        <Route path="/three" component={RankTest}></Route>
    </Route>
    <Route path="/discover/musiclist" component={Musiclist}></Route>
    <Route path="/discover/discoverdj" component={DiscoverDj}></Route>
    <Route path="/discover/discovermusician" component={DiscoverMusician}></Route>
    <Route path="/discover/newsong" component={Newsong}></Route>
  </Switch>
}

export default DisCoverRouter

redux 的一些核心代码

// 点击添加歌单的action
export const addSongListAction = (data) => {
  console.log(data, 'data');
  return async dispatch => {
    const res = await songDetailwithIdsReq({ids: data.ids})
    console.log(res, ' rdata');
    if (res.data.code === 200) {
      let data = res.data.songs
      dispatch({type: ADD_SONGLIST, payload: data})
    }
  }
}

// 添加歌单并且播放歌曲
export const playSongAction = (id, type = 2, songArr = []) => {
  return dispatch => {
    // 直接接收一个ids : String 加入歌单 播放最后一首
    if (type == 1) {
      // 播放全部
      let idarr = []
      songArr.forEach(item => {
        idarr.push(item.id)
      });
      let ids = idarr.join(',')
      console.log(ids, 'ids')

      dispatch(addSongListAction({ids}));
      let id = ''
      if (ids && ids.length) {
        let idarr = ids.split(',')
        id = idarr[idarr.length - 1]
      }else{
        id = ids
      }
      console.log(id, 'id')
      dispatch(setCurrentSongDetail(id));
      dispatch(addBrothernodechangetime())
    }else{
      dispatch(addSongListAction({ids: id}));
      dispatch(setCurrentSongDetail(id));
      dispatch(addBrothernodechangetime())
    }

  }
}

// 添加歌单
export const addSongAction = (ids, type = 2, songArr = []) => {
  return dispatch => {
    // 接收ids: String 直接加入歌单
    if (type == 1) {
      let idarr = []
      songArr.forEach(item => {
        idarr.push(item.id)
      });
      let idstr = idarr.join(',')
  
      dispatch(addSongListAction({ids: idstr}));
    }else{
      dispatch(addSongListAction({ids: ids}));
    }
  }
}

// reducer.js
const initDoinginfo = {
  currentSongMp3: {},
  currentSongDetail: {},
  currentSongsList: [],
  err_msg: null,
  sendlrcDetail: {},
  currenttime: 0,
  isPlaying: false, //true 是指playing(播放中)
  lrcArrTotal: {},
  mp3List: [],
  onEnd: false,
  brotherNodeChangeTime: 1
}
export const currentDoingInfo = (state = initDoinginfo, action) => {
  switch (action.type) {
    case SEND_LRC:
      return {...state, sendlrcDetail: {...action.payload}}
    case SET_CURRENTSONG_STATUS:
      console.log(action.payload, 'SET_CURRENTSONG_STATUS');
      return {...state, isPlaying: action.payload}
    case ADD_SONGLIST:
      let cList = [...state.currentSongsList]
      let pList = [...action.payload]
      cList.forEach((item1,index) => {
        pList.forEach(item2 => {
          if (item1.id === item2.id) {
            cList.splice(index,1)
            console.log('已添加歌单的歌曲item2 = ', item2.name);
          }
        })
      });
      let currentSongsList = [...cList, ...pList]
      return {...state, currentSongsList}
  }
}

三大详情页 (歌曲详情、歌词详情、歌单详情)

本人比较懒,没有写注释,这里只是贴了一小段代码。所有代码如果有什么不懂的可以随时私信我。

import React, { memo, useCallback, useRef, forwardRef, useMemo } from "react";
import MinAndMax from "@/components/MinAndMax";
import { Link, withRouter } from "react-router-dom";

import {
  handleDataLrc,
  addSongAction
} from "@/redux/actions";
import { songDetailwithIdsReq, getLrcWithIdReq } from "@/utils/api/user";
import {
  albumReq,
  commentPlaylistReq,
  pubCommentReq
} from "@/utils/api/album";
import { getTime } from "@/utils/date";

import style from "./style.css";
import ZjDetail from './GezjDetail'

function SongDetail(props) {
  const {
    match,
    addSong
  } = props;
  const [songDetail, setSongDetail] = useState(null);
  const commentRef = useRef(null);


  useEffect(() => {
    if (currentType == 2 && album) {
      setAuthorid(album.artist.id);
    } else if (currentType == 1 && songDetail) {
      setAuthorid(songDetail.ar[0].id);
    } else if (currentType == 3) {
      // setAuthorid('match.params.id')
    }
  }, [history.location.pathname]);

  useEffect(() => {
    console.log(currentType, ".params.type");
    let getData;
    if (currentType == 1) {
      console.log(currentType, "111");
      let data = { id: match.params.id };
      getData = async () => {
        const res = await songDetailwithIdsReq({ ids: match.params.id });
        if (res.data.code === 200) {
          setSongDetail(res.data.songs[0]);
        }

        const resTwo = await getLrcWithIdReq(data);
        if (resTwo.data.code === 200) {
          let lrcArr = handleDataLrc(false, resTwo.data.lrc.lyric);
          setLrcData(lrcArr);
        }

        const resthree = await commentMusicReq(data);
        if (resthree.data.code === 200) {
          setComments(resthree.data.comments);
          setHotcomments(resthree.data.hotComments);
          setTotal(resthree.data.total);
        }
      };
      setSongType(0)
    }
    getData();
  }, [history.location.pathname, authorid]);

  const noOpenup = useCallback(() => {
    alert('暂未开放!')
  }, [])

  const scrollComment = useCallback(() => {
    let height = commentRef.current.offsetTop
    document.documentElement.scrollTop = height
  })

  if (currentType == 2 && !album) {
    return <div>Loading...</div>;
  }

  if (currentType == 1 && !songDetail) {
    return <div>Loading...</div>;
  }

  return (
    <>
      <TopRedBorder />
      <MinAndMax>
      </MinAndMax>
    </>
  );
}

const mState = (state) => {
  return {};
};
const mDispatch = (dispatch) => {
  return {
    playSong: (...arg) => {
      dispatch(playSongAction(...arg))
    },
    addSong: (...arg) => {
      dispatch(addSongAction(...arg))
    }
  };
};
export default withRouter(connect(mState, mDispatch)(SongDetail));

如何避免使用yarn eject 暴露所有配置

参考地址

  1. 使用两个包()

    yarn add -D customize-cra react-app-rewired

  2. 更换package.json的script

    "scripts": {
        "start": "react-app-rewired start",
        "build": "react-app-rewired build",
        "test": "react-app-rewired test",
        "_eject": "react-scripts eject"
    },
    
  3. 根目录下建立一个配置重写文件 config-overrides.js ,刚刚安装的依赖就会注入 react 内帮我们 override 相应的配置

    const {
      override,
      addWebpackAlias
    } = require('customize-cra')
    
    const path = require('path')
    
    function resolve(dir) {
      return path.join(__dirname, dir)
    }
    
    module.exports = {
      webpack: override(
        addWebpackAlias({
          '@': resolve('src')
        })
    }
    
  4. 重新启动运行项目(yarn start)

播放器

这里的播放器代码只是一部分,请不要复制粘贴,具体代码的还要到项目源码中去。

// player.jsx
import React, {
  memo,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";
import { connect } from "react-redux";
import { withSoundCloudAudio } from "react-soundplayer/addons";
import {
  PlayButton,
  Progress,
  VolumeControl,
  PrevButton,
  NextButton,
} from "react-soundplayer/components";

import MinAndMax from "../MinAndMax";
import style from "./index.css";
import PlayerListTop from "./PlayerListTop";
import { setCurrentTime, setCurrentSongDetail } from '@/redux/actions'

const stylePass = {
  position: "fixed",
  bottom: "0",
  width: "100%",
  borderTop: `1px solid #ececec`,
};

const Player = memo((props) => {
  const { soundCloudAudio, playing } = props;

  const playbtnRef = useRef(null)

  const getTimeWithMh = useCallback(
    (duration) => {
      let minOne = parseInt(duration / 60);
      let secondOne = (duration / 60 - parseInt(minOne)).toFixed(2);
      secondOne = parseInt(secondOne * 60);
      if (secondOne / 10 < 1) {
        secondOne = "0" + secondOne;
      }
      return `${minOne}:${secondOne}`;
    },
    [duration, currentTime]
  );

  return (
    <MinAndMax bgc="#2e2d2d" stylePass={stylePass}>
      <div className={[style.content, style.playerContaniner].join(" ")}>
        <div className={style.btngroup}>
          <img
            src={require("./Icon/last.svg").default}
            alt="svg"
            className={style.iconNormal}
            onClick={() => lastandnextChick('prev')}
          />
          <img
            src={require(`./Icon/${playing ? 'pause' : 'play'}.svg`).default}
            alt={`${playing ? '暂停' : '播放'}`}
            onClick={togglePauseAndPlay}
            className={[style.iconNormal, style.iconpp].join(" ")}
          />
          <img
            src={require("./Icon/last.svg").default}
            alt="svg"
            className={style.iconNormal}
            onClick={() => lastandnextChick('next')}
            style={{ transform: "rotate(180deg)" }}
          />
          {/* <NextButton onPrevClick={() => lastandnextChick("next")} {...props} /> */}

          <VolumeControl
            className="mr2 flex flex-center"
            buttonClassName="flex-none h2 button button-transparent button-grow rounded"
            rangeClassName="custom-track-bg"
            volume={0.5}
            {...props}
          />
        </div>
        <div className={style.slider}>
          <img
            src={
              currentSongDetail &&
                currentSongDetail.al &&
                currentSongDetail.al.picUrl
                ? currentSongDetail.al.picUrl
                : "http://s4.music.126.net/style/web2/img/default/default_album.jpg"
            }
            alt="musicPic"
            className="musicPic"
          />
        </div>
        <div className={style.btngroup}>
          <div>
            {showPlaylist ? (
              <div className={style.playlistItem}>
                <PlayerListTop toggleShowPlaylist={showPlaylistFun} songsList={songsList} playSongGlobal={playSong} playing={playing} soundCloudAudio={soundCloudAudio}/>
              </div>
            ) : null}
            <div onClick={(e) => showPlaylistFun(e)} className={style.playListicon}>
              <img
                src={require("./Icon/plist.svg").default}
                alt="svg"
                className={style.iconNormal}
              />
              <div className={style.playCount}>{songsList.length}</div>
            </div>
          </div>
        </div>
      </div>
    </MinAndMax>
  );
});

const mDispatch = (dispatch) => {
  console.log('dispatch');
  return {
    setNewSongs: (data) => {
      console.log(data, 'data setCurrentSongDetail');
      dispatch(setCurrentSongDetail(data))
    },
  }
}
const mState = (state) => {
  return {
    onEnd: state.currentDoingInfo.onEnd,
    brotherNodeChangeTime: state.currentDoingInfo.brotherNodeChangeTime
  }
}
export default withSoundCloudAudio(connect(mState, mDispatch)(Player));

谢谢观看