專案打包指令碼配置針對生產環境已經做了修改,增加了對樣式的壓縮,把樣式統一打包到樣式檔案中。詳細請看第一節配置Stylus預處理語言。本節所有內容緊接上一節,上一節地址:juejin.im/post/5a3a6c…
上一節開發了推薦頁面,這一節實現專輯頁面開發、進入動畫和圖片拉伸動畫。話不多說,先看效果圖
頭部是一個很常見的標題加一個返回按鈕(標準app的做法~~~),上部分是專輯背景圖片,圖片下面就是專輯的歌曲列表,最底下就是專輯的簡介
資料抓取
開啟chrome瀏覽器,位址列輸入QQ音樂官網:y.qq.com。開啟後點選專輯
點選後,如下圖
開啟開發者工具(按F12或CTRL+SHIFT+I),然後任意選一張專輯點選
這個時候回彈出一個新的視窗,直接關閉它。回到剛才的開發者工具,可以看到有一個請求,這個請求就是獲取專輯詳情的
點開preview,這裡面就是我們需要的資料
接下來編寫獲取介面的程式碼
在api目錄下的config.js中,新增專輯詳情的url配置
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元件。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提供了三個元件
- Transition(過渡動畫元件。允許從一種狀態到另一種狀態的改變。預設跟蹤元件的進入和離開狀態)
- TransitionGroup(管理Transition元件集合)
- CSSTransition(使用css過渡和動畫的Transition元件)
詳細用法請看: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分支
後續更新中…