关于本站

本站是 Bojin Li 的个人网站。

历史

年份

技术栈

2019

GitHub Pages + Jekyll + Disqus,Huxpro 主题

2020

Vercel + Jekyll + Disqus,Huxpro 主题

2021

Vercel + React 17 + Tailwind CSS + Vite + TypeScript

2022

Vercel + Next.js 12 + Notion API + Giscus + p5 + three.js

2024

Vercel + Next.js 14/15 + Tailwind CSS + shadcn/ui

2026(当前版本,Haku)

Cloudflare Workers + TanStack Start + Vite + Tailwind CSS + Durable Objects

关于 Haku

Haku,参考自 Yohaku。延续了 Yohaku 对阅读的重视,包括留白、纸张和细节动效带来的感觉,也有着同样完美的富文本文章渲染。至于 Haku 的名字,一方面保留了 Yohaku 的一部分,另一方面,Haku 在日语里恰好也是我名字里「帛」字的读音。

Yohaku 是基于 Next.js 的,而本站最终部署在 Cloudflare Workers 上,且需要针对中国大陆特殊的网络环境做尽可能多的优化。所以本站使用了 TanStack Start、TanStack Router 和 Vite,并引入了 Durable Objects,这样在 Worker 上就能尽可能深入底层地优化 SSR 性能,也避开了 Next.js 的臃肿,以及它在非 Vercel 平台上的不确定性。

后端方面,Yohaku 配套的很多功能本站用不上,而 Haku 又有不少自己的定制和优化,所以参考 Yohaku 重写了一个 CMS,在协议上尽可能与 Yohaku 配套的 mx-space 保持一致。

整体架构

在 monorepo 里,分成 Web、Core、Admin 三个应用。

应用

职责

技术

部署

Web

公开网站,包括主页、文章和独立页面

React、TanStack Start / Router、Vite、Tailwind CSS

Cloudflare Workers

Core

内容、媒体、身份、评论等后端服务

NestJS、Fastify、Drizzle、PostgreSQL、Redis

自建服务器,Dokploy 管理

Admin

内容管理后台

React、Vite、CodeMirror、Haklex / Lexical

自建服务器,Dokploy 管理

Mermaid diagram

三个应用之间用共享的 TypeScript 和 Zod 契约来对接,契约在请求和响应的边界上也会校验数据。

服务端渲染

Haku 使用服务端渲染。第一次打开页面的时候,Worker 会先根据路由和语言取到需要的数据,生成带着内容的 HTML,再以流的形式交给浏览器,让内容不用等脚本加载完就能先看到。

接着浏览器会做 hydration,把服务器已经生成好的页面接管过来,让导航、菜单、评论这些交互开始工作。之后在站内跳转,就由 TanStack Router 来处理,只按需读取下一个页面要用的数据,不用每次都重新加载整份文档。

Mermaid diagram

为了让服务端渲染和浏览器接管能接得上,每个服务端请求都有独立的 Router、QueryClient 和 Jotai store,浏览器里也各有一份,所以不同读者的数据不会串在一起。入场动画则跟着路由一起处理,首次打开、普通跳转和前后退恢复各有各的行为,预取数据或者后台刷新的时候,页面也不会无缘无故重新播放一遍动画。

缓存

站点设置、导航信息和首页的各类汇总,这些内容对所有读者都是一样的,因此适合共享。它们会以公开快照的形式保存在 Cloudflare 的 Durable Objects 里,同一份数据的并发读取会被合并成一次,快照过期之后,则会先把保留的旧内容返回给读者,同时在后台去更新(stale-while-revalidate),减少读者等待。另外有新内容发布之后,旧快照会被标记为过期,缓存的更新还带着版本检查,一个更早发起的请求即使晚到了,也不能把旧数据写回去覆盖新的。

文章正文不进入这层共享缓存,每次都是从已发布的内容里读取,HTML 和服务端函数的响应也保持私有。

首页上展示的各种活动和统计数据也是类似的思路,它们由后端定期汇总好,读者打开首页的时候,不需要再临时去请求外部服务,外部服务偶尔慢一点,也不会拖慢正文的阅读。

访问速度优化

Haku 在访问速度上做了这些处理。

  • 站点启动数据尽可能缩小,只传导航和当前页面需要的信息,不夹带无关页面的内容。
  • 路由的 loader 和查询缓存衔接起来,减少同一次导航里的重复读取。
  • 富文本、地图和 3D 这些比较重的依赖,按使用场景拆开,需要的时候才加载。
  • 正文用到的字体、语言和渲染模块提前准备好,免得刷新和 hydration 时出现不一致。
  • 背景和 3D 的动效加了调度,页面隐藏或者读者开启了减少动效时会暂停或者降级成静态,尽量不去占用阅读的资源。

此外,Haku 在 Chrome 开发者工具的网络受限模式下反复测试了 SSR 和首屏,并引入了 Sentry 来定位线上出现的错误和性能问题。

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...