前言

之前在看自己博客刷新的时候,发现页面总会先出来一个短暂的 Load 动画:有时是全屏遮罩,有时是进度条,有时只是中间一个在转的小图标。

一开始我以为这是浏览器或者 Hexo 默认带的效果,后来翻了一下 Butterfly 的源码,才发现它其实是主题自己加的一层“过渡界面”。这篇文章就顺着这条线,记录一下它到底是怎么实现的。

它从哪里进入页面

Butterfly 会在全局布局里直接注入加载层。换句话说,页面刚开始渲染的时候,body 里面就已经先放进去了这层结构。

关键入口在布局文件里:

1
2
body
!=partial('includes/loading/index', {}, {cache: true})

这个 index 入口会再根据配置选择不同的加载方案:

1
2
3
4
5
if theme.preloader.enable
if theme.preloader.source === 1
include ./fullpage-loading.pug
else
include ./pace.pug

所以我现在看到的刷新动画,其实就两种形态:

  • source: 1 时是全屏 preloader
  • source: 2 时是 pace.js 进度条

全屏 Load 动画怎么工作

如果用的是全屏 preloader,页面会先渲染出一个固定定位的遮罩层:左右两块背景,加中间那个 spinner。效果很简单,但第一眼挺有存在感。

对应的核心结构如下:

1
2
3
4
5
6
7
8
9
#loading-box
.loading-left-bg
.loading-right-bg
.spinner-box
.configure-border-1
.configure-core
.configure-border-2
.configure-core
.loading-word= _p('loading')

这层样式通过固定定位把整个视口盖住,保证页面还没准备好之前,用户看到的不是半成品内容,而是一个完整的加载状态。

CSS 的关键点有三个:

  • #loading-box 以及左右背景块都是 position: fixed
  • spinner-box 覆盖在正中央,展示旋转动画
  • 当加上 .loaded 类后,左右背景会向两侧平移并退出视口,spinner 也会被隐藏

看到这里我就基本明白了:它并不是靠复杂的 JS 一帧一帧去画动画,而是切换 class,让 CSS 的 transition 和 keyframes 自己跑起来。

什么时候结束加载

真正决定动画什么时候消失的,是一段内联脚本。

它做了两件事:

1. 先进入加载状态

脚本一开始会执行 initLoading()

1
2
3
4
initLoading: () => {
$body.style.overflow = 'hidden'
$loadingBox.classList.remove('loaded')
}

这里会先把 body 的滚动锁住,避免页面还没加载完的时候,用户就已经开始乱滚导致视觉闪动。

2. 再在合适时机结束加载

随后它会根据页面状态决定什么时候移除加载层:

1
2
3
4
5
6
7
if (document.readyState === 'complete') {
preloader.endLoading()
} else {
window.addEventListener('load', preloader.endLoading)
document.addEventListener('DOMContentLoaded', preloader.endLoading)
setTimeout(preloader.endLoading, 7000)
}

这段逻辑其实挺典型,我自己看下来就是:

  • DOMContentLoaded 负责在 DOM 结构可用时尽快结束
  • load 负责等图片、样式、脚本这些资源真正加载完
  • setTimeout 是兜底,防止某些资源出问题后加载层一直挂着不消失

最终执行 endLoading() 时,会恢复滚动并给加载层加上 .loaded

1
2
3
4
5
endLoading: () => {
if ($loadingBox.classList.contains('loaded')) return
$body.style.overflow = ''
$loadingBox.classList.add('loaded')
}

loaded 类加上去之后,CSS 动画开始生效,左右背景滑出,整个 Load 动画也就结束了。这个过程很干脆,没有多余的逻辑。

为什么刷新时特别明显

“刷新页面”这个场景和“站内切页”不太一样。

刷新时浏览器会重新请求整页,主题的加载层也会从最初就出现在 HTML 里,所以用户通常会先看到一个完整的过渡界面,等页面就绪后再被移除。

如果主题启用了 PJAX,站内跳转又会多一层处理:

1
2
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
btf.addGlobalFn('pjaxComplete', preloader.endLoading, 'preloader_end')

这也意味着,不只是首次刷新会出现动画,后续的局部切页同样会沿用这一套加载逻辑。

如果想改成进度条

Butterfly 还预留了 pace.js 方案。它不会显示全屏遮罩,而是显示一条页面顶部的进度条。

对应逻辑更简单:

1
2
3
4
5
6
7
8
9
10
11
script.
window.paceOptions = {
restartOnPushState: false
}

btf.addGlobalFn('pjaxSend', () => {
Pace.restart()
}, 'pace_restart')

link(rel="stylesheet", href=url_for(theme.preloader.pace_css_url || theme.asset.pace_default_css))
script(src=url_for(theme.asset.pace_js))

如果你更偏好轻量、克制一点的视觉效果,我觉得把 preloader.source 切到 2 会更舒服一点。

配置入口

这部分配置在主题配置文件里:

1
2
3
4
5
6
preloader:
enable: false
# 1. fullpage-loading
# 2. pace (progress bar)
source: 1
pace_css_url:

如果要自己改配置,我觉得最需要关注的还是这两个点:

  • enable 控制是否启用加载动画
  • source 控制是全屏遮罩还是进度条