滚动恢复是一个我们倾向于认为理所当然的功能。对于传统的基于HTML的页面和导航(即服务器渲染的网站),浏览器一直在为我们处理这种互动。
随着从服务器端渲染向浏览器的转变,在客户端渲染的应用程序,甚至是JS增强的HTML页面都失去了这个非常有用的用户体验功能。让滚动恢复正常工作的最重要的地方之一是在电子商务网站的产品列表页(PLP)。在这篇文章中,我们将探讨一种手动恢复浏览器滚动位置的技术,从而使我们的电子商务用户获得一致的、高性能的体验。
设置场景
让我们想象一下,我们有一个电子商务网站,它的PLP每行显示四个产品,每页显示32个产品。每张产品卡大约有500px高(其中包括产品图片、产品名称、颜色/尺寸信息、价格和 "添加到购物车 "按钮)。
现在让我们假设一个浏览网站的用户的浏览器分辨率为1920 x 1080px。这意味着他们一次可以看到两行产品。总的来说,这个PLP有多达四个 "视口 "可供用户滚动:每页32个产品,每行四个产品,总共八行,每个视口两行。
现在让我们想象一下,一个用户在PLP上滚动,并点击了第15个产品,因此浏览器将他们导航到该产品的产品描述页(PDP),作为结果。这意味着他们已经在第四行选择了一个产品。
现在,用户想回到列表页,继续查看其余的产品,所以他们按下了返回按钮。浏览器向后导航,当列表页加载完毕后,可见的产品是在第一和第二行。
然后,用户不得不手动向下滚动,寻找他们刚刚浏览过的产品,以便继续查看其他产品。这是一个非常常见的导致电子商务用户沮丧的原因,并且导致他们对网站和潜在的品牌失去信任。
用户不知道为什么会发生这种情况,但作为开发者,我们知道。我们的PLP是在浏览器中完全渲染和填充的,所以当浏览器第一次绘制HTML时(可能只包括页眉和页脚),渲染产品的部分只是一个空的div。
当浏览器试图使用导航到PDP之前滚动条所在的Y坐标来恢复滚动位置时,页面就会不够长。浏览器无法准确地恢复滚动位置,因为它不知道我们的应用程序何时完成了渲染,因此也不知道何时才是恢复的正确时间。
在这些情况下,我们需要自己处理恢复滚动位置的问题。
在我们开始之前
为了真正理解什么是滚动恢复,让我们看一下我们需要手动控制滚动恢复的一些情况:服务器渲染的网页与JS渲染的内容(混合页面),以及完全在客户端渲染的应用程序(例如,React应用程序)。
首先,一个基线。这里我们有一个服务器渲染的网站,一旦开始渲染,浏览器就可以获得所有的HTML。
正如我们所看到的,从PDP导航回PLP,可以将滚动完美地恢复到我们最初在导航到PDP之前所点击的产品。
接下来,我们有一个混合页面。在这个页面中,大部分的HTML内容在第一次浏览时就可以使用,但有些内容--在本例中是产品列表--是由JavaScript随后填充的。
我们可以看到,在显示页眉和页脚,以及加载产品,然后渲染之间有一个延迟。
正如我们在这个例子中看到的,当导航回到PLP时,浏览器停留在视口的顶部,无法恢复滚动到我们在导航到PDP之前最初点击的产品的位置。
最后,在这里我们有一个用React构建的完全客户端应用程序。
在这最后一个例子中,我们看到了与混合方法类似的行为;也就是说,浏览器并不能将滚动恢复到最初点击的产品上。
实现滚动恢复
对于混合页面和完全的客户端应用程序来说,实现滚动恢复是非常相似的,因为我们将利用JavaScript来找到我们要恢复滚动位置的产品。在这篇文章中,我们将介绍在React应用程序中实现滚动恢复。
如果你想看一个混合应用的滚动恢复的实现,请查看本文的GitHub仓库。我们会时常引用仓库中的代码,但重要的代码会在本文中出现。
应用结构实例
我们将从一个非常基本的应用程序开始,它有两个页面:第一个页面包含32个产品,其源代码在一个叫做PLP.jsx 的组件中,第二个页面只是一个空白页,将作为我们的占位符PDP,源代码在一个叫做PDP.jsx 的组件中。
我们还有一个ProductCard.jsx ,用来渲染32种产品中的每一种。
第1步:存储所选产品的引用
首先,我们需要存储一个用户选择的产品的引用。我们将使用sessionStorage 和产品的ID来做这件事。
为什么是sessionStorage ?这只是为这个演示所选择的格式。我们可以很容易地使用一个全局状态管理器来存储要恢复的产品的ID,或者用其他方式在内存中保留这个值。另外,我们不需要这个数据长期存在(就像用localStorage )。如果用户关闭了标签,那么这些数据就可以被安全地遗忘。
实现这一目标所需的代码是一个简单的函数,当ProductCard 组件中的一个链接被激活时,该函数被调用。要做到这一点,我们在PLP 组件中定义该函数,并将其传递到ProductCard 组件中,当链接被点击时调用它。
PLP.jsx
const PLP = () => {
const persistScrollPosition = (id) => {
sessionStorage.setItem("scroll-position-product-id-marker", id);
};
return (
<ProductCard
product={product}
onSelect={persistScrollPosition}
/>
);
}
ProductCard.jsx
const ProductCard = (props) => {
const { product, onSelect } = props;
const { id } = product;
return (
<div>
{/* ... */}
<Link to="/pdp" onClick={() => onSelect(id)} />
{/* ... */}
</div>
);
}
第2步:手动恢复滚动到以前选择的产品上
现在我们已经存储了被点击的产品,我们需要在PLP再次呈现时将浏览器滚动到该产品。
为了做到这一点,我们将利用setState 中的一个回调函数,以便渲染应用程序的其余部分,然后将一个restorationRef 传递给需要滚动到视图的ProductCard 。
PLP.jsx
const PLP = () => {
// ...
const [productMarkerId] = React.useState(() => {
// Lazy initialise the productMarkerId
const persistedId = sessionStorage.getItem(
"scroll-position-product-id-marker"
);
sessionStorage.removeItem("scroll-position-product-id-marker");
return persistedId ? persistedId : null;
});
// ...
return (
<ProductCard
product={product}
onSelect={persistScrollPosition}
restorationRef={Number(productMarkerId) === product.id ? restorationRef : null}
/>
);
}
ProductCard.jsx
const ProductCard = () => {
const { restorationRef } = props;
React.useEffect(() => {
// restorationRef is only provided to the ProductCard that needs to be scrolled to
if (!restorationRef) {
return;
}
// Restoring scroll here ensures the previously selected product will always be restored, no matter how long the API request to get products takes
restorationRef.current.scrollIntoView({ behavior: 'auto', block: 'center' });
})
// ...
};
魔术发生在上面的ProductCard.jsx 组件的第11行。behavior 的值为auto ,因为浏览器滚动恢复通常不使用动画,但如果需要效果,我们可以很容易地使用smooth 。
至于block ,我们选择了center ,因为它可以确保没有粘性元素会挡住我们的路。例如,如果我们使用start ,并且我们有一个粘性标题(这在电子商务中非常常见),那么产品行的顶部将被该标题覆盖。尽管技术上是正确的,但这不会给人以准确的滚动恢复体验。
就这样吧!这就是我们需要手动实现滚动恢复的全部内容。
然而,如果我们真的想争取一个高质量的用户体验,那么我们应该再往前走几步。
第3步:有条件地将滚动恢复到之前选择的产品上
想象一下这样的场景:一个客户在PLP上选择了一个产品,并查看了该产品的描述页面。然后,他们使用菜单导航到另一个PLP,并点击不同产品的描述页面,然后再一次使用菜单返回到原来的PLP。用户在整个过程中一直在向前导航。你知道会发生什么吗?
好吧,滚动将恢复到几个导航之前选择的产品,即使用户正在向前移动到列表页。这与理想的行为相去甚远,而且可能会给用户带来困惑。
为了解决这个问题,我们可以在我们的懒惰状态初始化函数中添加一个检查,以确定用户是否真的回到了列表页,或者只是从网站的另一个页面再次导航到它。
在React中,我们可以使用React Router和useHistory 钩子,它看起来像这样。
import { useHistory } from 'react-router-dom';
const PLP = () => {
const history = useHistory();
// ...
const [productMarkerId] = React.useState(() => {
// History action will be POP when a user is "moving back" to a page. Alternative will be "PUSH"
if (history.action !== 'POP') {
return null;
}
// ...
});
};
下一个问题是,如果列表页使用非常常见的无限加载模式,在用户向下滚动页面时提供一个无尽的产品列表,会发生什么?当用户点击一个产品,然后点击返回按钮时,应用程序如何知道要加载什么,以使正确的产品能够被滚动恢复?让我们接下来看看这个问题。
第四步:无限加载的列表页的滚动恢复
对于我们的无限加载列表页,我们需要确保的第一件事是,我们要跟踪正在加载的页面。我们可以很容易地在一个状态值中做到这一点,但这对用户来说不是很有用,特别是对想分享产品页面的用户来说。
我们应该考虑在URL中跟踪最新加载的产品页面。这给了用户很好的反馈,可以看到他们已经加载了多少页产品(而不需要在应用程序的用户界面上搜索,试图找到这些信息),它也给了我们使用URL栏中的页码来恢复滚动的机会
在我们进入事情的技术层面之前,让我们更新一下我们的例子场景。
想象一下,我们的电子商务网站有一个单一的列表页。我们有32种产品,但我们不想一次把它们全部展示出来,并呈现一个巨大的、长的页面。相反,我们想只加载和呈现前12个,然后是后12个,最后是8个。我们要用无限加载的方法来做这件事。
当用户接近最后一行产品时,他们会看到一个 "Load Next "按钮,这将加载下一页产品,并将它们附加到当前列表的末尾。?page=3 随着页面的加载,当最后一页被加载时,URL中的查询字符串将更新为?page=2 。
然后,用户选择其中一个产品并浏览该产品的描述页面。当用户浏览完该描述页面并触发了返回按钮,浏览器将加载什么页面?
当我们返回时,浏览器将恢复我们在导航事件之前的最新URL,这将是?page=3 。只要用户最初选择的产品是在第三页,我们的滚动恢复就会顺利进行。
然而,如果用户向下滚动加载了所有三页的产品,然后向上滚动并点击了第二页数据集中的产品,会发生什么?当他们从描述页触发返回动作时会发生什么?
那么,第三页的结果仍然会被加载,而用户选择的产品不会被发现,所以滚动恢复不会发生。用户将被留在视口的顶部,查看第三页的第一行产品。这并不是用户所期望的,我们可以让这种体验变得更好。
回顾一下我们添加到PLP 组件的persistScrollPosition 功能。我们可以在此基础上再添加一个步骤,在导航事件发生之前更新URL中的查询字符串。onSelect 记住,ProductCard 的道具被添加到Link 组件的ProductCard 中的onClick 。
还要注意,在Link 组件中,onClick 函数将在导航到to 中的路径之前执行。
利用这一知识,我们可以在导航事件被触发之前迅速改变URL中的查询字符串。这意味着当用户从描述页面点击返回按钮时,浏览器将加载包含用户最初选择的产品的产品页面让我们在代码中看到这一点。
const PLP = () => {
// ...
const persistScrollPosition = (id, pageNo) => {
// Set the page value in the query string to match the page that the selected product is on
history.replace(`?page=${pageNo}`);
// ...
};
// ...
}
为了使本文的重点放在滚动恢复技术上,我不打算公布实现无限加载所需的所有变化。如果需要参考,可以在之前链接的GitHub仓库中找到该模式的实现。
正如我们在上面的代码中所看到的,我们只需调用history.replace ,并附上所选产品被加载的页面编号。这需要对添加到products 数组中的产品数据进行轻微的更新,这样它就能记住产品实际上属于哪一页。
有了这个非常简单的更新,我们就可以依靠浏览器用用户选择的产品的正确页码来恢复URL,不管有多少个页面被无限地加载,我们的滚动恢复机制就会启动,并按预期工作。
更进一步
为了创造良好的用户体验,在滚动恢复方面还有很多可以实现的地方。为了不使这篇文章太长,这里简要介绍一下那些可以使电子商务网站的用户体验更上一层楼的额外功能。
骨架式加载器
与其显示一个基本的加载信息,不如用 "占位符 "产品预先填充页面,这些产品在被替换时不会引起滚动滞留。当浏览器试图滚动恢复,但只滚动到(非常短的)页面的底部时,就会发生滚动滞留,因为产品还没有被渲染。
利用scrollRestoration 属性
通过将历史API中的scrollRestoration 属性设置为manual ,告诉浏览器我们要处理滚动恢复。这对避免滚动干扰也有帮助。
可访问性
考虑到滚动恢复的可及性。当用户向后浏览时,哪个元素将被赋予焦点?哪个元素应该被赋予焦点?用户选择的产品将被恢复到视口中,但如果用户随后点击Tab键会发生什么?
在上面的例子中,我们只跟踪一个产品进行滚动恢复,假设一个简单的PLP→PDP→(回)PLP模式。但是,如果用户通过多个不同的PLP和PDP(使用菜单)向前移动,然后决定点击一堆后退键,该怎么办?
本文中的示例代码不会处理这种情况,但是为这种功能更新它将是相当微不足道的。
最后的想法
如前所述,滚动恢复是一个我们以前完全依靠浏览器来处理的功能,所以我们从未考虑过这个问题。然而,随着越来越多的客户端和渐进式Web应用的建立,这个简单的功能正在丢失,它对电子商务应用的用户体验产生了负面影响。
在这篇文章中,我们讨论了一些简单的步骤,以确保我们建立的电子商务网站易于浏览,并且用户可以信任。
The postImplementing scroll restoration in ecommerce React appsappeared first onLogRocket Blog.