本節繼續開發歌手列表和歌手詳情vue
1.歌手列表react
用chrome瀏覽器打開QQ音樂官網,進入QQ音樂官網後打開開發者工具選擇Network選項,點擊js選項,在QQ音樂官網點擊歌手git
點開上圖紅框中的請求,在右側點擊Preview,下方就是歌手列表數據,具體的請求連接參數在Headers選項中查看github
2.歌手詳情web
選擇歌手列表中的任意一個歌手點擊,在左邊的Network中查看具體請求數據chrome
在api目錄下面的config.js中加入接口url配置redux
config.js
api
const URL = {
...
/*歌手列表*/
singerList: "https://c.y.qq.com/v8/fcg-bin/v8.fcg",
/*歌手詳情*/
singerInfo: "https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg"
};
複製代碼
在api下面新建singer.js,編寫接口請求方法瀏覽器
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);
}
複製代碼
接下來新建歌手模型類Singer,在model目錄下新建singer.js,屬性以下
export class Singer {
constructor(id, mId, name, img) {
this.id = id;
this.mId = mId;
this.name = name;
this.img = img;
}
}
複製代碼
根據歌手列表和歌手詳情返回的數據編寫兩個對象建立函數,在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`
);
}
複製代碼
先來看一下效果圖
歌手頁分兩塊,上部分是歌手分類,下部分就是對應的歌手列表。在歌手列表接口中有一個key參數,改參數就是對應的歌手分類,它是由第一欄分類和第二欄分類拼接而成的。在QQ音樂官網的歌手列表頁面中經過瀏覽器調試工具查看DOM結構能夠查看到分類對應的key值
其中data-key就是對應分類的key值
接下來初始化這些key值,回到componens下singer目錄中的SingerList.js,在SingerList.js中使用構造函數初始化分類所須要的key值
constructor(props) {
super(props);
this.types = [
{key:"all_all", name:"所有"},
{key:"cn_man", name:"華語男"},
{key:"cn_woman", name:"華語女"},
{key:"cn_team", name:"華語組合"},
{key:"k_man", name:"韓國男"},
{key:"k_woman", name:"韓國女"},
{key:"k_team", name:"韓國組合"},
{key:"j_man", name:"日本男"},
{key:"j_woman", name:"日本女"},
{key:"j_team", name:"日本組合"},
{key:"eu_man", name:"歐美男"},
{key:"eu_woman", name:"歐美女"},
{key:"eu_team", name:"歐美組合"},
{key:"other_other", name:"其它"}
];
this.indexs = [
{key:"all", name:"熱門"},
{key:"A", name:"A"},
{key:"B", name:"B"},
{key:"C", name:"C"},
{key:"D", name:"D"},
{key:"E", name:"E"},
...
];
}
複製代碼
省內部分代碼,完整代碼請在源碼中查看
而後初始化一些默認的state,繼續在constructor中增長如下代碼
this.state = {
loading: true,
typeKey: "all_all",
indexKey: "all",
singers: [],
refreshScroll: false
}
複製代碼
其中typeKey是第一欄默認選中的分類key,indexKey是第二欄默認選擇的分類key,singer存放歌手列表
在效果圖中每一個分類都是一行顯示,超出屏幕是能夠滾動的,這裏一樣使用第三節封裝的Scroll組件,爲何不使用瀏覽器自帶的overflow: scroll,固然是由於原生的滾動效果體驗太差,在有些瀏覽器自帶右滑後退,左滑前進,這個時候衝突就很雞肋了~_~。如今須要左右滾動,這時原來封裝的Scroll組件不知足需求,接下來對Scroll組件進行改造
Scroll組件是基於better-scroll封裝的,better-scroll默認支持縱向滾動,它也支持橫向滾動,縱向滾動將scrollY設置爲true,橫向滾動將scrollX設置爲true,有了這個配置後給Scroll組件增長一個direction屬性表示滾動方向,它有兩個值vertical(垂直方向)和horizontal(水平方向),默認值爲vertical,而後用prop-types限制direction屬性的值
代碼以下
Scroll.js
Scroll.defaultProps = {
direction: "vertical",
...
};
Scroll.propTypes = {
direction: PropTypes.oneOf(['vertical', 'horizontal']),
...
};
複製代碼
better-scroll配置參數修改以下
this.bScroll = new BScroll(this.scrollView, {
scrollX: this.props.direction === "horizontal",
scrollY: this.props.direction === "vertical",
//實時派發scroll事件
probeType: 3,
click: this.props.click
});
複製代碼
修改Scroll組件後,在SingerList.js的render方法中增長如下代碼
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>
);
複製代碼
使用Scroll組件包裝分類元素,傳入direction
import Scroll from "@/common/scroll/Scroll"
複製代碼
<Scroll direction="horizontal">
<div className="tag" ref="tag">
{tags}
</div>
</Scroll>
<Scroll direction="horizontal">
<div className="index" ref="index">
{indexs}
</div>
</Scroll>
複製代碼
此時Scroll組件的第一個子元素的寬度並無超過屏幕,須要設置爲它下面的全部子元素佔的寬度才能滾動,編寫一個初始化Scroll第一個子元素寬度的方法,並在componentDidMount中調用
import ReactDOM from "react-dom"
複製代碼
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`;
}
複製代碼
componentDidMount() {
//初始化導航元素總寬度
this.initNavScrollWidth();
}
複製代碼
樣式代碼請在源碼中查看
接下來調用歌手列表接口並渲染到頁面上
導入須要的模塊
import Loading from "@/common/loading/Loading"
import {getSingerList} from "@/api/singer"
import {CODE_SUCCESS} from "@/api/config"
import * as SingerModel from "@/model/singer"
複製代碼
在SingerList.js中增長如下方法
getSingers() {
getSingerList(1, `${this.state.typeKey + '_' + this.state.indexKey}`).then((res) => {
//console.log("獲取歌手列表:");
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
}, () => {
//刷新scroll
this.setState({refreshScroll:true});
});
}
}
});
}
複製代碼
在render方法return語句前增長如下代碼
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>
);
});
複製代碼
return語句後的代碼以下
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="正在加載..." show={this.state.loading}/>
</div>
);
複製代碼
使用react-lazylaod優化圖片加載,導入reat-lazyload,在歌手圖片外層使用Lazyload組件包裹,同時監聽Scroll組件滾動調用forceCheck方法檢測圖片是否出如今屏幕內
import LazyLoad, { forceCheck } from "react-lazyload"
複製代碼
<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>
複製代碼
<Scroll refresh={this.state.refreshScroll}
onScroll={() => {forceCheck();}} ref="singerScroll">
<div className="singer-container">
{singers}
</div>
</Scroll>
複製代碼
圖片加載更多說明見第三節優化圖片加載
在分類點擊的時候改變typeKey和indexKey的值,調用setState觸發組件更新,讓對應點擊的分欄選中,組件更新後再調用getSingers方法獲取歌手數
給分類添加點擊事件處理
handleTypeClick = (key) => {
this.setState({
loading: true,
typeKey: key,
indexKey: "all",
singers: []
}, () => {
this.getSingers();
});
}
handleIndexClick = (key) => {
this.setState({
loading: true,
indexKey: key,
singers: []
}, () => {
this.getSingers();
});
}
複製代碼
<a key={type.key}
className={type.key === this.state.typeKey ? "choose" : ""}
onClick={() => {this.handleTypeClick(type.key);}}>
{type.name}</a>
複製代碼
<a key={type.key}
className={type.key === this.state.indexKey ? "choose" : ""}
onClick={() => {this.handleIndexClick(type.key);}}>
{type.name}</a>
複製代碼
在compontents中的singer目錄下新建Singer.js和singer.styl
Singer.js
import React from "react"
import "./singer.styl"
class Singer extends React.Component {
render() {
return (
<div className="music-singer">
</div>
);
}
}
export default Singer
複製代碼
singer.styl代碼請在源碼中查看
爲Singer編寫容器組件Singer,在container目錄下新建Singer.js
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)
複製代碼
在歌手列表頁中加入子路由,和對應的點擊事件,點擊歌手進入歌手詳情頁。回到SingerList.js中,導入Route組件和Singer容器組件
import {Route} from "react-router-dom"
import Singer from "@/containers/Singer"
複製代碼
將Route組件放置以下位置
render() {
let {match} = this.props;
...
return (
<div className="music-singers">
...
<Loading title="正在加載..." show={this.state.loading}/>
<Route path={`${match.url + '/:id'}`} component={Singer}/>
</div>
);
}
複製代碼
給列表的.singer-wrapper元素增長點擊事件
toDetail = (url) => {
this.props.history.push({
pathname: url
});
}
複製代碼
<div className="singer-wrapper" key={singer.id}
onClick={() => {this.toDetail(`${match.url + '/' + singer.mId}`)}}>
...
</div>
複製代碼
繼續編寫Singer組件。在Singer組件的constructor構造函數中初始化如下state
constructor(props) {
super(props);
this.state = {
show: false,
loading: true,
singer: {},
songs: [],
refreshScroll: false
}
}
複製代碼
show用來控制組件進入動畫、singer存放歌手信息、songs存放歌曲列表。組件進入動畫使用第四節實現動畫中使用的react-transition-group
導入react-transition-group
import {CSSTransition} from "react-transition-group"
複製代碼
當組件掛載後將status修改成true
componentDidMount() {
this.setState({
show: true
});
}
複製代碼
而後使用CSSTransition組件包裹Singer的根元素
<CSSTransition in={this.state.show} timeout={300} classNames="translate">
<div className="music-singer">
</div>
</CSSTransition>
複製代碼
導入Header、Loadding和Scroll三個公用組件,接口請求方法getSingerInfo,接口成功CODE碼,歌手和歌曲模型類
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"
複製代碼
render方法中編寫如下代碼
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 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">歌曲 共{songs.length}首</div>
<div className="song-list">
{songs}
</div>
</div>
</Scroll>
</div>
<Loading title="正在加載..." show={this.state.loading}/>
</div>
</div>
</CSSTransition>
);
複製代碼
在componentDidMount中初始化.singer-container的top值,值設置爲.singer-img高度。而後getSingerInfo方法請求接口數據,請成功後更新singer和songs
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) => {
console.log("獲取歌手詳情:");
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);
//獲取歌曲vkey
this.getSongUrl(song, song.mId);
songs.push(song);
});
this.setState({
loading: false,
singer: singer,
songs: songs
}, () => {
//刷新scroll
this.setState({refreshScroll:true});
});
}
}
});
複製代碼
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`
}
}
}
});
}
複製代碼
監聽Scroll組件滾動實現上滑和往下拉伸效果
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`;
}
}
複製代碼
<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-button" onClick={this.playAll}>
<i className="icon-play"></i>
<span>播放所有</span>
</div>
複製代碼
和上一節同樣複製第5節的initMusicIco和startMusicIcoAnimation兩個函數,而後在componentDidMount中調用initMusicIco
this.initMusicIco();
複製代碼
在selectSong函數中調用startMusicIcoAnimation啓動動畫
selectSong(song) {
return (e) => {
this.props.setSongs([song]);
this.props.changeCurrentSong(song);
this.startMusicIcoAnimation(e.nativeEvent);
};
}
複製代碼
音符下落動畫具體請看歌曲點擊音符下落動畫
這一節主要內容是根據新的滾動需求改造了Scroll基礎組件,在實際開發中,封裝了一些基礎組件,前期可以知足需求,隨着新的功能出現可能會對基礎組件進行改造以知足新的需求。詳情在幾個頁面中都是很是類似的,其實這裏是能夠把它作爲一個公用的組件,獲取數據後封裝成其要求的數據格式傳入。我最近使用vue開發這個web音樂app,其中詳情頁就已經抽取出來了
完整項目地址:github.com/code-mcx/ma…
本章節代碼在chapter7分支
後續更新中...