先别急着写代码:一个个人博客的需求拆解与技术选型
先别急着写代码:一个个人博客的需求拆解与技术选型
这个项目最早差点死在第一行代码之前。
原因不是不会写,而是能选的东西太多:Go 还是 Python,SSR 还是 SPA,PostgreSQL 还是 Markdown 文件,Session 还是 JWT,要不要 Redis,要不要把应用也塞进 Docker。每个方案单独看都说得通,合在一起就像在给一间书房设计机场塔台。
后来我给自己定了一条规矩:先写清楚网站必须解决的问题,再允许技术名词进场。
这篇文章不是一份“最终正确答案”,而是一次完整的取舍过程。如果你也准备做个人站,希望它至少能帮你少画两版没用的架构图。
一、先把需求写成人话
我需要的网站并不复杂:
- 能发布 Markdown 文章。
- 访客能注册、登录和评论。
- 能放作品和一些在线工具。
- 页面要适合搜索引擎收录。
- 一台普通服务器就能长期运行。
暂时不需要:
- 多人协作写作
- 实时聊天
- 推荐算法
- 分布式部署
- 百万并发
- 一套能在技术大会上讲四十分钟的微服务治理方案
把“不做什么”写出来之后,很多选择会自己消失。
二、为什么选 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_md 和 content_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/ # 模板渲染、安全头、限流
八、这次选型真正学到的东西
技术选型不是回答“哪个技术更强”,而是回答:
- 当前问题是什么?
- 最简单的可靠方案是什么?
- 将来替换它要付出多少成本?
- 今天不做,会不会阻塞项目?
如果一个组件暂时没有使用场景,而且以后半天就能加上,那就让它继续待在 README 里,不要急着住进生产环境。
个人项目最稀缺的资源不是 CPU,也不是内存,是持续把它做下去的注意力。
架构应该保护这份注意力,而不是吃掉它。
评论
666
请先登录后再发表评论
去登录