给静态博客添加页面切换效果
经常折腾各种静态博客主题的同学可能会发现,大多数主题都没有添加页面间的切换效果。对于现在流行的单页应用,添加过渡效果并不是什么难事;但是对于静态博客来说,每次页面切换都是要刷新整个页面的“硬刷新”,想要添加切换效果就比较麻烦了。这篇文章里我使用了一个可能有点“过时”但意外的好用的方法来解决这个问题,它就是:PJAX。
1. PJAX与Barba.js
简单来说,PJAX就是PushState + Ajax,它通过这么几个步骤来避免页面的“硬刷新”:阻止点击链接时的正常行为(页面跳转);通过ajax读取新页面;手动更改地址栏的URL并将新的内容注入到页面中。常用的PJAX实现有jquery-pjax,以及我用到的Barba.js。
下面是Barba.js在用户点击链接时执行的操作:
用户点击链接时,Barba.js会:
- 检查链接是否有效,是否支持ajax。如果是的话,阻止链接的正常行为
- 使用push state API更改页面URL
- 通过XMLHttpRequest获取新页面
- 创建一个新的transition实例
- 新页面加载完成后,barba.js解析新的HTML(取得其中的
.barba-container
),并将其中的内容插入到#barba-wrapper
元素的DOM中。- transition实例会负责隐藏旧的container并显示新的container
- 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步:
第5步
当ajax加载新页面完成后,barba.js会从新页面中找到
.barba-container
元素,并把它插入到#barba-wrapper
元素中。这时候在页面的#barba-wrapper
元素下,会有两个.barba-container
元素,barba.js会给新插入的那个添加一个visibility: hidden;
,把它隐藏起来。第6步
现在就轮到
transition
出场了。它需要负责把两个.barba-container
中的旧的那个隐藏起来,并把新的那个显示出来。这个显示和隐藏的过程,就是我们添加切换效果的地方。第7步
transition
完成切换后,barba.js会把旧的那个.barba-container
移除掉,这样整个流程就完成了。
具体实现上,我们只要自定义一个继承了Barba.BaseTransition的transition
对象,并把它配置到barba.js里就可以了。BaseTransition
有这么几个成员:
Member | Description |
---|---|
start | transition开始的时候会自动调用这个函数。(你可以把它当做transition的构造函数) |
done | transition完成后,调用这个函数通知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)