上一节使用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构造函数中定义rankingListloadingrefreshScroll三个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>
&nbsp;-&nbsp;
<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.jsrankinginfo.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的更多说明见第四节实现动画

导入HeaderLoaddingScroll三个公用组件,接口请求方法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>

此时还缺少音符动画,复制上一节的initMusicIcostartMusicIcoAnimation两个函数在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实战(六):排行榜及歌曲本地持久化的更多相关文章

  1. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  2. React全家桶+Material-ui构建的后台管理系统

    一.简介 一个使用React全家桶(react-router-dom,redux,redux-actions,redux-saga,reselect)+Material-ui构建的后来管理中心. 二. ...

  3. webpack4 中的最新 React全家桶实战使用配置指南!

    最新React全家桶实战使用配置指南 这篇文档 是吕小明老师结合以往的项目经验 加上自己本身对react webpack redux理解写下的总结文档,总共耗时一周总结下来的,希望能对读者能够有收获, ...

  4. 使用react全家桶制作博客后台管理系统

    前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基于react全家桶(React.React-r ...

  5. 使用React全家桶搭建一个后台管理系统

    引子 学生时代为了掌握某个知识点会不断地做习题,做总结,步入岗位之后何尝不是一样呢?做业务就如同做习题,如果‘课后’适当地进行总结,必然更快地提升自己的水平. 由于公司采用的react+node的技术 ...

  6. react-music React全家桶项目,精品之作!

    React-Music 全家桶项目,精品之作! 一.简介 该项目是基于React全家桶开发的一个音乐播放器,技术栈采用:Webpack + React + React-redux + React-ro ...

  7. 初学者的React全家桶完整实例

    概述 该项目还有些功能在开发过程中,如果您有什么需求,欢迎您与我联系.我希望能够通过这个项目对React初学者,或者Babel/webpack初学者都有一定的帮助.我在此再强调一下,在我写的这些文章末 ...

  8. react全家桶从0搭建一个完整的react项目(react-router4、redux、redux-saga)

    react全家桶从0到1(最新) 本文从零开始,逐步讲解如何用react全家桶搭建一个完整的react项目.文中针对react.webpack.babel.react-route.redux.redu ...

  9. 使用react native制作的一款网络音乐播放器

    使用react native制作的一款网络音乐播放器 基于第三方库 react-native-video设计"react-native-video": "^1.0.0&q ...

随机推荐

  1. redis分布式映射算法

    redis分布式映射算法 一致性Hash算法的原理和实现 为了解决分布式系统中的负载均衡的问题 背景问题 有N台服务器提供缓存服务,需要对服务器进行负载均衡,将请求平均发到每台服务器上,每台服务器负载 ...

  2. vi操作笔记一

    vi命令  gg 到首行 shift + 4 跳到该行最后一个字符 shift + 6 跳到该行首个字符 shift + g 到尾行 vi 可视 G 全选 = 程序对齐   gg 到首行 vi 可视  ...

  3. 【Python】【基础知识】【异常】【Python的异常】报错、警告

    Python的异常 异常的层次结构: BaseException [所有异常的基类] +-- SystemExit [解释器请求退出] +-- KeyboardInterrupt [用户中断执行(通常 ...

  4. 小菜鸟之HTML常用

    html的基本结构是什么? 表示段落标签是什么?<p> 表示标题标签的是什么?<title>Css标签样式</title> 表示区域标签的是什么?<div&g ...

  5. JAVA汽车4S店管理系统

    JAVA汽车4S店管理系统源码(前台+后台)分为这5个大模块 系统设置 整车销售辅助销售汽修管理 汽修统计1.经理管理(增加 和删除功能)    表设计经理编号经理名年龄性别2.业务员管理(增删改查) ...

  6. php学习历程1——注册、登录(面向过程、面向对象)

    首先放一张天空之城 Php入门来的第一个小项目,首先做的是一个简陋的文章管理系统.有登录.注册.文章list.添加文章.修改文章.删除文章.分页这几个小功能. 面向过程的编码 面向对象的编码 首先做的 ...

  7. Python编程之注释

    一.注释 当你把变量理解透了,你就已经进入了编程的世界.随着学习的深入,用不了多久,你就可以写复杂的上千甚至上万行的代码啦,有些代码你花了很久写出来,过了些天再回去看,发现竟然看不懂了,这太正常了. ...

  8. flask 接收参数小坑

    前后端分离: 1.get方式: items = dict(request.args.items()) app_name = items["app_name"].strip() 或 ...

  9. 华为设备ACL与NAT技术

    ACL 访问控制列表(Access Control Lists),是应用在路由器(或三层交换机)接口上的指令列表,用来告诉路由器哪些数据可以接收,哪些数据是需要被拒绝的,ACL的定义是基于协议的,它适 ...

  10. tomcat 虚拟目录 连接池