远杨

通过页面跳转增加用户流量

远杨 · 2016-12-18翻译 · 1083阅读 原文链接

任何时候打断用户的体验,都会增加他们离开的机会。而从一个页面跳转到另一个页面通常会导致这种中断的发生, 且造成这种中断的原因通常是在加载时出现了白屏现象,或者是耗费了太长的加载时间,又或是在打开新页面的时候迫使用户离开了原来他们所在的页面。

要增强用户在页面跳转时的体验,以下方法均可:保持甚至美化用户所在的页面、保持用户的注意力或者提供有趣的视觉体验和积极的反馈。当然,页面跳转是可以做得即美观和乐趣,并且这样的页面跳转也会增强品牌效应。

在该篇文章中,我们将一步步地创建一个页面间的跳转。当然,我们也会讨论这种技术的利弊并告知如何打破其限制的方法。

例子

很多手机APP在视图间的跳转都做的不错。下面的这个例子便遵循了 Google’s material design 的规则。我们将看到动画是如何表达页面间的空间关系和层次关系。

Google’s material design上跳转的例子

为什么不在我们的网站上运用相同的方法呢?为什么我们认为让用户感知到每一次页面的变化是OK的呢?

网页间的跳转

单页面应用程序(SPA) 框架

在开始动手前,我想应该先说一下单页面应用程序(SPA)框架。如果你正在使用一个SPA框架(如AngularJS、Backbone.js 或者 Ember骨干),那么创建页面之间的跳转对你来说会更加容易。因为现目前已经可以将所有有关路由的操作交由JavaScript处理。当然,要了解你所使用的框架是如何来创建过渡页面的,你只用查阅相关文档即可。因为这些文档中已有一些很好的例子和教程。

错误的方式

我最开始使用了和下面类似的方法来创建页面间的跳转:

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

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

想法很简单,即: 当用户离开页面的时候使用一下动画,一旦用户进入一个新页面再开启另一个动画。 然而,很快,我便发现了这个方法存在以下局限性:

  • 因为我们并不知道新页面的加载会需要多长时间,所以动画可能会不太流畅。

  • 我们不能创建出能关联新、旧页面内容的跳转。

事实上,要想获得流畅的跳转,唯一的方法就是能对页面跳转的整个过程进行控制。也就是说,要做到:不更换页面。因此,我们必须改变我们解决问题的方法。

正确的方法

下面让我们来看一下创建一个具有淡入淡出效果的跳转的正确步骤。整个过程含有一个叫做pushState AJAX (or PJAX) navigation, 而这会使得网站变成一个单页面的网站。

这个方法不仅能创建出流畅且令人愉悦的跳转,还有一大能让我们收益的优势(我们将在下面的章节中谈到)。

阻止链接的默认行为

首先,创建一个点击事件的监听器供所有的链接使用。然后,阻止浏览器按照链接的默认行为进行跳转,并为其指定相应的处理方法。

// 注意,我们的目的在于将监听器绑到document对象上
// 只有这样,我们才能监听到以后加入该页面的链接
document.addEventListener('click', function(e) {
  var el = e.target;

  // 遍历节点集合知道找到一个具有href属性的节点
  while (el && !el.href) {
    el = el.parentNode;
  }

  if (el) {
    e.preventDefault();
    return;
  }
});

这种将事件监听器绑在父元素而不是特定节点的方法叫做 事件委托。而且该方法之所以奏效,有可能是因为HTML中的DOM具有事件冒泡的特性

获取页面

现在,我们已经阻止了浏览器进行页面的跳转操作。接着,我们可以使用(Fetch API)(https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)来手动获取页面。下面这个函数就可以根据给定的URL来获取指定页面的HTML内容。

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

对于那些不支持Fetch API的浏览器,我们需要加上 the polyfill文件,又或者使用过去所用的 [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)方法。

改变当前页面的URL

HTML5 具有一个非常好的API,即: pushState。这个API允许网站在未加载任何页面的情况下获取或者更改浏览器的浏览记录。下面这个例子中,我们就使用了这个API来更改现在的URL,而使其指向下个页面的URL。而这个例子对我们上面声明的点击事件的处理函数进行了一些修改。

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

  return;
}

你可能已经注意到了,我们加入了一个关于changePage函数的调用[该函数会在下面进行详细讲解]。不仅如此,该函数还是处理popstate事件的回调函数。而该事件会在浏览器当前的浏览记录被改变的时候(相当于用户点击了浏览器上的"上一步"按钮)触发。

 `window.addEventListener('popstate', changePage);`

此时,我们已经创建了一个非常基础的路由跳转系统 。而且,该系统具有主动和被动两种模式。 主动模式指的是用户点击了链接又或者我们使用pushState改变了URL的状态。而被动模式是指我们从popstate事件中才知道URL被改变的模式。但无论是那一种模式,我们都会调用changePage这个函数来获取新的URL并加载相关页面。

解析并添加新的内容

一般来说,人们所浏览的页面都具有像headerfooter这样的常用元素。现在,假设我们使用的所有页面都会是下面这个DOM结构(这个结构就是非凡杂志所使用的):

 <header>
  …
</header>

<main>
  <div class="cc">
     …
  </div>
</main>

<footer>
 …
</footer>

在变更页面的时候。唯一一个需要我们更换的内容便是cc容器中的内容。 因此,我们可以这样定义我们的changePage函数:

 var main = document.querySelector('main');

function changePage() {
  // 注意,此时的URL已经发生了变化
  var url = window.location.href;

  loadPage(url).then(function(responseText) {
    var wrapper = document.createElement('div');
        wrapper.innerHTML = responseText;

    var oldContent = document.querySelector('.cc');
    var newContent = wrapper.querySelector('.cc');

    main.appendChild(newContent);
    animate(oldContent, newContent);
  });
}

动画!

当用户点击链接的时候,changePage 函数就会去获取 相应页面的内容,然后抽取出其中的 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上找到。它实现了非凡杂志[Smashing Magazine]上的淡入淡出效果。而这些都是页面跳转中所用到的基础。

注意项和限制

我们所创建的这个例子还很不“完善”。事实上,我们还有一些事情没有考虑到。如:

  • 确保对正确的链接做出反应

在改变一个链接的默认行为之前,我们需要对其是否可变进行判断。举个例子,我们需要忽略掉一些特殊的链接,如:具有target="_blank" 设置的链接(该设置表示在另一个窗口打开页面) 、指向其他域名下的地址的链接。又或者其他的一些特殊情况,如:Control/Command + click 这个可以在另一个窗口中打开新页面的快捷键组合。

  • 在main容器之外改变元素

现目前,当页面变化的时候,所有cc 容器之外的内容都是一样的。然而这些内容有时也需要发生变化(而此时只能手动变化),如:页面的title,具有active 类的菜单元素和其它一些依赖网站的元素。

  • 管理JavaScript的生命周期.

现目前我们的页面变现的想一个单页面应用,而这使得浏览器不会自行更改页面。也因此,我们需要自行考虑JavaScript的生命周期 —举个例来说,相应事件的绑定和解绑、给插件重新求值和引入polyfills和第三方库的代码.

浏览器支持度

这种导航模式唯一需要的pushState API已经得到了现在所有浏览器的支持。这项技术 只能作为了一个渐进性增强的工具 。我们必须保证使用以前的方式来访问和存贮页面,并确保网站在JavaScript被禁用后仍然能正常运行。

如果你在使用一个SPA框架,不如考虑使用PJAX方法来完成更快的导航。这样做,你不仅会获得原有程序的支持,还能创建一个对于搜索引擎优化而言更友好的网站。

深入一下

我们可以通过对其的优化来打破这项技术的局限。下面提到的技巧会 加快导航速度, 在很大程度上增强用户的体验。

使用缓存

只需要稍微改变一下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)来定义一个永久缓存。

Animating Out the Current Page

Our crossfade effect requires that the next page be loaded and ready before the transition completes. With another effect, we might want to start animating out the old page as soon as the user clicks the link, which would give the user immediate feedback, a great aid to perceived performance.

By using promises, handling this kind of situation becomes very easy. The .all method creates a new promise that gets resolved as soon as all promises included as arguments are resolved.

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

预先获取下个页面

使用 PJAX 这个导航方法,会使得页面的转化速度加快一倍。这是因为浏览器不需要解析和运行新页面上任何的script文件又或者是样式文件。

然而,我们还可以再进一步,即:当用户悬停在连接上又或这开始点击链接时预先加载下一个页面。

立刻点击的例子

正如你所看到的,通常在用户点击链接前或者悬停在连接处会有200到300毫秒的时间。而这一 停滞时间 通常已足够加载一个页面了。

尽管如此,我们需要慎用预先加载这一方法。因为它很容易造成瓶颈。比如,页面有很多链接而且用户就在这些链接间来回“游走”。而因为链接是通过鼠标传送的,故而这个方法就会导致获取了全部的页面。

另一个我们可以跟踪和决定是否使用预先加载策略的因素就是用户的连接速度。 (这个功能以后可能会被Network Information API实现。)

输出部分

在我们的loadPage函数中,我们获取了页面全部的HTML内容。但实际上我们只需要cc 容器中的内容。如果我们使用了一种服务器端的语言,我们就可以对请求的来源进行判断,即:是否是一个AJAX请求。如果是,就只输出我们需要的内容(cc 容器)。通过 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)(http://barbajs.org)。这是一个很小的库(4 KB大小,解压状态)。它抽象了的所有复杂的操作并提供了一个友好、简洁和简单的API供开发人员使用。不仅如此,考虑到相关意见,它还包含可重用的跳转功能、缓存机制、预先获取机制和事件处理。它是一个开源项目,代码可在GitHub上获取。

Barba.js制作网页跳转效果的例子

结论

现在我们已经了解到如何创建一个淡入淡出的效果,也知道了使用PJAX导航来有效地将我们的网站转换成一个单页面应用的利与弊。除了这一转变本身(SPA)的优势,我们还知道了如何实现简单的缓存和可以用来加速加载新页面的预先加载机制。

整篇文章都是基于我个人的经验而写,涵盖了我从我工作过的关于页面跳转项目中所获得的知识。如果你有任何问题,不要犹豫地在推特上留言或联系我吧--我的信息是下面!

(rb, ml, al, il)

广告

译者远杨尚未开通打赏功能

相关文章