This section continues to develop the singer list and singer details

Interface data fetching

1. List of singers

Use Chrome browser to open QQ Music official website, enter QQ music official website, open developer tool to select Network option, click JS option, click singer in QQ Music official website

Click on the request in the red box above and click Preview on the right. Below is the singer list data. See the Headers option for the specific request link parameters

2. Singer details

Select any singer in the list of singers click to view the specific request data in the left Network

See the singer list and singer details in THE QQ Music API interface comb for the specific description of the interface

Interface request method

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

config.js

const URL = { ... / * singer list * / singerList: "https://c.y.qq.com/v8/fcg-bin/v8.fcg", details singer / * * / singerInfo: "https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg" };Copy the code

Create singer.js under the API and write the interface request method

singer.js

import jsonp from "./jsonp"
import {URL, PARAM, OPTION} from "./config"

export function getSingerList(pageNum, key) {
	const data = Object.assign({}, PARAM, {
		g_tk: 5381,
		loginUin: 0,
		hostUin: 0,
		platform: "yqq",
		needNewCode: 0,
		channel: "singer",
		page: "list",
		key,
		pagenum: pageNum,
		pagesize: 100
	});
	return jsonp(URL.singerList, data, OPTION);
}

export function getSingerInfo(mId) {
	const data = Object.assign({}, PARAM, {
		g_tk: 5381,
		loginUin: 0,
		hostUin: 0,
		platform: "yqq",
		needNewCode: 0,
		singermid: mId,
		order: "listen",
		begin: 0,
		num: 100,
		songstatus: 1
	});
	return jsonp(URL.singerInfo, data, OPTION);
}
Copy the code

Next, create the Singer model class Singer. In the model directory, create singer.js with the following properties

export class Singer { constructor(id, mId, name, img) { this.id = id; this.mId = mId; this.name = name; this.img = img; }}Copy the code

Write two object creation functions based on the data returned by the singer list and singer details. Write the following code in singer.js

export function createSingerBySearch(data) {
    return new Singer(
        data.singerid,
        data.singermid,
        data.singername,
        `http://y.gtimg.cn/music/photo_new/T001R68x68M000${data.singermid}.jpg?max_age=2592000`
    );
}

export function createSingerByDetail(data) {
    return new Singer(
        data.singer_id,
        data.singer_mid,
        data.singer_name,
        `http://y.gtimg.cn/music/photo_new/T001R300x300M000${data.singer_mid}.jpg?max_age=2592000`
    );
}
Copy the code

Singer List development

Let’s take a look at the renderings

The singer page is divided into two parts, the upper part is the singer classification, the lower part is the corresponding singer list. There is a key parameter in the singer list interface, and the parameter is the corresponding singer classification, which is spliced by the first column classification and the second column classification. In the singer list page of QQ Music official website, you can check the DOM structure through the browser debugging tool to see the corresponding key value of the classification

Data-key is the key value of the corresponding classification

We then initialize these keys, go back to singerList.js in the Singer directory under Componens, and use the constructor in singerList.js to initialize the keys needed for classification

constructor(props) { super(props); Enclosing types = [{key: "all_all," name: "all"}, {key: "cn_man," name: "Chinese man"}, {key: "cn_woman," name: "Chinese women"}, {key: "cn_team", Name: "Chinese combination"}, {key: "k_man," name: "Korean"}, {key: "k_woman," name: "South Korea female"}, {key: "k_team," name: "South Korea combination"}, {key: "j_man". Name: "Japan male"}, {key: "j_woman," name: "the Japanese female"}, {key: "j_team," name: "Japan combination"}, {key: "eu_man," name: "Europe and the United States male"}, {key: "eu_woman". Name: "Europe and the United States women"}, {key: "eu_team," name: "Europe and the United States combination"}, {key: "other_other," name: "other"}; This. Indexs = [{key: "all", the name: "hot"}, {key: "A", the name: "A"}, {key: "B", the name: "B"}, {key: "C", the name: "C"}, {key: "D". name:"D"}, {key:"E", name:"E"}, ... ] ; }Copy the code

Province part of the code, complete code please view in the source code

Then initialize some default states and continue adding the following code to constructor

this.state = {
	loading: true,
	typeKey: "all_all",
	indexKey: "all",
	singers: [],
	refreshScroll: false
}
Copy the code

TypeKey is the default category key selected in the first column, indexKey is the default category key selected in the second column, and Singer stores the list of singers

The Scroll component encapsulated in section 3 is also used here. Why not use overflow of browser: Scroll, of course, because of the poor experience of the original scrolling effect, in some browsers with right slide back, left slide forward, this time conflict is very boring ~_~. Now we need to Scroll left and right. At this time, the original encapsulated Scroll component does not meet the requirements. Next, we will transform the Scroll component

The Scroll component is wrapped around better-Scroll, which by default supports vertical Scroll, and it also supports horizontal Scroll, vertical Scroll sets scrollY to true, horizontal Scroll sets scrollX to true, With this configuration, add a direction attribute to the Scroll component to indicate the direction of the Scroll, which has two values vertical and horizontal, the default value is vertical, and then use prop-types to limit the direction attribute value

The following code

Scroll.js

Scroll.defaultProps = {
    direction: "vertical",
    ...
};

Scroll.propTypes = {
    direction: PropTypes.oneOf(['vertical', 'horizontal']),
    ...
};
Copy the code

Better-scroll configuration parameters are modified as follows

this.bScroll = new BScroll(this.scrollView, { scrollX: this.props.direction === "horizontal", scrollY: // probeType: 3, click: this.props. Click});Copy the code

After modifying the Scroll component, add the following code to the Render method of singerlist.js

let tags = this.types.map(type => (
    <a key={type.key}
       className={type.key === this.state.typeKey ? "choose" : ""}>
        {type.name}</a>
));
let indexs = this.indexs.map(type => (
    <a key={type.key}
       className={type.key === this.state.indexKey ? "choose" : ""}>
        {type.name}</a>
));
return (
    <div className="music-singers">
        <div className="nav">
            <div className="tag" ref="tag">
                {tags}
            </div>
            <div className="index" ref="index">
                {indexs}
            </div>
        </div>
    </div>
);
Copy the code

Wrap the classification element with the Scroll component, passing in direction

import Scroll from "@/common/scroll/Scroll"
Copy the code
<Scroll direction="horizontal">
    <div className="tag" ref="tag">
        {tags}
    </div>
</Scroll>
<Scroll direction="horizontal">
    <div className="index" ref="index">
        {indexs}
    </div>
</Scroll>
Copy the code

At this point, the width of the first child of the Scroll component does not exceed the width of the screen, so you need to set the width of all children below it to Scroll

import ReactDOM from "react-dom"
Copy the code
initNavScrollWidth() {
	let tagDOM = ReactDOM.findDOMNode(this.refs.tag);
	let tagElems = tagDOM.querySelectorAll("a");
	let tagTotalWidth = 0;
	Array.from(tagElems).forEach(a => {
		tagTotalWidth += a.offsetWidth;
	});
	tagDOM.style.width = `${tagTotalWidth}px`;

	let indexDOM = ReactDOM.findDOMNode(this.refs.index);
	let indexElems = indexDOM.querySelectorAll("a");
	let indexTotalWidth = 0;
	Array.from(indexElems).forEach(a => {
		indexTotalWidth += a.offsetWidth;
	});
	indexDOM.style.width = `${indexTotalWidth}px`;
}
Copy the code
ComponentDidMount () {// Initialize navigation element total width this.initNavScrollWidth(); }Copy the code

The style code is in the source code

Next call the singer list interface and render it to the page

Import the required modules

import Loading from "@/common/loading/Loading"
import {getSingerList} from "@/api/singer"
import {CODE_SUCCESS} from "@/api/config"
import * as SingerModel from "@/model/singer"
Copy the code

Add the following methods to singerlist.js

getSingers() { getSingerList(1, ` ${this. State. TypeKey + + this. '_' state. IndexKey} `). Then ((res) = > {/ / console log (" for singers list: "); if (res) { //console.log(res); if (res.code === CODE_SUCCESS) { let singers = []; res.data.list.forEach(data => { let singer = new SingerModel.Singer(data.Fsinger_id, data.Fsinger_mid, data.Fsinger_name, `https://y.gtimg.cn/music/photo_new/T001R150x150M000${data.Fsinger_mid}.jpg?max_age=2592000`); singers.push(singer); }); This.setstate ({loading: false, singers}, () => {// refreshScroll this.setstate ({refreshScroll:true}); }); }}}); }Copy the code

Add the following code before the Render method return statement

let singers = this.state.singers.map(singer => {
    return (
        <div className="singer-wraper" key={singer.id}>
            <div className="singer-img">
                <img src={singer.img} width="100%" height="100%" alt={singer.name}
                     onError={(e) => {
                         e.currentTarget.src = require("@/assets/imgs/music.png");
                     }}/>
            </div>
            <div className="singer-name">
                {singer.name}
            </div>
        </div>
    );
});
Copy the code

The code following the return statement is as follows

return ( <div className="music-singers"> ... <div className="singer-list"> <Scroll refresh={this.state.refreshScroll} ref="singerScroll"> <div ClassName ="singer-container"> {singers} </div> </Scroll> </div> <Loading title=" Loading..." show={this.state.loading}/> </div> );Copy the code

Use react-Lazylaod to optimize image loading, import at-LazyLoad, wrap the singer image with lazyLoad component, and monitor Scroll component to call forceCheck method to check whether the image appears in the screen

import LazyLoad, { forceCheck } from "react-lazyload"
Copy the code
<LazyLoad height={50}>
    <img src={singer.img} width="100%" height="100%" alt={singer.name}
         onError={(e) => {
             e.currentTarget.src = require("@/assets/imgs/music.png");
         }}/>
</LazyLoad>
Copy the code
<Scroll refresh={this.state.refreshScroll} onScroll={() => {forceCheck(); }} ref="singerScroll"> <div className="singer-container"> {singers} </div> </Scroll>Copy the code

See section 3 optimizing image loading for more instructions on image loading

Change the typeKey and indexKey values when sorting clicks, call setState to trigger component updates so that the corresponding column is selected, and then call the getSingers method to get the number of singers when the components are updated

Add click event handling to categories

handleTypeClick = (key) => {
    this.setState({
        loading: true,
        typeKey: key,
        indexKey: "all",
        singers: []
    }, () => {
        this.getSingers();
    });
}
handleIndexClick = (key) => {
    this.setState({
        loading: true,
        indexKey: key,
        singers: []
    }, () => {
        this.getSingers();
    });
}
Copy the code
<a key={type.key} className={type.key === this.state.typeKey ? "choose" : ""} onClick={() => {this.handleTypeClick(type.key); }}> {type.name}</a>Copy the code
<a key={type.key} className={type.key === this.state.indexKey ? "choose" : ""} onClick={() => {this.handleIndexClick(type.key); }}> {type.name}</a>Copy the code

Singer Details Development

Create singer.js and singer.styl in the Singer directory of Compontents

Singer.js

import React from "react"

import "./singer.styl"

class Singer extends React.Component {
    render() {
        return (
            <div className="music-singer">
            </div>
        );
    }
}

export default Singer
Copy the code

The singer.styl code is in the source code

Write the container component Singer for Singer and create singer.js under the container directory

import {connect} from "react-redux" import {showPlayer, changeSong, setSongs} from ".. /redux/actions" import Singer from ".. /components/singer/Singer" const mapDispatchToProps = (dispatch) => ({ showMusicPlayer: (status) => { dispatch(showPlayer(status)); }, changeCurrentSong: (song) => { dispatch(changeSong(song)); }, setSongs: (songs) => { dispatch(setSongs(songs)); }}); export default connect(null, mapDispatchToProps)(Singer)Copy the code

Add subroutes to the singer list page, and the corresponding click events, click the singer to enter the singer details page. Go back to Singerlist.js and import the Route component and Singer container component

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

Place the Route component as follows

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

Add click events to the list’s. Singer-wrapper element

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

Continue writing the Singer component. Initialize the following states in the Singer component’s constructor

constructor(props) {
    super(props);

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

Show is used to control components to enter animation, singer to store singer information, songs to store song list. Use section 4 to implement the react-transition-group used in the animation

Import the react – the transition – group

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

Change status to true when the component is mounted

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

Singer’s root element is then wrapped with the CSSTransition component

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

Import Header, Loadding and Scroll three common components, interface request method getSingerInfo, interface success CODE, singer and song model class

import Header from "@/common/header/Header"
import Scroll from "@/common/scroll/Scroll"
import Loading from "@/common/loading/Loading"
import {getSingerInfo} from "@/api/singer"
import {getSongVKey} from "@/api/song"
import {CODE_SUCCESS} from "@/api/config"
import * as SingerModel from "@/model/singer"
import * as SongModel from "@/model/song"
Copy the code

Write the following code in the Render method

let singer = this.state.singer; let songs = this.state.songs.map((song) => { return ( <div className="song" key={song.id}> <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="music-singer"> <Header title={singer.name} ref="header"></Header> <div style={{position:"relative"}}> <div ref="albumBg" className="singer-img" style={{backgroundImage: `url(${singer.img})`}}> <div className="filter"></div> </div> <div ref="albumFixedBg" className="singer-img fixed" style={{backgroundImage: `url(${singer.img})`}}> <div className="filter"></div> </div> <div className="play-wrapper" ref="playButtonWrapper"> <div className="play-button"> < I className="icon-play"></ I >< span> </span> </div> </div> </div> <div >< div ref="albumContainer" className="singer-container"> <div className="singer-scroll" style={this.state.loading === true ? {display:"none"} : {}}> <Scroll refresh={this.state-refreshScroll}> <div className="singer-wrapper"> <div className="song-count"> song < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > < songs > show={this.state.loading}/> </div> </div> </CSSTransition> );Copy the code

Initialize the top value of.singer-container in componentDidMount, which is set to.singer-img height. The getSingerInfo method then requests the interface data, please update singer and Songs after success

let albumBgDOM = ReactDOM.findDOMNode(this.refs.albumBg); let albumContainerDOM = ReactDOM.findDOMNode(this.refs.albumContainer); albumContainerDOM.style.top = albumBgDOM.offsetHeight + "px"; GetSingerInfo (this. Props. Match. Params. Id), then ((res) = > {the console. The log (" details for singer: "); if (res) { console.log(res); if (res.code === CODE_SUCCESS) { let singer = SingerModel.createSingerByDetail(res.data); singer.desc = res.data.desc; let songList = res.data.list; let songs = []; songList.forEach(item => { if (item.musicData.pay.payplay === 1) { return } let song = SongModel.createSong(item.musicData); // Get the song vkey this.getsongurl (song, song-.mid); songs.push(song); }); This.setstate ({loading: false, singer: singer, songs: songs}, () => {// refreshScroll this.setstate ({refreshScroll:true}); }); }}});Copy the code

getSongUrl

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

Listen for the Scroll component to slide up and pull down

scroll = ({y}) => {
    let albumBgDOM = ReactDOM.findDOMNode(this.refs.albumBg);
    let albumFixedBgDOM = ReactDOM.findDOMNode(this.refs.albumFixedBg);
    let playButtonWrapperDOM = ReactDOM.findDOMNode(this.refs.playButtonWrapper);
    if (y < 0) {
        if (Math.abs(y) + 55 > albumBgDOM.offsetHeight) {
            albumFixedBgDOM.style.display = "block";
        } else {
            albumFixedBgDOM.style.display = "none";
        }
    } else {
        let transform = `scale(${1 + y * 0.004}, ${1 + y * 0.004})`;
        albumBgDOM.style["webkitTransform"] = transform;
        albumBgDOM.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

Add the click play function to the songs. There are two places: one is to click on a single song to play, and the other is to click on all songs to 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-button" onClick={this.playAll}> < I className="icon-play"></ I >< span>Copy the code

Copy the initMusicIco and startMusicIcoAnimation functions from Section 5 as you did in the previous section, and then call initMusicIco in componentDidMount

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 song click note drop animation

The effect

conclusion

The main content of this section is that the basic Scroll components are reformed according to the new rolling requirements. In actual development, some basic components are encapsulated, which can meet the requirements in the early stage. With the emergence of new functions, the basic components may be reformed to meet the new requirements. The details are very similar in several pages. In fact, it can be used as a common component, which gets the data and then encapsulates it into the required data format. I recently developed this Web music app using VUE, where the details page has already been extracted

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

The code for this chapter is in chapter7