经常折腾各种静态博客主题的同学可能会发现,大多数主题都没有添加页面间的切换效果。对于现在流行的单页应用,添加过渡效果并不是什么难事;但是对于静态博客来说,每次页面切换都是要刷新整个页面的“硬刷新”,想要添加切换效果就比较麻烦了。这篇文章里我使用了一个可能有点“过时”但意外的好用的方法来解决这个问题,它就是:PJAX

1. PJAX与Barba.js

简单来说,PJAX就是PushState + Ajax,它通过这么几个步骤来避免页面的“硬刷新”:阻止点击链接时的正常行为(页面跳转);通过ajax读取新页面;手动更改地址栏的URL并将新的内容注入到页面中。常用的PJAX实现有jquery-pjax,以及我用到的Barba.js

下面是Barba.js在用户点击链接时执行的操作:

用户点击链接时,Barba.js会:

  1. 检查链接是否有效,是否支持ajax。如果是的话,阻止链接的正常行为
  2. 使用push state API更改页面URL
  3. 通过XMLHttpRequest获取新页面
  4. 创建一个新的transition实例
  5. 新页面加载完成后,barba.js解析新的HTML(取得其中的.barba-container),并将其中的内容插入到#barba-wrapper元素的DOM中。
  6. transition实例会负责隐藏旧的container并显示新的container
  7. trainsition结束后,旧的container会被移除

以上摘自Barba.js官网对自己的介绍。看不懂的话没关系(尤其是关于transition的部分),后面我会详细解释,现在我们先来看个示例。

2. QuickStart

2.1 安装

barba.js 支持 AMD, CommonJS 和 Browser global (使用 UMD)。

可以通过npm安装:

npm install barba.js --save-dev

或者直接在页面里引入:

<script src="barba.min.js" type="text/javascript"></script>

想让barba.js正常工作的话,还得给它一点页面结构的信息。通常在页面里加入这么个结构就可以了:

<div id="barba-wrapper">
  <div class="barba-container">
    ...Put here the content you wish to change between pages...
  </div>
</div>

然后在页面里初始化Barba.js:

//Please note, the DOM should be ready
Barba.Pjax.start();

是不是挺简单的?下面我们就自己动手试一试。

2.2 简单尝试

以我使用的Jekyll主题为例,这是我的页面的基本布局:


<html lang="{{ site.lang }}">

  {% include head.html %}
  
  <body>
    
    {% include header.html %}
    {% include aside.html %}
      
    <main class="content-wrapper">
      {{ content }}
    </main>
    
    {% include footer.html %}
    
    <script>
    </script>
  </body>
</html>

这里head.html、header.html、footer.html的内容在所有页面都是一样的,会发生变化的是aside.html和content。所以我们要做的就是把aside.html和content用#barba-wrapper.barba-container包起来,然后在在script中执行Barba.Pjax.start();。当然,不要忘记在head.html中引入barba.js。修改后的布局是这样的:

<html lang="{{ site.lang }}">

  {% include head.html %}
  
  <body>
    {% include header.html %}
    
    <div id="barba-wrapper">
      <div class="barba-container">
        {% include aside.html %}
        <main class="content-wrapper">
          {{ content }}
        </main>
      </div>
    </div>
    
    {% include footer.html %}
    
    <script>
        Barba.Pjax.start();
    </script>
  </body>
</html>

这样就可以了。打开浏览器的网络面板,点击任意链接,可以看到网络请求类型变成了xhr(XMLHttpRequest),请求的发起者也变成了barba.js:

现在,当我们点击页面上的链接的时候,barba.js会用ajax加载目标页面,加载完成后,会用新页面里的.barba-container元素替换旧的.barba-container。做到这些一共只需要5行代码,很简单吧?

3. 深入一点

我们的初衷是给页面添加切换效果,要做到这点需要对barba.js的transition有所了解。transition是barba.js里负责隐藏旧容器、显示新容器的对象。在第1节里我们介绍过barba.js的工作流程,其中需要特别解释的是第5步、第6步和第7步:

  1. 第5步

    当ajax加载新页面完成后,barba.js会从新页面中找到.barba-container元素,并把它插入到#barba-wrapper元素中。这时候在页面的#barba-wrapper元素下,会有两个.barba-container元素,barba.js会给新插入的那个添加一个visibility: hidden;,把它隐藏起来。

  2. 第6步

    现在就轮到transition出场了。它需要负责把两个.barba-container中的旧的那个隐藏起来,并把新的那个显示出来。这个显示和隐藏的过程,就是我们添加切换效果的地方。

  3. 第7步

    transition完成切换后,barba.js会把旧的那个.barba-container移除掉,这样整个流程就完成了。

具体实现上,我们只要自定义一个继承了Barba.BaseTransitiontransition对象,并把它配置到barba.js里就可以了。BaseTransition有这么几个成员:

MemberDescription
starttransition开始的时候会自动调用这个函数。(你可以把它当做transition的构造函数)
donetransition完成后,调用这个函数通知barba.js进行后续工作。千万别忘记调用这个函数!
oldContainer旧容器的HTMLElement
newContainerLoading加载新容器的Promise
newContainer新容器的HTMLElement(带有 visibility: hidden;) 注意,在newContainerLoading 完成前这个变量都是undefined!

barba.js的默认transition是HideShowTransition,这个transition很简单,我们自己来重新实现一下:

var HideShowTransition = Barba.BaseTransition.extend({
  start: function() {
    this.newContainerLoading.then(this.finish.bind(this));
  },

  finish: function() {
    document.body.scrollTop = 0;
    this.done();
  }
});

然后,把它设置给barba.js:

Barba.Pjax.getTransition = function() {
  return HideShowTransition;
};

看完这个例子,实现页面切换效果的方法也就呼之欲出了。只要修改finish函数,把旧容器淡出,新容器淡入就可以了。barba.js官方给出了一个淡入淡出的transition示例,这个例子使用了jQuery的.animate(),不过barba.js并不依赖jQuery,你也完全可以用其他JS库、原生javascript或CSS来实现。

var FadeTransition = Barba.BaseTransition.extend({
  start: function() {
    /**
     * This function is automatically called as soon the Transition starts
     * this.newContainerLoading is a Promise for the loading of the new container
     * (Barba.js also comes with an handy Promise polyfill!)
     */

    // As soon the loading is finished and the old page is faded out, let's fade the new page
    Promise
      .all([this.newContainerLoading, this.fadeOut()])
      .then(this.fadeIn.bind(this));
  },

  fadeOut: function() {
    /**
     * this.oldContainer is the HTMLElement of the old Container
     */

    return $(this.oldContainer).animate({ opacity: 0 }).promise();
  },

  fadeIn: function() {
    /**
     * this.newContainer is the HTMLElement of the new Container
     * At this stage newContainer is on the DOM (inside our #barba-container and with visibility: hidden)
     * Please note, newContainer is available just after newContainerLoading is resolved!
     */

    var _this = this;
    var $el = $(this.newContainer);

    $(this.oldContainer).hide();

    $el.css({
      visibility : 'visible',
      opacity : 0
    });

    $el.animate({ opacity: 1 }, 400, function() {
      /**
       * Do not forget to call .done() as soon your transition is finished!
       * .done() will automatically remove from the DOM the old Container
       */

      _this.done();
    });
  }
});

/**
 * Next step, you have to tell Barba to use the new Transition
 */

Barba.Pjax.getTransition = function() {
  /**
   * Here you can use your own logic!
   * For example you can use different Transition based on the current page or link...
   */

  return FadeTransition;
};

4. 一点小trick

对有背景图片的页面,切换到新页面后如果图片加载比较慢的话,还是会出现图片刷新的问题。通常我们会用一个固定的淡入效果来掩盖刷新过程,不过有了barba.js,我们可以做的更优雅一点。主要思路是,页面切换的时,不再FadeTransition那样隐藏旧容器显示新容器,而是把新容器里改变了的元素覆盖到旧容器里去。

还是以我的jekyll主题为例,我的页面背景是这样的:

<div class="cover-image" style="background-image: url(/path/to/background)"></div>

对它做一点小修改:

<div class="cover-image"></div>
<div class="cover-image cover-image-on" style="background-image: url(/path/to/background)"></div>

然后加入CSS:

.cover-image {
  opacity: 0;
  transition: opacity .4s ease-in-out
}

.cover-image-on {
  opacity: 1;
}

自定义trasition:

var OverwriteTransition = Barba.BaseTransition.extend({
    start: function() {
        this.newContainerLoading.then(this.switch.bind(this));
    },
  
    switch: function() {
        
        var $newContainer = $(this.newContainer);
        var $oldContainer = $(this.oldContainer);
        
        // 找到新背景图片URL
        var newCoverBg = $newContainer.find('.cover-image-on').css('background-image');
      
        /* 
        更新背景
        这里有两个.cover-image,带有.cover-image-on的是旧页面的背景,
        我们把新页面的背景设置到另一个里面去
        然后用imagesLoaded这个jQuery插件监视它的状态
        当新背景图片加载完成时,就通过增删.cover-image-on把旧的背景隐藏掉,新的显示出来
        */
        $oldContainer.find('.cover-image:not(.cover-image-on)').css('background-image', newCoverBg);
        $oldContainer.find('.cover-image:not(.cover-image-on)').addClass('cover-image-switch');
        $oldContainer.find('.cover-image-switch').imagesLoaded(
            {background: true},
            function() {
                $(".cover-image-on").removeClass("cover-image-on");
                $(".cover-image-switch").addClass("cover-image-on");
                $(".cover-image-switch").removeClass("cover-image-switch");
            }
        );

        // 同样的,新的页面内容也需要覆盖到旧容器里去
        // ...
        
        // scroll to top
        $("html, body").animate({ scrollTop: 0 }, 0);

        /*
        新container的内容已经覆盖到旧container里了
        所以交换transition里的两个container
        让barba.js销毁新容器,保留旧容器
        */
        var _new = this.newContainer;
        this.newContainer = this.oldContainer;
        this.oldContainer = _new;
        this.swapContainer.bind(this)();

        // done        
        this.done();
    }
});

  
Barba.Pjax.getTransition = function() {
    return OverwriteTransition;
};
Barba.Pjax.start();

效果是这样的:

图片加载效果

5. 就到这里吧

barba.js还提供了很多好用的功能,包括Views、缓存、预加载等,感兴趣的同学可以到他们的网站上去详细了解。

上一节里两个.cover-image互相切换的方法借鉴了journal这个主题的实现,致敬! ⤧  Next post 在Jekyll中使用highlight.js ⤧  Previous post 一起来写个简单的解释器(8)