Cairn
← 返回博客

先别急着写代码:一个个人博客的需求拆解与技术选型

先别急着写代码:一个个人博客的需求拆解与技术选型

这个项目最早差点死在第一行代码之前。

原因不是不会写,而是能选的东西太多:Go 还是 Python,SSR 还是 SPA,PostgreSQL 还是 Markdown 文件,Session 还是 JWT,要不要 Redis,要不要把应用也塞进 Docker。每个方案单独看都说得通,合在一起就像在给一间书房设计机场塔台。

后来我给自己定了一条规矩:先写清楚网站必须解决的问题,再允许技术名词进场。

这篇文章不是一份“最终正确答案”,而是一次完整的取舍过程。如果你也准备做个人站,希望它至少能帮你少画两版没用的架构图。

一、先把需求写成人话

我需要的网站并不复杂:

  1. 能发布 Markdown 文章。
  2. 访客能注册、登录和评论。
  3. 能放作品和一些在线工具。
  4. 页面要适合搜索引擎收录。
  5. 一台普通服务器就能长期运行。

暂时不需要:

  • 多人协作写作
  • 实时聊天
  • 推荐算法
  • 分布式部署
  • 百万并发
  • 一套能在技术大会上讲四十分钟的微服务治理方案

把“不做什么”写出来之后,很多选择会自己消失。

二、为什么选 Go,而不是先上熟悉的方案

个人站的后端需求主要是 HTTP、数据库、模板和文件处理。Go 在这几个方向上没有明显短板:

  • net/http 足够成熟
  • 编译后是单个二进制
  • 内存占用可控
  • 并发处理简单
  • 部署时不需要额外运行时

更重要的是,Go 会逼着项目保持直接。它不太鼓励在每个业务动作上叠五层魔法。

路由选择 chi:

r := chi.NewRouter()
r.Get("/healthz", healthz)
r.Get("/posts/{slug}", postHandler.Detail)

chi 没有重新发明 HTTP,它只是把标准库路由补得更顺手。对小型单体项目来说,这种“存在感很低”的框架反而舒服。

三、为什么文章不直接存 Markdown 文件

一开始我认真考虑过“文章就是仓库里的 .md 文件”。

优点很明显:

  • 简单
  • Git 天然有版本历史
  • 不需要文章表

但评论、草稿状态、发布时间、用户权限、后续搜索都会让文件方案逐渐长出数据库的形状。既然早晚要有数据库,不如从一开始把文章作为结构化数据管理。

最终选择 PostgreSQL,文章表保存两份正文:

CREATE TABLE posts (
    id            BIGSERIAL PRIMARY KEY,
    slug          VARCHAR(255) NOT NULL UNIQUE,
    title         VARCHAR(255) NOT NULL,
    content_md    TEXT NOT NULL,
    content_html  TEXT NOT NULL,
    status        VARCHAR(32) NOT NULL DEFAULT 'draft',
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    published_at  TIMESTAMPTZ
);

为什么同时存 content_mdcontent_html

  • Markdown 原文用于编辑和重新渲染。
  • HTML 用于阅读时直接输出。
  • 发布时做一次高亮和安全过滤,阅读时不必重复计算。

这是空间换计算,但文章 HTML 的体积很小,没必要在这里表演极限节省。

四、为什么是 SSR,而不是 React

这个网站的核心动作是“读文章”。大多数页面在服务端拿到数据后就能完整生成:

请求 -> 查询 PostgreSQL -> 渲染模板 -> 返回 HTML

SSR 的直接好处:

  • 搜索引擎拿到完整内容
  • 首屏不等待 JavaScript 请求 API
  • 前后端只维护一套路由
  • 不需要设计一整套 JSON DTO
  • 部署时只有一个应用

评论提交需要局部刷新,使用 HTMX:

<form
  hx-post="/posts/example/comments"
  hx-target="#comment-section"
  hx-swap="outerHTML">
</form>

服务器返回新的评论区 HTML,HTMX 替换旧节点。它解决的是“局部更新”,不是接管整个前端。

JSON 格式化、文本对比这类纯浏览器工具,则使用原生 JavaScript。不同问题用不同工具,不必为了技术栈统一而强行统一。

五、为什么没有 Redis

我曾经把 Redis 写进第一版架构,理由是“以后可以做缓存和 Session”。

这句话的问题在于,“以后可以”几乎适用于所有中间件。

当前项目:

  • 单机部署
  • 访问量很小
  • Session 存 PostgreSQL
  • 没有异步任务
  • 没有热点查询

此时增加 Redis,真实收益是零,真实成本包括:

  • 多一个进程
  • 多一份配置
  • 多一种故障
  • 多一个需要备份或重建的状态

所以结论不是“Redis 没用”,而是“现在没有任何数据证明需要它”。

基础限流可以先用进程内计数器。哪天服务需要多实例,再把限流状态迁到 Redis。那时问题是真实存在的,设计也会更准确。

六、认证用 Session,不用 JWT

JWT 经常被误认为“现代登录方案”的默认答案。

但这个项目是单体服务,Session 更直接:

登录成功
  -> 生成随机 token
  -> token 存 sessions 表
  -> token 写入 HttpOnly Cookie
  -> 每次请求用 token 查用户

Session 可以立即删除,服务端始终掌握登录状态。JWT 的无状态优势在单机项目里没有发挥空间,注销和权限变更反而更麻烦。

Cookie 至少需要:

http.Cookie{
    HttpOnly: true,
    Secure:   production,
    SameSite: http.SameSiteLaxMode,
}

密码只保存 bcrypt 哈希,永远不保存原文。

七、最终架构

开发环境:

浏览器
  -> Air 热重载的 Go 服务
      -> PostgreSQL Docker 容器

生产环境:

Internet
  -> Caddy :443
      -> Go 127.0.0.1:8080
          -> PostgreSQL 127.0.0.1:5432

模块结构:

internal/
├── app/       # 依赖装配和路由
├── auth/      # 用户、Session、认证中间件
├── blog/      # 文章和评论
├── tool/      # 在线工具
├── config/    # 配置加载
└── web/       # 模板渲染、安全头、限流

八、这次选型真正学到的东西

技术选型不是回答“哪个技术更强”,而是回答:

  1. 当前问题是什么?
  2. 最简单的可靠方案是什么?
  3. 将来替换它要付出多少成本?
  4. 今天不做,会不会阻塞项目?

如果一个组件暂时没有使用场景,而且以后半天就能加上,那就让它继续待在 README 里,不要急着住进生产环境。

个人项目最稀缺的资源不是 CPU,也不是内存,是持续把它做下去的注意力。

架构应该保护这份注意力,而不是吃掉它。

评论

  • a
    antimushroom

    666

请先登录后再发表评论

去登录