React全家桶構建一款Web音樂App實戰(三):推薦頁開發及公用組件封裝

接着上一節內容,這一節抓取QQ音樂移動Web端推薦頁面接口和PC端最新專輯接口數據。經過這些接口數據開發推薦頁面。首先看一下效果圖css

頁面結構前端

推薦頁面主要分輪播和最新專輯兩塊,其中輪播圖片來自QQ音樂移動Web端推薦頁面的接口,最新專輯則從PC端抓取的,整個推薦頁面超出屏幕是能夠滾動的vue

輪播圖和最新專輯數據抓取

用chrome瀏覽器打開手機調試模式,輸入QQ音樂移動端地址:m.y.qq.com。打開後點擊Network,而後點擊XHR,能夠看到有一個ajax請求。點開後,選擇preview,紅色框內就是咱們最後須要的輪播數據react

在chrome瀏覽器輸入QQ音樂pc官網:y.qq.comcss3

JSONP使用

這裏接口用的是ajax請求,用這種方式存在跨域限制,前端是不能直接請求的,好在QQ音樂仍是很人性化的基本上大部分接口都支持jsonp請求。jsonp原理具體不作過多解釋了。爲了使用jsonp,這裏使用一款jsonp插件,首先安裝jsonp依賴git

npm install jsonp --save
複製代碼

安裝完成後開始編寫代碼。爲了養成好的編程習慣呢,一般會把接口請求代碼存放到api目錄下面,不少人會接口的url一同寫在請求的代碼中,這裏呢,咱們把url抽取出來放到單獨的一個文件裏面便於管理。es6

說明:這一章節是在上一章節的基礎上繼續開發的,上一章節傳送門:juejin.im/post/5a3738…,輪播數據接口和最新專輯接口說明見:juejin.im/post/5a3522…github

src目錄下面新建api目錄,而後新建config.js文件,在config.js文件中編寫URL、一些接口公用參數、jsonp參象、接口code碼等常量web

config.jsajax

const URL = {
    /*推薦輪播*/
    carousel: "https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg",
    /*最新專輯*/
    newalbum: "https://u.y.qq.com/cgi-bin/musicu.fcg"
};

const PARAM = {
    format: "jsonp",
    inCharset: "utf-8",
    outCharset: "utf-8",
    notice: 0
};

const OPTION = {
    param: "jsonpCallback",
    prefix: "callback"
};

const CODE_SUCCESS = 0;

export {URL, PARAM, OPTION, CODE_SUCCESS};
複製代碼

在ES6之前寫ajax的時候各類函數回調代碼,ES6提供了Promise對象,它能夠將異步代碼以同步的形式編寫具體用法請看阮老師的教程Promise對象。咱們這裏使用Promise對象將jsonp代碼封裝成同步代碼形式。在api目錄下面新建jsonp.js文件 jsonp.js

import originJsonp from "jsonp"

let jsonp = (url, data, option) => {
    return new Promise((resolve, reject) => {
        originJsonp(buildUrl(url, data), option, (err, data) => {
            if (!err) {
                resolve(data);
            } else {
                reject(err);
            }
        });
    });
};

function buildUrl(url, data) {
    let params = [];
    for (var k in data) {
        params.push(`${k}=${data[k]}`);
    }
    let param = params.join("&");
    if (url.indexOf("?") === -1) {
        url += "?" + param;
    } else {
        url += "&" + param;
    }
    return url;
}

export default jsonp
複製代碼

上述代碼大體說明下,在Promise構造函數內調用jsonp,固然請求成功的時候會調用resolve函數把data的值傳出去,請求錯誤的時候會調用reject函數將err的值傳出去。buildUrl函數是把json對象的參數拼接到url後面最後變成xxxx?參數名1=參數值1&參數名2=參數值2這種形式

爲了方便管理,咱們把請求的代碼都模塊化。在api目錄下面新建recommend.js對應Recommend頁面組件用到的相關請求 recommend.js

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

export function getCarousel() {
	const data = Object.assign({}, PARAM, {
		g_tk: 701075963,
		uin: 0,
		platform: "h5",
		needNewCode: 1,
		_: new Date().getTime()
	});
	return jsonp(URL.carousel, data, OPTION);
}

export function getNewAlbum() {
	const data = Object.assign({}, PARAM, {
		g_tk: 1278911659,
		hostUin: 0,
		platform: "yqq",
		needNewCode: 0,
		data: `{"albumlib":
		{"method":"get_album_by_tags","param":
		{"area":1,"company":-1,"genre":-1,"type":-1,"year":-1,"sort":2,"get_tags":1,"sin":0,"num":50,"click_albumid":0},
		"module":"music.web_album_library"}}`
	});
	const option = {
		param: "callback",
		prefix: "callback"
	};
	return jsonp(URL.newalbum, data, option);
}
複製代碼

在上述代碼中使用Object.assign()函數把對象進行合併,相同的屬性值會被覆蓋。注意第一個參數使用一個空對象目的是爲了避免干擾PARAM對象的數據,若是把PARAM做爲第一個參數,那麼後面使用這個PARAM對象它裏面的屬性就會擁有上一次合併以後的屬性,其實有些屬性咱們是不須要的

推薦頁面開發和數據接口調用

在React組件中有不少生命週期函數,幾個生命週期函數以下

函數名 觸發時間點
componentDidMount 在第一次DOM渲染後調用
componentWillReceiveProps 在組件接收到一個新的prop時被調用。在初始化render時不會被調用
shouldComponentUpdate 在組件接收到新的props或者state時被調用。在初始化時或者使用forceUpdate時不被調用
componentWillUpdate 組件接收到新的props或者state但尚未render時被調用。在初始化時不會被調用
componentDidUpdate 組件完成更新後當即調用。在初始化時不會被調用
componentWillUnmount 組件從 DOM 中移除的時候馬上被調用

通常的咱們會在componentDidMount函數中獲取DOM,對DOM進行操做。React每次更新都會調用render函數,使用shouldComponentUpdate能夠幫助咱們控制組件是否更新,返回true組件會更新,返回false就會阻止更新,這也是性能優化的一種手段。componentWillUnmount一般用來銷燬一些資源,好比setInterval、setTimeout函數調用後能夠在該周期函數內進行資源釋放

那麼咱們應該在那個生命週期函數裏面發送接口請求?

答案是componentDidMount

咱們應該在組件掛載完成後面進行請求,防止異部操做阻塞UI

回到項目中繼續編寫Recommend組件。推薦頁面輪播咱們使用swiper插件來實現,swiper更多用法見官網:www.swiper.com.cn

安裝swiper

npm install swiper@3.4.2 --save
複製代碼

注意:這裏使用3.x的版本。4.0的版本目前在移動端有問題,筆者在手機端訪問後一片空白。

使用swiper

Recommend.js中導入swiper和相關樣式

import Swiper from "swiper"
import "swiper/dist/css/swiper.css"
複製代碼

Recommend.js

import React from "react"
import Swiper from "swiper"
import {getCarousel} from "@/api/recommend"
import {CODE_SUCCESS} from "@/api/config"
import "./recommend.styl"
import "swiper/dist/css/swiper.css"


class Recommend extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            sliderList: []
        };
    }
    componentDidMount() {
        getCarousel().then((res) => {
            console.log("獲取輪播:");
            if (res) {
                console.log(res);
                if (res.code === CODE_SUCCESS) {
                    this.setState({
                        sliderList: res.data.slider
                    }, () => {
                        if(!this.sliderSwiper) {
                            //初始化輪播圖
                            this.sliderSwiper = new Swiper(".slider-container", {
                                loop: true,
                                autoplay: 3000,
                                autoplayDisableOnInteraction: false,
                                pagination: '.swiper-pagination'
                            });
                        }
                    });
                }
            }
        });

    }
    toLink(linkUrl) {
        /*使用閉包把參數變爲局部變量使用*/
        return () => {
            window.location.href = linkUrl;
        };
    }
    render() {
        return (
            <div className="music-recommend">
                <div className="slider-container">
                    <div className="swiper-wrapper">
                        {
                            this.state.sliderList.map(slider => {
                                return (
                                    <div className="swiper-slide" key={slider.id}>
                                        <a className="slider-nav" onClick={this.toLink(slider.linkUrl)}>
                                            <img src={slider.picUrl} width="100%" height="100%" alt="推薦"/>
                                        </a>
                                    </div>
                                );
                            })
                        }
                    </div>
                    <div className="swiper-pagination"></div>
                </div>
            </div>
        );
    }
}

export default Recommend
複製代碼

上述代碼在componentDidMount方法中發送jsonp請求,請求成功後調用setState更新ui,setState第二個參數是一個回調函數,當組件更新完成後會當即調用,這個時候咱們在回調函數裏面初始化swiper

接下來開發最新專輯列表,在constructor構造函數的state中增長一個newAlbums屬性存放最新專輯列表

this.state = {
    sliderList: [],
    newAlbums: []
};
複製代碼

而後從recommend.js中導入getNewAlbum

import {getCarousel, getNewAlbum} from "@/api/recommend"
複製代碼

針對專輯信息咱們封裝一個類模型。使用類模型的好處可使代碼重複利用,方便後續繼續使用,ui對應的數據清晰,把ui須要的字段統一做爲類的屬性,根據屬性就能很清楚的知道ui須要哪些數據

模型類統一放置在model目錄下面。在src目錄下新建model目錄,而後新建album.js文件

album.js

/**
 *  專輯類模型
 */
export class Album {
    constructor(id, mId, name, img, singer, publicTime) {
        this.id = id;
        this.mId = mId;
        this.name = name;
        this.img = img;
        this.singer = singer;
        this.publicTime = publicTime;
    }
}

/**
 *  經過專輯列表數據建立專輯對象函數
 */
export function createAlbumByList(data) {
    return new Album(
        data.album_id,
        data.album_mid,
        data.album_name,
        `http://y.gtimg.cn/music/photo_new/T002R300x300M000${data.album_mid}.jpg?max_age=2592000`,
        filterSinger(data.singers),
        data.public_time
    );
}

function filterSinger(singers) {
    let singerArray = singers.map(singer => {
        return singer.singer_name;
    });
    return singerArray.join("/");
}
複製代碼

上述代碼album類經過構造函數給屬性初始化值,在每一個接口獲取的專輯信息字段都不同,因此針對每一個接口的請求使用一個對象建立函數來建立album對象

Recommend.js中import這個文件

import * as AlbumModel from "@/model/album"
複製代碼

comentDidMount中增長如下代碼

getNewAlbum().then((res) => {
    console.log("獲取最新專輯:");
    if (res) {
        console.log(res);
        if (res.code === CODE_SUCCESS) {
            //根據發佈時間降序排列
            let albumList = res.albumlib.data.list;
            albumList.sort((a, b) => {
                return new Date(b.public_time).getTime() - new Date(a.public_time).getTime();
            });
            this.setState({
                newAlbums: albumList
            });
        }
    }
});
複製代碼

render方法中增長如下代碼

let albums = this.state.newAlbums.map(item => {
    //經過函數建立專輯對象
    let album = AlbumModel.createAlbumByList(item);
    return (
        <div className="album-wrapper" key={album.mId}>
            <div className="left">
                <img src={album.img} width="100%" height="100%" alt={album.name}/>
            </div>
            <div className="right">
                <div className="album-name">
                    {album.name}
                </div>
                <div className="singer-name">
                    {album.singer}
                </div>
                <div className="public—time">
                    {album.publicTime}
                </div>
            </div>
        </div>
    );
});
複製代碼

return塊中的代碼以下

<div className="music-recommend">
    <div className="slider-container">
        <div className="swiper-wrapper">
            {
                this.state.sliderList.map(slider => {
                    return (
                        <div className="swiper-slide" key={slider.id}>
                            <a className="slider-nav" onClick={this.toLink(slider.linkUrl)}>
                                <img src={slider.picUrl} width="100%" height="100%" alt="推薦"/>
                            </a>
                        </div>
                    );
                })
            }
        </div>
        <div className="swiper-pagination"></div>
    </div>
    <div className="album-container">
        <h1 className="title">最新專輯</h1>
        <div className="album-list">
            {albums}
        </div>
    </div>
</div>
複製代碼

樣式recommend.styl文件沒有列出,可在源代碼中查看

到此界面及數據渲染已經完成

使用Better-Scroll封裝Scroll組件

在推薦頁面中最新專輯列表已經超出了屏幕高度,而外層定位的元素並無設置overflow: scroll,這個時候是不能滾動的。這裏咱們使用一款better-scroll(一位國人大牛黃軼寫的)插件來實現列表的滾動,在項目中會有不少列表須要滾動因此把滾動列表抽象成一個公用的組件

better-scroll是一個移動端滾動插件,基於iscroll重寫的。普通的網頁滾動效果是很死板的,better-scroll具備拉伸、回彈的效果而且滾動的時候具備慣性,很接近原生體驗。better-scroll更多相關內容見github地址:github.com/ustbhuangyi…。相信不少人在vue中都用過better-scroll,由於better-scroll的做者很好的把它運用在了vue中,幾乎一說到better-scroll你們就會想到vue(2333~~~)。其實better-scroll是利用原生js編寫的,因此在全部使用原生js的框架中幾乎都能使用它,這裏我將在React中的運用better-scroll

首先在src目錄下新建一個common目錄用來存放公用的組件,新建scroll文件夾,而後在scroll文件夾下新建Scroll.jsscroll.styl文件。先來分析一下怎麼設計這個Scroll組件,better-scroll的原理就是外層一個固定高度的元素,這個元素有一個子元素,當子元素的高度超過父元素時就能夠發生滾動,那麼子元素裏面的內容從何而來?React爲咱們提供了一個props的children屬性用來獲取組件的子組件,這樣就能夠用Scroll組件去包裹須要滾動的內容。在Scroll組件內部的列表,會隨着增長或減小原生而發生變化,這個時候元素的高度也會發生變化,better-scroll須要從新計算高度,better-scroll爲咱們提供了一個refresh方法用來從新計算以保證正常滾動,組件發生變化會觸發React的componentDidUpdate周期函數,因此咱們在這個函數裏面對better-scroll進行刷新操做,同時須要一個props來告訴Scroll是否刷新。某些狀況下咱們須要手動調用Scroll組件去刷新better-scroll,這裏對外暴露一個Scroll組件的refresh方法。better-scroll默認是禁止點擊的,須要提供一個控制是否點擊的props,爲了監聽滾動Scroll須要對外暴露一個函數,便於使用Scroll的組件監聽滾動進行其餘操做。當組件銷燬時咱們把better-scroll綁定的事件取消以及better-scroll實例給銷燬掉,釋放資源

安裝better-scroll

npm install better-scroll@1.5.5 --save
複製代碼

這裏使用1.5.5的版本,在開發的時候使用的版本。寫這個篇文章的時候已經更新到1.6.x了,做者仍是很勤快的

對組件的props進行類型檢查,這裏使用prop-types庫。類型檢查是爲了提前發現開發問題,避免一些bug產生

安裝prop-types

npm install prop-types --save
複製代碼

編寫Scroll組件

Scroll.js

import React from "react"
import ReactDOM from "react-dom"
import PropTypes from "prop-types"
import BScroll from "better-scroll"
import "./scroll.styl"

class Scroll extends React.Component {
    componentDidUpdate() {
        //組件更新後,若是實例化了better-scroll而且須要刷新就調用refresh()函數
        if (this.bScroll && this.props.refresh === true) {
            this.bScroll.refresh();
        }
    }
    componentDidMount() {
        this.scrollView = ReactDOM.findDOMNode(this.refs.scrollView);
        if (!this.bScroll) {
            this.bScroll = new BScroll(this.scrollView, {
                //實時派發scroll事件
                probeType: 3,
                click: this.props.click
            });

            if (this.props.onScroll) {
                this.bScroll.on("scroll", (scroll) => {
                    this.props.onScroll(scroll);
                });
            }

        }
    }
    componentWillUnmount() {
        this.bScroll.off("scroll");
        this.bScroll = null;
    }
    refresh() {
        if (this.bScroll) {
            this.bScroll.refresh();
        }
    }
    render() {
        return (
            <div className="scroll-view" ref="scrollView">
                {/*獲取子組件*/}
                {this.props.children}
            </div>
        );
    }
}

Scroll.defaultProps = {
    click: true,
    refresh: false,
    onScroll: null
};

Scroll.propTypes = {
    //是否啓用點擊
    click: PropTypes.bool,
    //是否刷新
    refresh: PropTypes.bool,
    onScroll: PropTypes.func
};

export default Scroll
複製代碼

上訴代碼中ref屬性來標記div元素,使用ReactDOM.findDOMNode函數來獲取dom對象,而後傳入better-scroll構造函數中初始化。在Scroll組件中調用外部組件的方法只須要把外部組件的函數經過props傳入便可,這裏就是onScroll函數

scroll.styl

.scroll-view
  width: 100%
  height: 100%
  overflow: hidden
複製代碼

scroll.styl中就是一個匹配父容器寬高的樣式

接下來在Recommend組件中加入Scroll組件,導入Scroll組件

import Scroll from "@/common/scroll/Scroll"
複製代碼

在state中增長refreshScroll用來控制Scroll組件是否刷新

this.state = {
    sliderList: [],
    newAlbums: [],
    refreshScroll: false
};
複製代碼

使用Scroll組件包裹Recommend組件的內容,Scroll組件增長一個根元素

<div className="music-recommend">
    <Scroll refresh={this.state.refreshScroll}>
        <div>
        <div className="slider-container">
            <div className="swiper-wrapper">
                {
                    this.state.sliderList.map(slider => {
                        return (
                            <div className="swiper-slide" key={slider.id}>
                                <a className="slider-nav" onClick={this.toLink(slider.linkUrl)}>
                                    <img src={slider.picUrl} width="100%" height="100%" alt="推薦"/>
                                </a>
                            </div>
                        );
                    })
                }
            </div>
            <div className="swiper-pagination"></div>
        </div>
        <div className="album-container">
            <h1 className="title">最新專輯</h1>
            <div className="album-list">
                {albums}
            </div>
        </div>
        </div>
    </Scroll>
</div>
複製代碼

在獲取最新專輯數據更新專輯列表後調用setState讓Scroll組件刷新

this.setState({
    newAlbums: albumList
}, () => {
    //刷新scroll
    this.setState({refreshScroll:true});
});
複製代碼

實現的效果以下圖

底部有52px的bottom是爲了後面miniplayer組件預留

Loading組件的封裝

此時Recommend頁面組件仍是不夠完善的,當網絡請求耗費不少時間的時候界面什麼都沒有,體驗很很差。通常在網絡請求的時候都會加一個loading效果,告訴用戶此時正在加載數據。這裏把Loading組件抽取成公用的組件

common下新建loading目錄,而後在loading目錄下新建Loading.jsloading.styl,另外在loading下面放入一張loading.gif圖片 Loading.js

import React from "react"
import loadingImg from "./loading.gif"
import "./loading.styl"

class Loading extends React.Component {
    render() {
        let displayStyle = this.props.show === true ?
            {display:""} : {display:"none"};
        return (
            <div className="loading-container" style={displayStyle}>
                <div className="loading-wrapper">
                    <img src={loadingImg} width="18px" height="18px" alt="loading"/>
                    <div className="loading-title">{this.props.title}</div>
                </div>
            </div>
        );
    }
}

export default Loading
複製代碼

Loading組件只接受一個show屬性明確當前組件是否顯示,title是顯示的文字內容

loading.styl

.loading-container
  position: absolute
  top: 0
  left: 0
  width: 100%
  height: 100%
  z-index: 999
  display: flex
  justify-content: center
  align-items: center
  .loading-wrapper
    display: inline-block
    font-size: 12px
    text-align: center
    .loading-title
      margin-top: 5px
複製代碼

回到Recommend組件中。導入Loading組件

import Loading from "@/common/loading/Loading"
複製代碼

在state中增長loading屬性

this.state = {
    loading: true,
    sliderList: [],
    newAlbums: [],
    refreshScroll: false
};
複製代碼

當專輯列表加載完成後隱藏Loading組件,只須要將loading狀態值修改成false

this.setState({
    loading: false,
    newAlbums: albumList
}, () => {
    //刷新scroll
    this.setState({refreshScroll:true});
});
複製代碼

優化圖片加載

專輯列表中有不少圖片,一個屏幕放不下列表中的全部圖片而且用戶不必定就會看滾動查看全部的數據,這個時候須要使用圖片懶加載功能,當用戶滾動列表,圖片顯示出來時才加載,幫助用戶節省流量,這也是爲何移動端須要使用體積小的庫進行開發的緣由。這裏使用一個react-lazyload庫github地址:github.com/jasonslyvia…,它實際上是組件的懶加載,用它來實現圖片懶加載

安裝react-lazyload

npm install react-lazyload --save
複製代碼

在Recommend.js中導入react-lazyload

import LazyLoad from "react-lazyload"
複製代碼

使用LazyLoad組件包裹圖片

<LazyLoad>
    <img src={album.img} width="100%" height="100%" alt={album.name}/>
</LazyLoad>
複製代碼

這個時候運行發現一個問題,當滾動專輯列表的時候,從屏幕外進入屏幕內的圖沒有了

這是由於react-lazylaod庫監聽的是瀏覽器原生的scroll和resize事件,當出如今屏幕的時候纔會加載。而這裏使用的是better-scroll的滾動,better-scroll是基於css3的transform實現的,因此當圖片出如今屏幕內時天然沒法被加載

解決辦法

經過查閱react-lazyload的github的使用說明,發現提供了一個forceCheck函數,當元素沒有經過scroll或者resize事件加載時強制檢查元素位置,這個時候若是出如今屏幕內就會被當即加載。藉助Scroll組件暴露的onScroll屬性就能夠監聽到Scroll組件的滾動

此時修改import

import LazyLoad, { forceCheck } from "react-lazyload"
複製代碼

在Scroll組件上增長onScroll,在處理函數中調用forceCheck

<Scroll refresh={this.state.refreshScroll} 
	onScroll={(e) => {
		/*檢查懶加載組件是否出如今視圖中,若是出現就加載組件*/
		forceCheck();}}>
	...
</Scroll>
複製代碼

總結

這一節主要介紹了接口請求代碼的合理規劃、推薦接口和最新專輯接口調用、better-scroll在React中的運用(應better-scroll做者要求)、公用組件Scroll和Loading組件的封裝。在作圖片懶加載優化的時候,剛開始考慮到通常的懶加載都是經過監聽原生scroll或reset事件來實現的。這裏使用了better-scroll,須要一個適當的時候手動進行加載,剛好react-lazyload提供了forceCheck方法,結合better-scroll的refresh方法就能夠到達這個需求

完整項目地址:github.com/code-mcx/ma…

本章節代碼在chapter3分支

後續更新中...

相關文章
相關標籤/搜索