react + iscroll5
react + iscroll5
经过几天的反复折腾,总算做出一个体验还不错的列表页了,主要支持了下拉刷新,上拉加载两个功能。
一开始直接采用了react-iscroll插件,它是基于iscroll插件开发的组件。但是开发过程中,发现它内部封装的行为非常固化,限制了我对iscroll的控制能力,因此我转而直接基于iscroll插件实现。
网上也有一些基于浏览器原生滚动条实现的方案,找不到特别好的博客说明,而iscroll是基于Js模拟的滚动条(滚动条也是一个div哦),其兼容性更好,所以还是选择iscroll吧。
先体验效果
在讲解实现之前,可以先体验一下app整体效果。如果使用桌面浏览器访问,必须进入开发者模式,启动手机仿真,并使用鼠标左键触发滑动,否则无法达到真机效果(点我进入)!建议还是扫描二维码直接在手机浏览器中体验,二维码如下:
下载demo源码
点击这里下载源码,之后一起看一下实现中需要注意的事项和思路。
实现关键点
本篇实现了MsgListPage这个组件,支持消息列表的滚动查看,下拉刷新,上拉加载功能。
这里使用了开源的iscroll5实现滚动功能,它对iscroll4重构并修复若干bug,是目前主流版本。网上鲜有iscroll5实现下拉刷新,上拉加载功能的好例子,提供的仅是一些思路,绝大多数实现都是修改iscroll5源码,并不完美。我这次的实现不需要修改iscroll5源码,其实通过巧妙的设计是可以完美的实现这些特效的。
代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
import React from "react";import {Link} from "react-router";import $ from "jquery";import style from "./MsgListPage.css";import iScroll from "iscroll/build/iscroll-probe"; // 只有这个库支持onScroll,从而支持bounce阶段的事件捕捉export default class MsgListPage extends React.Component { constructor(props, context) { super(props, context); this.state = { items: [], pullDownStatus: 3, pullUpStatus: 0, }; this.page = 1; this.itemsChanged = false; this.pullDownTips = { // 下拉状态 0: '下拉发起刷新', 1: '继续下拉刷新', 2: '松手即可刷新', 3: '正在刷新', 4: '刷新成功', }; this.pullUpTips = { // 上拉状态 0: '上拉发起加载', 1: '松手即可加载', 2: '正在加载', 3: '加载成功', }; this.isTouching = false; this.onItemClicked = this.onItemClicked.bind(this); this.onScroll = this.onScroll.bind(this); this.onScrollEnd = this.onScrollEnd.bind(this); this.onTouchStart = this.onTouchStart.bind(this); this.onTouchEnd = this.onTouchEnd.bind(this); } componentDidMount() { const options = { // 默认iscroll会拦截元素的默认事件处理函数,我们需要响应onClick,因此要配置 preventDefault: false, // 禁止缩放 zoom: false, // 支持鼠标事件,因为我开发是PC鼠标模拟的 mouseWheel: true, // 滚动事件的探测灵敏度,1-3,越高越灵敏,兼容性越好,性能越差 probeType: 3, // 拖拽超过上下界后出现弹射动画效果,用于实现下拉/上拉刷新 bounce: true, // 展示滚动条 scrollbars: true, }; this.iScrollInstance = new iScroll(`#${style.ListOutsite}`, options); this.iScrollInstance.on('scroll', this.onScroll); this.iScrollInstance.on('scrollEnd', this.onScrollEnd); this.fetchItems(true); } fetchItems(isRefresh) { if (isRefresh) { this.page = 1; } $.ajax({ url: '/msg-list', data: {page: this.page}, type: 'GET', dataType: 'json', success: (response) => { if (isRefresh) { // 刷新操作 if (this.state.pullDownStatus == 3) { this.setState({ pullDownStatus: 4, items: response.data.items }); this.iScrollInstance.scrollTo(0, -1 * $(this.refs.PullDown).height(), 500); } } else { // 加载操作 if (this.state.pullUpStatus == 2) { this.setState({ pullUpStatus: 0, items: this.state.items.concat(response.data.items) }); } } ++this.page; console.log(`fetchItems=effected isRefresh=${isRefresh}`); } }); } /** * 点击跳转详情页 */ onItemClicked(ev) { // 获取对应的DOM节点, 转换成jquery对象 let item = $(ev.target); // 操作router实现页面切换 this.context.router.push(item.attr('to')); this.context.router.goForward(); } onTouchStart(ev) { this.isTouching = true; } onTouchEnd(ev) { this.isTouching = false; } onPullDown() { // 手势 if (this.isTouching) { if (this.iScrollInstance.y > 5) { this.state.pullDownStatus != 2 && this.setState({pullDownStatus: 2}); } else { this.state.pullDownStatus != 1 && this.setState({pullDownStatus: 1}); } } } onPullUp() { // 手势 if (this.isTouching) { if (this.iScrollInstance.y <= this.iScrollInstance.maxScrollY - 5) { this.state.pullUpStatus != 1 && this.setState({pullUpStatus: 1}); } else { this.state.pullUpStatus != 0 && this.setState({pullUpStatus: 0}); } } } onScroll() { let pullDown = $(this.refs.PullDown); // 上拉区域 if (this.iScrollInstance.y > -1 * pullDown.height()) { this.onPullDown(); } else { this.state.pullDownStatus != 0 && this.setState({pullDownStatus: 0}); } // 下拉区域 if (this.iScrollInstance.y <= this.iScrollInstance.maxScrollY + 5) { this.onPullUp(); } } onScrollEnd() { console.log("onScrollEnd" + this.state.pullDownStatus); let pullDown = $(this.refs.PullDown); // 滑动结束后,停在刷新区域 if (this.iScrollInstance.y > -1 * pullDown.height()) { if (this.state.pullDownStatus <= 1) { // 没有发起刷新,那么弹回去 this.iScrollInstance.scrollTo(0, -1 * $(this.refs.PullDown).height(), 200); } else if (this.state.pullDownStatus == 2) { // 发起了刷新,那么更新状态 this.setState({pullDownStatus: 3}); this.fetchItems(true); } } // 滑动结束后,停在加载区域 if (this.iScrollInstance.y <= this.iScrollInstance.maxScrollY) { if (this.state.pullUpStatus == 1) { // 发起了加载,那么更新状态 this.setState({pullUpStatus: 2}); this.fetchItems(false); } } } shouldComponentUpdate(nextProps, nextState) { // 列表发生了变化, 那么应该在componentDidUpdate时调用iscroll进行refresh this.itemsChanged = nextState.items !== this.state.items; return true; } componentDidUpdate() { // 仅当列表发生了变更,才调用iscroll的refresh重新计算滚动条信息 if (this.itemsChanged) { this.iScrollInstance.refresh(); } return true; } render() { let lis = []; this.state.items.forEach((item, index) => { lis.push( <li key={index} to={`/msg-detail-page/${index}`} onClick={this.onItemClicked}> {item.title}{index} </li> ); }) // 外层容器要固定高度,才能使用滚动条 return ( <div id={style.ScrollContainer}> <div id={style.ListOutsite} style={{height: window.innerHeight}} onTouchStart={this.onTouchStart} onTouchEnd={this.onTouchEnd}> <ul id={style.ListInside}> <p ref="PullDown" id={style.PullDown}>{this.pullDownTips[this.state.pullDownStatus]}</p> {lis} <p ref="PullUp" id={style.PullUp}>{this.pullUpTips[this.state.pullUpStatus]}</p> </ul> </div> </div> ); }}MsgListPage.contextTypes = { router: () => { React.PropTypes.object.isRequired }}; |
思路
- 在react的componentDidMount回调中,DOM已经渲染完成。此时进行iscroll插件的初始化,监听其scroll和scrollEnd两个插件回调用于滚动监听,同时,调用fetchItems发起首次数据加载。
- 在react的shouldComponentUpdate回调中,我判断并记录本次render是否对ul的元素进行了增删,从而在componentDidUpdate回调中决策是否需要为iscroll进行refresh刷新,因为如果iscroll容器内的元素数量发生变动,iscroll是需要重新计算整个高度等信息的。
- 为了获知用户是否在触屏,我给div注册了onTouchStart和onTouchEnd两个事件函数,这主要是为了区分滚动条是因为触屏拖拽移动,还是因为惯性移动。
- 在iscroll的onScroll回调中,专门处理用户的触屏行为。我判断y坐标确认当前滚动条所处的范围是顶部的上拉区域,还是底部的下拉区域。当处于上拉区域中的时候,根据拖拽的偏移量展现不同的文案,下拉区域也是一样。
- 在iscroll的onScrollEnd回调中,专门处理滚动结束后的状态判断,主要是判断用户是否此前的触屏行为是否触发了下载需求,如果产生了下载需求那么发起网络调用fetchItems。
- 需要注意,下拉刷新条也位于iscroll容器内,在它能被用户可见但又没有抵达刷新触发偏移量之前,如果用户没有触屏那么应该立即向上滚动把下拉提示条滚到视野范围外。上拉加载条也位于iscroll容器内,但是它总是可以被用户看见,所以对应的处理逻辑相对简单。
- 不要在onScroll内调用scrollTo等移动滚动条的函数,因为onScroll内调用ScrollTo会导致继续回调onScroll,如此往复像在打乒乓球,是不合理的。我的实现中,onScroll仅仅检测用户的触屏行为(不处理惯性滑动),而onScrollEnd中才进行对应的逻辑处理或者发起scrollTo,而scrollTo触发的是惯性滑动(isTouching=false),因而又不会造成onScroll的困扰。
- 点击某一行会跳转到MsgDetailPage组件,这是通过注册onClick事件回调,并通过this.context.router操作react-router的路由实现的切换。
- 如果iscroll内元素太少没有产生滚动条,那么会影响上述的效果实现逻辑。因此,我给<ul>元素设置了min-height:150%的高度,也就是最小溢出iscroll容器50%,保证滚动条总是存在,并且刷新提示条 有足够的滚动范围逃离用户视线。
- 如果你在手机浏览器里上下拖拽,有时候会发现页面整体在移动,而不是滚动条滚动。为了解决这个问题,我在react的根容器里,捕获了body的touchmove事件,调用了preventDefault()阻止了浏览器默认行为。
必须注意,所有的网络请求都是模拟的,并没有动态的后端计算。
本文实现了非常有意思的动画效果,也非常实用。
另外,第3个组件『留言提交页』因为精力原因,不打算继续写完了。
当前访问路径如果是:列表页 -> 详情页 -> 返回列表页,会发现列表页内容重新刷新了,滚动条也没有停留在原先的位置上。这是因为每次路由切换,都是重新分配一个component对象进行重新渲染,所以状态没有保存,我当然可以在跳转详情页之前把列表页的状态保存到一个全局变量里或者localStorage里,但是这毕竟比较麻烦。
为了实现状态保存,redux就是在做类似的框架级支持,所以我可能接下来真的要学学redux了,学无止境,太可怕!
react + iscroll5的更多相关文章
- react + iscroll5 实现完美 下拉刷新,上拉加载
经过几天的反复折腾,总算做出一个体验还不错的列表页了,主要支持了下拉刷新,上拉加载两个功能. 一开始直接采用了react-iscroll插件,它是基于iscroll插件开发的组件.但是开发过程中,发现 ...
- react组件的生命周期
写在前面: 阅读了多遍文章之后,自己总结了一个.一遍加强记忆,和日后回顾. 一.实例化(初始化) var Button = React.createClass({ getInitialState: f ...
- 十分钟介绍mobx与react
原文地址:https://mobxjs.github.io/mobx/getting-started.html 写在前面:本人英语水平有限,主要是写给自己看的,若有哪位同学看到了有问题的地方,请为我指 ...
- RxJS + Redux + React = Amazing!(译一)
今天,我将Youtube上的<RxJS + Redux + React = Amazing!>翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: https:/ ...
- React 入门教程
React 起源于Facebook内部项目,是一个用来构建用户界面的 javascript 库,相当于MVC架构中的V层框架,与市面上其他框架不同的是,React 把每一个组件当成了一个状态机,组件内 ...
- 通往全栈工程师的捷径 —— react
腾讯Bugly特约作者: 左明 首先,我们来看看 React 在世界范围的热度趋势,下图是关键词“房价”和 “React” 在 Google Trends 上的搜索量对比,蓝色的是 React,红色的 ...
- 2017-1-5 天气雨 React 学习笔记
官方example 中basic-click-counter <script type="text/babel"> var Counter = React.create ...
- RxJS + Redux + React = Amazing!(译二)
今天,我将Youtube上的<RxJS + Redux + React = Amazing!>的后半部分翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: ht ...
- React在开发中的常用结构以及功能详解
一.React什么算法,什么虚拟DOM,什么核心内容网上一大堆,请自行google. 但是能把算法说清楚,虚拟DOM说清楚的聊聊无几.对开发又没卵用,还不如来点干货看看咋用. 二.结构如下: impo ...
随机推荐
- sharepoint 2013 更改搜索server组态
1.新搜索server在.安装sharepoint server 2013,并连接到一个现有的sharepoint server领域,完成后.您可以配置新的搜索server. 打开sharepoint ...
- Sqlserver到处数据到Excel
转:http://www.cnblogs.com/litianfei/archive/2007/08/10/850823.html ) drop procedure [dbo].[p_exporttb ...
- android自定义View之钟表诞生记
很多筒子觉得自定义View是高手的象征,其实不然.大家觉得自定义View难很多情况下可能是因为自定义View涉及到了太多的类和API,把人搞得晕乎乎的,那么今天我们就从最简单的绘图API开始,带大家来 ...
- [置顶] Datalist嵌套datalist,页面传值,加密,数据绑定
<asp:DataList ID="dlMajor" runat="server" CssClass="dllist" OnItemD ...
- JVM笔记3:Java垃圾收集算法与垃圾收集器
当前商业虚拟机的垃圾收集都采用"分代收集"算法,即根据对象生命周期的不同,将内存划分几块,一般为新生代和老年代,不同的代根据其特点使用最合适的垃圾收集算法 一,标记-清除算法: 该 ...
- Magento入门开发教程
Modules->模块 Controller->控制器 Model->模型 Magento是这个星球上最强大的购物车网店平台.当然,你应该已经对此毫无疑问了.不过,你可能还不知道,M ...
- js_BOM_05
1.下拉级联 |-select的API |-如何获得选中的option? |-如何创建option? |-如何将option添加到select? |-如何移 ...
- C# HTTP 请求
public class HttpHelper { /// <summary> /// 创建GET方式的HTTP请求 /// </summary> public static ...
- JS实现各种页面的刷新
JS实现各种页面的刷新功能 1.刷新当前页面 opener.location.replace(opener.location.href); 或者window.opener.window.locatio ...
- android实现倒计时
前言 在打开爱奇艺等app的欢迎界面的时候,右上角有一个倒计时的控件.倒计时完了以后进入主界面.现在我们来实现这个功能. 方法一 利用java的类Timer,TimerTask还有android的H ...