广州的十一月依然带着些许闷热,宿舍里的空调还在尽职尽责地发出低频的嗡嗡声。室友们大都已经睡了,只有我桌面上外接显示器的冷光还亮着。
当时我正在写一篇关于最近折腾服务器的博客,写到一半,熟练地把截图拖进之前一直用的某个免费图床。转圈,等待,然后弹出了一个红色的「502 Bad Gateway」。我盯着那个红色的错误提示看了好几秒,又刷新了一遍之前写的几篇文章,发现有几张配图已经变成了刺眼的裂开的图标。
图床这个东西,用别人的总不放心。免费的随时可能跑路,大厂的云存储如果不绑定防盗链,哪天被恶意刷了流量,醒来可能就是一套海景房没了。
我靠在椅背上,看着屏幕上未完成的 Markdown 文档,突然觉得有点索然无味。大四了,保研或者找工作的兵荒马乱已经过去,拿了两次国奖,绩点稳在 4.02,按理说现在是我大学四年最闲的一段时间。既然闲着,既然别人的不好用,不如自己搭一个。
但既然要做,就不想只做一个套着 Bootstrap 模板的简陋上传框。我想做得好看一点,或者说,特别一点。
现在的 SaaS 产品 UI 都太像了。Tailwind CSS 普及之后,到处都是圆角、大面积留白、精致的毛玻璃阴影和毫无生气的莫兰迪色系。看多了,会觉得这些界面像医院的走廊一样,干净,高效,但也极其无聊。
我打开 Figma,新建了一个画布。脑子里浮现出的是我小时候家里那台大头 CRT 显示器,还有千禧年左右那些粗糙但充满生命力的网页。我决定选 Y2K(千禧年复古)风格。那种 2000 年代互联网的粗糙美感——高饱和度的霓虹色、金属质感的渐变、像素化的字体、带着厚重边框的视窗,还有一种不加掩饰的「机器感」。
设计阶段比我想象的要花时间。Y2K 不是随便弄点花花绿绿的颜色堆砌,它需要一种克制的混乱。我参考了早期 Windows 98 的 UI 逻辑,但去掉了那种沉闷的灰色调。主色调我选了高对比度的亮粉色和荧光绿,背景则是带有细微噪点纹理的深邃黑。字体方面,我找了一款开源的等宽像素字体,用来显示图片的元数据和 URL,这让整个界面看起来像是一个极客的私人控制台。按钮没有做圆角,就是硬朗的直角矩形,悬浮时会有明显的像素位移和刺眼的边框反色。看着 Figma 里逐渐成型的界面,那种久违的、纯粹为了视觉愉悦而设计的兴奋感慢慢回来了。
技术栈的选择倒没怎么纠结,TypeScript 全栈是我这几年的肌肉记忆。前端毫不犹豫地上了 Next.js,后端为了轻量,没有单独起服务,直接用了 Next.js 的 API Routes。
最核心的存储方案,我考虑过直接存本地硬盘,但服务器的系统盘空间实在有限。用云厂商的 OSS 或是 S3,又总担心账单超标。最后我选择了 MinIO。一台便宜的大硬盘 VPS,跑一个 MinIO 的 Docker 容器,就能提供完全兼容 S3 协议的 API。这意味着现在的代码不需要任何修改,哪怕以后我真的暴富了想迁移到 AWS,也只需要换一下环境变量里的 Endpoint 和 Access Key。
上传流程的体验是我最看重的。我没用任何现成的上传组件,自己用原生 HTML5 的 Drag and Drop API 手搓了拖拽区域。为了让交互更有质感,当文件拖入虚线框时,背景会闪烁高频的 Y2K 警示色。
图片体积是个绕不过去的问题。原图直接传,既浪费服务器带宽,以后博客加载起来也慢。我在 API Route 里接入了 Sharp 库,接管了上传流。无论扔进来的是多大的 PNG 还是 JPG,后端都会在内存里自动将其转换为 WebP 格式,并把质量压缩到 80%。
我还记得写完这段压缩逻辑的那个凌晨。我打开 Chrome DevTools 的 Network 面板,把一张 4.5MB 的单反原图拖进浏览器。进度条一闪而过,控制台打印出日志:转换完成,最终体积 120KB。我放大图片对比了一下,肉眼几乎看不出画质损耗。看着 Network 面板里那个极短的请求瀑布流,那种工程师特有的、隐秘的满足感,比拿到奖学金的瞬间还要真实。
后来,我又加了一个看起来有点「杀鸡用牛刀」的功能:AI 智能打标。
起因是我发现,图床里的图片一旦多起来,找图简直是灾难。按时间线翻找太蠢了,但我又懒得在每次上传时手动输入描述。既然现在 AI 这么火,为什么不让机器帮我干这事?
我没有去调 OpenAI 的 API,因为要花钱,而且把私人图片传给第三方总觉得违背了我「自建图床」的初衷。我翻了翻 Github,看中了 OpenAI 早期开源的 CLIP 模型。这个模型可以很好地理解图片和文本的关联。我用 Python 写了个很薄的 FastAPI 封装,把 CLIP 模型部署在另一台带点算力的闲置服务器上。
现在,当图片上传并转码成 WebP 后,Next.js 会把图片流转发给这个 Python 服务。几秒钟后,CLIP 会返回一组英文标签。比如我传一张在广商食堂拍的烧鸭饭,它会自动打上 food, plate, meat, indoor 的标签。虽然有时候它会把我的机械键盘认成 typewriter(打字机),但这种带着点人工智障的复古感,反而意外地契合这个 Y2K 图床的调性。
当然,开发过程中也踩了不少坑。Next.js 的 API Routes 在处理 multipart/form-data 时非常难用。默认的 body 解析器会把二进制文件破坏掉。我不得不关掉默认配置,引入了 formidable 来手动解析数据流。有一阵子,每次上传大文件,服务器的内存就飙升不降。我盯着服务器的 htop 面板看了一下午,最后发现是 Node.js 的 Stream 没有被正确消费和销毁,导致了内存泄漏。改了几行管道(pipe)的代码,看着内存曲线重新变得平稳,我长舒了一口气,顺手拿起桌上已经冷掉的半杯咖啡喝了一口。
现在,这个项目已经上线,部署在我的个人域名下。我给它起名叫 Snaply。
我把它开源在了 Github 上(snaply)。说实话,Star 不多,毕竟市面上好用的图床工具太多了,没人会真的需要一个门槛这么高、风格又这么非主流的图床。
但没关系,我自己用得很开心。
现在我写博客的流程变成了这样:在 VSCode 里敲着字,需要配图时,切到浏览器,打开那个闪烁着霓虹色彩的 Snaply 界面。把截图拖进去,听着机械键盘清脆的敲击声,看着进度条走完,然后点击那个像素风的「Copy MD」按钮。
剪贴板里就已经准备好了一段完美的  代码。
看着文章里稳稳加载出来的图片,我常常会想起那个因为 502 报错而决定自己写代码的夜晚。
有时候,做项目最好的动力,真的就是「别人的不好用,我自己做一个」。不需要什么宏大的愿景,不需要去想怎么改变世界,只是为了解决自己眼前的这点不爽,为了把一件每天都要用的工具,打磨成自己最喜欢的样子。这就足够了。