为什么要给域名申请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() { body.style.display = 'none' for (let i = 0; i < bannerImagesData.length; i++) { const layerChildConfig = bannerImagesData[i] const layer = document.createElement('div') layer.classList.add('layer') const img = document.createElement('img') img.src = item.url layer.appendChild(img) app.appendChild(layer) } body.style.display = 'block' 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
| #app { position: relative; overflow: hidden; margin: 0 auto; min-width: 1000px; min-height: 155px; max-height: 240px; height: 10vw; }
.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; }
|
准备工作就完成了,你会看到如下的界面,所有元素我们都添加了进来,现在层次已经有了,图片位置还很乱,需要给它们设置上初始偏移值来调整位置,但先不急,让我们先看看贯穿整个交互方式的鼠标事件。
鼠标事件 & 执行动画
我们这里主要会用到三个鼠标事件,分别是 mouseenter
、mousemove
和 mouseleave
,分别代表鼠标的进入事件、移动事件以及离开事件,我们将在容器上绑定这三个事件监听,在鼠标进入时记录初始化位置,鼠标移动时减去初始值就得到偏移值,这个偏移值将是接下来所有变换的核心系数,这里我们取 clientX
或 PageX
来计算偏移量(页面不包含滚动条的情况下,二者是等效的),相关代码如下:
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");
const header = document.getElementById("page-header");
(async function () { const index = Math.floor(Math.random() * 10 + 1) const response = await fetch(`/bilibiliBanner/images/${index}/data.json`) const curBannerData = await response.json() let layers = []; let compensate = 0; function init() { compensate = window.innerWidth > 1650 ? window.innerWidth / 1650 : 1; app.style.display = "none";
for (let i = 0; i < curBannerData.length; i++) { const layerChildConfig = curBannerData[i];
const layer = document.createElement("div"); layer.classList.add("layer");
const child = document.createElement(layerChildConfig.tagName); if (layerChildConfig.tagName === 'video') { child.loop = true; child.autoplay = true; child.muted = true; } child.src = layerChildConfig.src;
child.style.width = `${layerChildConfig.width * compensate}px`; child.style.height = `${layerChildConfig.height * compensate}px`; 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})` layer.appendChild(child); app.appendChild(layer); } app.style.display = ""; layers = document.querySelectorAll("#app .layer"); } init()
let initX = 0; 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) { if (layers.length <= 0) return; for (let i = 0; i < layers.length; i++) { const layerChildConfig = curBannerData[i]; let translateX = layerChildConfig.transform.translateX + moveX * (layerChildConfig.a || 0); let scale = layerChildConfig.transform.scale + (layerChildConfig.f || 0) * moveX let translateY = layerChildConfig.transform.translateY + moveX * (layerChildConfig.g || 0); 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(() => { child.style.transition = 'all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)' child.style.width = `${layerChildConfig.width * compensate}px`; child.style.height = `${layerChildConfig.height * compensate}px`; 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('正在下载资源中...');
function countDirectories(dirPath) { let folderCount = 0; const files = fs.readdirSync(dirPath); files.forEach(() => { folderCount++; }); return folderCount; }
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 });
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 { await page.goto("https://www.bilibili.com/", { waitUntil: "domcontentloaded", }); await page.waitForSelector(".animated-banner"); await sleep(3000);
let layerElements = await page.$$(".animated-banner .layer"); for (let i = 0; i < layerElements.length; i++) { const layerFirstChild = await page.evaluate(async (el) => { const pattern = /translate\(([-.\d]+)px, ([-.\d]+)px\)/; const pattern2 = /rotate\(([-.\d]+)deg\)/ const pattern3 = /scale\(([.\d]+)\)/ const init = {} const { width, height, src, style, tagName } = el.firstElementChild; const matches = style.transform.match(pattern); init.translateX = +matches.slice(1)[0] init.translateY = +matches.slice(1)[1] const matches2 = style.transform.match(pattern2) const deg = +matches2[1] init.rotate = deg const matches3 = style.transform.match(pattern3) const scale = +matches3[1] init.scale = scale return { tagName: tagName.toLowerCase(), opacity: [style.opacity], transform: init, width, height, src }; }, layerElements[i]); await download(layerFirstChild) }
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);
layerElements = await page.$$(".animated-banner .layer"); for (let i = 0; i < layerElements.length; i++) { const arr = await page.evaluate(async (el) => { const pattern = /translate\(([-.\d]+)px, ([-.\d]+)px\)/; const pattern2 = /rotate\(([-.\d]+)deg\)/ const pattern3 = /scale\(([.\d]+)\)/ const { style } = el.firstElementChild const matches = style.transform.match(pattern); const matches2 = style.transform.match(pattern2) const deg = + matches2[1] const matches3 = style.transform.match(pattern3) const scale = + matches3[1] return [...matches.slice(1).map(x => +x), deg, scale, style.opacity] }, layerElements[i]);
data[i].a = (arr[0] - data[i].transform.translateX) / 1000 data[i].g = (arr[1] - data[i].transform.translateY) / 1000 data[i].r = (arr[2] - data[i].transform.rotate) / 1000 data[i].f = (arr[3] - data[i].transform.scale) / 1000 data[i].opacity.push(arr[4]) } } catch (error) { console.error("Error:", error); }
async function download(item) { const fileArr = item.src.split("/"); const filename = fileArr[fileArr.length - 1] const fileSavePath = `${folderPath}/${filename}`
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); data.push({ ...item, ...{ src: `/bilibiliBanner/images/${cnts + 1}/${filename}` } }); }
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.pageX
和 e.clientX
是两种常用的鼠标坐标属性,但它们的计算方式和应用场景有本质区别。
e.pageX
表示鼠标指针相对于整个文档(document)左上角的水平坐标。如果页面存在滚动条,pageX
包含滚动距离
示例:页面垂直滚动 200px 时,点击窗口左上角,pageX = 0
,pageY = 200
。
e.clientX
表示鼠标指针相对于浏览器视口(viewport)左上角的水平坐标,不包含滚动距离
示例:页面垂直滚动 200px 时,点击窗口左上角,clientX = 0
,clientY = 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) { 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]); console.log(match[1]); console.log(match[2]); console.log(match[3]);
|