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的更多相关文章

  1. react + iscroll5 实现完美 下拉刷新,上拉加载

    经过几天的反复折腾,总算做出一个体验还不错的列表页了,主要支持了下拉刷新,上拉加载两个功能. 一开始直接采用了react-iscroll插件,它是基于iscroll插件开发的组件.但是开发过程中,发现 ...

  2. react组件的生命周期

    写在前面: 阅读了多遍文章之后,自己总结了一个.一遍加强记忆,和日后回顾. 一.实例化(初始化) var Button = React.createClass({ getInitialState: f ...

  3. 十分钟介绍mobx与react

    原文地址:https://mobxjs.github.io/mobx/getting-started.html 写在前面:本人英语水平有限,主要是写给自己看的,若有哪位同学看到了有问题的地方,请为我指 ...

  4. RxJS + Redux + React = Amazing!(译一)

    今天,我将Youtube上的<RxJS + Redux + React = Amazing!>翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: https:/ ...

  5. React 入门教程

    React 起源于Facebook内部项目,是一个用来构建用户界面的 javascript 库,相当于MVC架构中的V层框架,与市面上其他框架不同的是,React 把每一个组件当成了一个状态机,组件内 ...

  6. 通往全栈工程师的捷径 —— react

    腾讯Bugly特约作者: 左明 首先,我们来看看 React 在世界范围的热度趋势,下图是关键词“房价”和 “React” 在 Google Trends 上的搜索量对比,蓝色的是 React,红色的 ...

  7. 2017-1-5 天气雨 React 学习笔记

    官方example 中basic-click-counter <script type="text/babel"> var Counter = React.create ...

  8. RxJS + Redux + React = Amazing!(译二)

    今天,我将Youtube上的<RxJS + Redux + React = Amazing!>的后半部分翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: ht ...

  9. React在开发中的常用结构以及功能详解

    一.React什么算法,什么虚拟DOM,什么核心内容网上一大堆,请自行google. 但是能把算法说清楚,虚拟DOM说清楚的聊聊无几.对开发又没卵用,还不如来点干货看看咋用. 二.结构如下: impo ...

随机推荐

  1. 批处理:循环解压不同文件夹下的zip压缩包

    结构如下 A文件夹: A1文件.zip A2文件.zip A3文件.zip B文件夹: B1文件.zip B2文件.zip B3文件.zip ...... 批处理文件:rezip.bat如下 @ech ...

  2. [Javascript] Implement zip function

    1. Use a for loop to traverse the videos and bookmarks array at the same time. For each video and bo ...

  3. HBase中的备份和故障恢复方法

    本文将对Apache HBase可用的数据备份机制和大量数据的故障恢复/容灾机制做简要介绍. 随着HBase在重要的商业系统中应用的大量添加,很多企业须要通过对它们的HBase集群建立健壮的备份和故障 ...

  4. mysqlbinlog 用法

    操作命令: show binlog events in 'binlog.000016' limit 10; reset master 删除所有的二进制日志 flush logs  产生一个新的binl ...

  5. 利用QT制作我们自己的一个计算器

    有了前面的经验就比较容易创建一个 属于我们自己的计算器了. 一些简单的拖拽就可以实现了. 界面设计部分: 转到槽之后的代码部分: #include "widget.h" #incl ...

  6. C++学习(四)

    一.拷贝构造函数和拷贝赋值运算符1.拷贝构造:用一个已有的对象,构造和它同类型的副本对象——克隆.2.形如class X {  X (const X& that) { ... }};的构造函数 ...

  7. 技术QQ群

    欢迎加入moss技术交流群. 欢迎加入微信技术交流群.

  8. 【转】【SQL SERVER】怎样处理作业中的远程服务器错误(42000)

    (SQL SERVER)怎样处理作业中的远程服务器错误(42000) 问: 1.我创建了一个链接服务器. 2.在两台服务器之间创建了新的SQL用户. 3.编写了访问链接服务器的SQL语句,执行成功. ...

  9. 插入排序算法--直接插入算法,折半排序算法,希尔排序算法(C#实现)

    插入排序算法主要分为:直接插入算法,折半排序算法(二分插入算法),希尔排序算法,后两种是直接插入算法的改良.因此直接插入算法是基础,这里先进行直接插入算法的分析与编码. 直接插入算法的排序思想:假设有 ...

  10. swing容器继承重绘问题解决

    swing容器继承重绘问题解决   以JPanel为例,继承JPanel,想动态为器更换背景,这就涉及到重绘问题.一下是本人重写代码: package ui; import java.awt.Grap ...