惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
宝玉的分享
宝玉的分享
I
InfoQ
P
Privacy International News Feed
V
V2EX
IT之家
IT之家
S
SegmentFault 最新的问题
D
Darknet – Hacking Tools, Hacker News & Cyber Security
V2EX - 技术
V2EX - 技术
C
CERT Recently Published Vulnerability Notes
C
Check Point Blog
The Register - Security
The Register - Security
爱范儿
爱范儿
博客园 - 三生石上(FineUI控件)
AWS News Blog
AWS News Blog
M
MIT News - Artificial intelligence
C
Cyber Attacks, Cyber Crime and Cyber Security
F
Fortinet All Blogs
B
Blog
N
Netflix TechBlog - Medium
B
Blog RSS Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Last Week in AI
Last Week in AI
T
Threatpost
Forbes - Security
Forbes - Security
U
Unit 42
A
Arctic Wolf
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Palo Alto Networks Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recorded Future
Recorded Future
L
Lohrmann on Cybersecurity
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Proofpoint News Feed
月光博客
月光博客
Spread Privacy
Spread Privacy
MongoDB | Blog
MongoDB | Blog
Jina AI
Jina AI
I
Intezer
V
Visual Studio Blog
阮一峰的网络日志
阮一峰的网络日志
The Hacker News
The Hacker News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
LangChain Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园_首页
MyScale Blog
MyScale Blog
腾讯CDC
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
量子位

半方池水半方田

小球称重问题及其引申的思考 蓝色的结构色 病毒是不是生物? 把博客发布交给 GitHub Actions 省外居民身份证的补换领(苏州) hexo-filter-titlebased-link:构建你的数字花园 Vercel 应用实践学习 通过与 Keycloak 配合实现博客文章的受限访问功能 非公开的文章 Soft Mode 非公开的文章 Strict Mode 试试用代码块在 Hexo 中插入实况照片 动态图片测试 Keycloak 的部署以及 Hexo-Butterfly 网页应用的接入 随机访问文章的实现(Sitemap+本地缓存优化) Authentik 的部署记录 Waline 自建 Auth 认证 记一次行李托运理赔流程(南航) Docker 镜像的制作、拉取与运行 Hexo-Butterfly 主题中对 Algolia 搜索框 Power By 的定制 Hexo 动态加载配置(钩子函数) 设计原则 VSCode 中的配置
Hexo-Butterfly 主题 Preloader 加载页定制
wuanqin · 2026-04-13 · via 半方池水半方田

在最初使用 Butterfly 时,为了加速页面的展示速度,减少等待焦虑,我一度拒绝使用全局加载页。但是由于近期博客集成了 站内文章Keycloak,在检查、维护登录态的环节中,不可避免要经历页面被重新刷新的环节。对于普通用户来说,这个莫名其妙的重刷新是非常影响体验的,于是我想到了重新引入全局加载页进行「遮丑」,优化体验。

下面我们通过代码重构,使 Butterfly 支持更多的全局加载页。首先打开 \themes\butterfly\layout\includes\loading\index.pug 修改为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
if theme.preloader.enable
// 可以拓展更多的 if-else
if theme.preloader.source === 9997
include ./wave-loading.pug
else if theme.preloader.source === 9998
include ./check-list-loading.pug
else if theme.preloader.source === 9999
include ./fullpage-loading.pug
else
include ./pace.pug

script.
(()=>{
const $loadingBox = document.getElementById('loading-box')
const $body = document.body
const preloader = {
endLoading: () => {
if ($loadingBox.classList.contains('loaded')) return
$body.style.overflow = ''
$loadingBox.classList.add('loaded')
},
initLoading: () => {
$body.style.overflow = 'hidden'
$loadingBox.classList.remove('loaded')
}
}

preloader.initLoading()

if (document.readyState === 'complete') {
preloader.endLoading()
} else {
window.addEventListener('load', preloader.endLoading)
document.addEventListener('DOMContentLoaded', preloader.endLoading)
// Add timeout protection: force end after 7 seconds
setTimeout(preloader.endLoading, 7000)
}

if (!{theme.pjax && theme.pjax.enable}) {
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
btf.addGlobalFn('pjaxComplete', preloader.endLoading, 'preloader_end')
}
})()

相应的,我们还需要支持样式文件的按需加载。打开 \themes\butterfly\source\css\_layout\loading.styl,将代码修改为:

我们做了什么?原本在这个文件中,主题直接将默认的全局加载页的样式放到这里。而现在我们要对其进行多配置的改造。因此我们需要将这个文件定位为一个样式加载的入口,这个入口会根据主题的配置选择性的加载样式文件。这些样式文件都放在 \themes\butterfly\source\css\_layout\loading\ 目录下。

至于全局加载页,我们可以自己做一套出来,或者参考 Codepen 现成项目进行移植。 @Akilarの糖果屋关于Loading Animation 的文章中详细的讲述了这一方法,并给出了它移植好的几套全局加载页。接下来我将分享我自己移植的两个全局加载页以及移植思路。

这样的项目 HTML 结构很简单、而且是纯 CSS 项目。直接喂 AI 就能直接生成好 pugstyl 的代码。我在这个项目中额外添加的样式以及优化的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
script.  
(function() {
const waveDuration = 2500;
let waveStartTime = sessionStorage.getItem('oceanWaveStartTime');
if (!waveStartTime) {
waveStartTime = Date.now();
sessionStorage.setItem('oceanWaveStartTime', waveStartTime);
}
const elapsed = (Date.now() - parseInt(waveStartTime)) % waveDuration;
document.documentElement.style.setProperty('--wave-offset', '-' + elapsed + 'ms');
})();

#loading-box
.loading-left-bg
.loading-right-bg
.ocean-wrapper
.ocean
.wave
svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 1600 198', width='1600', height='198')
defs
linearGradient(id='wave-gradient-1', x1='50%', x2='50%', y1='-10.959%', y2='100%')
//- 使用变量控制渐变色
stop(stop-color='var(--wave-top)', stop-opacity='.25', offset='0%')
stop(stop-color='var(--ocean-color)', offset='100%')
path(fill='url(#wave-gradient-1)', fill-rule='evenodd', d='M.005 121C311 121 409.898-.25 811 0c400 0 500 121 789 121v77H0s.005-48 .005-77z', transform='matrix(-1 0 0 1 1600 0)')
svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 1600 198', width='1600', height='198')
path(fill='url(#wave-gradient-1)', fill-rule='evenodd', d='M.005 121C311 121 409.898-.25 811 0c400 0 500 121 789 121v77H0s.005-48 .005-77z', transform='matrix(-1 0 0 1 1600 0)')

.wave
svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 1600 198', width='1600', height='198')
defs
linearGradient(id='wave-gradient-2', x1='50%', x2='50%', y1='-10.959%', y2='100%')
stop(stop-color='var(--wave-top)', stop-opacity='.25', offset='0%')
stop(stop-color='var(--ocean-color)', offset='100%')
path(fill='url(#wave-gradient-2)', fill-rule='evenodd', d='M.005 121C311 121 409.898-.25 811 0c400 0 500 121 789 121v77H0s.005-48 .005-77z', transform='matrix(-1 0 0 1 1600 0)')
svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 1600 198', width='1600', height='198')
path(fill='url(#wave-gradient-2)', fill-rule='evenodd', d='M.005 121C311 121 409.898-.25 811 0c400 0 500 121 789 121v77H0s.005-48 .005-77z', transform='matrix(-1 0 0 1 1600 0)')

注意到,原项目中它是一个 HTML+CSS+JavaScript 的项目。对于 JavaScript,你可以通过 inject 引入这个脚本。但是为了提高耦合性,我建议将 JavaScript 写入到 pug 模板中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// https://codepen.io/hoqqanen/pen/zvqGEG  
#loading-box
.loading-left-bg
.loading-right-bg
.spinner-box
//- 这里替换成了你新项目的 SVG 滚动动画
#page
#phrase_box svg(width='100%', height='100%')
defs
mask#mask(maskUnits='userSpaceOnUse', maskContentUnits='userSpaceOnUse')
linearGradient#linearGradient(gradientUnits='objectBoundingBox', x2='0', y2='1')
stop(stop-color='white', stop-opacity='0', offset='0%')
stop(stop-color='white', stop-opacity='1', offset='30%')
stop(stop-color='white', stop-opacity='1', offset='70%')
stop(stop-color='white', stop-opacity='0', offset='100%')
rect(width='100%', height='100%', fill='url(#linearGradient)')
g(width='100%', height='100%', style='mask: url(#mask);')
g#phrases

script.
var checkmarkIdPrefix = "loadingCheckSVG-";
var checkmarkCircleIdPrefix = "loadingCheckCircleSVG-";
var verticalSpacing = 50;

function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}

function createSVG(tag, properties, opt_children) {
var newElement = document.createElementNS("http://www.w3.org/2000/svg", tag);
for (prop in properties) {
newElement.setAttribute(prop, properties[prop]);
}
if (opt_children) {
opt_children.forEach(function (child) {
newElement.appendChild(child);
})
}
return newElement;
}

function createPhraseSvg(phrase, yOffset) {
var text = createSVG("text", {
fill: "white",
x: 50,
y: yOffset,
"font-size": 18,
"font-family": "Arial"
});
text.appendChild(document.createTextNode(phrase + "..."));
return text;
}
function createCheckSvg(yOffset, index) {
var check = createSVG("polygon", {
points: "21.661,7.643 13.396,19.328 9.429,15.361 7.075,17.714 13.745,24.384 24.345,9.708 ",
fill: "rgba(255,255,255,1)",
id: checkmarkIdPrefix + index
});
var circle_outline = createSVG("path", {
d: "M16,0C7.163,0,0,7.163,0,16s7.163,16,16,16s16-7.163,16-16S24.837,0,16,0z M16,30C8.28,30,2,23.72,2,16C2,8.28,8.28,2,16,2 c7.72,0,14,6.28,14,14C30,23.72,23.72,30,16,30z",
fill: "white"
})
var circle = createSVG("circle", {
id: checkmarkCircleIdPrefix + index,
fill: "rgba(255,255,255,0)",
cx: 16,
cy: 16,
r: 15
})
var group = createSVG("g", {
transform: "translate(10 " + (yOffset - 20) + ") scale(.9)"
}, [circle, check, circle_outline]);
return group;
}

function addPhrasesToDocument(phrases) {
phrases.forEach(function (phrase, index) {
var yOffset = 30 + verticalSpacing * index;
document.getElementById("phrases").appendChild(createPhraseSvg(phrase, yOffset));
document.getElementById("phrases").appendChild(createCheckSvg(yOffset, index));
});
}

function easeInOut(t) {
var period = 200;
return (Math.sin(t / period + 100) + 1) / 2;
}

document.addEventListener("DOMContentLoaded", function (event) {
var phrases = shuffleArray(["Feeding unicorns", "Grabbing tasks", "Collating conversations", "Reticulating splines", "Pondering emptiness", "Considering alternatives", "Shuffling bits", "Celebrating moments", "Generating phrases", "Simulating workflow", "Empowering humanity", "Being aspirational", "Doing the hokey pokey", "Bueller", "Cracking jokes", "Slacking off"]);
addPhrasesToDocument(phrases);
var start_time = new Date().getTime();
var upward_moving_group = document.getElementById("phrases");
upward_moving_group.currentY = 0;
var checks = phrases.map(function (_, i) {
return {
check: document.getElementById(checkmarkIdPrefix + i),
circle: document.getElementById(checkmarkCircleIdPrefix + i)
};
});

function animateLoading() {
var now = new Date().getTime();
upward_moving_group.setAttribute("transform", "translate(0 " + upward_moving_group.currentY + ")");
upward_moving_group.currentY -= 1.35 * easeInOut(now);
checks.forEach(function (check, i) {
var color_change_boundary = -i * verticalSpacing + verticalSpacing + 15;
if (upward_moving_group.currentY < color_change_boundary) {
var alpha = Math.max(Math.min(1 - (upward_moving_group.currentY - color_change_boundary + 15) / 30, 1), 0);
check.circle.setAttribute("fill", "rgba(255, 255, 255, " + alpha + ")");
var check_color = [Math.round(255 * (1 - alpha) + 120 * alpha), Math.round(255 * (1 - alpha) + 154 * alpha)];
check.check.setAttribute("fill", "rgba(255, " + check_color[0] + "," + check_color[1] + ", 1)");
}
})
if (now - start_time < 30000 && upward_moving_group.currentY > -710) {
requestAnimationFrame(animateLoading);
}
}

animateLoading();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
script.
(()=>{
const $loadingBox = document.getElementById('loading-box')
const $body = document.body
const preloader = {
endLoading: () => {
if ($loadingBox.classList.contains('loaded')) return
$body.style.overflow = ''
$loadingBox.classList.add('loaded')
},
initLoading: () => {
$body.style.overflow = 'hidden'
$loadingBox.classList.remove('loaded')
}
}

preloader.initLoading()

if (document.readyState === 'complete') {
preloader.endLoading()
} else {
window.addEventListener('load', preloader.endLoading)
document.addEventListener('DOMContentLoaded', preloader.endLoading)
// Add timeout protection: force end after 7 seconds
setTimeout(preloader.endLoading, 7000)
}

if (!{theme.pjax && theme.pjax.enable}) {
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
btf.addGlobalFn('pjaxComplete', preloader.endLoading, 'preloader_end')
}
})()

如果我们需要自己控制全局加载页的关闭时机,就需要注释掉 if-else 中的逻辑,并把 preloader.endLoading 暴露给外界进行调用。具体更改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
script.
(()=>{
const $loadingBox = document.getElementById('loading-box')
const $body = document.body
const preloader = {
endLoading: () => {
if ($loadingBox.classList.contains('loaded')) return
$body.style.overflow = ''
$loadingBox.classList.add('loaded')
},
initLoading: () => {
$body.style.overflow = 'hidden'
$loadingBox.classList.remove('loaded')
}
}

preloader.initLoading()

// 注释掉自动移除逻辑
// 我们不希望它在 window.onload 时自动消失,我们要手动控制
/*
if (document.readyState === 'complete') {
preloader.endLoading()
} else {
window.addEventListener('load', preloader.endLoading)
document.addEventListener('DOMContentLoaded', preloader.endLoading)
// Add timeout protection: force end after 7 seconds
setTimeout(preloader.endLoading, 7000)
}
*/

// 暴露到全局,让外部 JS (Keycloak) 可以调用
window.endPreloader = preloader.endLoading

if (!{theme.pjax && theme.pjax.enable}) {
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
btf.addGlobalFn('pjaxComplete', preloader.endLoading, 'preloader_end')
}
})()

@Akilar 不愧是 Butterfly 魔改老祖。那个时候生成式 AI 还没有大规模应用,他直接就手搓出一个机甲风炫酷博客,到现在四年过去,它的魔改教程依然具备极高的参考价值。我只能说非常的🐮B!

在项目移植过程中,我们自身需要理解好 Butterfly 是如何处理全局加载页,并喂给模型相应示例,才可能得到一个想要的结果。对于复杂移植过程,不同水平的 AI 能力高下立判。每次都得捏着鼻子运行低级模型生成的💩,导致提示词越写越没有耐心。令人惊讶的是,Gemini 3 Pro 生成的代码总能达到满意的水平,以至于每当限额时间解除时,我都会抱着一种毕恭毕敬的心理,像向上帝虔诚祈求那样,规整地整理我的诉求🌚。