摘要生成中...
AI 摘要
Hunyuan-lite
本文包含对 Hexo-Butterfly 主题源代码进行修改的内容。

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

本文的主要内容:

  • 介绍 Hexo-Butterfly 主题全局加载页的逻辑
  • 魔改 Hexo-Butterfly 主题源代码,使其更容易加入更多全局加载配置
  • 介绍我移植成功的两个 Codepen 全局加载页项目。更多项目可参考 @Akilarの糖果屋关于Loading Animation 的文章。

check-list-loading-26041265468435146541.gif

waveLoading26041265sa1f3asd5f16.gif

Hexo-Butterfly 主题默认的全局加载页

Butterfly 主题原生自带加载页的功能,我们只需要在主题配置 _config.butterfly.yml 中开启即可:

1
2
3
4
5
6
7
8
9
# 加载动画 Loading Animation
preloader:
enable: true
# source
# 1. fullpage-loading
# 2. pace (progress bar)
source: 1
# pace theme (see https://codebyzach.github.io/pace/)
pace_css_url:

preloader 开启,并且 source 选择为 1 时,主题会使用默认的全局加载页面,展示结束后会从两边摊开形成双开门效果。

Full-page-loading-260412111efvwef1we1.gif

让 Hexo-Butterfly 支持更多的全局加载页

模板侧的修改(Pug)

下面我们通过代码重构,使 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')
}
})()

主要修改点:

  • 前半部分 if-else 逻辑修改,这样我们就可以通过主题配置文件中的 source 字段,选择我们要使用的全局加载页。
  • 后半部分是控制全局加载页的启闭代码。原本是放在主题默认的 themes\butterfly\layout\includes\loading\fullpage-loading.pug 中。把这段逻辑抽出来放在这是为了更好地统一控制全局加载页的启闭。
如果想自己控制全局加载页的启闭,可看文末拓展章节。

样式侧的修改(Stylus)

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

1
2
3
4
5
6
7
// 可以拓展更多的 if-else
if hexo-config('preloader.enable') && hexo-config('preloader.source') == 9997
@import './loading/wave-loading'
else if hexo-config('preloader.enable') && hexo-config('preloader.source') == 9998
@import './loading/check-list-loading'
else if hexo-config('preloader.enable') && hexo-config('preloader.source') == 9999
@import './loading/fullpage-loading'

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

原先主题自带的全局加载页样式,我们可以另外新建文件 ./loading/fullpage-loading 放到里面,作为一个备选项。

新增全局加载页

如果我们需要新增全局加载页并应用它,我们需要做的是:

  • 准备好全局加载页的 pug 模板和 styl 样式文件
  • 分别将它们放到正确的位置,位置取决你的代码中怎么写。如果你按照上一章节已经改好了模板和样式的入口文件,那么:
    • pug 模板放在 \themes\butterfly\layout\includes\loading\
    • styl 样式放在 \themes\butterfly\source\css\_layout\loading\
  • 修改以下入口文件的 if-else 逻辑,自己分配匹配的 source 号:
    • \themes\butterfly\layout\includes\loading\index.pug
    • \themes\butterfly\source\css\_layout\loading.styl
  • 主题配置文件 _config.butterfly.yml 中开启 Preloader 并选择相应的 source 即可。

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

波浪加载页

移植自 Codepen 项目:CSS & SVG Waves Animation

img

waveloading-dark260412sfd321asdf13s_compressed.gif

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

  • 夜间模式的支持;
  • 移除掉原项目中引用网络 SVG 图片,直接将 SVG 图片代码嵌入 PUG 模板中;
  • 动画性能优化;
  • 全局加载页的消失效果:波浪从下面开始消失,背景清晰度变化;
  • 如遇页面重新刷新,波浪不会从头来,保证波浪平滑;
  • 其他样式优化(调快动画速度)。

对于你想增加的效果,你可以叫 AI 帮你微调即可。

AI 调整项目时,请注意进行版本管理并审查变动的代码。

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)')
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
// ======================================================
// 1. 日夜间模式变量 (一键调色盘)
// ======================================================
:root
--loading-bg: rgba(255, 255, 255, 0.2)
--ocean-color: #177ecd
--wave-top: #57BBC1
--ocean-theme-filter: none
--wave-offset: 0s // 新增:用于接收 JS 传递的动画时间偏移量

[data-theme='dark']
--loading-bg: rgba(0, 0, 0, 0.4)
--ocean-color: #061a29
--wave-top: #0a3d62
--ocean-theme-filter: brightness(0.8) saturate(0.8)

// ======================================================
// 2. 结构样式 (性能优化版)
// ======================================================
#loading-box
position: fixed
top: 0
left: 0
width: 100vw
height: 100vh
z-index: 99999
display: flex
justify-content: center
align-items: center
pointer-events: auto
visibility: visible
background: var(--loading-bg)
backdrop-filter: blur(15px)
-webkit-backdrop-filter: blur(15px)
transition: background 0.5s

.ocean-wrapper
position: absolute
bottom: 0
left: 0
width: 100%
height: 100%
overflow: hidden
filter: var(--ocean-theme-filter)
transition: filter 0.5s

.ocean
height: 160px
width: 100%
position: absolute
bottom: 0
left: 0
background: var(--ocean-color) // 自动同步主体色
transform: translate3d(0, 0, 0)
transition: background 0.5s

.wave
position: absolute
top: -198px
width: 3200px
height: 198px
display: flex
will-change: transform
// 修改:将默认的 0s 延迟替换为 var(--wave-offset)
animation: wave 2.5s cubic-bezier(0.36, 0.45, 0.63, 0.53) var(--wave-offset) infinite
transform: translate3d(0, 0, 0)
opacity: 1

svg
width: 1600px
height: 198px
flex-shrink: 0
display: block

&:nth-of-type(2)
top: -175px
// 修改:结合波浪自身的错开时间 (-0.125s) 和全局偏移量
animation: wave 2.5s cubic-bezier(0.36, 0.45, 0.63, 0.53) calc(var(--wave-offset) - 0.125s) infinite
opacity: 1
svg
// 修改:结合上下起伏的错开时间 (-1.25s) 和全局偏移量
animation: swell 2.5s ease calc(var(--wave-offset) - 1.25s) infinite

// ======================================================
// 3. 退出动画
// ======================================================
&.loaded
pointer-events: none
animation: container-exit 0.8s cubic-bezier(0.4, 0, 0.2, 1) forwards
.ocean
animation: ocean-exit-playful 0.8s cubic-bezier(0.4, 0, 0.2, 1) forwards

// ======================================================
// 4. 关键帧动画 (GPU 加速版)
// ======================================================
@keyframes container-exit
0%
backdrop-filter: blur(15px)
background: var(--loading-bg)
visibility: visible
100%
backdrop-filter: blur(0px)
background: rgba(255, 255, 255, 0)
visibility: hidden
display: none

@keyframes ocean-exit-playful
0%
transform: translate3d(0, 0, 0)
100%
transform: translate3d(0, 250%, 0)

@keyframes wave
0%
transform: translate3d(0, 0, 0)
100%
transform: translate3d(-1600px, 0, 0)

@keyframes swell
0%, 100%
transform: translate3d(0, -25px, 0)
50%
transform: translate3d(0, 5px, 0)

我选择波浪全局加载页和向下消失的逻辑,算是呼应上 站内文章海浪页脚 了…

Check List 加载页

移植自 Codepen 项目:Corange Loading Screen

check-list-loading-26041265468435146541.gif

对于这一个项目,我也没有做太多修改,主要为:

  • 移除原项目中无关内容
  • 保留双开门效果

注意到,原项目中它是一个 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
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
gold = #FFB900  
coral = #FC636B
brightCoral = #FF6D92

#loading-box
.loading-left-bg,
.loading-right-bg
position fixed
z-index 1000
width 50%
height 100%
background linear-gradient(to top right, mix(coral, white, 90%) 10%, mix(brightCoral, white, 90%) 65%, mix(gold, white, 90%) 125%)
.loading-right-bg
right 0

.spinner-box
position fixed
z-index 1001
display flex
justify-content center
align-items center
width 100%
height 100vh

#page
opacity = 90%
position absolute
left 0
top 0
width 100%
height 100%
display flex
justify-content center
align-items center
background linear-gradient(to top right, mix(coral, white, opacity) 10%, mix(brightCoral, white, opacity) 65%, mix(gold, white, opacity) 125%)
transition opacity 1s
z-index -1

#phrase_box
display flex
flex-flow column
height 150px
overflow hidden
width 260px
z-index 10

#phrases
animation slide-phrases-upward 20s

for $i in 0..20
#loadingCheckCircleSVG-{$i}
animation fill-to-white 5000ms
animation-delay ($i - 1.5) * 1s
fill white
opacity 0

for $i in 0..20
#loadingCheckSVG-{$i}
animation fill-to-coral 5000ms
animation-delay ($i - 1.5) * 1s

&.loaded
.loading-left-bg
transition all 0.5s
transform translate(-100%, 0)
.loading-right-bg
transition all 0.5s
transform translate(100%, 0)
.spinner-box
display none

// keyframes 动画名不会污染
@keyframes slide-phrases-upward
for $i in 0..20
{$i * 5%}
transform translateY($i * -50px)

@keyframes fill-to-white
0%
opacity 0
10%
opacity 1
100%
opacity 1

@keyframes fill-to-coral
0%
fill white
10%
fill brightCoral
100%
fill brightCoral

拓展

自主控制全局加载页的关闭时机

下面这段代码是主题默认的控制全局加载页的逻辑:

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')
}
})()

这段代码的作用:

  • 定义加载控制对象 preloader,设置好启闭函数
  • preloader.initLoading() 决定了一进页面就开始加载
  • 后面的 if-else 函数控制关闭加载页面的逻辑:
    • 如果页面已经加载完成,直接关闭
    • 所有资源(图片、视频、样式)都加载完、DOM 结构渲染完,直接关闭
    • 兜底逻辑:7 秒后强制关闭。防止网络异常导致 loading 永远不消失。
  • 最后一个 if:PJAX 无刷新跳转支持

如果我们需要自己控制全局加载页的关闭时机,就需要注释掉 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')
}
})()

这样,外部的自定义 JavaScript 脚本可以通过以下方式执行全局加载页的关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 执行加载页的关闭
function finish() {
if (isLoaderEnded) return;
if (typeof window.endPreloader === 'function') {
window.endPreloader();
isLoaderEnded = true;
logger.info(" Loading 界面已关闭");
}
}

// 兜底逻辑

document.addEventListener('DOMContentLoaded', async () => {
setTimeout(() => {
if (!isLoaderEnded) logger.warn("超时,强制 Finish ");
finish();
}, CACHE_TIMES.MILLISECONDS_OF_LOADING_TIMEOUT);

// 你的其他逻辑
}

后记

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

image.png

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

这篇文章不是我第一个 AI 重塑博客的项目,还有更多的博客日程近期将陆续上线~

本文参考

工具:免费在线 GIF 动图压缩工具 - docsmall