一、前言 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.yml 的 inject.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/,至少观察三个完整周期,然后检查:
首屏立即有默认图;
下一张加载完成后才开始淡入;
坏图不会造成空白;
切到后台一分钟再返回,不会连续快速跳图;
手机端背景裁切合理;
导航、标题和下滑箭头仍可点击;
开启减少动态效果后不执行缩放动画。
十三、第十一步:线上缓存排查 代码更新但线上仍像旧版本时,按顺序检查:
源码中的 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
一个可靠轮播的核心不是定时器,而是状态顺序:旧图始终作为当前可见状态,新图只有在确认可用后才能接管。再配合默认图、可见性恢复和缓存版本,摄影轮播才能长期稳定运行。