項目打包腳本配置針對生產環境已經作了修改,增長了對樣式的壓縮,把樣式統一打包到樣式文件中。詳細請看第一節配置Stylus預處理語言。本節全部內容緊接上一節,上一節地址:juejin.im/post/5a3a6c…css
上一節開發了推薦頁面,這一節實現專輯頁面開發、進入動畫和圖片拉伸動畫。話很少說,先看效果圖react
頭部是一個很常見的標題加一個返回按鈕(標準app的作法~~~),上部分是專輯背景圖片,圖片下面就是專輯的歌曲列表,最底下就是專輯的簡介git
打開chrome瀏覽器,地址欄輸入QQ音樂官網:y.qq.com。打開後點擊專輯github
點擊後,以下圖web
打開開發者工具(按F12或CTRL+SHIFT+I),而後任意選一張專輯點擊chrome
這個時候回彈出一個新的窗口,直接關閉它。回到剛纔的開發者工具,能夠看到有一個請求,這個請求就是獲取專輯詳情的express
點開preview,這裏面就是咱們須要的數據npm
接下來編寫獲取接口的代碼編程
在api目錄下的config.js中,添加專輯詳情的url配置json
config.js
const URL = {
/*推薦輪播*/
carousel: "https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg",
/*最新專輯*/
newalbum: "https://u.y.qq.com/cgi-bin/musicu.fcg",
/*專輯信息*/
albumInfo: "https://c.y.qq.com/v8/fcg-bin/fcg_v8_album_info_cp.fcg"
};
複製代碼
在api下的recommend.js中添加獲取專輯請求的方法
recommend.js
export function getAlbumInfo(albumMid) {
const data = Object.assign({}, PARAM, {
albummid: albumMid,
g_tk: 1278911659,
hostUin: 0,
platform: "yqq",
needNewCode: 0
});
return jsonp(URL.albumInfo, data, OPTION);
}
複製代碼
爲了進入專輯詳情頁面,須要在推薦頁面中實現點擊專輯項跳轉到專輯詳情的路由。先建立專輯頁面組件Album.js 在src下的components下面新建album文件夾,而後在album下面新建Album.js和album.styl
Album.js
import React from "react"
import "./album.styl"
class Album extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
}
render() {
return (
<div className="music-album">
Album
</div>
);
}
}
export default Album
複製代碼
album.styl
.music-album
position: fixed
top: 0
left: 0
right: 0
bottom: 0
background-color: #212121
z-index: 100
複製代碼
點開Recommend.js(src下面components中的recommend目錄下面)。導入Route和Album.js
Recommend.js
import {Route} from "react-router-dom"
import Album from "../album/Album"
複製代碼
在render方法第一行增長
let {match} = this.props;
複製代碼
match是路由經過props傳遞給組件的包含了url、參數等相關信息。而後在根元素下面添加子路由
<div className="music-recommend">
<Scroll refresh={this.state.refreshScroll}
onScroll={(e) => {
/*檢查懶加載組件是否出如今視圖中,若是出現就加載組件*/
forceCheck();}}>
...
</Scroll>
<Loading title="正在加載..." show={this.state.loading}/>
<Route path={`${match.url + '/:id'}`} component={Album} />
</div>
複製代碼
最後給每個專輯包裹元素添加點擊事件
let albums = this.state.newAlbums.map(item => {
//經過函數建立專輯對象
let album = AlbumModel.createAlbumByList(item);
return (
<div className="album-wrapper" key={album.mId}
onClick={this.toAlbumDetail(`${match.url + '/' + album.mId}`)}>
...
</div>
);
});
複製代碼
toAlbumDetail(url) {
/*scroll組件會派發一個點擊事件,不能使用連接跳轉*/
return () => {
this.props.history.push({
pathname: url
});
}
}
複製代碼
這裏使用react路由提供的history對象來實現編程路由跳轉,使用閉包函數把每次循環傳入的url做爲局部變量。這樣每次點擊item獲取到的都是對應傳遞的url
在整個項目中標題是很常見的,這裏把頭部標題和返回按鈕封裝成一個公用的Header組件。Header組件接受一個title標題,返回按鈕點擊的時候具備返回的功能,其實就是路由的返回,這裏在Header組件內部處理這個點擊事件 在src下面的common目錄下新建header文件夾,在header文件夾下面新建Header.js和header.styl
Header.js
import React from "react"
import "./header.styl"
class MusicHeader extends React.Component {
handleClick() {
window.history.back();
}
render() {
return (
<div className="music-header">
<span className="header-back" onClick={this.handleClick}>
<i className="icon-back"></i>
</span>
<div className="header-title">
{this.props.title}
</div>
</div>
);
}
}
export default MusicHeader
複製代碼
上訴代碼的handleClick函數中也可使用history.goBack()來實現路由的回退。返回按鈕使用的是一個字體圖標,在App.js中引入字體圖標樣式,做爲全局引入,這樣全部的組件均可以使用字體圖標樣式
import "../assets/stylus/font.styl"
複製代碼
header.styl
.music-header
position: fixed
width: 100%
height: 55px
line-height: 55px
color: #FFFFFF
text-align: center
font-size: 18px
.header-back
position: absolute
left: 10px
font-size: 22px
.header-title
margin: 0 40px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
複製代碼
在上一節已經爲專輯數據建立了一類模型,對於專輯詳情接口只須要一個建立對象的函數便可。在src下面的model目錄中的album.js中新增如下代碼
/**
* 經過專輯詳情數據建立專輯對象函數
*/
export function createAlbumByDetail(data) {
return new Album(
data.id,
data.mid,
data.name,
`http://y.gtimg.cn/music/photo_new/T002R300x300M000${data.mid}.jpg?max_age=2592000`,
data.singername,
data.aDate
);
}
複製代碼
專輯列表中有不少歌曲數據,這裏爲歌曲數據建立一個Song類,方便後續使用。一樣在src下的model中新建song.js,編寫一個建立Song類對象的函數
song.js
/**
* 歌曲類模型
*/
export class Song {
constructor(id, mId, name, img, duration, url, singer) {
this.id = id;
this.mId = mId;
this.name = name;
this.img = img;
this.duration = duration;
this.url = url;
this.singer = singer;
}
}
/**
* 建立歌曲對象函數
*/
export function createSong(data) {
return new Song(
data.songid,
data.songmid,
data.songname,
`http://y.gtimg.cn/music/photo_new/T002R300x300M000${data.albummid}.jpg?max_age=2592000`,
data.interval,
"",
filterSinger(data.singer)
);
}
function filterSinger(singers) {
let singerArray = singers.map(singer => {
return singer.name;
});
return singerArray.join("/");
}
複製代碼
專輯頁中須要用到上一節封裝的Scroll組件和Loading組件以及封裝的Header組件。回到Album.js中導入這個三個組件
import Header from "@/common/header/Header"
import Scroll from "@/common/scroll/Scroll"
import Loading from "@/common/loading/Loading"
複製代碼
導入專輯請求函數,接口成功狀態碼常量,專輯和歌曲模型類
import {getAlbumInfo} from "@/api/recommend"
import {CODE_SUCCESS} from "@/api/config"
import * as AlbumModel from "@/model/album"
import * as SongModel from "@/model/song"
複製代碼
Album.js主要代碼以下
class Album extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
album: {},
songs: [],
refreshScroll: false
}
}
componentDidMount() {
let albumBgDOM = ReactDOM.findDOMNode(this.refs.albumBg);
let albumContainerDOM = ReactDOM.findDOMNode(this.refs.albumContainer);
albumContainerDOM.style.top = albumBgDOM.offsetHeight + "px";
getAlbumInfo(this.props.match.params.id).then((res) => {
console.log("獲取專輯詳情:");
if (res) {
console.log(res);
if (res.code === CODE_SUCCESS) {
let album = AlbumModel.createAlbumByDetail(res.data);
album.desc = res.data.desc;
let songList = res.data.list;
let songs = [];
songList.forEach(item => {
let song = SongModel.createSong(item);
songs.push(song);
});
this.setState({
loading: false,
album: album,
songs: songs
}, () => {
//刷新scroll
this.setState({refreshScroll:true});
});
}
}
});
}
render() {
let album = this.state.album;
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 (
<div className="music-album">
<Header title={album.name} ref="header"></Header>
<div style={{position:"relative"}}>
<div ref="albumBg" className="album-img" style={{backgroundImage: `url(${album.img})`}}>
<div className="filter"></div>
</div>
<div ref="albumFixedBg" className="album-img fixed" style={{backgroundImage: `url(${album.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="album-container">
<div className="album-scroll" style={this.state.loading === true ? {display:"none"} : {}}>
<Scroll refresh={this.state.refreshScroll}>
<div className="album-wrapper">
<div className="song-count">專輯 共{songs.length}首</div>
<div className="song-list">
{songs}
</div>
<div className="album-info" style={album.desc? {} : {display:"none"}}>
<h1 className="album-title">專輯簡介</h1>
<div className="album-desc">
{album.desc}
</div>
</div>
</div>
</Scroll>
</div>
<Loading title="正在加載..." show={this.state.loading}/>
</div>
</div>
);
}
}
複製代碼
上訴代碼在componentDidMount中經過match.params.id獲取參數id,再發送請求獲取到數據後先建立Album對象再建立Song列表,而後調用setState更新ui。此時歌曲還缺乏文件地址,歌曲文件地址接口獲取見juejin.im/post/5a3522…
在api目錄下的config中添加歌曲vkey地址,而後新建song.js,編寫用來獲取歌曲vkey請求
config.js
/*歌曲vkey*/
songVkey: "https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg"
複製代碼
song.js
import jsonp from "./jsonp"
import {URL, PARAM} from "./config"
export function getSongVKey(songMid) {
const data = Object.assign({}, PARAM, {
g_tk: 1278911659,
hostUin: 0,
platform: "yqq",
needNewCode: 0,
cid: 205361747,
uin: 0,
songmid: songMid,
filename: `C400${songMid}.m4a`,
guid: 3655047200
});
const option = {
param: "callback",
prefix: "callback"
};
return jsonp(URL.songVkey, data, option);
}
複製代碼
在Album.js中導入上述方法
import {getSongVKey} from "@/api/song"
複製代碼
編寫一個獲取歌曲vkey的方法
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`
}
}
}
});
}
複製代碼
對歌曲進行遍歷的時候調用getSongUrl獲取歌曲文件地址
songList.forEach(item => {
let song = SongModel.createSong(item);
//獲取歌曲vkey
this.getSongUrl(song, item.songmid);
songs.push(song);
});
複製代碼
song是一個對象,對象是引用類型,把song傳遞給getSongUrl的第一個參數,他們指向的是同一塊內存,也就是說他們是同一個的實例,那麼他們的url屬性也是同樣的。在getSongUrl中修改了url也就修改了傳遞進去的song對象的url屬性
在不少app中頁面進入時都會有平移動畫,這樣看起來界面跳轉不會顯得很生硬。這裏使用react-transition-group動畫庫來實現動畫。
注意:這裏使用的是2.x版本,1.x和2.x版本api相差很大。詳細請看github
react-transition-group提供了三個組件
詳細用法請看:reactcommunity.org/react-trans…
這裏使用CSSTransition組件來作動畫。先安裝react-transition-group
npm install react-transition-group --save
複製代碼
在Album.js中導入CSSTransition
import {CSSTransition} from "react-transition-group"
複製代碼
給Album組件添加一個show屬性用來控制動畫的狀態
this.state = {
show: false,
loading: true,
album: {},
songs: [],
refreshScroll: false
}
複製代碼
使用CSSTransition組件包裹Album組件的根元素
<CSSTransition in={this.state.show} timeout={300} classNames="translate">
<div className="music-album">
...
</div>
</CSSTransition>
複製代碼
CSSTransition接受in、timeout、classNames三個props。其中in控制組件的狀態。當in爲true時,組件的子元素會應用translate-enter、translate-enter-active樣式,當in爲false時,組件的子元素會應用translate-exit、translate-exit-active樣式。timeout指定過渡時間
這裏只實現組件進入動畫,組件離開動畫可經過Header組件的返回按鈕點擊事件結合動畫鉤子函數實現
這個動畫使用的樣式會被用在多處,咱們把樣式寫在App.styl中
.translate-enter
transform: translate3d(100%, 0, 0)
&.translate-enter-active
transition: transform .3s
transform: translate3d(0, 0, 0)
複製代碼
在組件掛載完成後,也就是componentDidMount函數中將show設置爲true,讓組件應用translate-enter、translate-enter-active樣式從而實現過渡動畫
componentDidMount() {
this.setState({
show: true
});
...
}
複製代碼
先來看gif圖
上圖中列表往上滾動會覆蓋圖片,當超過頭部高度的時候會隱藏,圖片上半部分Header高度的區域顯示在列表上方。向下拉伸時圖片會跟着放大。向上滾動效果主要利用元素的position定位,和z-index設置層級關係,圖片這裏作了兩個一樣的元素,它們都相對父元素進行定位,一個是用來默認顯示,另一個隱藏而且高度只有Header高,隱藏的元素層級比列表要高。當裏列表往上滾動超過Header的底部時就顯示隱藏的圖片。向下滾動利用監聽Scroll組件的滾動事件,根據滾動的高度給圖片設置對應的scale值,同時給按鈕設置margin-top
先給滾動列表設置溢出隱藏,覆蓋Scroll組件的樣式
.scroll-view
overflow: visible
複製代碼
監聽Scroll組件的滾動,判斷y是否小於0,小於0表示向上滾動。當滾動y值的絕對值加上Header的高度大於圖片高度的時候此時已經超過了Header的底部,這個時候顯示隱藏的圖片,向下滾動沒有達到Header的底部時隱藏圖片(這裏使用了兩張圖片,其實也可使用一張圖片,當滾動到Header組件的底部的時候設置圖片的高度和z-index便可)
/**
* 監聽scroll
*/
scroll = ({y}) => {
let albumBgDOM = ReactDOM.findDOMNode(this.refs.albumBg);
let albumFixedBgDOM = ReactDOM.findDOMNode(this.refs.albumFixedBg);
if (y < 0) {
if (Math.abs(y) + 55 > albumBgDOM.offsetHeight) {
albumFixedBgDOM.style.display = "block";
} else {
albumFixedBgDOM.style.display = "none";
}
}
}
複製代碼
<Scroll refresh={this.state.refreshScroll} onScroll={this.scroll}>
...
</Scroll>
複製代碼
接下來處理圖片拉伸,在if (y < 0)增長else塊,當y大於0時表示向下滾動
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`;
}
}
複製代碼
album.styl完整代碼見結尾源碼地址
這一節使用history對象來實現編程子路由跳轉,簡單的介紹了react-transition-group作過渡動畫,後面會介紹使用react-transition-group結合鉤子函數實現動畫效果。還利用上一節封裝的Scroll組件的滾動事件實現了列表滾動和圖片拉伸效果,主要是明白如何經過滾動的y值判斷是上拉仍是下拉、圖片的佈局設計以及如何判斷列表滾動到了Header組件底部
完整項目地址:github.com/code-mcx/ma…
本章節代碼在chapter4分支
後續更新中...