前言:辛苦了大半个月,终于我的网易云音热要上线了。话不多说直接上链接。感谢高老哥帮忙配置nginx,感谢 Binaryify大佬的网易接口支持,如果大家觉得项目不错请给一个 star 吧 !
首先 看看实现的功能
- 首页UI
- 歌曲详情页面
- 歌单详情页面
- 专辑详情页面
- 详情页的播放和添加歌单
- 音乐播放器列表
- 歌曲单曲循环
以下是一些核心代码
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 暴露所有配置
-
使用两个包()
yarn add -D customize-cra react-app-rewired
-
更换package.json的script
"scripts": {"start": "react-app-rewired start","build": "react-app-rewired build","test": "react-app-rewired test","_eject": "react-scripts eject"},复制代码
-
根目录下建立一个配置重写文件 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') }) }复制代码
-
重新启动运行项目(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 &¤tSongDetail.al &¤tSongDetail.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));复制代码