一、前言

Butterfly 的 index_img 适合一张首页背景,但摄影博客往往希望轮换多张作品。简单定时修改 background-image 会出现蓝色空档、坏图卡死、切回后台标签后突然连跳,以及线上缓存仍执行旧代码等问题。本篇从保留默认图开始,逐步实现预加载、双图层交叉淡入和缓存版本管理。

二、交互演示:一张背景怎样变成稳定轮播

Hexo 博客根目录
├─ _config.butterfly.yml # 默认图、轮播清单和脚本版本
├─ scripts/home-banner-config.js # 构建期把配置交给首页
└─ source/js/home-banner-carousel.js # 预加载和交叉淡入

首页摄影轮播流程

三、第一步:保留 index_img 作为兜底

打开 _config.butterfly.yml

index_img: /images/home/default.jpg

不要把默认图删掉。轮播 JavaScript 未加载、配置为空或所有图片失败时,它仍是可见背景。

四、第二步:增加轮播配置

在同一配置文件加入:

home_banner:
interval: 8000
images:
- /images/home/01.jpg
- /images/home/02.jpg
- /images/home/03.jpg

mask:
header: rgba(0, 30, 55, 0.22)

建议切换间隔不少于 5 秒,过快会分散阅读注意力,也增加图片请求频率。

新增图片时先保留上一张已经验证可用的图,确认新图正常加载后再删除旧图。

五、第三步:构建时把配置注入首页

浏览器无法直接读取 YAML。新建 scripts/home-banner-config.js

'use strict'

const safeJson = value => JSON.stringify(value)
.replaceAll('<', '\\u003c')
.replaceAll('>', '\\u003e')
.replaceAll('&', '\\u0026')

const normalizeRoot = root => {
const value = String(root || '/')
return value.endsWith('/') ? value : `${value}/`
}

const urlFor = value => {
if (/^(?:https?:)?\/\//i.test(value)) return value
return `${normalizeRoot(hexo.config.root)}${String(value).replace(/^\/+/, '')}`
}

只向首页写入数据:

hexo.extend.filter.register('after_render:html', (html, locals) => {
const page = locals?.page
if (!page || page.__index !== true) return html

const banner = hexo.theme.config.home_banner || {}
const images = Array.isArray(banner.images)
? banner.images.map(urlFor).filter(Boolean)
: []

const payload = {
images,
interval: Math.max(3000, Number(banner.interval) || 8000)
}

const config = `<script>window.__HOME_BANNER__=${safeJson(payload)}</script>`
return html.replace('</head>', `${config}</head>`)
}, 20)

不要把轮播配置注入每一篇文章。

六、第四步:建立当前图和下一张图

新建 source/js/home-banner-carousel.js

(() => {
const initHomeBanner = () => {
const config = window.__HOME_BANNER__
const header = document.querySelector('#page-header.full_page')

if (!header || !Array.isArray(config?.images) ||
config.images.length < 2 || header.dataset.carouselBound === 'true') {
return
}

header.dataset.carouselBound = 'true'

const currentSlide = document.createElement('div')
const nextSlide = document.createElement('div')
currentSlide.className = 'home-banner-slide is-current is-visible'
nextSlide.className = 'home-banner-slide is-next'
header.prepend(currentSlide, nextSlide)
}
})()

为什么不用一个元素直接换背景?因为浏览器需要下载并解码新图。单层在这个间隙可能露出 Butterfly 原背景色;双层可以让旧图持续可见。

七、第五步:为图层加入样式

样式可以放进 source/css/custom.css

#page-header.full_page {
isolation: isolate;
}

.home-banner-slide {
position: absolute;
z-index: -3;
inset: 0;
background-position: center;
background-size: cover;
opacity: 0;
transform: scale(1.015);
transition: opacity 1.1s ease, transform 7s ease;
}

.home-banner-slide.is-visible {
opacity: 1;
transform: scale(1);
}

@media (prefers-reduced-motion: reduce) {
.home-banner-slide { transition: none; }
}

确保标题、导航和向下箭头的层级高于轮播图层。

八、第六步:预加载下一张图片

const preload = url => new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => resolve(image.currentSrc || url)
image.onerror = reject
image.src = url
})

切换逻辑:

const showNext = async () => {
index = (index + 1) % images.length

try {
const url = await preload(images[index])
nextSlide.style.backgroundImage = `url("${url}")`

void nextSlide.offsetWidth
nextSlide.classList.add('is-visible')

window.setTimeout(() => {
currentSlide.classList.remove('is-visible')
swapSlides()
scheduleNext()
}, 1100)
} catch {
scheduleNext()
}
}

void nextSlide.offsetWidth 强制浏览器提交新背景的初始状态,下一行添加类名时才会触发淡入过渡。

九、第七步:先让新图可见,再退休旧图

错误顺序是:

隐藏旧图 → 等待新图 → 显示新图

正确顺序是:

预加载新图 → 放进后层 → 淡入新图 → 隐藏旧图 → 交换图层职责

这条顺序是消除“先变蓝再显示下一张”的关键。

十、第八步:处理后台标签页

浏览器把页面放到后台后会限制计时器。监听可见性:

document.addEventListener('visibilitychange', () => {
if (document.hidden) {
window.clearTimeout(timer)
timer = undefined
return
}

scheduleNext()
})

恢复页面时重新计时,不要补执行后台积压的全部切换。

十一、第九步:把脚本加入 Butterfly

_config.butterfly.ymlinject.bottom 中加入:

inject:
bottom:
- <script src="/js/home-banner-carousel.js?v=20260808-1" defer></script>

查询版本非常重要。若线上对 JS 设置了长期缓存,每次修改运行时代码后都应更新版本号。

十二、第十步:构建和本地验证

npm run clean
npm run build
npm run verify
hexo server

打开 http://localhost:4000/,至少观察三个完整周期,然后检查:

  1. 首屏立即有默认图;
  2. 下一张加载完成后才开始淡入;
  3. 坏图不会造成空白;
  4. 切到后台一分钟再返回,不会连续快速跳图;
  5. 手机端背景裁切合理;
  6. 导航、标题和下滑箭头仍可点击;
  7. 开启减少动态效果后不执行缩放动画。

十三、第十一步:线上缓存排查

代码更新但线上仍像旧版本时,按顺序检查:

源码中的 JS 是否更新

Actions 是否使用最新提交构建

服务器上的 home-banner-carousel.js 是否更新

HTML 是否引用新的 ?v=版本

CDN / 浏览器是否仍命中旧缓存

不要仅凭“直接打开 JS 地址看起来是新代码”就结束排查,还要确认首页 HTML 实际引用的 URL。

十四、反盗链导致的假 404

图床可能要求首页 Referer。直接在地址栏打开图片时返回 404,不代表首页请求也失败。可以使用带 Referer 的请求检查,或者在浏览器网络面板观察首页真实请求。

安全策略应该只放行博客域名,不要为了轮播关闭全部反盗链保护。

十五、常见问题

1. 轮播出现蓝色空档

旧图隐藏过早。必须确认新图已经 onload,并完成淡入后再移除旧图。

2. 第一张图闪一下后消失

检查图层 z-index,避免 .post-bg 或自定义背景层被设置到页面背景之后。

3. 修改间隔没有生效

确认构建期注入的 window.__HOME_BANNER__ 已更新,并执行 hexo clean

4. 线上仍执行旧脚本

更新 inject.bottom 中的版本号,并核对 CDN 缓存头。

十六、维护入口与结语

  • 图片、间隔、遮罩和版本:_config.butterfly.yml
  • 构建期传参:scripts/home-banner-config.js
  • 预加载和切换:source/js/home-banner-carousel.js
  • 图层样式:source/css/custom.css

一个可靠轮播的核心不是定时器,而是状态顺序:旧图始终作为当前可见状态,新图只有在确认可用后才能接管。再配合默认图、可见性恢复和缓存版本,摄影轮播才能长期稳定运行。