为什么要给域名申请SSL证书

只有给域名申请了SSL证书,才能以https协议访问域名,才能确保客户端和服务端的会话是加密的,是安全的。

https

http是不安全的,在客户端与服务端使用http通信的时候,请求响应报文都是明文的,消息被截获后通信内容就泄漏了。

https在http的基础上,使用TLS/SSL加密,从而确保通信是安全的。

使用https协议访问某个域名/网站,除了基本的DNS解析,TCP三次握手建立连接,还要经过SSL握手,然后才能开始加密通信

TLS/SSL

TLS/SSL加密过程中既使用了对称加密,也使用了非对称加密。使用非对称加密是为了得到一个会话密钥,这个会话密钥并没有进行传输,而是双方通过计算得出来的,握手成功后,再使用这个会话密钥进行对称加密。

对称加密

使用同一把密钥进行加密和解密。发送方和接收方必须共享相同的密钥。优点是解密速度快,适合大量数据的加密,缺点是密钥必须共享,这个过程密钥可能被窃取。

非对称加密

使用一对密钥:公钥(公开)和私钥(保密)。公钥用于加密,私钥用于解密。优点是只传输公钥,私钥不进行传输,泄漏风险很小,很安全,缺点是解密速度慢

SSL证书

SSL证书其实就是保存在源服务器的数据文件,想要证书生效必须向CA申请,表明域名是属于谁的(可以理解为域名必须实名认证,有人需要为这个域名负责),还包含了公钥和私钥

TLS/SSL握手过程

在握手的过程中,服务端会把自己的SSL证书公钥发送给客户端验证,浏览器会通过查询浏览器的证书信任列表来判断这个证书是否有效,证书无效则浏览器显示这个连接不安全,有效则继续进行后续操作,经过非对称加密得到一个会话密钥,握手结束。

在握手过程中第一随机数,第二随机数,和公钥都是明文传输的,就意味着有暴露的风险,但是第三随机数的传输是经过公钥加密的,只能用私钥解密,也就是只有服务端知道第三随机数是什么,这就是一次非对称加密,然后再用那3个随机数计算得到会话密钥,会话密钥没有进行传输所以是安全的,握手结束后,后续的通信都用这个会话密钥加密。

详细解释可参考:HTTPS是什么?加密原理和证书。SSL/TLS握手过程_哔哩哔哩_bilibili

为什么接入cdn加速后还要申请SSL证书

在我们不接入cdn加速服务之前,我们无论是访问用户名.github.io还是自定义域名,最终都是从GitHub pages服务器拿到数据,SSL握手的对象也是githubpages服务器, github为我们免费生成的SSL证书是存储在githubpages服务器的;但是我们接入cdn服务后,我们就不是从源服务器(githubpages服务器)取数据了,而是cdn服务器,握手的对象就是cdn服务器,所以我们还需要手动为我们的自定义域名(或者默认域名)申请一次SSL证书,存储在CDN服务器中,后续我们才能使用https协议访问cdn服务器。

gulp和webpack的区别

Gulp是基于nodejs流的前端构建工具,可以实现文件的转换,压缩,合并,监听,自动部署等功能。gulp拥有强大的插件库,基本上满足开发需求,而且开发人员也可以根据自己的需求开发自定义插件。难得是,gulp只有五个api,容易上手。配置文件是gulpfile.js

1
2
3
4
5
6
7
8
const gulp = require('gulp');
const sass = require("gulp-sass")

gulp.task("sassStyle",function() {
gulp.src("style/*.scss")//入口文件
.pipe(sass())
.pipe(gulp.dest("style"))//目标文件
})

上面就是一个基本的gulpfile配置文件,实现了scss文件到css文件的转换;在终端输入gulp sassStyle(sassStyle是任务名)就能够进行文件处理了。
对于gulp而言,会有一个task,这个task只会做一件事,比如将sass格式的文档转换成css文件;对于一个task而言,会有一个入口文件,即gulp.src,最会有一个目标文件,即gulp.dest;一入一出,可以将gulp理解为 一元函数,输入一个x,根据funcion产出一个y

webpack的主要作用是解析模块之间的依赖关系,并把他们有条理的打包起来,顺便把模块化的代码转化成浏览器可以识别的代码。webpack从入口文件开始,递归找出所有依赖的模块,并使用配置的loader解析模块,使之变为可用的模块,而在webpack会在各个特定的时期广播对应事件,插件监听这些事件,在某个事件中进行特定的操作。通俗一点来说,webpack本身来递归找到各个文件之间的依赖关系,在这个过程中,使用loaders对文件进行解析,最后,在各个不同的事件阶段,插件可以对文件进行一个统一的处理。

虽然Webpack与gulp都是前端工程化的管理工具,但是二者的侧重点不同——gulp更加关注的是自动化的构建工具,你把代码写好了,gulp会帮你编译、压缩、解析。而Webpack关注的是在模块化背景下的打包工作;它侧重的还是如何将依赖的文件合理的组织起来,并且实现按需加载。

详细参考文章:Javascript五十问——从源头细说Webpack与Gulp - Javascript 五十问 - SegmentFault 思否

把博客部署到vercel上为什么加载的更快

免费提供的CDN服务

Vercel 利用其全球分布的 CDN 边缘节点来缓存和分发静态内容。这意味着用户可以从离他们最近的服务器获取内容,减少了数据传输的延迟。虽然github也提供免费的CDN服务,但是 CDN 覆盖范围不如 Vercel 广泛。

自动文件压缩

Vercel 会自动对你的代码进行压缩和优化,减少文件大小,加快加载时间。例如,它会对 JavaScript 和 CSS 文件进行压缩,移除不必要的空格和注释。

深入研究B站banner

下面所有内容都参考自:如何用原生 JS 复刻 Bilibili 首页头图的视差交互效果最近发现 B 站首页头图的交互效果非常有趣,本文将通过 - 掘金

准备工作

打开浏览器控制台,查看B站Banner的 HTML 结构

不难看出,我们接下来的思路就是,把 banner 中所有的图片(或者video)用一个 div.layer 包住,将所有layer堆叠起来,然后编写鼠标事件,对每张图片(或者video)应用相应的变换(transform)操作,由于接下来的操作我们都用 JS 来完成,所以布局很简单,只需要一个 div 来充当容器,就相当于b站中的animated-banner

1
<div id="app">loading...</div>

把图片素材通过 JS 添加进容器中,我们创建一个数组来描述这些图片,数据的结构暂时如下所示:

1
2
3
4
5
6
7
8
const bannerImagesData = [
{
url: 'https://xxxx/abcdegfsa.webp',
},
{
url: 'https://xxxx/dsaasdsaasdds.webp',
}, ...........
]

然后我们把 bannerImagesData 循环并添加到容器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const app = document.getElementById('app')
let layers = []

function initItems() {
//这里先给容器设置 display: 'none' 的作用是,无论添加多少图片都只会回流重绘两次。
//设置display: 'none'时回流重绘一次,切换回display = 'block'再触发一次
//这个操作也被叫做"离线操作"
body.style.display = 'none'
for (let i = 0; i < bannerImagesData.length; i++) {
const layerChildConfig = bannerImagesData[i]
// 创建 layer
const layer = document.createElement('div')
// 添加类名
layer.classList.add('layer')
// 创建img
const img = document.createElement('img')
img.src = item.url
// 将img添加到layer中
layer.appendChild(img)
// 将layer添加到容器中
app.appendChild(layer)
}
body.style.display = 'block'
// 把创建好的 layers 缓存起来,方便后续操作
layers = document.querySelectorAll(".layer")
}
initItems()

对应的样式规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* index.css */
#app {
position: relative;
overflow: hidden;
margin: 0 auto;
min-width: 1000px;
min-height: 155px;
max-height: 240px;
height: 10vw;
}
/* 所有layer都开启了绝对定位,且完全覆盖了父元素#app */
.layer {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}

img {
user-select: none;
/*让鼠标事件穿透到父元素*/
pointer-events: none;
}

准备工作就完成了,你会看到如下的界面,所有元素我们都添加了进来,现在层次已经有了,图片位置还很乱,需要给它们设置上初始偏移值来调整位置,但先不急,让我们先看看贯穿整个交互方式的鼠标事件。

鼠标事件 & 执行动画

我们这里主要会用到三个鼠标事件,分别是 mouseentermousemovemouseleave,分别代表鼠标的进入事件移动事件以及离开事件,我们将在容器上绑定这三个事件监听,在鼠标进入时记录初始化位置,鼠标移动时减去初始值就得到偏移值,这个偏移值将是接下来所有变换的核心系数,这里我们取 clientXPageX 来计算偏移量(页面不包含滚动条的情况下,二者是等效的),相关代码如下:

1
2
3
4
5
6
7
8
const app = document.getElementById('app')
let initX = 0 // 初始值
let moveX = 0 // 偏移值

app.addEventListener('mouseenter', (e) => initX = e.clientX)
app.addEventListener('mousemove', (e) => {
moveX = e.clientX - initX
})

获取到偏移值后,我想你已经迫不及待地想要让画面跟随鼠标动起来了,我们先来尝试一下吧,CSS 变换属性为 transform,它可以接收多个值,其中 translate() 可以让元素发生偏移,从而改变显示位置,接下来我们即是要将偏移值应用到其中,我们定义一个 animate 方法用于执行动画,该方法中循环取出所有元素并应用变换:

1
2
3
4
5
6
7
// 动画执行
function animate() {
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
layer.style.transform = `translate(${moveX}px, 0)`
}
}

接着在前面的 mousemove 回调事件中加入 animate(),此时画面里移动鼠标,所有图片应该都会紧紧跟随鼠标的位置而变化了,但在浏览器中,我们通常不会这么执行动画,而是采用 requestAnimationFrame 来辅助执行,它会通过浏览器的刷新频率来调度动画帧,自动以最佳的性能进行渲染,修改代码如下:

1
2
3
4
5
6
7
8
body.addEventListener('mousemove', (e) => {
moveX = e.pageX - initX
requestAnimationFrame(mouseMove)
})

function mouseMove() { // 滑动操作
animate()
}

到这里都还没什么难度,虽然离最终效果相距甚远,但基本就只剩下对细节的亿点处理了,我们来具体看看B站是怎么做的。

视差效果原理

在视差效果中,通常会使用多张具有不同视角的图片或分层的图像,通过透视、位移等处理方式,让观察者感受到物体的前后关系和深度差异。

我们打开控制台观察B站首页头图对应的 DOM 结构,会看到处理的对应变换包括了:平移(translate)、旋转(rotate)、缩放(scale)等,此外还有透明度可能也会随之改变。

通过鼠标移动产生的偏移值(x轴方向偏移值),我们可以按一定比例,设置对应的变换属性来达到最终效果。

后续内容参考:如何用原生 JS 复刻 Bilibili 首页头图的视差交互效果最近发现 B 站首页头图的交互效果非常有趣,本文将通过 - 掘金

Github地址:palxiao/bilibili-banner: 一键复刻 B 站首页动态 Banner,本仓库记录其历史Banner以供学习和欣赏(自2023-08-21开始)

代码分析

index.js

在原文章博主给出的代码的基础上,进行了优化,包括

  • 修改变量名使其更语义化
  • 重新封装函数让代码逻辑性更强,更容易读懂
  • 解决原作者在代码上犯的小错误(比如错误的给layer添加样式,而不是img/video),使最终效果更流畅,更接近b站原生的banner。
  • 在回正动画实现上,借助了css的过渡,并未使用原作者的线性插值法(用js实现回正动画)
  • 删除了些认为不必要的功能,简化了代码,方便迁移到自己的Hexo博客
  • 除此之外,还添加了许多注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
const app = document.getElementById("app");
//header是app的父元素
//app是使用绝对定位的子元素,完全覆盖header
//nav元素也是使用了绝对定位的header的子元素,且置顶压住了app
//后续我们给header加上mousemove和mouseleave的事件监听,为什么?
//即便我们鼠标是在nav或者app上移动,mousemove事件也会冒泡到父元素header
//但如果我们只给app添加mousemove,当鼠标移动到nav上,就不会触发mousemove事件
//为此时 nav 元素挡住了 app 元素,成为实际响应鼠标事件的元素
//因为我们希望鼠标在nav或者app上移动时,banner都能动,所以我们将mousemove监听添加到父元素上
//如果我们只给app添加mouseleave监听,当鼠标移动到nav(2个元素是同级关系),
//就会触发app的mouseleave事件,播放回正动画,这样用户可交互的范围就变小了
//但如果我们给header添加mouseleave的事件监听,只要鼠标不离开headr的范围,就不会触发mouseleave
//所以我们给header添加mouseleave的事件监听
const header = document.getElementById("page-header");

(async function () {
// 随机取一个banner来展示
// 10表示当前有10个banner,如果爬取了更多banner,此处应该被修改
const index = Math.floor(Math.random() * 10 + 1)
const response = await fetch(`/bilibiliBanner/images/${index}/data.json`)
const curBannerData = await response.json()
let layers = []; // 所有layer的DOM集合
let compensate = 0; // 视窗补偿值
// 添加图片元素(进行添加dom吗,修改dom的操作)
function init() {
//根据窗口宽度,计算补偿值compensate,用于动态调整元素尺寸和位置
compensate = window.innerWidth > 1650 ? window.innerWidth / 1650 : 1;
//进行离线操作,防止触发多次回流
app.style.display = "none";

for (let i = 0; i < curBannerData.length; i++) {
const layerChildConfig = curBannerData[i];

//创建layer
const layer = document.createElement("div");
layer.classList.add("layer");

// 创建子元素
const child = document.createElement(layerChildConfig.tagName);
// 如果子元素是video
if (layerChildConfig.tagName === 'video') {
// autoplay = true 尝试自动播放,但现代浏览器(如 Chrome 76+)会阻止有声自动播放。
// 通过 muted = true 静音绕过此限制
// loop = true 使视频播放结束后自动重播
child.loop = true; child.autoplay = true; child.muted = true;
}
child.src = layerChildConfig.src;

// 应用补偿值到元素的宽高
// 根据item中的信息设置img或者video的宽高
child.style.width = `${layerChildConfig.width * compensate}px`;
child.style.height = `${layerChildConfig.height * compensate}px`;
// 应用补偿值到变换矩阵的第4、5项(translateX/Y,偏移值)
let translateX = layerChildConfig.transform.translateX * compensate
let translateY = layerChildConfig.transform.translateY * compensate
let rotate = layerChildConfig.transform.rotate
let scale = layerChildConfig.transform.scale
// 添加偏移
child.style.transform = `translate(${translateX}px,${translateY}px) rotate(${rotate}deg) scale(${scale})`
// 将img或者video添加到div.layer
layer.appendChild(child);
// 将div.layer添加到div.app
app.appendChild(layer);
}
// 显示app,进行一次批量的回流
app.style.display = "";
// 所有layer都添加完毕后,重新捕获layer,给layers赋值
layers = document.querySelectorAll("#app .layer");
}
// 页面加载的时候,进行一次初始化操作,创建对应的layer结构,并初始化样式
init()

//鼠标在banner上的初始x坐标
let initX = 0;
//鼠标在banner上,在x轴方向移动的距离
let moveX = 0;

// 计算线性插值
lerp = (start, end, amt) => (1 - amt) * start + amt * end;

function mouseMove(e) {
moveX = e.pageX - initX;
requestAnimationFrame(() => {
animate(moveX);
})
}
// 动画执行
function animate(moveX) {
//如果不存在layers,直接返回
if (layers.length <= 0) return;
//每次鼠标在banner上移动,遍历所有layer,对每个layer中的子元素都应用变换
for (let i = 0; i < layers.length; i++) {
// 当前layer的子元素对应的配置信息
const layerChildConfig = curBannerData[i];
// 下面代码的核心就是利用moveX来计算新的样式并应用
// 当前translateX
let translateX = layerChildConfig.transform.translateX + moveX * (layerChildConfig.a || 0);
// 当前scale
let scale = layerChildConfig.transform.scale + (layerChildConfig.f || 0) * moveX
// 当前translateY
let translateY = layerChildConfig.transform.translateY + moveX * (layerChildConfig.g || 0);
// 当前rotate
let rotate = layerChildConfig.transform.rotate + moveX * (layerChildConfig.r || 0)
// 透明度变化
layers[i].firstChild.style.opacity = lerp(
layerChildConfig.opacity[0],
layerChildConfig.opacity[1],
(moveX / window.innerWidth) * 2
);
// 一次性应用所有变化
layers[i].firstChild.style.transform = `translate(${translateX}px,${translateY}px) rotate(${rotate}deg) scale(${scale})`
}
}
header.addEventListener('mouseenter',(e)=>{
initX = e.pageX;
})
header.addEventListener("mousemove", mouseMove);

// 鼠标已经离开了视窗,执行回正动画
function leave() {
//修改一些标记量
layers.forEach((layer, i) => {
const child = layer.firstChild
const layerChildConfig = curBannerData[i];
child.addEventListener('transitionend', () => {
child.style.transition = '';
}, { once: true });
requestAnimationFrame(() => {
//回正的时候给每个layer都添加过渡
child.style.transition = 'all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
// 应用补偿值到元素的宽高
// 根据item中的信息设置img或者video的宽高
child.style.width = `${layerChildConfig.width * compensate}px`;
child.style.height = `${layerChildConfig.height * compensate}px`;
// 应用补偿值到(translateX/Y,偏移值)
let translateX = layerChildConfig.transform.translateX * compensate
let translateY = layerChildConfig.transform.translateY * compensate
let rotate = layerChildConfig.transform.rotate
let scale = layerChildConfig.transform.scale
// 添加偏移
child.style.transform = `translate(${translateX}px,${translateY}px) rotate(${rotate}deg) scale(${scale})`
})
})
}
header.addEventListener("mouseleave", leave);
})()

grap.js

这个文件主要是用来爬取当前b站的banner的

在原博主代码基础上优化如下:

  • 添加了许多注释
  • 补充了y轴方向的加速度g
  • 删除了些认为不必要的代码,方便迁移到自己的Hexo博客
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");

//提示用户
console.log('正在下载资源中...');
//计算images文件夹内的目录数
function countDirectories(dirPath) {
let folderCount = 0;
const files = fs.readdirSync(dirPath);
files.forEach(() => {
folderCount++;
});
return folderCount;
}
//自定义睡眠函数,立即返回一个promise对象,立即开启一个定时器,在指定的时间后修改promise对象的状态
function sleep(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, timeout);
});
}
let cnts = countDirectories(path.resolve(__dirname, './images'))

const folderPath = path.resolve(__dirname, "./images/" + (cnts + 1));
//创建目录
fs.mkdirSync(folderPath, { recursive: true });

//初始化data数组(后续数组中的元素都是对象),用来存储banner中每个layer的信息
const data = [];
//下面是一个立即执行函数,里面书写了许多代码
(async () => {
// 启动无头浏览器
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
// 创建新标签页
const page = await browser.newPage();
// 设置视口尺寸
page.setViewport({
width: 1650,
height: 800
})
//开始进行爬取工作
try {
// 导航到B站首页
await page.goto("https://www.bilibili.com/", {
waitUntil: "domcontentloaded",
});
// 并等待动画横幅加载
await page.waitForSelector(".animated-banner");
//等待3s
await sleep(3000);

// 使用page.$$()获取所有 ".layer" 元素(图层元素)
// 类似document.querySelectorAll的效果, layerElements是一个伪数组
let layerElements = await page.$$(".animated-banner .layer");
// 遍历图层提取样式数据
for (let i = 0; i < layerElements.length; i++) {
// 分析page.evaluate(el=>{},element):
// Puppeteer会将element转换为浏览器环境中的 DOM 元素句柄(ElementHandle)
// 映射到回调函数的参数 el上
const layerFirstChild = await page.evaluate(async (el) => {
// 关于下面正则表达式如果不理解,建议去复习js的正则表达式
// 匹配初始transform属性中的translate值,精准捕获X/Y轴位移像素值。
const pattern = /translate\(([-.\d]+)px, ([-.\d]+)px\)/;
// 匹配初始transform属性中的rotate值
const pattern2 = /rotate\(([-.\d]+)deg\)/
// 匹配初始transform属性中的scale值
const pattern3 = /scale\(([.\d]+)\)/
// 记录初始偏移值,缩放值,旋转角度
const init = {}
// el.firstElementChild就是每个layer中的img或者video元素
const { width, height, src, style, tagName } = el.firstElementChild;
// 调用字符串的match方法,传入一个正则表达式
const matches = style.transform.match(pattern);//匹配translate的结果
init.translateX = +matches.slice(1)[0]
init.translateY = +matches.slice(1)[1]
const matches2 = style.transform.match(pattern2)//匹配rotate的结果
const deg = +matches2[1]
init.rotate = deg
const matches3 = style.transform.match(pattern3)//匹配scale的结果
const scale = +matches3[1]
init.scale = scale
// 传入的回调函数的返回值,最终会赋值给layerFirstChild
return { tagName: tagName.toLowerCase(), opacity: [style.opacity], transform: init, width, height, src };
}, layerElements[i]);
// 下载并保存数据
await download(layerFirstChild)
}

// 完成后模拟偏移banner
// 选择器获取"横幅容器元素"
let element = await page.$('.animated-banner')
// boundingBox()获取其视口坐标
let { x, y } = await element.boundingBox()
// 先垂直偏移50px,是触发悬停效果
await page.mouse.move(x + 0, y + 50)
// 瞬间水平滑动1000px,steps:1表示无中间过渡帧,模拟快速拖拽
await page.mouse.move(x + 1000, y, { steps: 1 })
await sleep(1200);

// 动画结束后DOM可能更新,需重新获取.layer元素句柄,避免引用失效
layerElements = await page.$$(".animated-banner .layer"); // 重新获取
for (let i = 0; i < layerElements.length; i++) {
const arr = await page.evaluate(async (el) => {
// 匹配偏移后transform属性中的translate值,精准捕获X/Y轴位移像素值。
const pattern = /translate\(([-.\d]+)px, ([-.\d]+)px\)/;
// 匹配偏移后transform属性中的rotate值
const pattern2 = /rotate\(([-.\d]+)deg\)/
// 匹配偏移后transform属性中的scale值
const pattern3 = /scale\(([.\d]+)\)/
//解构出style
const { style } = el.firstElementChild
const matches = style.transform.match(pattern);//匹配banner移动后 translate的值
const matches2 = style.transform.match(pattern2)//匹配banner移动后rotate的值
const deg = + matches2[1]
const matches3 = style.transform.match(pattern3)//匹配banner移动后scale的值
const scale = + matches3[1]
return [...matches.slice(1).map(x => +x), deg, scale, style.opacity]
}, layerElements[i]);

// 计算得到x轴上的'速度'a,其实就是一个比例关系
data[i].a = (arr[0] - data[i].transform.translateX) / 1000
// 计算得到y轴上的'速度'g,其实就是一个比例关系
data[i].g = (arr[1] - data[i].transform.translateY) / 1000
// 计算旋转的'速度'r
data[i].r = (arr[2] - data[i].transform.rotate) / 1000
// 计算缩放的'速度's
data[i].f = (arr[3] - data[i].transform.scale) / 1000
// 补充滑动1000px后的透明度
data[i].opacity.push(arr[4])
}
} catch (error) {
console.error("Error:", error);
}

//传入的item是一个对象
async function download(item) {
const fileArr = item.src.split("/");
// fileArr[fileArr.length - 1]被用来获取最后一个'/'后的内容,也就是文件名
const filename = fileArr[fileArr.length - 1]
// 得到图片的存储路径
const fileSavePath = `${folderPath}/${filename}`

// 使用fetch下载图片
const content = await page.evaluate(async (url) => {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
return { buffer: Array.from(new Uint8Array(buffer)) };
}, item.src);
// 得到图片数据
const fileData = Buffer.from(content.buffer);
// 异步写入
fs.promises.writeFile(fileSavePath, fileData).catch(console.error);
// 将每个layer中的firstChild的信息对象,push到data数组
// 下载好图片后,修改图片src为本地下载路径,而不是网络路径
data.push({ ...item, ...{ src: `/bilibiliBanner/images/${cnts + 1}/${filename}` } });
}


// 同时把data数组以json的格式,保存到data.json文件中
fs.writeFileSync(`${folderPath}/data.json`, JSON.stringify(data, null, 2));
console.log('正在写入本地文件...');
await sleep(300)
await browser.close();
console.log('banner 下载完毕');
})();

知识点

e.pageX和e.clientX的区别

在 JavaScript 事件处理中,e.pageXe.clientX 是两种常用的鼠标坐标属性,但它们的计算方式和应用场景有本质区别。

  • e.pageX
    表示鼠标指针相对于整个文档(document)左上角的水平坐标。如果页面存在滚动条,pageX 包含滚动距离

    示例:页面垂直滚动 200px 时,点击窗口左上角,pageX = 0pageY = 200

  • e.clientX
    表示鼠标指针相对于浏览器视口(viewport)左上角的水平坐标,不包含滚动距离

    示例:页面垂直滚动 200px 时,点击窗口左上角,clientX = 0clientY = 0

如果不存在滚动的页面(也就是body的高度小于等于视口的页面),二者的效果是相同的。

window.onblur

window.onblur 监听的是当前浏览器窗口(或标签页)失去焦点时触发的事件。这意味着如果用户切换到了另一个应用程序、点击了另一个浏览器窗口或标签,或者在某些情况下,打开了一个弹出窗口或对话框,当前窗口将失去焦点,并触发 onblur 事件。

1
2
3
window.onblur = function() {
console.log("Window lost focus");
};

requestAnimationFrame

  • requestAnimationFrame 是一个在H5中,由浏览器提供的JavaScript API

  • 用来解决setTimeout中的宏任务,不按时调用的问题(因为宏任务需要等待同步任务和微任务),我们可以把它当作一个会按时执行,且不需要传入事件间隔的定时器

  • 它也会返回一个id,也可以使用cancelAnimationFrame传入指定的id,来取消指定的开启的requestAnimationFrame

  • 它被设计用来在下一次页面重绘之前,执行指定的回调函数,这意味着它会与显示器的刷新率同步,通常为每秒60次(即60Hz),当然这也取决于设备的具体情况。

  • 这个API主要用于创建流畅的动画效果,和其他需要高性能的操作

  • requestAnimationFrame 本身会合并多次调用,确保每帧只执行一次回调;requestAnimationFrame 的“合并”并非覆盖,而是将所有注册的回调函数,按调用顺序加入帧级回调队列。浏览器会在下一帧渲染前,依次执行队列中的所有回调。若在同一帧内多次调用:

    1
    2
    requestAnimationFrame(() => console.log(1));
    requestAnimationFrame(() => console.log(2));

    输出顺序为 1 → 2,而非仅执行最后一个。

  • 浏览器渲染管线遵循以下顺序:JavaScript执行 → 样式计算 → 布局 → 绘制,所有 requestAnimationFrame 回调,会在“JavaScript执行”阶段完成(依次执行),确保同一帧内的多次修改都执行后,才进行渲染。

1
2
3
4
5
6
7
8
9
function mouseMove(e) {
// 直接调用 requestAnimationFrame,无需节流
// 本身会合并多次调用,确保每帧只执行一次回调
moveX = e.pageX - initX;
requestAnimationFrame(() => {
animate(moveX);
});
}
header.addEventListener("mousemove", mouseMove);

参考资料:【全网首发:更新完】H5新增的神器 – requestAnimationFrame 到底解决的是什么问题??【前端必会核心】_哔哩哔哩_bilibili

其他

  • slice(-2):总是尝试获取最后两个字符。如果字符串长度小于或等于2,则返回整个字符串。

  • fs.existsSync:传入文件或者文件夹的路径,判断文件或者文件夹是否存在,返回值是布尔值。

  • fs.unlinkSync(filePath):传入文件路径,同步删除文件

  • /translate\(([-.\d]+px), ([-.\d]+px)\)/

    • [-.\d]是一个字符组,表示匹配负号,小数点和数字
    • +表示匹配一次或者多次
    • px表示匹配px
    • \(表示对括号进行转义,因为小括号在正则表达式中有其他意义
    • \)同理,对括号转移,表示单纯表示匹配括号
    • ()包裹[-.\d]+px表述创建一个匹配组,匹配组匹配到的结果,可以在match方法的返回值中访问到
  • str.match:返回一个数组,第一个元素是str,后续元素就是匹配到的匹配组

    1
    2
    3
    4
    5
    6
    7
    8
    const pattern = /(\d{4})-(\d{2})-(\d{2})/;
    const str = "2023-10-05";
    const match = str.match(pattern);

    console.log(match[0]); // "2023-10-05"(完整匹配)
    console.log(match[1]); // "2023"(第一个捕获组)
    console.log(match[2]); // "10"(第二个捕获组)
    console.log(match[3]); // "05"(第三个捕获组)