React全家桶构建一款Web音乐App实战(六):排行榜及歌曲本地持久化
上一节使用Redux管理歌曲相关数据,实现核心播放功能,播放功能是本项目最复杂的一个功能,涉及各个组件之间的数据交互,播放逻辑控制。这一节继续开发排行榜列表和排行榜详情,以及把播放歌曲和播放歌曲列表的持久化到本地。步入主题
排行榜列表和详情接口抓取
使用chrome浏览器切换到手机模式输入QQ音乐移动端网址https://m.y.qq.com。进入后切换到Network,先把所有的请求清除掉,点击排行榜然后查看请求

点开第一个请求,点击Preview。排行榜列表数据如下图,

接着选择一个排行榜点击进去(先清除所有请求列表),就可以查看到排行榜详情的请求,点击请求的链接选择Preview查看排行榜详情数据

接口请求方法
在api目录下面的config.js中加入接口url配置,
const URL = {
...
/*排行榜*/
rankingList: "https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg",
/*排行榜详情*/
rankingInfo: "https://c.y.qq.com/v8/fcg-bin/fcg_v8_toplist_cp.fcg",
...
};
在api目录下新建ranking.js,用来存放接口请求方法
ranking.js
import jsonp from "./jsonp"
import {URL, PARAM, OPTION} from "./config"
export function getRankingList() {
const data = Object.assign({}, PARAM, {
g_tk: 5381,
uin: 0,
platform: "h5",
needNewCode: 1,
_: new Date().getTime()
});
return jsonp(URL.rankingList, data, OPTION);
}
export function getRankingInfo(topId) {
const data = Object.assign({}, PARAM, {
g_tk: 5381,
uin: 0,
platform: "h5",
needNewCode: 1,
tpl: 3,
page: "detail",
type: "top",
topid: topId,
_: new Date().getTime()
});
return jsonp(URL.rankingInfo, data, OPTION);
}
上诉代码提供了两个接口请求方法,稍后会调用这两个方法
接下来为排行榜建立一个模型类ranking,在model目录下面新建ranking.js。ranking类拥有的属性如下
export class Ranking {
constructor(id, title, img, songs) {
this.id = id;
this.title = title;
this.img = img;
this.songs = songs;
}
}
ranking包含songs歌曲列表,在ranking.js首行导入同目录下的song.js
import * as SongModel from "./song"
针对排行榜列表接口返回的数据创编写一个创建ranking对象函数
export function createRankingByList(data) {
const songList = [];
data.songList.forEach(item => {
songList.push(new SongModel.Song(0, "", item.songname, "", 0, "", item.singername));
});
return new Ranking (
data.id,
data.topTitle,
data.picUrl,
songList
);
}
这里接口只返回songname和singernam字段,把歌曲其它信息赋值上空字符串或者0
同样对于排行榜详情接口编写一个创建ranking对象函数
export function createRankingByDetail(data) {
return new Ranking (
data.topID,
data.ListName,
data.pic_album,
[]
);
}
歌曲列表给一个空数组
排行榜列表开发
先来看一下效果图

在排行榜列表中每一个item中都对应一个ranking对象,item中的前三个歌曲信息对应ranking对象中的songs数组,后面把接口获取的数据进行遍历创建ranking数组,ranking对象中再创建song数组,在组件的render函数中进行遍历渲染ui
回到原来的Ranking.js。在constructor构造函数中定义rankingList、loading、refreshScroll三个state,分别表示Ranking组件中的排行榜列表、是否正在进行接口请求、是否需要刷新Scroll组件
constructor(props) {
super(props);
this.state = {
loading: true,
rankingList: [],
refreshScroll: false
};
}
导入刚刚编写的接口请求函数,接口请求成功的CODE码和ranking模型类。在组件Ranking组件挂载完成后,发送接口请求
import {getRankingList} from "@/api/ranking"
import {CODE_SUCCESS} from "@/api/config"
import * as RankingModel from "@/model/ranking"
componentDidMount() {
getRankingList().then((res) => {
console.log("获取排行榜:");
if (res) {
console.log(res);
if (res.code === CODE_SUCCESS) {
let topList = [];
res.data.topList.forEach(item => {
if (/MV/i.test(item.topTitle)) {
return;
}
topList.push(RankingModel.createRankingByList(item));
});
this.setState({
loading: false,
rankingList: topList
}, () => {
//刷新scroll
this.setState({refreshScroll:true});
});
}
}
});
}
上述代码中(/MV/i.test(item.topTitle)用来过滤mv排行榜,获取数据后将loading更新为false,最后当列表数据渲染完成后更改refreshScroll状态为true,使Scroll组件重新计算列表高度
在这个组件中依赖Scroll和Loading组件,导入这两个组件
import Scroll from "@/common/scroll/Scroll"
import Loading from "@/common/loading/Loading"
render方法代码如下
render() {
return (
<div className="music-ranking">
<Scroll refresh={this.state.refreshScroll}>
<div className="ranking-list">
{
this.state.rankingList.map(ranking => {
return (
<div className="ranking-wrapper" key={ranking.id}>
<div className="left">
<img src={ranking.img} alt={ranking.title}/>
</div>
<div className="right">
<h1 className="ranking-title">
{ranking.title}
</h1>
{
ranking.songs.map((song, index) => {
return (
<div className="top-song" key={index}>
<span className="index">{index + 1}</span>
<span>{song.name}</span>
-
<span className="song">{song.singer}</span>
</div>
);
})
}
</div>
</div>
);
})
}
</div>
</Scroll>
<Loading title="正在加载..." show={this.state.loading}/>
</div>
);
}
ranking.styl请在源码中查看
这个列表中有图片,同样需要对图片加载进行优化,导入第三节优化图片加载使用的react-lazyload插件
import LazyLoad, { forceCheck } from "react-lazyload"
使用LazyLoad组件包裹图片,并传入height
<div className="ranking-wrapper" key={ranking.id}>
<div className="left">
<LazyLoad height={100}>
<img src={ranking.img} alt={ranking.title}/>
</LazyLoad>
</div>
...
</div>
监听Scroll组件的onScroll,滚动的时候检查图片是否出现在屏幕内,如果可见立即加载图片
<Scroll refresh={this.state.refreshScroll}
onScroll={() => {forceCheck();}}>
...
</Scroll>
排行榜详情开发
在ranking目录下新建RankingInfo.js和rankinginfo.styl
RankingInfo.js
import React from "react"
import "./rankinginfo.styl"
class RankingInfo extends React.Component {
render() {
return (
<div className="ranking-info">
</div>
);
}
}
export default RankingInfo
rankinginfo.styl请在最后的源码中查看
RankingInfo组件需要操作Redux中的歌曲和歌曲列表,为RankingInfo编写对应的容器组件Ranking,在container目录下新建Ranking.js
import {connect} from "react-redux"
import {showPlayer, changeSong, setSongs} from "../redux/actions"
import RankingInfo from "../components/ranking/RankingInfo"
const mapDispatchToProps = (dispatch) => ({
showMusicPlayer: (show) => {
dispatch(showPlayer(show));
},
changeCurrentSong: (song) => {
dispatch(changeSong(song));
},
setSongs: (songs) => {
dispatch(setSongs(songs));
}
});
export default connect(null, mapDispatchToProps)(RankingInfo)
进入排行榜详情的入口在排行榜列表页中,所以先在排行榜中增加子路由和点击跳转事件。导入route组件和Ranking容器组件
import {Route} from "react-router-dom"
import RankingInfo from "@/containers/Ranking"
将Route组件放置在如下位置
render() {
let {match} = this.props;
return (
<div className="music-ranking">
...
<Loading title="正在加载..." show={this.state.loading}/>
<Route path={`${match.url + '/:id'}`} component={RankingInfo}/>
</div>
);
}
给列表的.ranking-wrapper元素增加点击事件
toDetail(url) {
return () => {
this.props.history.push({
pathname: url
});
}
}
<div className="ranking-wrapper" key={ranking.id}
onClick={this.toDetail(`${match.url + '/' + ranking.id}`)}>
</div>
继续编写RankingInfo组件。在RankingInfo组件的constructor构造函数中初始化以下state
constructor(props) {
super(props);
this.state = {
show: false,
loading: true,
ranking: {},
songs: [],
refreshScroll: false
}
}
其中show用来控制组件进入动画、ranking存放排行榜信息、songs存放歌曲列表。组件进入动画继续使用第四节实现动画中使用的react-transition-group,导入CSSTransition组件
import {CSSTransition} from "react-transition-group"
在组件挂载以后,将show状态改为true
componentDidMount() {
this.setState({
show: true
});
}
用CSSTransition组件包裹RankingInfo的根元素
<CSSTransition in={this.state.show} timeout={300} classNames="translate">
<div className="ranking-info">
</div>
</CSSTransition>
关于CSSTransition的更多说明见第四节实现动画
导入Header、Loadding和Scroll三个公用组件,接口请求方法getRankingInfo,接口成功CODE码,排行榜和歌曲模型类等
import ReactDOM from "react-dom"
import Header from "@/common/header/Header"
import Scroll from "@/common/scroll/Scroll"
import Loading from "@/common/loading/Loading"
import {getRankingInfo} from "@/api/ranking"
import {getSongVKey} from "@/api/song"
import {CODE_SUCCESS} from "@/api/config"
import * as RankingModel from "@/model/ranking"
import * as SongModel from "@/model/song"
componentDidMount中增加以下代码
let rankingBgDOM = ReactDOM.findDOMNode(this.refs.rankingBg);
let rankingContainerDOM = ReactDOM.findDOMNode(this.refs.rankingContainer);
rankingContainerDOM.style.top = rankingBgDOM.offsetHeight + "px";
getRankingInfo(this.props.match.params.id).then((res) => {
console.log("获取排行榜详情:");
if (res) {
console.log(res);
if (res.code === CODE_SUCCESS) {
let ranking = RankingModel.createRankingByDetail(res.topinfo);
ranking.info = res.topinfo.info;
let songList = [];
res.songlist.forEach(item => {
if (item.data.pay.payplay === 1) { return }
let song = SongModel.createSong(item.data);
//获取歌曲vkey
this.getSongUrl(song, item.data.songmid);
songList.push(song);
});
this.setState({
loading: false,
ranking: ranking,
songs: songList
}, () => {
//刷新scroll
this.setState({refreshScroll:true});
});
}
}
});
获取歌曲文件函数
getSongUrl(song, mId) {
getSongVKey(mId).then((res) => {
if (res) {
if(res.code === CODE_SUCCESS) {
if(res.data.items) {
let item = res.data.items[0];
song.url = `http://dl.stream.qqmusic.qq.com/${item.filename}?vkey=${item.vkey}&guid=3655047200&fromtag=66`
}
}
}
});
}
组件挂载完成以后调用getRankingInfo函数去请求详情数据,请求成功后调用setState设置ranking和songs的值触发render函数重新调用,在对歌曲列表遍历的时候调用getSongUrl去获取歌曲地址
render方法代码如下
render() {
let ranking = this.state.ranking;
let songs = this.state.songs.map((song, index) => {
return (
<div className="song" key={song.id}>
<div className="song-index">{index + 1}</div>
<div className="song-name">{song.name}</div>
<div className="song-singer">{song.singer}</div>
</div>
);
});
return (
<CSSTransition in={this.state.show} timeout={300} classNames="translate">
<div className="ranking-info">
<Header title={ranking.title}></Header>
...
<div ref="rankingContainer" className="ranking-container">
<div className="ranking-scroll" style={this.state.loading === true ? {display:"none"} : {}}>
<Scroll refresh={this.state.refreshScroll}>
<div className="ranking-wrapper">
<div className="ranking-count">排行榜 共{songs.length}首</div>
<div className="song-list">
{songs}
</div>
<div className="info" style={ranking.info ? {} : {display:"none"}}>
<h1 className="ranking-title">简介</h1>
<div className="ranking-desc">
{ranking.info}
</div>
</div>
</div>
</Scroll>
</div>
<Loading title="正在加载..." show={this.state.loading}/>
</div>
</div>
</CSSTransition>
);
}
监听Scroll组件滚动,实现上滑和往下拉伸效果
scroll = ({y}) => {
let rankingBgDOM = ReactDOM.findDOMNode(this.refs.rankingBg);
let rankingFixedBgDOM = ReactDOM.findDOMNode(this.refs.rankingFixedBg);
let playButtonWrapperDOM = ReactDOM.findDOMNode(this.refs.playButtonWrapper);
if (y < 0) {
if (Math.abs(y) + 55 > rankingBgDOM.offsetHeight) {
rankingFixedBgDOM.style.display = "block";
} else {
rankingFixedBgDOM.style.display = "none";
}
} else {
let transform = `scale(${1 + y * 0.004}, ${1 + y * 0.004})`;
rankingBgDOM.style["webkitTransform"] = transform;
rankingBgDOM.style["transform"] = transform;
playButtonWrapperDOM.style.marginTop = `${y}px`;
}
}
<Scroll refresh={this.state.refreshScroll} onScroll={this.scroll}>
...
</Scroll>
详细说明请看第四节实现动画列表滚动和图片拉伸效果
接下来给歌曲增加点击播放功能,一个是点击单个歌曲播放,另一个是点击全部播放
selectSong(song) {
return (e) => {
this.props.setSongs([song]);
this.props.changeCurrentSong(song);
};
}
playAll = () => {
if (this.state.songs.length > 0) {
//添加播放歌曲列表
this.props.setSongs(this.state.songs);
this.props.changeCurrentSong(this.state.songs[0]);
this.props.showMusicPlayer(true);
}
}
<div className="song" key={song.id} onClick={this.selectSong(song)}>
...
</div>
<div className="play-wrapper" ref="playButtonWrapper">
<div className="play-button" onClick={this.playAll}>
<i className="icon-play"></i>
<span>播放全部</span>
</div>
</div>
此时还缺少音符动画,复制上一节的initMusicIco和startMusicIcoAnimation两个函数在componentDidMount中调用initMusicIco
this.initMusicIco();
在selectSong函数中调用startMusicIcoAnimation启动动画
selectSong(song) {
return (e) => {
this.props.setSongs([song]);
this.props.changeCurrentSong(song);
this.startMusicIcoAnimation(e.nativeEvent);
};
}
音符下落动画具体请看上一节歌曲点击音符下落动画
效果如下

歌曲本地持久化
当每次进入网页的时候退出页面前播放的歌曲以及播放列表都会消失,为了实现上一次播放的歌曲以及歌曲列表在下一次打开网页还会继续存在,使用H5的本地存储localStorage对象来实现歌曲持久化到。localStorage有setItem()和 getItem()两个方法,前者存储用来一个键值对的数据,后者通过key获取对应的值,localStorage会在当前域名下存储数据,更多用法请戳这里
在util目录下新建一个歌曲持久化工具类storage.js
storage.js
let localStorage = {
setCurrentSong(song) {
window.localStorage.setItem("song", JSON.stringify(song));
},
getCurrentSong() {
let song = window.localStorage.getItem("song");
return song ? JSON.parse(song) : {};
},
setSongs(songs) {
window.localStorage.setItem("songs", JSON.stringify(songs));
},
getSongs() {
let songs = window.localStorage.getItem("songs");
return songs ? JSON.parse(songs) : [];
}
}
export default localStorage
上诉代码中有设置当前歌曲、获取当前歌曲、设置播放列表和获取播放列表四个方法。在使用localStorage存储数据的时候,借助JSON.stringify()将对象转化成json字符串,获取数据后再使用JSON.parse()将json字符串转化成对象
在Redux中,初始化的song和songs从localStorage中获取
import localStorage from "../util/storage"
const initialState = {
showStatus: false, //显示状态
song: localStorage.getCurrentSong(), //当前歌曲
songs: localStorage.getSongs() //歌曲列表
};
修改歌曲的reducer函数song调用时将歌曲持久化到本地
function song(song = initialState.song, action) {
switch (action.type) {
case ActionTypes.CHANGE_SONG:
localStorage.setCurrentSong(action.song);
return action.song;
default:
return song;
}
}
添加歌曲列表或删除播放列表中的歌曲的时将歌曲列表持久化到本地
function songs(songs = initialState.songs, action) {
switch (action.type) {
case ActionTypes.SET_SONGS:
localStorage.setSongs(action.songs);
return action.songs;
case ActionTypes.REMOVE_SONG_FROM_LIST:
let newSongs = songs.filter(song => song.id !== action.id);
localStorage.setSongs(newSongs);
return newSongs;
default:
return songs;
}
}
在所有的组件触发修改歌曲或歌曲列表的reducer函数时都会进行持久化操作。这样修改之后Player组件需要稍作修改,当选择播放歌曲后退出重新进入时,会报如下错误,这是因为第一次调用Player组件的render方法歌曲已经存在,此时if判断成立访问audioDOM时dom还没挂载到页面

报错代码片段
//从redux中获取当前播放歌曲
if (this.props.currentSong && this.props.currentSong.url) {
//当前歌曲发发生变化
if (this.currentSong.id !== this.props.currentSong.id) {
this.currentSong = this.props.currentSong;
this.audioDOM.src = this.currentSong.url;
this.audioDOM.load();
}
}
增加一个if判断
if (this.audioDOM) {
this.audioDOM.src = this.currentSong.url;
this.audioDOM.load();
}
playOrPause方法修改如下
playOrPause = () => {
if(this.state.playStatus === false){
//表示第一次播放
if (this.first === undefined) {
this.audioDOM.src = this.currentSong.url;
this.first = true;
}
this.audioDOM.play();
this.startImgRotate();
this.setState({
playStatus: true
});
}else{
...
}
}
总结
这一节相对于上一节比较简单,大部分动画效果在上几节都已经做了说明,另外在最近刚刚新增了歌手功能,可以在github仓库中通过预览地址体验
完整项目地址:https://github.com/code-mcx/mango-music
本章节代码在chapter6分支
后续更新中...
React全家桶构建一款Web音乐App实战(六):排行榜及歌曲本地持久化的更多相关文章
- 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入
使用react全家桶制作博客后台管理系统 前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...
- React全家桶+Material-ui构建的后台管理系统
一.简介 一个使用React全家桶(react-router-dom,redux,redux-actions,redux-saga,reselect)+Material-ui构建的后来管理中心. 二. ...
- webpack4 中的最新 React全家桶实战使用配置指南!
最新React全家桶实战使用配置指南 这篇文档 是吕小明老师结合以往的项目经验 加上自己本身对react webpack redux理解写下的总结文档,总共耗时一周总结下来的,希望能对读者能够有收获, ...
- 使用react全家桶制作博客后台管理系统
前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基于react全家桶(React.React-r ...
- 使用React全家桶搭建一个后台管理系统
引子 学生时代为了掌握某个知识点会不断地做习题,做总结,步入岗位之后何尝不是一样呢?做业务就如同做习题,如果‘课后’适当地进行总结,必然更快地提升自己的水平. 由于公司采用的react+node的技术 ...
- react-music React全家桶项目,精品之作!
React-Music 全家桶项目,精品之作! 一.简介 该项目是基于React全家桶开发的一个音乐播放器,技术栈采用:Webpack + React + React-redux + React-ro ...
- 初学者的React全家桶完整实例
概述 该项目还有些功能在开发过程中,如果您有什么需求,欢迎您与我联系.我希望能够通过这个项目对React初学者,或者Babel/webpack初学者都有一定的帮助.我在此再强调一下,在我写的这些文章末 ...
- react全家桶从0搭建一个完整的react项目(react-router4、redux、redux-saga)
react全家桶从0到1(最新) 本文从零开始,逐步讲解如何用react全家桶搭建一个完整的react项目.文中针对react.webpack.babel.react-route.redux.redu ...
- 使用react native制作的一款网络音乐播放器
使用react native制作的一款网络音乐播放器 基于第三方库 react-native-video设计"react-native-video": "^1.0.0&q ...
随机推荐
- centos7 的system
1.vim /etc/systemd/system/alertmanager.service [Unit] Description=Alertmanager After=network-online. ...
- 18.linux日志收集数据到hdfs上面
先创建一个目录 在这个job目录下创建upload.sh文件 [hadoop@node1 ~]$ pwd /home/hadoop [hadoop@node1 ~]$ mkdir job [hadoo ...
- git合并时冲突<<<<<<< HEAD
<<<<<<< HEAD 本地代码 ======= 拉下来的代码 >>>>>>>
- 什么是阿里云ACE认证
作为国内最大的云计算服务商,阿里云不仅引领着中国云计算技术在全球范围内扩大版图,同时还责无旁贷的承担着培养.壮大中国云生态人才力量的使命. 阿里云自2015年推出国内首个公有云技术认证以来,短短三年已 ...
- Servlet简单例子
一.项目结构 二.index.jsp <%@ page contentType="text/html; charset=utf-8" %> <html> & ...
- Redis迁移键
迁移键: move key db 用于在Redis内部进行数据迁移 dump key + restore key ttl value 可以实现在不同的Redis实例之间进行数据迁移 127.0.0.1 ...
- Docker结合Jenkins构建持续集成环境
1.环境说明: jenkins+svn:192.168.71.142 测试环境:192.168.71.145 生产环境:192.168.71.148 操作系统:centos7. Maven3. Tom ...
- python线程间通信
#!/usr/bin/python # -*- coding:utf8 -*- from threading import Thread, Lock import random def test_th ...
- Flask:上下文管理
1. werkzurg from werkzur.serving import run_simple def run(environ,start_response): reuturn [b'hello ...
- Pytorch中randn和rand函数的用法
Pytorch中randn和rand函数的用法 randn torch.randn(*sizes, out=None) → Tensor 返回一个包含了从标准正态分布中抽取的一组随机数的张量 size ...