通过页面转换改善用户流程

已发表: 2022-03-10
快速总结↬任何时候用户的体验被打断,他们离开的机会就会增加。 从一个页面切换到另一个页面通常会通过显示无内容的白色闪烁、加载时间过长或以其他方式使用户脱离新页面打开之前所处的上下文而导致此中断。

页面之间的转换可以通过保留(甚至改善)用户的上下文、保持他们的注意力以及提供视觉连续性和积极反馈来增强体验。 同时,页面过渡也可以在美学上令人愉悦和有趣,并且如果做得好可以加强品牌。

Page Transitions

在本文中,我们将逐步创建页面之间的过渡。 我们还将讨论这种技术的优缺点以及如何将其推向极限。

例子

许多移动应用程序都很好地利用了视图之间的转换。 在下面的示例中,它遵循 Google 的材料设计指南,我们看到动画如何传达页面之间的层次和空间关系。

为什么我们不对我们的网站使用相同的方法? 为什么我们可以让用户感觉每次页面更改时他们都被传送了?

跳跃后更多! 继续往下看↓

如何在网页之间转换

SPA 框架

在动手之前,我应该先谈谈单页应用程序 (SPA) 框架。 如果您使用的是 SPA 框架(例如 AngularJS、Backbone.js 或 Ember),那么在页面之间创建过渡会容易得多,因为所有路由都已由 JavaScript 处理。 请参阅相关文档以了解如何使用您选择的框架转换页面,因为可能有一些很好的示例和教程。

错误的方法

我第一次尝试在页面之间创建过渡看起来或多或少是这样的:

 document.addEventListener('DOMContentLoaded', function() { // Animate in }); document.addEventListener('beforeunload', function() { // Animate out });

这个概念很简单:当用户离开页面时使用一个动画,当新页面加载时使用另一个动画。

然而,我很快发现这个解决方案有一些局限性:

  • 我们不知道加载下一页需要多长时间,因此动画可能看起来不流畅。
  • 我们无法创建结合上一页和下一页内容的过渡。

事实上,实现流畅和平滑过渡的唯一方法是完全控制页面更改过程,因此根本不更改页面。 因此,我们必须改变解决问题的方法。

正确的方式

让我们看看以正确的方式在页面之间创建简单的淡入淡出过渡所涉及的步骤。 它涉及一种叫做pushState AJAX(或 PJAX)导航的东西,它本质上会将我们的网站变成一种单页网站。

这种技术不仅实现了平滑和愉快的过渡,而且我们还将受益于其他优势,我们将在本文后面详细介绍。

防止默认链接行为

第一步是为所有要使用的链接创建一个click事件侦听器,防止浏览器执行其默认行为并自定义其处理页面更改的方式。

 // Note, we are purposely binding our listener on the document object // so that we can intercept any anchors added in future. document.addEventListener('click', function(e) { var el = e.target; // Go up in the nodelist until we find a node with .href (HTMLAnchorElement) while (el && !el.href) { el = el.parentNode; } if (el) { e.preventDefault(); return; } });

这种将事件侦听器添加到父元素而不是将其添加到每个特定节点的方法称为事件委托,并且由于 HTML DOM API 的事件冒泡特性而成为可能。

获取页面

现在我们已经在浏览器尝试更改页面时中断了它,我们可以使用 Fetch API 手动获取该页面。 让我们看看下面的函数,它在给定 URL 时获取页面的 HTML 内容。

 function loadPage(url) { return fetch(url, { method: 'GET' }).then(function(response) { return response.text(); }); }

对于不支持 Fetch API 的浏览器,可以考虑添加 polyfill 或使用老式的XMLHttpRequest

更改当前 URL

HTML5 有一个很棒的 AP​​I,叫做pushState ,它允许网站在不加载任何页面的情况下访问和修改浏览器的历史记录。 下面,我们用它来修改当前 URL 为下一页的 URL。 请注意,这是对我们之前声明的锚点击事件处理程序的修改。

 if (el) { e.preventDefault(); history.pushState(null, null, el.href); changePage(); return; }

您可能已经注意到,我们还添加了对名为changePage的函数的调用,稍后我们将详细介绍。 相同的函数也将在popstate事件中调用,当浏览器的活动历史条目发生更改时会触发该事件(例如当用户单击浏览器的后退按钮时):

 window.addEventListener('popstate', changePage);

有了这一切,我们基本上是在构建一个非常原始的路由系统,其中我们有主动和被动模式。

当用户单击链接并使用pushState更改 URL 时,我们使用主动模式,而当 URL 更改并且我们收到popstate事件通知时,我们正在使用被动模式。 无论哪种情况,我们都将调用changePage ,它负责读取新 URL 并加载相关页面。

解析并添加新内容

通常,被导航的页面将具有共同的元素,例如headerfooter 。 假设我们在所有页面上使用以下 DOM 结构(这实际上是 Smashing Magazine 本身的结构):

动画!

当用户单击一个链接时, changePage函数获取该页面的 HTML,然后提取cc容器并将其添加main元素中。 此时,我们的页面上有两个cc容器,第一个属于上一页,第二个属于下一页。

下一个函数animate负责通过重叠两个容器、淡出旧容器、淡入新容器和移除旧容器来交叉淡入淡出。 在本例中,我使用 Web Animations API 创建淡入淡出动画,当然您可以使用任何您喜欢的技术或库。

 function animate(oldContent, newContent) { oldContent.style.position = 'absolute'; var fadeOut = oldContent.animate({ opacity: [1, 0] }, 1000); var fadeIn = newContent.animate({ opacity: [0, 1] }, 1000); fadeIn.onfinish = function() { oldContent.parentNode.removeChild(oldContent); }; }

最终代码可在 GitHub 上找到。

这些是转换网页的基础知识!

警告和限制

我们刚刚创建的这个小例子远非完美。 其实我们还没有考虑到一些事情:

  • 确保我们影响正确的链接。
    在更改链接的行为之前,我们应该添加一个检查以确保它应该被更改。 例如,我们应该忽略所有带有target="_blank"的链接(在新选项卡中打开页面),所有指向外部域的链接,以及其他一些特殊情况,如Control/Command + click (也会在一个新标签)。
  • 更新主内容容器之外的元素。
    目前,当页面发生变化时, cc容器之外的所有元素都保持不变。 但是,其中一些元素需要更改(现在只能手动完成),包括文档的title 、具有active类的菜单元素,以及可能取决于网站的许多其他元素。
  • 管理 JavaScript 的生命周期。
    我们的页面现在表现得像一个 SPA,其中浏览器本身不会更改页面。 因此,我们需要手动处理 JavaScript 生命周期——例如,绑定和取消绑定某些事件、重新评估插件以及包括 polyfill 和第三方代码。

浏览器支持

我们正在实现的这种导航模式的唯一要求是pushState API,它在所有现代浏览器中都可用。 这种技术完全可以作为一种渐进增强。 这些页面仍以通常的方式提供和访问,并且当 JavaScript 被禁用时,网站将继续正常工作。

如果您使用的是 SPA 框架,请考虑改用 PJAX 导航,以保持快速导航。 这样做,您将获得传统支持并创建一个对 SEO 更友好的网站。

走得更远

我们可以通过优化它的某些方面来继续推动这项技术的极限。 接下来的几个技巧将加快导航速度,显着提升用户体验。

使用缓存

通过稍微改变我们的loadPage函数,我们可以添加一个简单的缓存,它可以确保已经访问过的页面不会重新加载。

 var cache = {}; function loadPage(url) { if (cache[url]) { return new Promise(function(resolve) { resolve(cache[url]); }); } return fetch(url, { method: 'GET' }).then(function(response) { cache[url] = response.text(); return cache[url]; }); }

您可能已经猜到了,我们可以通过 Cache API 或其他客户端持久存储缓存(如 IndexedDB)使用更永久的缓存。

动画出当前页面

我们的淡入淡出效果要求在过渡完成之前加载并准备好下一页。 通过另一个效果,我们可能希望在用户单击链接后立即开始为旧页面设置动画,这将为用户提供即时反馈,对感知性能有很大帮助。

通过使用 Promise,处理这种情况变得非常容易。 .all方法创建一个新的 Promise,一旦所有作为参数包含的 Promise 都被解析,该 Promise 就会被解析。

 // As soon as animateOut() and loadPage() are resolved… Promise.all[animateOut(), loadPage(url)] .then(function(values) { …

预取下一页

仅使用 PJAX 导航,页面更改通常几乎是默认导航的两倍,因为浏览器不必解析和评估新页面上的任何脚本或样式。

但是,当用户将鼠标悬停在链接上或开始触摸链接时,我们可以通过开始预加载下一页来走得更远。

可以看到,用户的悬停和点击通常会有 200 到 300 毫秒的延迟。 这是死时间,通常足以加载下一页。

话虽如此,明智地预取,因为它很容易成为瓶颈。 例如,如果您有一个很长的链接列表并且用户正在滚动浏览它,则此技术将预取所有页面,因为链接在鼠标下方通过。

在决定是否预取时,我们可以检测并考虑的另一个因素是用户的连接速度。 (也许这将在未来通过网络信息 API 实现。)

部分输出

在我们的loadPage函数中,我们正在获取整个 HTML 文档,但实际上我们只需要cc容器。 如果我们使用服务器端语言,我们可以检测请求是否来自特定的自定义 AJAX 调用,如果是,则仅输出它需要的容器。 通过使用 Headers API,我们可以在获取请求中发送自定义 HTTP 标头。

 function loadPage(url) { var myHeaders = new Headers(); myHeaders.append('x-pjax', 'yes'); return fetch(url, { method: 'GET', headers: myHeaders, }).then(function(response) { return response.text(); }); }

然后,在服务器端(在本例中使用 PHP),我们可以在仅输出所需容器之前检测我们的自定义标头是否存在:

 if (isset($_SERVER['HTTP_X_PJAX'])) { // Output just the container }

这将减少 HTTP 消息的大小,同时也减少服务器端的负载。

包起来

在几个项目中实现了这项技术之后,我意识到一个可重用的库将非常有用。 这将节省我在每个场合实施它的时间,让我可以专注于过渡效果本身。

因此诞生了 Barba.js,这是一个小型库(4 KB 压缩和 gZip'd),它抽象出所有这些复杂性,并为开发人员提供了一个漂亮、干净和简单的 API 供开发人员使用。 它还考虑了视图,并带有可重用的转换、缓存、预取和事件。 它是开源的,可在 GitHub 上找到。

结论

我们现在已经了解了如何创建淡入淡出效果以及使用 PJAX 导航将我们的网站有效地转换为 SPA 的优缺点。 除了转换本身的好处之外,我们还看到了如何实现简单的缓存和预取机制来加速新页面的加载。

整篇文章都是基于我的个人经验以及我在我从事的项目中实现页面转换所学到的知识。 如果您有任何问题,请随时发表评论或在 Twitter 上与我联系——我的信息如下!

关于 SmashingMag 的进一步阅读

  • 用户体验设计中的智能转换
  • 向多设备世界过渡的设计
  • 使用 Web 技术提供原生体验