嗨害嗨!这里是又在不务正业的前端菜鸡茶无味de一天。关注我的朋友可能看到这个标题已经审美疲劳啦,好吧,我也是真没想到 复刻 B 站首页 Banner 这个内容我能写三篇文章……为什么会有这个番外篇呢,原因是上次我把复刻 Banner 加上了自动化并发布到 Github 上(详情查看:三分钟复刻 B 站首页动态 Banner),然后最近 B 站的 Banner 又更新啦,由于复刻最新效果已无需再写任何前端代码,于是我想让一位 Java 后端选手来试试,最终他调试半天自信提交一个 PR,结果却实在是差强人意。

其实这次 B 站更新的效果并不复杂,但由于需要调试的图层数量也不少,且重叠度较高,不得不承认一个事实是:手动调试大约的确是真的很考验眼力

Description

我也实在不是闲的,是真的忍不住啊!又抽时间给这个项目加了一个功能,通过 puppeteer 模拟鼠标滑动来自动计算每个图层的速度变化率,从而实现精确复刻!例如下图鼠标滑动一段相同距离后的对比图,可以看到所有细节都是 1:1 完美对应:

ps. 以上配图使用 迅排设计 在线制作生成,这是我独立开发的一个功能强大的在线海报图片设计器,目前还在持续迭代中,项目开源地址:https://github.com/palxiao/poster-design ,欢迎体验及 Star 项目~

关键技术讲解

完整的代码可以查看 Github 仓库和上期文章,这里只讲本次实现的几个关键节点。

首先将鼠标放置在 banner 上方,然后在 (0, 50) 的位置上开始模拟滑动,滑动距离我们随便定义个 1000 像素:

1
2
3
4
5
let element = await page.$(".animated-banner");
let { x, y } = await element.boundingBox();
await page.mouse.move(x + 0, y + 50);
await page.mouse.move(x + 1000, y, { steps: 1 });
await sleep(1200);

其中 mouse.move 的第三个参数为 options,接收以下选项:

  • steps:指定鼠标移动的步数,默认为 1。可以将其设置为一个大于 1 的整数,以使鼠标移动更平滑。
  • delay:指定每个步骤之间的延迟时间(以毫秒为单位),默认为 0。可以将其设置为一个正整数,以使鼠标移动更慢,或者设置为 nullundefined 来禁用延迟。

接着在偏移后计算每个图层的相对位置,再次获取每个图层的数据,正则匹配出最新的偏移位置(即 translateX ),减去原来的数值再除以上面滑动的距离 1000,得出速度变化率,重新复写到 data 中,其它参数变化率理论上都可以如法炮制,这里只是懒得写了,就只计算一个速率先~

1
2
3
4
5
6
7
8
9
layerElements = await page.$$(".animated-banner .layer"); // 重新获取
for (let i = 0; i < layerElements.length; i++) {
const skew = await page.evaluate(async (el) => {
const pattern = /translate\(([-.\d]+px), ([-.\d]+px)\)/;
const matches = el.firstElementChild.style.transform.match(pattern);
return matches.slice(1).map((x) => +x.replace("px", ""))[0];
}, layerElements[i]);
data[i].a = (skew - data[i].transform[4]) / 1000;
}

这里主要就是运用了 page.evaluate,它接收一个函数作为参数,该函数会被传递到浏览器的页面上下文中执行,这就相当于我们在页面中打开控制台进行操作,然后将结果 return 回来就能在 node 中得到我们所需的值了。

最后就是写入文件,原本是需要手动编写 config.js 内容来增加引用,这里我顺便也给自动化了,方式比较简单,我事先在文件中两处位置写上一段注释作为“埋点”,然后就是 node 读取文件,遍历每一行,匹配到对应的部分就加入预设的内容,最后输出为新的文件,代码就比较简单,没有做去重之类的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 读取文件
let codes = fs.readFileSync("config.js", "utf8");
// 模板 - 插入JSON配置
const newConfig = `{
name: "${process.argv[2]}",
data: await banner_${date.replaceAll("-", "")}.json()
},`;
// 模板 - 插入文件引用
const newImport = `const banner_${date.replaceAll("-", "")} = await fetch('./assets/${date}/data.json?r='+Math.random())`;
// 循环拆解每一行
const codeCollector = [];
for (const iterator of codes.split("\n")) {
codeCollector.push(iterator);
if (iterator.indexOf("-- ADD NEW --") !== -1) {
codeCollector.push(newConfig);
} else if (iterator.indexOf("-- IMPORT --") !== -1) {
codeCollector.push(newImport);
}
}
// 写入新文件
fs.writeFileSync("config.js", codeCollector.join("\n"));

终端中执行一个 Node.js 脚本时,可以通过命令行带入一些参数作为输入,这些参数可以在 process.argv 这个数组中取得。

最终的效果就是敲入一行命令回车就完成对 B 站首页 Banner 的完美复刻了~ 下面看看演示。

项目演示

首先拉取仓库到本地:

1
git clone https://github.com/palxiao/bilibili-banner.git

运行 pnpm iyarn / npm i 安装项目环境依赖;

例如,在 10 月 25 号这天发现 B 站更新了 Banner,可以运行:

npm run grab "打工松鼠 - 猫头鹰"(需要给 Banner 起个名字)

等待一会脚本执行完成后,运行 npm run serve,访问 http://127.0.0.1:8080 即可看到最新的效果。

项目 Github 地址https://github.com/palxiao/bilibili-banner

在线查看效果演示https://palxiao.github.io/bilibili-banner/

  • 只推荐使用 Chrome 浏览器访问。
  • 在线 Demo 托管在 GithubPages,国内线路访问可能会抽风,必要时请 ke xue 上网。
  • 部分小伙伴克隆项目可能速度不佳,可以通过修改本地 host 跳过 DNS 解析加速访问,推荐使用 SwitchHosts 管理,附远程 host 链接:https://gitlab.com/ineo6/hosts/-/raw/master/next-hosts

之后每次 B 站更新,基本就可以做到一键自动复刻了,如果交互效果复杂一些,无非还是旋转平移缩小放大,只需要手动再微调一下即可。

Description

结束

之前写的内容比较干货,这篇只作为一个补充,也希望能对你有所帮助或启发!我是茶无味de一天(公众号: 品味前端),一名平凡的前端 Developer,希望与你共同成长~

(技术原理篇)原生 JS 复刻 Bilibili 首页头图的视差交互效果