背景
B 站首页顶部有一条很长的动态壁纸,不是一张图,而是十几到几十层 PNG 叠加,鼠标移动时各层以不同速度偏移,产生视差效果。每次季节更替或活动时,B 站都会换一套新壁纸。
上图是 B 站首页的动态壁纸,由 28 层 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 Pages(
ariesfun.github.io)
GitHub Actions 必须跑在 GitHub 仓库上,所以先把源码同步一份过去:
1 | 本地 Gitea(源码)──push──▶ GitHub(blog-source,Private) |
推送源码到 GitHub
1 | git remote add github git@github.com:ariesfun/blog-source.git |
配置 Deploy Key
Actions 需要权限把产物推到 Pages 仓库:
- 生成密钥对:
1
2ssh-keygen -t ed25519 -C "blog-source-actions" \
-f ~/.ssh/blog-source-deploy -N "" - 公钥 →
ariesfun.github.io仓库的 Settings → Deploy keys(勾选 Allow write access) - 私钥 →
blog-source仓库的 Settings → Secrets → Actions,命名为HEXO_DEPLOY_KEY
走过的弯路
一开始我把 workflow 塞进了产物仓库——因为源码不在 GitHub,只能让 CI 跟着产物走。
为此需要两个 hack。第一个是 Hexo 插件,在构建后把 .github/ 复制进产物:
1 | // scripts/copy-github-dir.js |
第二个是 _config.yml 的 deploy.ignore_hidden: false(否则隐藏目录不会被部署上去)。
能跑,但不该这么做:
| 问题 | 说明 |
|---|---|
| 产物不纯净 | 产物仓库本该能随时整体重建,却混着 CI 配置 |
| 排查困难 | workflow 消失时,得先想到「是不是插件没跑」 |
| 脆弱 | 插件没装、配置丢失都会静默失效 |
结论:让每个仓库只做该做的事——产物仓库只管输出,CI 配置待在源码仓库。
改成源码同步后,上面两个 hack 全部删除。
Workflow 配置
.github/workflows/deploy.yml(放在源码仓库里):
1 | name: 构建并部署博客 |
首跑验证
- Actions 页面 → 「构建并部署博客」→ Run workflow 手动触发
- 等 2~3 分钟,确认显示绿色 ✓
- 打开博客确认底部壁纸正常显示
踩过的坑
| 现象 | 原因 | 解法 |
|---|---|---|
| 图片全部 403 | B 站 CDN 有 Referer 校验 | <img referrerpolicy="no-referrer"> |
| 壁纸完全不显示 | 主题构建时把 src 换成 1×1 占位图 |
JS 里从 data-lazy-src 恢复 src |
| 鼠标移动时闪烁 | 视差在图片加载完成前就启用 | 等所有图片 load 后再绑定视差 |
| 每次判定「壁纸变了」 | CDN 域名轮换(i0/i1/i2) | 指纹只取文件名,不含域名 |
npm ci 报 lock 不同步 |
package-lock.json 未更新 |
本地 npm install 后提交 lock |
| Actions 装依赖失败 | 新版 Ubuntu 的 libasound2 改名 |
用 puppeteer browsers install chrome --install-deps |
效果
- 文件数:页面从 222 个文件降到 194 个(不再打包 28 张图片)
- 清单体积:仅 3 KB
- 更新延迟:B 站换壁纸后最多 6 小时,博客自动同步
- 无需重建:清单更新后前端立即生效,不需要
hexo deploy
参考
- palxiao/bilibili-banner — 前端视差交互 + 回正动画的参考实现
- B 站首页 — 动态壁纸设计团队
完。








