背景
B 站首页顶部有一条很长的动态壁纸,不是一张图,而是十几到几十层 PNG 叠加,鼠标移动时各层以不同速度偏移,产生视差效果。每次季节更替或活动时,B 站都会换一套新壁纸。
我想让博客底部也出现这条壁纸,而且要实时跟着 B 站走——B 站换了,博客也跟着换,不需要我手动去改。
整体架构
1 | ┌─────────────┐ 抓取脚本 ┌─────────────────┐ |
核心思路:不下载图片,只存清单。清单里记录每层图片的 URL 和视差系数,前端访问时直接 fetch 这份清单,图片走 B 站 CDN——零带宽成本,B 站换了图博客自动跟着变。
第一步:抓包分析 B 站壁纸是怎么渲染的
打开 B 站首页,F12 看 Elements,找到 .animated-banner 这个容器:
1 | <div class="animated-banner"> |
每层都是一个 <img>,通过 transform: translate(x, y) scale(s) 控制位置和大小。鼠标移动时,B 站用 JS 计算每层的位移量——移得越远,视差越强。
关键发现
- 图片不是一张,是 20~30 层 PNG —— 远景层(天空、远山)移动慢,近景层(人物、前景)移动快
- B 站没有公开 API —— 壁纸数据是页面加载后用 JS 动态插入 DOM 的
- CDN 域名会轮换 ——
i0.hdslb.com、i1.hdslb.com、i2.hdslb.com随机出现,同一个壁纸可能从不同节点加载
第二步:写抓取脚本,生成图层清单
因为没有 API,只能用无头浏览器打开页面,抓取 DOM 里的信息。
抓取什么
- 每层图片的 URL
- 视差系数(怎么算?见下一步)
视差系数怎么测量
B 站的视差是基于鼠标位置的:鼠标在屏幕左端时各层在一个位置,移到右端时各层偏移不同距离。
我的做法是:
- 把鼠标移到 banner 左端 → 记录各层
transform - 把鼠标移到 banner 右端 → 记录各层
transform - 比较同一层两次的
translateX差值,除以鼠标移动距离,得到位移系数
1 | // 伪代码 |
清单格式
1 | { |
fingerprint:所有图层文件名的 SHA1 摘要,用来判断是否换了壁纸(CDN 域名变化不算换)kx:水平位移系数(正值向右移,负值向左移)ks:缩放系数
第三步:前端渲染
博客用的是 Hexo + Butterfly 主题。前端需要:
- fetch 清单 —— 从
https://ariesfun.github.io/bili-banner.json拉取 - 对比指纹 —— 如果和当前不一样,说明 B 站换壁纸了,重建图层
- 叠加渲染 —— 按清单里的 URL 创建
<img>,设置referrerpolicy="no-referrer"(绕过 B 站防盗链) - 视差交互 —— 监听
mousemove,根据鼠标在 banner 内的位置,计算每层transform
关键代码片段
1 | // 1. 拉取清单 |
首屏兜底
fetch 是异步的,首屏可能空白。解决方案:构建时把同一份清单内联到 HTML 里(Hexo 的 _data/ 机制),前端先用内联数据渲染,同时后台 fetch 最新清单——如果不一样再热更新。
第四步:GitHub Actions 定时同步
现在前端能实时拉清单了,但谁来更新清单?
我的环境:
- 博客源码在本地 Gitea(不在 GitHub)
- 构建产物在 GitHub Pages(
ariesfun.github.io)
方案:让 GitHub Actions 跑在产物仓库上,定时执行抓取脚本,有变化就提交。
怎么把 workflow 塞进产物仓库
Hexo 默认构建时不复制隐藏目录(.github/),而且 include 配置对它无效(实测过)。解决:写一个 Hexo 插件,在 after_generate 钩子中手动把 .github/ 复制到 public/:
1 | // scripts/copy-github-dir.js |
同时 _config.yml 的 deploy 段加 ignore_hidden: false,确保 hexo deploy 时把 .github/ 一起推上去。
Workflow 配置
1 | name: 同步 B 站壁纸 |
首跑验证
- GitHub 仓库 → Settings → Actions → General → Workflow permissions → Read and write permissions
- Actions 页面 → 「同步 B 站壁纸」→ Run workflow 手动触发
- 等 1~2 分钟,看是否显示绿色 ✓
踩过的坑
| 现象 | 原因 | 解法 |
|---|---|---|
| 图片全部 403 | B 站 CDN 有 Referer 校验 | <img referrerpolicy="no-referrer"> |
source/.github/ 不出现在产物里 |
Hexo 跳过隐藏目录 | after_generate 钩子手动复制 |
| 每次判定「壁纸变了」 | CDN 域名轮换(i0/i1/i2) | 指纹只取文件名,不含域名 |
| 轮播图空白 | 隐藏元素不会被懒加载 | 用 opacity 切换,不用 visibility: hidden |
| Node 20 废弃警告 | GitHub Actions 平台升级 | node-version: '22' |
效果
- 文件数:页面从 222 个文件降到 194 个(不再打包 28 张图片)
- 清单体积:仅 3 KB
- 更新延迟:B 站换壁纸后最多 6 小时,博客自动同步
- 无需重建:清单更新后前端立即生效,不需要
hexo deploy
源码
完整的 workflow 和脚本在博客源码仓库的 .github/ 目录下,前端渲染代码在 source/js/footer-banner.js。
完。






