React开发实践6--详细说说滚动记忆
需求
我们的APP是一个电商hybrid APP!APP的几乎所有呈现部分,都由前端来完成。
首先说一下需求: 在17年的九十月份,接到业务部门的一些关于APP体验的反馈。其中一点是说用户在浏览首页的时候,点到某个商品详情里去,再返回的时候,并没有回到离开页面的位置,而是回到了页面的顶部,于是用户又需要重新从头开始浏览首页上的商品。为了更好地说明问题,他们还拿京东APP作为对比,比照了京东的体验,我才发现这一块的确有很大改进的空间。他只是举出了首页这一个示例,实际上在各个商品列表页也都有同样的问题。
#尝试找到核心问题
接到这个需求的时候,我其实有点茫然。搞不清楚这个问题的技术核心是什么,还专门去某乎提了一个问题手机京东web版用了什么技术能够让页面在刷新之后滚动到刷新之前的位置?(为了写这篇文,我又看了下这个答案的日志,我提问是在17年12月底) ,问题很快得到了回复:
这个是浏览器自带技能,无需手动实现。
我后来验证了一番,这果然是浏览器的自带技能。那么接下来问题又来了,为什么浏览器本身自带的技能,在我们开发的网站上就发挥不出来了呢?
经过了一番搜索之后,搞明白了这个道理。
浏览器在做什么
我们想一下这个过程,假如我们用电脑chrome浏览器打开一个小说阅读网站的阅读页,比如这个第40章 华山论剑,往下滚动使屏幕位置超过一屏,这个时候刷新页面,我们会发现浏览器记住了你刷新屏幕前的位置(上次阅读到的位置),于是你就可以继续阅读了,这是一个很赞的设计,浏览器帮助我们做到了。
接下来,我们打开一个购物网站,比如大名鼎鼎的某宝(https://www.taobao.com/),打开首页之后还是往下滚动,一直滚动到页脚的位置,刷新页面。有没有发现,虽然页面还是往下滚动了一下,但是并没有到达页脚的位置。这是为什么呢?
小说网站的阅读页,由于内容固定(当然固定了,反正就是这一章节的内容),直接把所有的内容写到了HTML中,于是我们解释一下刚刚的行为。假设浏览器是个人,比如chrome。
当然,做过前端开发的都知道,我们前端开发要面对的一大问题就是浏览器兼容性。因此在这里,我也必须要强调,我现在所说的chrome,是在linux系统内核下(包括Android)的chrome v55+,因为实际在测试中也发现很多国产手机浏览器比如小米MIUI内置的浏览器,或者是我自己魔趣8.1系统下内置的via浏览器都能够实现,前进回退记忆。(我先说一下我理解的前进后退记忆以及滚动记忆在本文中的意义。前进后退记忆即是说,当用户从A页面进入B页面,再返回到A页面,回到A页面不会触发任何网络请求,从A页面再回到B页面也不会触发任何网络请求。
)前进回退记忆是浏览器为了用户体验帮开发者做的一个工作,当回退的时候无需再去重复访问刚刚已经访问过的网址,不用加载前面已经加载过的内容。chrome 为什么原生不支持这个特性呢?我个人认为是chrome 还是想把这个自主权交给开发者。然而即便是像我前面提到的via浏览器,在处理history api来进行页面跳转的时候,在前进回退这件事上做得也并不够彻底。从我目前观察到的结论是:如果页面的跳转是通过a标签这样的方式来完成的(我可以认为他是真跳转,如果是真跳转,打开chrome的控制台,跳转的时候,勾选preserve log,跳转的时候会看到类似“Navigated to https://www.baidu.com/“ 这样的提示,假跳转则没有),这个时候via浏览器是可以实现页面的前进回退记忆的。反之,如果页面的跳转是通过history api来完成的(我可以认为他是假跳转),这个时候via浏览器认为需要把前进回退记忆这件事交给网站开发者自己来处理。
所以,前面我已经排除了很多干扰因素,也说明了这些干扰因素存在的原因。接下来,chrome虽然不支持前进后退记忆这件事,但是能够支持滚动记忆。也就是上面提到了例子,虽然它不支持前进后退的记忆,但是它支持滚动记忆,只要你在当前会话中有访问过某个页面,下次访问就还是会尽量自动滚动到上次滚动到的位置。
我接下来就聊一聊这个滚动记忆
我: 我把华山论剑这章看到了当前页面距离页顶8000px的位置,你知道的吧?
chrome: 我知道的亲
我:我要刷新页面喽
chrome: 好的,再你刷新页面之后,我还把华山论剑这章帮你滚动到距离页顶8000px的位置,好让你继续看接下来的情节
我:谢谢了,我刷新了
chrome: 我滚完了
我:我看到了,谢谢你。这个大结局很精彩
可是再看看某宝的反映呢?
我: 我已经看到了淘宝网页脚距离页顶8000px的位置了,你知道的吧?
chrome: 我知道的亲
我:我要刷新页面喽
chrome: 好的,再你刷新页面之后,我还把淘宝网页脚距离页顶8000px的位置了,好让你继续剁手
我:谢谢了,我刷新了
chrome: 我开始干活了。啊,等等,你当前淘宝页面只有4000px啊,抱歉,那我只能给你滚动4000px的位置了。
我:你明明知道我上次浏览这个页面的时候还有至少8000px呢,干嘛不等等,等Javascript把页面其他内容补充上,就有不止8000px了。
chrome: 亲,帮你滚动到上次浏览的位置,这个工作啊,我一回只能做一次。你不是想靠Javascript来补充HTML的内容么!那你就别来求我了。
我认为就是这么个过程,简单总结一下。浏览器会在从加载到load完成这个过程来做「滚动到上次浏览位置」这件事。而如果你的页面并非纯HTML写的,而是通过JS来插入节点完成的,就的确会出现刚才某宝网那样的问题。
React在做什么
知道了这样一件事,那么我们来看看我们的问题。现在先不谈首页,我们只说商品列表。从商品列表页到达某个商品详情页,然后再从详情页返回到商品列表页,我们来看看这个时候各个组件生命周期的变化。
首先,如果/productlist
这个路由切换到/product:productId
这个路由
组件 | 生命周期 |
---|---|
Productlist | willUnmount |
Product | didmount |
而如果通过触发返回,从/product:productId
这个路由切换到/productlist
这个路由呢?
组件 | 生命周期 |
---|---|
Productlist | didmount |
Product | willUnmount |
可以看到,Productlist 组件每次都需要进入一个新的生命周期。众所周知,为了更好的用户体验。我们往往会在页面中数据请求和处理的过程中,给用户一个loading的效果,因此用户每次返回后最先看到的都是loading,而不是展示的商品列表。
看到问题了吗?
在React的设计中,一个组件mount的时候(第一次render),就会把当时的组件内容渲染好后作为HTML进行输出,而之后这个组件update的过程,我们都可以理解为是React源码最终通过JS向DOM中插入节点或删除节点。于是,我们写的React页面反映出来的效果就是下面这样的:
我: 我已经看到了商品列表页面,距离页顶8000px的位置了,你知道的吧?
chrome: 我知道的亲
我:我进入了A商品的详情页,你记得等我回到商品列表页的时候,给我滚到刚才我离开前的位置
chrome: 好的
我: 我要回到商品列表页了
chrome: 我知道你的意思。我要干活了。。。等等,这个商品列表页只有一屏的高度。是个loading页面,不好意思,我不能继续向下滚了。
我:为什么
chrome: 我前面不是跟你解释过了么,还来问我
我能做什么
前面说了这么多,解决办法是什么呢?其实类似于我前面写的《React开发实践--4嵌套路由的应用》
最后
如果读者朋友能够看到这里,稍微有点质疑精神的朋友都会说,你说了这么一大堆,我凭什么相信你呢?
好吧,干货来了。
Here’s the Scroll Restoration Spec.
, 当然了,这其实也只是一个草案,可以看出来,各家浏览器对这个滚动的行为应该怎么处理,还没有达成一致。但是从前面我提到过的chrome,via浏览器,MIUI内置浏览器可以看出来,他们所表现出来的行为总体还是很接近这个草案的。