2026.0806 CLOUDY

个人博客 项目技术档案

项目概览

Moonlight 是 Ninentropy 的个人网站与作品集,仍在开发中。

整站以滚动叙事为主轴,承载若干互相独立的功能模块(地图、创作工具、博客、Agent Builder 等),所有模块共用一套全局状态、平滑滚动与暗黑主题。


技术栈

框架与语言

  • Next.js 15.5(App Router)
  • React 19 + React Compiler(babel-plugin-react-compiler)
  • TypeScript 5.7

样式

  • Tailwind CSS v4(PostCSS 插件,无独立 config 文件)
  • 模块级 CSS Modules(动画、滚动相关样式不走 Tailwind)

动画与滚动

  • GSAP 3.14 + ScrollTrigger + @gsap/react
  • Lenis 1.1(平滑滚动,与 GSAP ticker 同步驱动)
  • Lottie-react 2.4(首页签名动画)

地图

  • MapLibre GL 5(CartoDB 免 Key,矢量瓦片,WebGL 渲染)

状态与数据

  • Zustand 5(全局状态:isLoading / isScrollLocked / isHomePage / isHomePageReady)
  • next-themes 0.4(暗黑模式,data-theme 属性切换)
  • nuqs 2.8(URL search params 类型安全管理)

数据库与持久化

  • Prisma 6 + SQLite
  • Marker 表(地图标记)+ PageView 表(站点分析)
  • SQLite 文件被 git 追踪,随代码一起部署

自定义运行时

  • 自写 server.ts(Node http + Next.js 接管 + ws 模块 WebSocket 升级)
  • tsx 直接运行 TypeScript 服务端源码

博客与内容管线

  • gray-matter / remark(GFM)/ remark-rehype / rehype-stringify / github-slugger / unist-util-visit
  • 共享 src/lib/markdown.ts:blog 和 Agent Builder 复用同一套 Markdown → HTML 管线

Agent Builder 依赖

  • @xyflow/react(节点图编辑器)
  • js-tiktoken(token 计数)

目录结构

src/
├── app/                                    App Router
│   ├── layout.tsx                          全局元信息 + Inter 字体 + Providers
│   ├── page.tsx                            首页:StartAnimation + HomePageClientWrapper (React.lazy)
│   ├── _components/                        首页专属客户端组件(Next.js 不路由)
│   │   ├── ClientWrapper.tsx              路径 → isHomePage 标记
│   │   ├── Providers.tsx                  next-themes + nuqs
│   │   ├── SmoothScroll.tsx               Lenis + GSAP ticker 统一驱动
│   │   ├── StartAnimation.tsx             Lottie 签名 → 轮询 isHomePageReady → 预加载 Brush Worker
│   │   ├── HomePageClient.tsx             首页入口 → setHomePageReady
│   │   ├── HomePageClientWrapper.tsx      React.lazy + Suspense 异步 chunk
│   │   ├── BorderScroll.tsx               SVG 边框描边动画
│   │   ├── sectionA.tsx / sectionB.tsx / mainSection.tsx / sectionC.tsx
│   │   └── HorizontalScroll.tsx           水平滚动轨道
│   ├── map/                               食旅纪行 & 旷野之息(MapLibre + Markers CRUD)
│   ├── life/                              浮生札记(博客 SSG + 客户端 TOC)
│   │   ├── page.tsx                       列表页(日期降序)
│   │   └── [slug]/page.tsx                详情页(SSG,dynamicParams = false)
│   ├── project/
│   │   ├── tornpaper/                     TornPaper SVG 撕裂纸编辑器
│   │   ├── brush/                         Brush PS 风格笔刷编辑器
│   │   ├── agent/                         Agent Builder(LLM 编排)
│   │   ├── stslink/                       StsLink 杀戮尖塔实时观战
│   │   └── analytics/                     站点分析仪表盘
│   ├── design/                            设计作品展示(TornPaper + Brush 卡片)
│   ├── games/                             占位
│   └── sonance/                           占位
├── components/
│   ├── TornPaper.tsx                       SVG 撕裂纸(SplitMix32 确定性哈希)
│   ├── BrushStroke.tsx                    笔刷运行时组件 + 导出渲染器
│   └── BackLink.tsx                       router.back() → push(fallbackHref),渲染 <a>
├── store/useAppStore.ts                    Zustand 全局状态
├── lib/
│   ├── db.ts                              Prisma 单例
│   ├── markdown.ts                        Markdown → HTML(blog + agent 复用)
│   ├── posts.ts                           Markdown 解析 + TOC 提取(life 博客)
│   ├── stslink-ws.ts                      WebSocket 服务端
│   ├── brush/                             Brush 核心引擎
│   │   ├── deterministic.ts              SplitMix32 确定性哈希
│   │   ├── compute.ts                    纯计算引擎(buildStrokeData / dab / easing 分桶)
│   │   ├── cache.ts                      StrokeData + SVG 双缓存
│   │   ├── worker.ts                     Web Worker
│   │   └── preloader.ts                  StartAnimation 阶段预加载编排
│   └── agent/                             Agent Builder 核心
└── assets/ae/                             Lottie JSON + Animation 导出数据

content/posts/                              博客 Markdown 源文件
prisma/
├── schema.prisma                           Marker + PageView 模型
└── dev.db                                  开发用 SQLite(git 追踪)
server.ts                                   自定义服务器入口
ecosystem.config.cjs                        PM2 配置
.github/workflows/deploy.yml                CI/CD 自动部署

代码分离 & 启动时序

HomePageClientWrapperReact.lazy + <Suspense fallback={null}> 将 GSAP / ScrollTrigger / sectionA/B/C / mainSection 等重型依赖打入异步 chunk,不阻塞首屏。

StartAnimation(首屏关键 chunk)在 Lottie 签名动画循环期间:

  1. 轮询 Zustand isHomePageReady(异步 chunk 是否加载完毕)
  2. 满足最短 2.5s 展示时间
  3. 期间通过 Web Worker 预计算 Brush 笔触 SVG 数据并缓存

四个 Zustand 状态协同:

  • isLoading — loading 遮罩可见性
  • isScrollLocked — Lenis 滚动锁定
  • isHomePage — 当前路径是否为首页
  • isHomePageReady — 异步 chunk 就绪信号

首次访问:2.5s Lottie + 轮询 isHomePageReady → CSS 淡出 → 解锁滚动。 重访sessionStorage['startAnimationPlayed'] → 跳过 loading,仅锁滚动 500ms。

SectionB 动画同样使用 sessionStorage['sectionBAnimationPlayed'] 短路重访。


技术选型说明

为什么用 App Router 而非 Pages Router

App Router 默认服务端组件、内置异步数据获取与 generateStaticParams博客的 SSG、地图页的服务端初始数据加载、布局共享都能在路由层直接解决,不需要再引入额外的数据获取层。

为什么用 GSAP + ScrollTrigger 而不是 Framer Motion

首页的滚动叙事是多步、可逆、相互联动的时间线(边框描边 → 垂直层位移 → 水平轨道 → 最终扩展铺满)。GSAP 的 timeline 模型与 ScrollTrigger 的 scrub 机制对这种"按滚动进度回放任意复杂动画"的场景更合适。Framer Motion 更适合组件级的进入/退出过渡。

为什么用 Lenis 配合 GSAP

原生滚动在跨浏览器、跨设备上的曲线不一致;Lenis 提供统一的平滑滚动曲线,并能与 GSAP ticker 同步驱动,避免两个 RAF 循环错开导致的画面抖动。

为什么用 MapLibre 而非 Mapbox / Leaflet

  • 不需要 Mapbox 的 API key 和按量计费
  • 矢量瓦片性能优于 Leaflet 的栅格瓦片
  • 渲染由 WebGL 驱动,与现有 Canvas / SVG 内容栈一致

为什么用 SQLite + Prisma

  • 单文件即数据库,零运维,部署只要复制文件
  • 地图标记 + 站点分析数据量级在万以内,SQLite 完全够用
  • Prisma 提供类型安全的查询和 schema 管理(prisma db push

为什么自写 server.ts

StsLink 联机工具需要 WebSocket。Next.js 自带 server 不直接暴露升级钩子;自写一个 Node HTTP server 包住 Next,再在 upgrade 事件里挂 WS handler,是最干净的做法。

为什么博客用 Markdown 而不用数据库

  • 博客是随代码部署的内容,不是用户产生的数据,进数据库没有收益
  • Markdown 文件版本化、可 diff、可在 IDE 里直接编辑
  • App Router 的 generateStaticParams + dynamicParams = false 让所有文章在 build 时静态化,运行时零开销

为什么引入 React Compiler

React 19 的 React Compiler(babel-plugin-react-compiler)在构建时自动 memo 组件和 hooks,减少手工 useMemo/useCallback/React.memo 的心智负担,同时保证动画密集场景下的渲染性能。

为什么用 SplitMix32 确定性哈希

TornPaper 和 Brush 的 SVG 需要在服务端和客户端产生完全一致的输出,避免 hydration mismatch。Math.sin() / Math.random() 在不同环境下不可复现;SplitMix32(Math.imul + 位运算)是纯整数确定性算法,SSR 和客户端结果完全一致。


已完工模块

首页 /

  • 滚动叙事主时间线由 HomePageClient 驱动
  • 分段:StartAnimationBorderScroll(SVG 描边) → sectionAsectionB(撕裂纸 + Brush 笔触装饰动画) → mainSection(3D 六边形卡片) → sectionC(友链) → HorizontalScroll
  • 平滑滚动由 SmoothScroll 整合 Lenis 与 GSAP ticker
  • SectionB 的 Brush 装饰 SVG 由 Web Worker 在 StartAnimation 阶段预计算,GSAP tl.call 在指定时间点直接 createElement + innerHTML 插入,播放完毕 animationend 事件自动销毁

足迹地图 /map

  • 基于 MapLibre GL 渲染世界地图,CartoDB 免 Key
  • 标记分类:food(食旅纪行)/ wild(旷野之息)
  • Marker 数据通过 Server Actions 持久化于 SQLite
  • NODE_ENV === 'development' 时开放增删改,生产只读

TornPaper 工具 /project/tornpaper

  • 分栏式编辑器:图层 / 场景 / 属性 / 点位
  • SplitMix32 确定性算法保证 SSR/客户端输出一致
  • 支持撤销 / 重做(Ctrl+ZShift+Ctrl+Z
  • Delete 删除选中点位
  • 可导出为自包含 SVG

Brush 笔刷编辑器 /project/brush

  • PS 风格笔刷引擎:自由手绘路径原样保留,沿路径盖章 dab
  • 支持图像笔尖、收尖、抖动、散布、颜色抖动
  • Web Worker 异步计算,不阻塞主线程
  • 可导出带动画的自包含 SVG 装饰元素
  • 详细架构见 docs/brush.md

Agent Builder /project/agent

  • 可视化 LLM Agent 编排工具
  • 一个 Start 节点 = 一个 Agent,BFS 自动推导编排关系
  • 三条铁律:遇 Start 停、连 Start in → 编排边、节点不共享
  • API 双协议(Anthropic / OpenAI),SSE 流式 → rAF 批量渲染
  • 基于 @xyflow/react 节点图编辑器
  • 详细架构见 docs/agent 相关文档
  • 杀戮尖塔联机辅助
  • WebSocket 通道由自写 server.ts/project/stslink/ws 路径挂载升级
  • 同种匹配 / 单人模式 / Mod 调试
  • globalThis 防 HMR;每连接 10 msg/s 限流;SSE 订阅 + REST 快照

博客模块 /life

  • Markdown 源文件位于 content/posts/
  • src/lib/posts.ts 负责 frontmatter 解析、AST 注入 heading id、提取 TOC、转 HTML
  • src/lib/markdown.ts 提供共享的 Markdown → HTML 管线(remark GFM → remark-rehype → rehype-stringify)
  • /life 列表页按日期降序
  • /life/[slug] 详情页是 SSG(dynamicParams = false),附带客户端 TOC(IntersectionObserver 高亮 + 平滑滚动)

设计作品展示 /design

  • TornPaper + Brush 两个创作工具的展示卡片页
  • 内联 SVG 预览(TornPaper 展示分层撕裂 + 闭合圆斑;Brush 展示 dab 盖章 + 收尖 + 飞白颗粒)
  • 悬停动效,跳转至对应工具页

站点分析 /project/analytics

  • 仪表盘:今日 PV/UV、平均停留时长、跳出率、平均访问深度
  • 累计统计 + 来源分布(7 天)
  • 今日每小时 PV 柱状图 + 最近 7 天 PV 趋势
  • Top 10 页面排行 + 最近访问实时列表
  • 数据通过 PageView 模型持久化于 SQLite

未完工 / 占位模块

  • /games — 游戏人生(计划嵌入 web 端小游戏)
  • /sonance — 千音雅集(练琴日记)

均为占位页。


部署

本地开发

npm install
npx prisma generate
npx prisma db push        # Schema → dev.db
npm run dev                # 标准 Next.js 开发
npm run dev:ws             # 启用自定义 server,含 WebSocket(StsLink 需要)

生产构建

npm run build
npm run start              # 标准 Next.js 启动
npm run start:ws           # 自定义 server(tsx server.ts),含 WebSocket

进程管理(PM2)

配置文件 ecosystem.config.cjs

{
  name: "backend",
  script: "npm",
  args: "run start:ws",
  env: { NODE_ENV: "production" }
}
pm2 start ecosystem.config.cjs    # 首次启动
pm2 startOrReload ecosystem.config.cjs --update-env  # 热重载

CI/CD(GitHub Actions)

监听 main 分支 push,在自托管 runner 上自动部署:

  1. Checkoutfetch-depth: 0 拉取全部历史(含 SQLite 文件)
  2. 停服pm2 stop backend,释放 SQLite 文件锁
  3. DB 对齐git checkout prisma/dev.db,用仓库版本覆盖服务器本地改动
  4. 装依赖npm install
  5. Prisma 生成npx prisma generate
  6. 构建npm run build
  7. 重启pm2 startOrReload ecosystem.config.cjs --update-env

完整配置见 .github/workflows/deploy.yml

数据库

DATABASE_URL="file:./prisma/dev.db"。SQLite 文件被 git 追踪,CI 构建时会自动对齐到仓库版本。

开发时修改 schema 后:

npx prisma db push    # Schema → dev.db(无需 migration)

当前进度

  • 完成约 60%
  • 已具备完整功能的模块:首页滚动叙事、足迹地图、TornPaper、Brush、Agent Builder、StsLink、博客、Design 展示、站点分析
  • 待开发:游戏人生、千音雅集、统一的页面间导览系统