接着上一节内容,这一节抓取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
这里接口用的是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.js
ajax
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文件没有列出,可在源代码中查看
到此界面及数据渲染已经完成
在推荐页面中最新专辑列表已经超出了屏幕高度,而外层定位的元素并无设置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.js和scroll.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组件预留
此时Recommend页面组件仍是不够完善的,当网络请求耗费不少时间的时候界面什么都没有,体验很很差。通常在网络请求的时候都会加一个loading效果,告诉用户此时正在加载数据。这里把Loading组件抽取成公用的组件
在common下新建loading目录,而后在loading目录下新建Loading.js和loading.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分支
后续更新中...