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

 

首先 看看实现的功能

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

React Hooks实现网易云音乐PC版_React Hooks

以下是一些核心代码

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>  <Routeexactpath="/"render={() => (          <Discover><DiscoverRoutes />  </Discover>)}
      ></Route>  <Routepath="/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 的一些核心代码

// 点击添加歌单的actionexport 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.jsconst 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.offsetTopdocument.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.jsximport 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}>  <imgsrc={require("./Icon/last.svg").default}alt="svg"className={style.iconNormal}onClick={() => lastandnextChick('prev')}
          />          <imgsrc={require(`./Icon/${playing ? 'pause' : 'play'}.svg`).default}alt={`${playing ? '暂停' : '播放'}`}onClick={togglePauseAndPlay}className={[style.iconNormal, style.iconpp].join(" ")}
          />  <imgsrc={require("./Icon/last.svg").default}alt="svg"className={style.iconNormal}onClick={() => lastandnextChick('next')}
            style={{ transform: "rotate(180deg)" }}
          />
          {/* <NextButton onPrevClick={() => lastandnextChick("next")} {...props} /> */}          <VolumeControlclassName="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}>  <imgsrc={  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}>              <imgsrc={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));复制代码

谢谢观看