博客中几个组件的实现
在开发博客时做的几个小组件,记录一下他们的实现过程。
数字时钟
数字时钟是这个博客中第一个实现的组件,网上实现指针时钟的教程和demo很多,但是大多是钟表样的时钟, 比较近似我想实现的效果的也是一些翻牌式的数字时钟效果,因此打算自己实现一个电子表式的时钟。
具体实现
先拆基本单位
观察电子表的结构可以看出,所有的数字都是由七个条形拼凑出来的。七个条形中除了中间的横条不同外,其余六个都属于同一类“带尖角的段码”,因此基本单位其实只需要实现三种形状:
- 普通段码
- 中间横条
- 分隔用的冒号
这里我没有去找现成字体,而是自己写了一个 BaseStick 组件来作为每一个发光条。现在这一版里,BaseStick 内部已经不是单纯靠 clip-path 去裁一个形状了,
而是直接用 SVG path 把段码本体和两端的尖角画出来。这样做的好处是边缘更可控,缩放时也会比纯 CSS 裁切稳定一点。画好之后再通过 rotate 改变朝向,
分别复用成上、下、左、右四种位置。
再处理数字映射
接着再写一层 DigitalNumber 组件,将七个长条先按照 8 的结构摆好,并给它们从上到下编号。
真正控制不同数字显示的部分则交给一个映射表来完成,例如 0 需要亮起哪几段,1 需要关闭哪几段,都在 stickActiveMap 里写死。这样渲染时只需要根据当前字符去读取对应的配置,再把 active 状态传给每根数码管即可。
两个小细节
这里有个小细节是,关闭状态的长条我没有直接让它消失,而是返回一个同尺寸的空白占位,这样数字切换时整体布局不会抖动。现在段码本体虽然换成了 SVG,
但这个思路本身没有变。
另外冒号也单独拆了一个分支,直接渲染两个小圆角方块,实现上反而比七段数码管本体更省事。
最外层时钟逻辑
最外层的 DigitalClock 组件就比较直接了,每秒通过 setInterval 取一次当前时间,然后把 toTimeString() 截取成 HH:MM:SS 这样的字符数组逐个渲染。
至于为什么不直接找一个电子表字体来显示,主要还是为了切换时这种一段一段亮起和熄灭的感觉,手搓出来会更像自己想要的那个味道。
backToTop
具体实现
为什么要做
这个组件的出发点比较简单,就是文章页滚得比较深时还是希望有一个顺手的回顶按钮,不然每次都靠鼠标滚轮或者触控板往回翻体验还是差点意思。
实现思路
实现上我没有做得太复杂,本体就是一个固定在页面右下角的按钮,点击后直接调用 scrollTo(0, 0) 返回顶部。图标用了 @ant-design/icons 里的 CaretUpOutlined,
样式上则继续沿用站点已有的主题色和暗黑模式配色,这样放进右侧边栏区域也不会显得太突兀。
预留的扩展点
按钮内部我还额外留了一个 data-id="--scroll-position" 的节点,原本是想后面拿它来显示当前滚动进度百分比,这样它除了“返回顶部”之外还能兼顾一点阅读进度提示的作用。
不过现在这一版还只是先把结构预留出来,功能上仍然是一个比较朴素的 backToTop。
点击放礼花
这个组件纯粹是因为觉得好玩才加的。配色上参考了 canvas-confetti 那套比较活泼的调色盘,但实现上没有引入第三方库,而是自己用原生 Canvas 手写了一个轻量粒子系统。实际效果就是你在页面任意位置点一下,会有一小簇彩色粒子从点击处轻轻炸开,带一点重力下落和旋转,最后慢慢消失,整体感觉比较精致,不会太抢戏。
具体实现
整体思路
礼花效果本质上是在一个全屏的 canvas 上画一堆小粒子,每个粒子有自己的位置、速度、颜色、形状和生命周期。点击时在鼠标位置生成一批粒子,然后通过 requestAnimationFrame 逐帧更新它们的状态并重新绘制。
canvas 层用 pointer-events-none 覆盖在整个页面上方,这样它只负责渲染,不会拦截任何点击事件,页面本身的链接、按钮都能正常使用。
粒子设计
每个粒子的数据结构大概长这样:
x、y:当前位置vx、vy:水平和垂直速度life/maxLife:当前生命值和最大生命值,用来控制渐隐color:从 15 种预设颜色里随机挑一个size:粒子大小,2 到 4 像素之间随机,整体偏小巧gravity:重力加速度,让粒子有抛物线下落的感觉rotation/rotationSpeed:旋转角度和旋转速度,方形和三角形粒子转起来会更有动感shape:形状,圆形、方形、三角形三种随机
生成粒子时,速度方向是 0 到 2π 随机角度,速度大小控制在 1 到 4 之间,这样爆炸范围不会太大,比较克制。垂直速度额外减了 2,让粒子先微微向上飞一段再下落,更像真实的礼花。
动画循环
每一帧里,先清空画布,然后遍历所有存活的粒子,更新它们的位置、速度和生命值。生命值按 1 / maxLife 递减,减到 0 以下就从数组里移除。绘制时根据剩余生命值设置 globalAlpha,实现淡出效果。
速度上除了重力之外,还乘了一个 0.98 的空气阻力系数,让粒子不会一直飞太远。
一个细节:只响应单纯的点击
一开始监听的是 window 上的 click 事件,但很快发现一个问题:选中文本的时候也会触发礼花,体验不太好。所以后面加了一层判断,只有当点击的目标不是文本输入类元素,且用户没有在拖拽选区时,才生成粒子。
更简单的做法是把事件监听从 click 换成只在真正想触发的地方响应,或者判断 window.getSelection()?.isCollapsed,如果用户在选中文本就不放礼花。我最后选的是后者,在 handleClick 里先检查 window.getSelection()?.toString() 是否为空,不为空就直接 return,这样复制文章段落时就不会被礼花打扰了。
文章标题导航
具体实现
为什么要有目录
文章标题导航主要是为了解决长文阅读时来回跳转不方便的问题。尤其是像技术文章这种天然会拆出很多二三级标题,如果侧边没有目录,只靠用户自己滚动去找位置还是会有点累。
先把标题信息收集起来
这部分实现的思路也很直接,在文章详情页的最外层我给 article 加了一个固定的 id,组件挂载后通过 querySelectorAll 一次性取出其中所有带 id 的 h1 到 h6,
把标题文本、标题层级以及它们距离文档顶部的偏移量整理成一个数组存进状态里。这样目录区域只需要遍历这个数组,就能把当前文章的标题结构渲染出来。
目录层级怎么展示
缩进效果没有额外写复杂规则,而是直接根据标题级别计算左边距,例如 h2 不缩进,h3、h4 逐级向右错开一点,这样目录的层级关系基本就能看出来了。
点击目录项时本质上还是锚点跳转,因为标题本身已经有 id,直接拼 #${id} 就能定位过去。
当前标题高亮
比较核心的是当前阅读位置的高亮处理。这里监听了页面的 scroll 和 resize 事件,再结合 window.scrollY + 96 去和每个标题的 offsetTop 比较,
找出当前已经滚动到的最后一个标题作为激活项。这个 96 相当于给顶部导航和页面留一点缓冲,不然高亮切换会显得偏晚。最后再把命中的目录项换成主题蓝色,
这样用户在阅读时就能比较直观地知道自己看到文章的哪一节了。