In the last section, Redux was used to manage data related to songs and realize the core playback function. The playback function is the most complex function of this project, involving data interaction between various components and logical control of playback. This section continues to develop chart lists and chart details, as well as persisting play songs and playlist locally. Enter the theme

Leaderboard list and detail interface fetching

Use Chrome browser to switch to mobile mode and enter QQ music mobile terminal url m.y.qq.com. After entering, switch to Network, delete all the requests first, click the leaderboard and then view the requests

Click on the first request and click Preview. The leaderboard list data is shown below.

Select a leaderboard and click in (clear all request list first) to view the leaderboard details of the request. Click on the link in the request and select Preview to view the leaderboard details data

Interface request method

Add the interface URL configuration to config.js in the API directory.

const URL = { ... List * / * / rankingList: "https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg", list details / * * / rankingInfo: "https://c.y.qq.com/v8/fcg-bin/fcg_v8_toplist_cp.fcg", ... };Copy the code

Create a new ranking. Js file in the API directory to store interface request methods

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);
}
Copy the code

The appeal code provides two interface request methods that will be invoked later

Next create a model class ranking for the leaderboards. Create a new ranking. Js under the Model directory. The Ranking class has the following attributes

export class Ranking { constructor(id, title, img, songs) { this.id = id; this.title = title; this.img = img; this.songs = songs; }}Copy the code

Ranking contains a list of songs and imports song-js in the same directory at the top of ranking

import * as SongModel from "./song"
Copy the code

Create a ranking object function for the data returned by the ranking list interface

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
    );
}
Copy the code

Here the interface returns only the SongName and Singernam fields, assigning the rest of the song to an empty string or 0

Also write a create ranking object function for the ranking detail interface

export function createRankingByDetail(data) {
    return new Ranking (
        data.topID,
        data.ListName,
        data.pic_album,
        []
    );
}
Copy the code

The list of songs is given an empty array

Leaderboard list development

Let’s take a look at the renderings

Each item in the ranking list corresponds to a ranking object. The first three songs in the item correspond to the Songs array in the ranking object. Then, the data obtained by the interface is traversed to create a ranking array, and song array is created in the ranking object. Iterate through the render UI in the render function of the component

Go back to the original Ranking. Js. In the constructor constructor, we define three states: rankingList, Loading, and refreshScroll, which respectively indicate the Ranking list in the Ranking component, whether an interface request is in progress, and whether the Scroll component needs to be refreshed

constructor(props) {
    super(props);

    this.state = {
        loading: true,
        rankingList: [],
        refreshScroll: false
    };
}
Copy the code

Import the interface request function you just wrote, the interface request success CODE, and the Ranking Model class. Send the interface request after the Component Ranking component is mounted

import {getRankingList} from "@/api/ranking"
import {CODE_SUCCESS} from "@/api/config"
import * as RankingModel from "@/model/ranking"
Copy the code
ComponentDidMount () {componentDidMount() {getRankingList().then((res) => {console.log(" componentDidMount: "); 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}, () => {// refreshScroll this.setstate ({refreshScroll:true}); }); }}}); }Copy the code

In the above code (/MV/i.test(item.toptitle) is used to filter MV leaderboards and update loading to false after data is obtained. Finally, when the list data is rendered, the refreshScroll state is changed to True so that the Scroll component recalculates the list height

Import Scroll and Loading components that depend on this component

import Scroll from "@/common/scroll/Scroll"
import Loading from "@/common/loading/Loading"
Copy the code

The render method code is as follows

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>
    );
}
Copy the code

Look at ranking. Styl in the source code

The react-lazyLoad plugin is used in section 3 to optimize image loading

import LazyLoad, { forceCheck } from "react-lazyload"
Copy the code

Wrap the image with the LazyLoad component and pass height

<div className="ranking-wrapper" key={ranking.id}> <div className="left"> <LazyLoad height={100}> <img src={ranking.img}  alt={ranking.title}/> </LazyLoad> </div> ... </div>Copy the code

Listen to the onScroll of the Scroll component and check whether the picture appears on the screen while scrolling. If so, load the picture immediately

<Scroll refresh={this.state.refreshScroll} onScroll={() => {forceCheck(); }} >... </Scroll>Copy the code

Leaderboard Details Development

Create rankingInfo.js and rankingInfo.styl in the ranking directory

RankingInfo.js

import React from "react"

import "./rankinginfo.styl"

class RankingInfo extends React.Component {
    render() {
        return (
            <div className="ranking-info">

            </div>
        );
    }
}

export default RankingInfo
Copy the code

Rankinginfo.styl is available in the final source code

The RankingInfo component needs to manipulate songs and song lists in Redux. Write a corresponding container component Ranking for RankingInfo. Create a new Ranking

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)Copy the code

The entry to the leaderboard details is in the leaderboard list page, so add sub-routing and click-to-jump events to the leaderboard first. Import the Route component and Ranking container component

import {Route} from "react-router-dom"
import RankingInfo from "@/containers/Ranking"
Copy the code

Place the Route component in the following position

render() { let {match} = this.props; return ( <div className="music-ranking"> ... <Loading title=" Loading..." show={this.state.loading}/> <Route path={`${match.url + '/:id'}`} component={RankingInfo}/> </div> ); }Copy the code

Add click events to the list’s. Ranking – Wrapper element

toDetail(url) { return () => { this.props.history.push({ pathname: url }); }}Copy the code
<div className="ranking-wrapper" key={ranking.id}
    onClick={this.toDetail(`${match.url + '/' + ranking.id}`)}>
</div>
Copy the code

Continue writing the RankingInfo component. Initialize the following states in the constructor constructor of the RankingInfo component

constructor(props) {
    super(props);

    this.state = {
        show: false,
        loading: true,
        ranking: {},
        songs: [],
        refreshScroll: false
    }
}
Copy the code

Show is used to control the component’s entry into animation, ranking stores the information on the charts, and Songs stores the list of songs. Continue with the react-transition-group used in the animation in Section 4 and import the CSSTransition component

import {CSSTransition} from "react-transition-group"
Copy the code

After the component is mounted, change the show state to true

componentDidMount() {
    this.setState({
        show: true
    });
}
Copy the code

Wrap the root element of RankingInfo with the CSSTransition component

<CSSTransition in={this.state.show} timeout={300} classNames="translate">
    <div className="ranking-info">
    </div>
</CSSTransition>
Copy the code

See section 4 Implementing Animations for more information on CSSTransition

Import Header, Loadding and Scroll three common components, interface request method getRankingInfo, interface success CODE, ranking and song model class, etc

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"Copy the code

Add the following code to 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) = > {the console. The log (" access list details: "); 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); // Get the song vkey this.getsongurl (song, item.data.songmid); songList.push(song); }); this.setState({ loading: false, ranking: ranking, songs: SongList}, () => {// refreshScroll this.setstate ({refreshScroll:true}); }); }}});Copy the code

Gets the song file function

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`
                }
            }
        }
    });
}
Copy the code

After the component is mounted, getRankingInfo is called to request the detailed data. After the request is successful, setState is called to set the ranking and songs values to trigger the render function to call again. GetSongUrl is called to get the song address while traversing the song list

The render method code is as follows

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"> ranking < songs > < song > < song > < song > < song > < song > < song > < song > < song > < song > < song > < song > < song > < song > {display:"none"}}> <h1 className="ranking-title"> Profile </h1> <div className="ranking-desc"> {ranking. Info} </div> </div> </div> </Scroll> </div> <Loading title= show={this.state.loading}/> </div> </div> </CSSTransition> ); }Copy the code

Monitor the Scroll component to slide up and pull down

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`;
    }
}
Copy the code
<Scroll refresh={this.state.refreshScroll}  onScroll={this.scroll}>
    ...
</Scroll>
Copy the code

For details, see section 4 for animated list scrolling and image stretching

Next, add the click play function to the song, one is to click a single song to play, another is to click all play

selectSong(song) { return (e) => { this.props.setSongs([song]); this.props.changeCurrentSong(song); }; } playAll = () => {if (this.state.songs. Length > 0) {// Add props. SetSongs (this.state.songs); this.props.changeCurrentSong(this.state.songs[0]); this.props.showMusicPlayer(true); }}Copy the code
<div className="song" key={song.id} onClick={this.selectSong(song)}>
    ...
</div>
Copy the code
<div className="play-wrapper" ref="playButtonWrapper"> <div className="play-button" onClick={this.playAll}> <i The className = "icon - play" > < / I > < span > play all < / span > < / div > < / div >Copy the code

Note animation is missing at this point, so call initMusicIco in componentDidMount by copying initMusicIco and startMusicIcoAnimation from the previous section

this.initMusicIco();
Copy the code

Call startMusicIcoAnimation in the selectSong function to start the animation

selectSong(song) {
    return (e) => {
        this.props.setSongs([song]);
        this.props.changeCurrentSong(song);
        this.startMusicIcoAnimation(e.nativeEvent);
    };
}
Copy the code

Note drop animation for details, see the previous section on the song click note drop animation

Results the following

Local persistence of songs

When entering the web page each time exit the page before the song and the playlist will disappear, in order to achieve the last played song and song list will continue to exist in the next open the web page, use the localStorage of H5 localStorage object to achieve the song persistence to. SetItem () and getItem() are two methods: setItem() and getItem() are two methods: setItem() and getItem() are two methods: setItem() and getItem() are two methods: setItem() and getItem() are two methods: setItem() and getItem()

Create a new song persistence utility class storage.js in the util directory

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
Copy the code

The appeal code has four methods: set the current song, get the current song, set the playlist and get the playlist. When using localStorage to store data, use json.stringify () to convert the object into a JSON string. After retrieving the data, use json.parse () to convert the JSON string into an object

In Redux, the initialized song and Songs are retrieved from localStorage

import localStorage from ".. /util/storage"Copy the code
Const initialState = {showStatus: false, / / show the state of song: localStorage. GetCurrentSong (), / / the current song songs: Localstorage.getsongs () // List of songs};Copy the code

Modify the reducer function song call to persist the songs locally

function song(song = initialState.song, action) { switch (action.type) { case ActionTypes.CHANGE_SONG: localStorage.setCurrentSong(action.song); return action.song; default: return song; }}Copy the code

Persist a playlist locally when adding or deleting songs from a playlist

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;
    }
}
Copy the code

Persistence occurs when all components trigger the Reducer function to modify a song or a list of songs. If the render method of the Player component is used for the first time, the song already exists, and the DOM has not been mounted to the audioDOM

Error code snippet

/ / gets the current play songs from redux if (this. Props. CurrentSong && enclosing props. CurrentSong. Url) {/ / current song hair change if (this. CurrentSong. Id! == this.props.currentSong.id) { this.currentSong = this.props.currentSong; this.audioDOM.src = this.currentSong.url; this.audioDOM.load(); }}Copy the code

Add an if judgment

if (this.audioDOM) {
    this.audioDOM.src = this.currentSong.url;
    this.audioDOM.load();
}
Copy the code

The playOrPause method is modified as follows

PlayOrPause = () => {if(this.state.playStatus === false){this.first === undefined) {this.audiodom.src =  this.currentSong.url; this.first = true; } this.audioDOM.play(); this.startImgRotate(); this.setState({ playStatus: true }); }else{ ... }}Copy the code

conclusion

This section is relatively simple compared to the previous one, most of the animation effects have been explained in the last few sections, and the singer function has been recently added, which can be experienced in the Github repository through the preview address

Full project address: github.com/dxx/mango-m…

The code for this chapter is in chapter6