React全家桶構建一款Web音樂App實戰(四):專輯頁開發及其動畫實現

項目打包腳本配置針對生產環境已經作了修改,增長了對樣式的壓縮,把樣式統一打包到樣式文件中。詳細請看第一節配置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.jsalbum.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目錄下面)。導入RouteAlbum.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.jsheader.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屬性

實現動畫

  1. 專輯頁進入動畫

在不少app中頁面進入時都會有平移動畫,這樣看起來界面跳轉不會顯得很生硬。這裏使用react-transition-group動畫庫來實現動畫。

注意:這裏使用的是2.x版本,1.x和2.x版本api相差很大。詳細請看github

react-transition-group提供了三個組件

  1. Transition(過渡動畫組件。容許從一種狀態到另外一種狀態的改變。默認跟蹤組件的進入和離開狀態)
  2. TransitionGroup(管理Transition組件集合)
  3. 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接受intimeoutclassNames三個props。其中in控制組件的狀態。當in爲true時,組件的子元素會應用translate-entertranslate-enter-active樣式,當in爲false時,組件的子元素會應用translate-exittranslate-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-entertranslate-enter-active樣式從而實現過渡動畫

componentDidMount() {
    this.setState({
        show: true
    });
    ...
}
複製代碼
  1. 列表滾動和圖片拉伸效果

先來看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分支

後續更新中...

相關文章
相關標籤/搜索