ztachi 数字边疆技术架构解析

ztachi 数字边疆技术架构解析

ztachi 数字边疆技术架构解析

2026/4/13nuxtvue

「代码即游戏,AI 即伙伴。」这是 ztachi 数字边疆的 slogan,也概括了这网站想传达的东西——不是技术堆栈的堆砌,而是一个真正值得探索的数字空间。

这篇文章聊聊这个站点是怎么从零到一的。选择什么技术、为什么这么选、踩过什么坑,都记录一下。

技术选型

Nuxt 4.4 + Vue 3.5,TypeScript 全程类型安全。服务端渲染由 Nitro 驱动,部署在 Cloudflare Pages 上,利用边缘网络让全球访客都能获得接近本地的加载速度。

存储层用 Cloudflare D1(边缘 SQLite)存业务数据,R2 存封面、头像这些静态资源。认证这块比较特殊,后面单独讲。

UI 组件选的 Nuxt UI v4(基于 Reka UI + Tailwind CSS),图标用 @nuxt/icon + Iconify。国际化 @nuxtjs/i18n,邮件发信用 Resend,错误监控用 Sentry

前端架构

目录结构

app/
├── assets/css/      # 全局样式,CSS 变量定义主题
├── components/      # Vue 组件,自动导入
├── composables/     # 组合式函数
├── layouts/         # 页面布局
├── middleware/      # 路由守卫(auth / login / admin)
├── pages/           # 文件路由
├── plugins/         # 客户端插件
├── types/           # TypeScript 类型
└── utils/           # 工具函数

组件用同名文件夹嵌套组织,components/admin/home/FeaturedItems/index.vue 自动命名为 AdminHomeFeaturedItems,结构清晰也好维护。

主题系统

主题色是淡粉色系——这在技术圈确实少见,但配合深色背景和紫色光芒,效果意外地好。亮色模式转柔和玫瑰色调,暗色模式则是高饱和霓虹感。

CSS 变量定义在 assets/css/main.css,运行时配置在 app/app.config.ts。字体用 Orbitron 撑科技感,Noto Sans SC 保中文可读性,通过 @nuxtjs/google-fonts 本地预加载,不依赖 Google CDN。

Canvas 粒子背景

首页和关于页的背景不是静态图,是用 Canvas 画的三层动态效果:

星空粒子:120 个发光点,随机大小和透明度,在页面里缓慢漂移。亮度和透明度会呼吸变化,不会一直亮着或暗着。

星云光晕:3 个径向渐变圆球,色调在紫色范围内缓慢波动,模拟深空星云那种朦胧感。比静态背景图沉浸得多。

量子连线:5 条虚线在粒子间游走,走的是随机路线,用 setLineDash 画成虚线,模拟量子纠缠的非局域关联效果。

三层叠加分层绘制。暗色模式增强发光、降低整体透明度,亮色模式反之。主题切换通过 MutationObserver 监听 html.dark class 变化自动触发重载。resize 事件防抖 200ms,页面隐藏时 cancelAnimationFrame 暂停动画——这些细节决定了流畅度。

3D 标题动画

首页大标题「探索未知的边界」和关于页的标题都不是简单居中显示。每个字符都有独立的 CSS 3D 变换:

.char-3d {
  transform:
    translateY(var(--y))
    rotate(var(--rotate))
    scale(var(--scale));
  animation: charExplode var(--delay) cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

--y 交替 ±12px 上下偏移,--rotate 交替 ±4° 倾斜,--scale 0.85-1.07 缩放,--delay 基于字符索引级联递增。字符从模糊缩放状态「炸开」到最终位置,弹性缓动曲线增加物理感。

动画参数全部由 CSS calc() 和自定义属性计算,不走 JavaScript。好处是浏览器主线程无负担,动画路径每次刷新一致。

滚动动画

关于页使用 GSAP ScrollTrigger 做滚动叙事。故事卡片从左滑入、爱好卡片从右入场、使命宣言渐显、分类卡片依次浮现——每个元素有独立的入场方向和延迟配置,stagger 间隔 100ms。

这套模式比元素直接出现更有沉浸感,像是在跟随滚动逐步揭示信息。

SSR 水合稳定性

页面是 SSR 渲染的,必须保证服务端和客户端首次渲染结构一致。本站的处理方式:

  • 依赖客户端状态的 UI(登录态、主题切换)用 <ClientOnly> + fallback 骨架屏
  • fallback 结构和真实渲染的节点数量、层级完全对应,避免 hydration mismatch
  • 用户头像用原生 <img> 标签而非 NuxtImg,防止 IPX 处理器干扰 R2 资源 URL

后端架构

API 设计

Nitro 的文件路由约定,文件路径即端点:

server/api/
├── articles/     # 文章 CRUD
├── admin/        # 后台管理
├── auth/         # 认证(代理至独立 Worker)
├── comments/     # 评论系统
├── home/        # 首页数据
├── tools/        # 工具管理
├── upload/       # 文件上传
└── user/         # 用户操作

响应统一格式:{ code: 0, message: "ok", data: ... }。错误码 HTTP状态码 * 1000 + 业务序号,如 40001 表示参数无效,40101 表示未授权。查错时直接定位问题类别。

数据库

用原生 D1 API 操作数据库,通过 event.context.cloudflare.env.DB 取绑定。每个业务模块(article / comment / tool)独立维护表初始化函数,CREATE TABLE IF NOT EXISTS 幂等创建,重复执行不报错。

封装 d1All(查询)和 d1Run(增删改)两个函数,统一错误处理和类型转换。Drizzle ORM 这块暂未启用,核心逻辑走原生 API。

文件上传安全

上传接口三层校验:

  1. MIME 白名单image/jpegimage/pngimage/webp 三种
  2. 大小限制:封面 5MB 上限
  3. 魔数校验:读取文件头字节验证格式——JPEG 是 FF D8 FF,PNG 是 89 50 4E 47,WebP 是 52 49 46 46...57 45 42 50。改扩展名骗不过这关。

上传后存 R2 covers/avatars/ 目录,返回相对路径 URL,前端通过 /api/upload/cover/[key] 路由访问。

限流机制

注册、验证邮件等高风险操作有内存级限流。Map<string, { nextAllowedAt: number }> 记录状态,同一 key 120 秒窗口期内只放一次请求进来。Workers 单 isolate 内存有限但够用,这个量级的防护绰绰有余。

认证系统

这是整个项目里最折腾的一块。

独立 Worker 架构

认证逻辑跑在独立的 Cloudflare Worker 上,和主站 Nitro 应用分开部署。

原因:Better Auth 需要开启 nodejs_compat 来支持 crypto 等 Node.js API,而 Nitro 构建时 unenv 会向全局 process 注入 polyfill。这俩在同一 Worker 产物里撞上了——表现为 Cannot read private member #t 这类隐秘错误,堆栈指向原生对象而非业务代码。

试过几种「打补丁」方案:编译后字符串替换、调整 nodejs_compat 开关组合、修改 unenv 配置……短期内可能凑效,但全靠对内部实现的猜测吃饭,版本更新随时可能崩。

最终方案:认证逻辑拆出去,独立 Worker 跑纯净的 nodejs_compat,不依赖 unenv。冲突从根本上消失,职责边界也清爽了。

教训:需要给编译产物做手术的时候,往往是架构边界划错了。

认证流程

注册走邮件验证:提交信息 → 创建未验证用户 → 发验证邮件(Resend)→ 用户点链接 → emailVerified = true → 自动登录。

登录支持三种:邮箱密码(需先验证)、Google OAuth、GitHub OAuth。会话 cookie HTTP Only,7 天有效期,1 天刷新一次。

邮件系统

选 Resend 的理由:API 简洁、免费额度充足(100封/天)、送达率比自建 SMTP 高出一大截。

邮件模板和网站风格统一:科幻风渐变卡片、「星际旅行者,你好」开场白、圆角 CTA 按钮。验证码 15 分钟有效期,验证链接 24 小时。

国际化

@nuxtjs/i18n 管翻译,路由策略 prefix_except_default:中文(默认)无 URL 前缀,英文 /en/ 前缀。翻译文件按模块组织(authadmincommoneditor 等),避免一个大 JSON 几千行。

服务端 API 通过 x-locale 请求头判断语言,返回对应语言的错误提示。客户端 useI18n() 自动同步 locale 到 cookie,刷新页面不丢语言状态。

部署架构

Cloudflare Pages

主站跑在 Cloudflare Pages,构建命令 pnpm build,输出 dist。环境变量在 Dashboard 配置,绑定 D1 / R2 / KV 三个存储服务。

存储绑定

服务 名称 绑定标识
D1 homepage DB
R2 homepage BUCKET
KV 2d7de98e... KV

Workers 路由

生产环境把 ztachi.com/api/auth/* 路由到独立认证 Worker,OAuth 回调仍落在站点同源,避免跨域。

本地开发

双进程模式:

  • 终端 1:pnpm dev:auth 启动认证 Worker(8787)
  • 终端 2:pnpm dev 启动 Nuxt 主站(3000),.env 配置 NUXT_BETTER_AUTH_PROXY_TARGET=http://127.0.0.1:8787 代理认证请求

生产环境代理配置留空,认证直连边缘 Worker。

写在最后

这个站点从构思到上线,踩了不少坑。最大的一个就是认证模块的架构演进——从硬塞进 Nitro,到打补丁勉强跑通,再到拆成独立 Worker。每一次推翻重来都在提醒我:架构边界比代码质量更重要

另一个感受是细节的力量。粒子背景的配色切换、3D 标题的入场节奏、滚动动画的 stagger 间隔……这些看起来不起眼的细节,累积起来决定了整个站点的气质。技术是手段,体验才是目的。

如果你正在做类似的技术选型或架构决策,希望这篇文章有些参考价值。欢迎留言交流。

评论