接着上一节内容,这一节抓取QQ音乐移动Web端推荐页面接口和PC端最新专辑接口数据。通过这些接口数据开发推荐页面。首先看一下效果图

页面结构
推荐页面主要分轮播和最新专辑两块,其中轮播图片来自QQ音乐移动Web端推荐页面的接口,最新专辑则从PC端抓取的,整个推荐页面超出屏幕是可以滚动的
轮播图和最新专辑数据抓取
用chrome浏览器打开手机调试模式,输入QQ音乐移动端地址:m.y.qq.com。打开后点击Network,然后点击XHR,可以看到有一个ajax请求。点开后,选择preview,红色框内就是我们最后需要的轮播数据

在chrome浏览器输入QQ音乐pc官网:y.qq.com

JSONP使用
这里接口用的是ajax请求,用这种方式存在跨域限制,前端是不能直接请求的,好在QQ音乐还是很人性化的基本上大部分接口都支持jsonp请求。jsonp原理具体不做过多解释了。为了使用jsonp,这里使用一款 jsonp 插件,首先安装jsonp依赖
npm install jsonp --save
安装完成后开始编写代码。为了养成好的编程习惯呢,通常会把接口请求代码存放到api目录下面,很多人会接口的url一同写在请求的代码中,这里呢,我们把url抽取出来放到单独的一个文件里面便于管理。
说明:这一章节是在上一章节的基础上继续开发的,上一章节传送门: juejin.im/post/5a3738… ,轮播数据接口和最新专辑接口说明见: juejin.im/post/5a3522…
在 src 目录下面新建 api 目录,然后新建 config.js 文件,在 config.js 文件中编写URL、一些接口公用参数、jsonp参象、接口code码等常量
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" }; 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代码封装成同步代码形式。在 src 目录下面新建 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这种形式
为了方便管理,我们把请求的代码都模块化。在 src 目录下面新建 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.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 refresh={this.state.refreshScroll}> <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> </Scroll>
在获取最新专辑数据更新专辑列表后调用 setState 让Scroll组件刷新
this.setState({ newAlbums: albumList }, () => { //刷新scroll this.setState({refreshScroll:true}); });
实现的效果如下图


底部有52px的bottom是为了后面miniplayer组件预留
Loading组件的封装
此时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 分支
后续更新中...
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。