前言:辛苦了大半个月,终于我的网易云音热要上线了。话不多说直接上链接。感谢高老哥帮忙配置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>
<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 暴露所有配置
参考地址
-
使用两个包()
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.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));