跳到正文

这个博客是怎么做的

1 分钟阅读

从技术选型到每个功能模块的设计思路——写给想自己造一个的人,也写给好奇这里有什么的人。

这不是一个”用 Hexo 十分钟搭好”的博客。

从第一行代码到现在,它长出了暴风雨背景、照片地图、本地音乐播放器、Passkey 登录、手机扫码上传、命令面板、逐字风铃歌词……每一个功能都是因为”想要”而存在,不是因为”应该有”。

这篇文章把它拆开来看。


技术选型

框架用 SvelteKit 2 + Svelte 5,全站 runes 模式。选它的理由很简单:响应式粒度细、编译产物轻、写起来接近”直接描述 UI 该是什么样子”而不是”描述怎么更新 UI”。

样式是 Tailwind CSS v4 + shadcn-svelte。shadcn 的组件不打包进来,而是直接把源码复制到项目里,想改什么就改什么。

内容用 MDsveX 处理——Markdown 文件可以直接嵌 Svelte 组件,博客文章本身就是 Svelte 组件。加一篇文章只需要在 content/posts/ 里新建一个 .md 文件。

部署在 Vercel,通过 GitHub Actions 自动触发,不用 OAuth 授权,用 token 鉴权。


背景:一场会呼吸的暴风雨

打开页面,背景在下雨。

不是 GIF,不是视频,是两层叠加的 Canvas 动画:Lightning.svelte 负责分形闪电,Rain.svelte 负责其余所有——双层景深雨滴、涟漪环、落地溅点、粒子流场水面、风向变化。

水面有四种湍流模式,每次加载随机选一种:正弦波、fBm 分形布朗运动、旋度流场、涡旋流。Perlin 噪声驱动水面形变,Ken Perlin 2002 年的经典实现,直接内联在组件里。

闪电是分形路径加中点位移,有多级分支、回击时序、大气辉光渲染。

两个组件共享一个 weather store,里面只有一个值:雨势强度。这是它们之间唯一的通信——雨强高时,闪电频率也跟着变。深色模式下用 CSS dark:invert 反转,白天是晴空背景,雨滴变成深色,代码零改动。


照片:地图 + 时间线

照片页有两种浏览方式,通过 URL 参数切换,支持浏览器前进后退。

地图模式用 MapLibre GL 渲染,聚合逻辑跑在 Web Worker 里,用 Supercluster 算法——把成千上万张带 GPS 的照片按缩放级别聚类,主线程零压力。点击聚合点展开画廊,点击单张进入全屏详情页。

时间线模式按拍摄时间分组,每一天一个区块。滚动时当前日期吸附在顶部,像日历一样。搜索也跑在 Worker 里,支持中文和拼音,不会因为大量照片卡顿主线程。

照片有”隐藏”功能。普通访客看不到标记为隐藏的照片;管理员登录后可以看,也可以在当前 session 里临时切换某张的可见性,不写回服务器。


音乐:一个认真做的本地播放器

完整的本地音乐播放器,文件存在服务器,不依赖任何流媒体服务。

架构分三层:audioEngine 封装 Web Audio 链路,musicStore 持有所有响应式状态,UI 层只从 store 取数据。三层之间只有单向依赖,engine 不知道 store 的存在,store 不知道 UI 的存在。

Web Audio 链路:10 段参数均衡器 + 可热插拔的效果预设(混响、3D 声场、人声增强、超低重音等九种)+ 频谱分析节点。效果切换通过 effectBus bypass 机制,无缝无杂音。

可视化用 AnalyserNode 实时读取频谱,除了常规的律动动画,还有一套基于滚动 RMS 历史百分位数的高潮检测——副歌来临时,背景雨景会有感知。

歌词支持 LRC 格式,当前行逐字拆开,每个字以顶端为悬挂点像风铃一样摇摆,相邻字交错延迟,形成波浪。歌词来源:随文件附带的 .lrc,或者通过 LRCLIB API 在线抓取后持久化到服务端。

播放器有三种形态——dot(只有进度条)、mini(一行紧凑控件)、full(完整界面)。全屏播放页用 macOS Genie 动效从底栏弹出,用 clip-path: inset() 实现,不依赖任何动画库。


登录:Passkey + Telegram,两条路

管理功能(上传照片/音乐、查看隐藏内容)需要登录。有两种方式:

Passkey:用设备的生物识别(Face ID、指纹、Windows Hello)完成注册和登录。基于 WebAuthn 标准,服务端用 @simplewebauthn/server 验证,私钥从不离开设备。

Telegram:通过 Telegram Bot 的 OAuth widget 登录,服务端用 HMAC-SHA256 验证签名,防止数据被篡改。

两种方式都是无密码的。登录成功后 session 以 cookie 形式保存,跨标签页通过 BroadcastChannel 同步状态,不需要刷新页面。


上传:手机扫码直传

在电脑前,掏出手机,扫一下页面上的二维码,就能从手机相册直接上传照片或音乐到服务器。

实现:后端生成一个短期 token,前端把带 token 的 URL 渲染成二维码。手机扫码后打开一个简单的上传页,通过该 token 接口 POST 文件。token 有时效,用完即失效。

上传照片后,服务端自动提取 EXIF(GPS、时间、相机型号),生成缩略图,写入元数据 JSON。上传音乐后,自动解析 ID3 tag(标题、艺人、封面图),尝试从 LRCLIB 获取同步歌词,最后内联更新 store,不需要刷新页面。


命令面板

⌘K(或 Ctrl+K)打开,支持模糊搜索所有博客文章,包括拼音匹配——输入”svelte”或”shuang”都能找到。用 pinyin-pro 做拼音转换和模糊匹配。键盘导航,↑↓ 选择,Enter 跳转,Esc 关闭。


i18n:中英文切换

整个博客支持中英文切换,包括 UI 文案、日期格式、所有模块。语言设置存在 localStorage,切换即时生效,不刷新页面。

i18n 模块本身是一个 Svelte 5 runes 对象,locale 是响应式的,t$derived。任何组件只要读 t.xxx 就自动订阅语言变化,不需要额外的 provider 或 context。


Web Workers

重计算都在主线程之外:

  • cluster.worker.ts — 地图聚合
  • search.worker.ts — 照片全文搜索
  • timeline.worker.ts — 时间线分组
  • creation.worker.ts — 相册创建
  • scan.worker.ts — 照片扫描
  • music/metadata/worker.ts — 音频元数据解析

Worker 之间用 reqId 匹配请求和回调,可以并发发出多个任务,不需要排队。


整个博客没有用任何内容管理系统,没有数据库(除了浏览器端的 IndexedDB),没有第三方登录依赖(Passkey 是本地的,Telegram 是可选的)。所有内容都是文件,所有逻辑都在代码里,所有状态都是可控的。

这是我觉得”自己的博客”该有的样子。

© 2026 chenzylab