架构总览:Monorepo 结构与容器化部署
本文是《墨言博客助手》系列的第 2 篇。项目源码已开源:https://github.com/2692341798/InkWords
引言:为什么需要架构设计?
想象一下你要建造一栋房子。你不会直接把砖块、水泥、木材堆在一起就开始施工,而是先画好设计图纸 ,规划好房间布局 ,确定好水电线路。软件开发也是如此,良好的架构设计就是我们的"设计图纸"。
今天,我将带你深入了解墨言博客助手的整体架构设计。无论你是刚接触全栈开发的小白,还是有经验的开发者,这篇文章都会让你清晰地理解:
- Monorepo 是什么?为什么选择它?
- Docker 如何让开发环境"一次配置,处处运行"?
- 前端、后端、数据库如何优雅地协同工作?
一、Monorepo:一体化的代码仓库
什么是 Monorepo?
Monorepo(单一仓库)是一种将所有相关项目的代码放在同一个版本控制仓库中的策略。与之相对的是 Multi-repo(多仓库),即每个项目都有自己的独立仓库。
生活化比喻:
- Multi-repo:像是一个小区,每栋楼(项目)有自己的物业(仓库)、自己的门禁(配置)
- Monorepo:像是一栋综合大楼,所有楼层(前端、后端、文档)共享同一个大堂(仓库)、同一套安保(CI/CD)
墨言项目的目录结构
让我们看看项目的根目录结构:
InkWords/
├── frontend/ # 前端代码(React + Vite)
├── backend/ # 后端代码(Go + Gin)
├── docker-compose.yml # 容器编排配置文件
├── .trae/ # 项目文档和规范
│ ├── rules/ # 工程规范
│ └── documents/ # 架构文档
└── README.md # 项目说明
为什么选择 Monorepo?
- 代码共享方便:前后端可以共享 TypeScript 类型定义、工具函数等
- 依赖管理统一:所有依赖版本集中管理,避免版本冲突
- 开发体验一致:一键启动所有服务,无需在多个仓库间切换
- CI/CD 简化:一次构建、测试、部署整个应用
二、Docker-First:环境一致性的保障
Docker 解决了什么问题?
在传统开发中,最让人头疼的问题之一就是 "在我电脑上能运行,为什么在你那里不行?"。这通常是因为:
- 操作系统不同(Windows/macOS/Linux)
- 运行时版本不同(Node.js 18 vs 20,Go 1.20 vs 1.21)
- 依赖库版本不同
- 环境变量配置不同
Docker 通过容器化技术,将应用及其所有依赖打包成一个标准化的单元,确保在任何地方运行结果一致。
Docker Compose:多服务的编排工具
当我们的应用包含多个服务(前端、后端、数据库)时,手动启动和管理每个容器会很麻烦。Docker Compose 就是解决这个问题的工具。
让我们逐行解析项目的 docker-compose.yml 文件:
yaml
# docker-compose.yml
version: '3.8' # 指定 Compose 文件格式版本
services: # 定义所有服务
db: # 数据库服务
image: postgres:14-alpine # 使用 PostgreSQL 14 的 Alpine 版本(轻量)
container_name: inkwords-db # 容器名称
environment: # 环境变量
POSTGRES_USER: inkwords # 数据库用户名
POSTGRES_PASSWORD: inkwords_password # 数据库密码
POSTGRES_DB: inkwords_db # 数据库名称
ports:
- "5432:5432" # 宿主机端口:容器端口
volumes:
- pgdata:/var/lib/postgresql/data # 数据持久化
restart: unless-stopped # 自动重启策略
healthcheck: # 健康检查
test: ["CMD-SHELL", "pg_isready -U inkwords -d inkwords_db"]
interval: 10s
timeout: 5s
retries: 5
backend: # 后端服务
build:
context: ./backend # 构建上下文目录
dockerfile: Dockerfile # Dockerfile 路径
container_name: inkwords-backend
env_file:
- ./backend/.env # 从文件加载环境变量
environment:
# 覆盖 .env 中的配置,使用容器网络连接数据库
- DATABASE_URL=postgres://inkwords:inkwords_password@db:5432/inkwords_db?sslmode=disable
- FRONTEND_URL=http://localhost
ports:
- "8081:8080" # 后端服务映射到 8081 端口
volumes:
- uploads:/app/uploads # 上传文件持久化
depends_on: # 依赖关系
db:
condition: service_healthy # 等待数据库健康检查通过
restart: unless-stopped
frontend: # 前端服务
build:
context: ./frontend
dockerfile: Dockerfile
container_name: inkwords-frontend
ports:
- "80:80" # HTTP 端口
- "5173:80" # 开发常用端口(兼容 OAuth 回调)
depends_on:
- backend # 依赖后端服务
restart: unless-stopped
volumes: # 定义持久化卷
pgdata: # 数据库数据卷
uploads: # 上传文件卷
关键概念解释
1. 容器间通信
注意后端连接数据库的 URL:
yaml
DATABASE_URL=postgres://inkwords:inkwords_password@db:5432/inkwords_db?sslmode=disable
这里的 db 不是 localhost,而是服务名。Docker Compose 会自动创建一个内部网络,容器间可以通过服务名相互访问。
2. 数据持久化
yaml
volumes:
- pgdata:/var/lib/postgresql/data
这行配置创建了一个名为 pgdata 的 Docker Volume,将容器内的数据库数据持久化到宿主机。即使容器被删除,数据也不会丢失。
3. 健康检查
yaml
healthcheck:
test: ["CMD-SHELL", "pg_isready -U inkwords -d inkwords_db"]
后端服务通过 condition: service_healthy 确保数据库完全启动后才启动,避免了连接失败的问题。
三、多阶段构建:优化镜像体积
为什么需要多阶段构建?
直接构建的 Docker 镜像往往包含编译工具、源代码等不必要的文件,导致镜像体积庞大。多阶段构建可以显著减小最终镜像的大小。
后端 Dockerfile 解析
dockerfile
# backend/Dockerfile
# 第一阶段:构建阶段
FROM golang:1.25-alpine AS builder # 使用 Go 官方镜像作为构建环境
WORKDIR /app # 设置工作目录
# 安装必要的系统依赖
RUN apk add --no-cache git tzdata
# 下载依赖(利用 Docker 层缓存)
COPY go.mod go.sum ./ # 先复制依赖文件
RUN go mod download # 下载依赖(这一层会被缓存)
# 复制源码并构建
COPY . . # 复制所有源代码
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server # 编译
# 第二阶段:运行阶段
FROM alpine:3.19 # 使用极简的 Alpine 作为运行环境
WORKDIR /app
# 设置时区为上海,并安装必要的运行时依赖
RUN apk add --no-cache tzdata ca-certificates git && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
# 从构建阶段复制编译产物
COPY --from=builder /app/server .
# 暴露端口
EXPOSE 8080
# 启动服务
CMD ["./server"]
关键优化点:
- 分离构建和运行环境:构建阶段使用完整的 Go 环境,运行阶段使用极简的 Alpine
- 利用层缓存 :先复制
go.mod和go.sum,这样依赖下载层可以被缓存,加快构建速度 - 静态编译 :
CGO_ENABLED=0确保生成静态二进制文件,不依赖系统库
前端 Dockerfile 解析
dockerfile
# frontend/Dockerfile
# 第一阶段:构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
# 复制 package 文件(利用缓存)
COPY package*.json ./
# 安装依赖
RUN npm ci # 使用 ci 命令确保依赖版本精确
# 复制源码并构建
COPY . .
RUN npm run build # 执行构建命令
# 第二阶段:Nginx 运行阶段
FROM nginx:alpine
# 设置时区为上海
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
# 移除默认的 Nginx 静态文件
RUN rm -rf /usr/share/nginx/html/*
# 复制自定义 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 从构建阶段复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 暴露端口
EXPOSE 80
# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
四、一键启动:完整的开发体验
启动步骤
- 克隆项目
bash
git clone https://github.com/2692341798/InkWords.git
cd InkWords
- 配置环境变量
bash
# 复制后端环境变量模板
cp backend/.env.example backend/.env
# 编辑 .env 文件,配置必要的参数
- 一键启动所有服务
bash
docker compose up -d --build
- 查看服务状态
bash
docker compose ps
- 访问应用
- 前端:http://localhost
- 后端 API:http://localhost:8081
- 查看日志
bash
# 查看所有服务日志
docker compose logs -f
# 查看特定服务日志
docker compose logs -f backend
- 停止服务
bash
docker compose down
架构流程图
外部服务
Docker 内部网络
静态文件请求
API 请求 /api/*
用户访问 http://localhost
Nginx 前端容器
请求类型判断
返回 React 构建产物
代理到后端容器
Go 后端容器
数据库操作
PostgreSQL 容器
调用 LLM API
DeepSeek API
五、工程规范:保证代码质量
后端规范要点
-
目录结构标准化
backend/
├── cmd/server/ # 程序入口
├── internal/ # 内部包(外部无法导入)
│ ├── api/ # HTTP 处理器
│ ├── service/ # 业务逻辑
│ ├── parser/ # 代码解析器
│ └── llm/ # LLM 相关逻辑
├── pkg/ # 可公开的包
└── go.mod # 依赖管理 -
依赖注入模式
go
// 示例:使用依赖注入的业务服务
type BlogService struct {
db *gorm.DB
llm LLMClient
parser CodeParser
}
func NewBlogService(db *gorm.DB, llm LLMClient, parser CodeParser) *BlogService {
return &BlogService{
db: db,
llm: llm,
parser: parser,
}
}
前端规范要点
- 组件化开发
jsx
// 示例:函数式组件
const BlogEditor = ({ content, onSave }) => {
const [text, setText] = useState(content);
return (
<div className="editor-container">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
className="w-full h-64 p-4 border rounded"
/>
<button
onClick={() => onSave(text)}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
>
保存
</button>
</div>
);
};
- 状态管理分离
javascript
// 示例:Zustand store
import { create } from 'zustand';
const useBlogStore = create((set) => ({
blogs: [],
loading: false,
fetchBlogs: async () => {
set({ loading: true });
const response = await api.get('/api/blogs');
set({ blogs: response.data, loading: false });
},
addBlog: async (blog) => {
const response = await api.post('/api/blogs', blog);
set((state) => ({ blogs: [...state.blogs, response.data] }));
}
}));
总结
通过今天的讲解,你应该已经理解了:
- Monorepo 的优势:统一管理、简化协作、提升开发体验
- Docker 的价值:环境一致性、依赖隔离、简化部署
- 多阶段构建的重要性:减小镜像体积、提高安全性
- 工程规范的必要性:保证代码质量、便于团队协作
墨言博客助手的架构设计遵循了 "简单、清晰、可维护" 的原则。我们使用成熟的技术栈,结合合理的架构模式,打造了一个既适合学习又适合生产的全栈项目。
实践建议
- 本地开发 :直接使用
docker compose up启动所有服务 - 代码修改 :修改代码后,使用
docker compose up -d --build重新构建 - 调试技巧 :
- 使用
docker compose logs -f实时查看日志 - 使用
docker exec -it inkwords-backend sh进入容器内部
- 使用
- 生产部署 :同样的
docker-compose.yml稍作修改即可用于生产环境
记住:好的架构不是一开始就完美无缺的,而是在不断迭代中逐渐完善的。理解这些设计原则,比记住具体配置更重要。
下期预告:后端基石:Go 项目初始化与数据库模型设计
在下一篇文章中,我们将深入后端代码,学习:
- 如何初始化一个标准的 Go 项目结构
- 如何使用 GORM 设计数据库模型
- 如何实现数据库迁移和种子数据
- 如何编写可测试的业务逻辑代码
准备好你的 Go 开发环境,我们下期见!