Feature-Sliced Design 架构在现代健身平台开发中的实践与思考
1. 项目概述一个现代健身教练平台的诞生如果你和我一样曾经在健身房里对着手机试图从一堆零散的笔记、截图和记忆里拼凑出今天的训练计划或者在网上搜索某个动作的正确姿势结果却陷入各种广告和矛盾信息的海洋那么你一定能理解一个统一、开源、且真正好用的健身平台是多么稀缺。Workout.cool 正是为了解决这些痛点而生的。它不仅仅是一个工具更像是一个数字化的私人教练和训练日志的集合体。你可以把它想象成一个开源的、可自托管的“健身版 Notion”它集成了庞大的动作库、个性化的训练计划制定以及详尽的进度追踪功能。这个项目的核心价值在于它的“完整性”和“自主权”。市面上不缺健身App但要么是闭源的数据不由你掌控要么功能单一只能记录不能规划要么就是充斥着昂贵的订阅和广告。Workout.cool 的愿景是提供一个从动作库查询、计划编排到执行记录的全链路解决方案并且所有代码都公开透明你可以完全部署在自己的服务器上你的训练数据永远只属于你自己。它适合所有健身爱好者从刚刚踏入健身房的新手需要清晰的动作指导和计划模板到有多年经验的老手希望精细化管理自己的周期化训练和长期数据。2. 核心架构设计为什么选择 Feature-Sliced Design当我开始重构这个项目时技术选型的首要原则就是“可维护性”和“可扩展性”。一个健身平台的功能会不断演进可能会加入社交功能、饮食追踪、AI动作识别等。如果代码结构从一开始就混乱那么添加新功能将变成一场灾难。这就是我坚定采用Feature-Sliced Design (FSD)架构的原因。2.1 FSD 架构深度解析FSD 不是一个新的框架而是一种组织前端代码的方法论。它的核心思想是按业务功能Feature来切片Slice而不是按技术角色如 components, pages, utils来分层。传统的 MVC 或按技术分层的模式在项目变大后经常会出现“牵一发而动全身”的情况。比如修改一个“训练计划”的功能你可能需要同时改动components/、pages/、store/、api/等多个目录下的文件逻辑分散认知负担极重。FSD 通过严格的层级依赖规则解决了这个问题。在 Workout.cool 的src/目录下层级是这样的shared/: 最底层的共享资源如 UI 组件库基于 shadcn/ui、工具函数、类型定义、配置文件。这一层不包含任何业务逻辑可以被任何上层引用。entities/: 领域实体如User用户、Exercise训练动作、WorkoutPlan训练计划。这里定义了这些核心业务对象的数据结构、基础操作如数据验证的 schema和与它们相关的最纯粹的业务逻辑。它依赖于shared/。features/:这是 FSD 的核心。每个独立的业务功能都是一个独立的文件夹。例如features/exercise-management/动作管理、features/workout-planning/计划制定、features/auth/用户认证。每个feature内部是自包含的拥有自己的ui/组件、model/状态、hooks、lib/工具函数、api/数据请求。feature可以依赖entities和shared但绝对不能直接依赖另一个feature。这保证了功能的隔离性。widgets/: 由多个features组合而成的、可重用的复杂 UI 块。例如一个侧边栏 (Sidebar) 可能集成了用户信息 (feature/auth)、导航菜单涉及多个功能链接。它依赖于features和更下层。processes/: 描述跨多个功能的复杂用户流程。例如“用户从选择动作到生成一个完整计划并保存”这个流程可能涉及exercise-management、workout-planning等多个 feature。这一层在 Workout.cool 的当前版本中可能用得不多但为未来复杂交互预留了空间。app/: 这是 Next.js App Router 的入口主要定义页面路由 (page.tsx)、布局 (layout.tsx) 和加载状态。它负责将widgets和features组装成具体的页面。这种结构的好处是显而易见的。当我要开发“动作收藏夹”功能时我只需要在features/exercise-management/下新增相关的组件和逻辑或者创建一个新的features/exercise-favorites/。我不需要担心会意外破坏“训练计划”模块的代码。新成员加入项目也能通过功能目录快速定位相关代码极大降低了上手成本。实操心得FSD 的启动成本与长期收益采用 FSD 在项目初期确实需要更多的设计思考你要清晰地划分功能边界。有时会纠结某个逻辑应该放在entity还是feature里。我的经验法则是如果这个逻辑是某个实体与生俱来的、不依赖特定 UI 或流程的就放在entity比如计算某个动作的 1RM 最大重量公式如果这个逻辑是为了支持某个特定的用户交互比如一个可搜索、可过滤的动作列表那就放在feature里。虽然前期多花了一点时间但当项目迭代到第三个大版本时我们几乎没有遇到“代码不敢改”的情况新增功能的速度反而越来越快。2.2 技术栈选型背后的逻辑确定了架构接下来是具体的技术选型。每一环的选择都经过了深思熟虑Next.js (App Router): 这是全栈 React 框架的当前最佳实践。App Router 提供了基于文件系统的路由、服务端组件、流式渲染等现代特性。对于 Workout.cool 这种内容驱动型应用服务端渲染 (SSR) 或静态生成 (SSG) 对首屏性能和 SEO 至关重要。例如动作库的列表页完全可以静态生成而用户个人的训练记录页则需要服务端动态渲染并保护。TypeScript: 对于任何严肃的项目TypeScript 都是必选项。它能在编译时捕获大量潜在的错误比如错误地传递了Exercise对象的属性提供了极佳的代码提示和文档功能让团队协作和后期维护变得轻松。Prisma PostgreSQL: 数据库层需要处理复杂的关系数据用户、计划、计划中的每日训练、每日训练中的具体动作组。Prisma 以其类型安全的 ORM 著称其schema.prisma文件本身就是一份极佳的数据模型文档。配合 PostgreSQL 的 JSONB 字段我们可以灵活地存储训练计划的结构如超级组、递减组同时保持强大的查询能力。Tailwind CSS shadcn/ui: 样式方案选择了功能类优先的 Tailwind CSS它能实现极致的定制化和开发效率。而基于 Radix UI 原语构建的 shadcn/ui 组件库则提供了无障碍、样式可定制、代码属于项目本身的精美组件。这避免了传统 UI 库的捆绑依赖和样式冲突问题。Better-Auth: 身份认证是一个复杂且容易出错的部分。Better-Auth 是一个新兴的、全栈类型安全的认证库它简化了会话管理、OAuth 集成如 Google/GitHub 登录等流程让我们能更专注于业务逻辑。这套技术栈组合确保了项目在开发体验、性能、类型安全和长期可维护性上都有一个坚实的基础。3. 核心功能实现与实操要点3.1 动作数据库的构建与导入一个健身平台的核心资产就是它的动作库。Workout.cool 包含一个涵盖力量训练、有氧、柔韧性等类别的综合性动作数据库。每个动作都包含多语言名称、详细图文描述、视频演示链接、关联的主要肌肉群、所需器械等属性。数据结构设计 在 Prisma Schema 中Exercise模型是核心。除了基础字段我们通过一个关联模型ExerciseAttribute来处理动作的多样化属性。这种“实体-属性-值”EAV模式的变体允许我们灵活地为动作添加任意类型的标签而无需频繁修改数据库结构。model Exercise { id String id default(cuid()) name String // 本地化名称 nameEn String // 英文名称 description String? // 本地化描述 (HTML) descriptionEn String? // 英文描述 videoUrl String? // 演示视频URL thumbnailUrl String? // 视频缩略图 slug String unique // 用于生成友好URL attributes ExerciseAttribute[] // 关联的属性 map(exercises) } model ExerciseAttribute { id String id default(cuid()) exercise Exercise relation(fields: [exerciseId], references: [id], onDelete: Cascade) exerciseId String name AttributeName // 枚举如 PRIMARY_MUSCLE, EQUIPMENT, TYPE value String // 对应的值如 “CHEST”, “BARBELL”, “STRENGTH” unique([exerciseId, name, value]) map(exercise_attributes) } enum AttributeName { PRIMARY_MUSCLE SECONDARY_MUSCLE EQUIPMENT TYPE // STRENGTH, CARDIO, etc. DIFFICULTY }CSV 数据导入实操 项目提供了完整的脚本 (pnpm run import:exercises-full) 来从 CSV 文件导入数据。这是初始化或批量更新动作库的关键。准备 CSV 文件脚本要求一个特定格式的 CSV。关键点在于一个动作的多条属性如主要肌肉、器械、类型需要拆分成多行但共享同一个id。这对应了数据库中的一对多关系。运行导入命令确保数据库已启动并运行迁移。命令会读取 CSV解析每一行创建或更新Exercise记录并为其创建关联的ExerciseAttribute。处理关联与去重脚本内部使用了事务Transaction来确保一个动作及其所有属性的原子性操作。unique([exerciseId, name, value])这个复合唯一索引防止了重复属性的插入。注意事项数据一致性与清洗在实际操作中最大的坑往往来自原始数据。从不同来源爬取或整理的动作数据格式往往不统一。在导入前必须进行严格的数据清洗检查id是否唯一且稳定确保name和nameEn非空验证videoUrl的格式将attribute_value规范化为预定义的枚举值如“胸大肌”统一为“CHEST”。我建议编写一个小的预处理脚本使用papaparse库读取 CSV用Joi或Zod进行验证和转换然后再交给导入脚本。这能避免因脏数据导致整个导入过程失败。3.2 训练计划引擎的设计训练计划模块是平台的“大脑”。它需要允许用户自由地创建结构化计划例如“为期 12 周的增肌计划每周训练 4 天每天包含 4-6 个动作每个动作有特定的组数、次数、休息时间”。数据模型设计 这里采用了嵌套的树状结构来模拟计划的层级。WorkoutPlan: 顶层计划包含名称、描述、周期如 12 周、创建者等信息。PlanWeek: 计划中的某一周。一个计划有多周。PlanDay: 某一周中的某一天如“周一胸肩三头”。一天包含多个训练项目。PlanItem: 一天中的一个具体训练项目。它关联到一个Exercise动作并包含具体的执行参数sets组数、reps次数可以是范围如“8-12”、restSec组间休息秒数、rpe自觉强度等。order字段用于控制当天动作的排序。这种设计非常灵活可以描述从最简单的“练一天休一天”到复杂的“非线性周期计划”。通过 GraphQL 或精心设计的 RESTful API前端可以按需加载整个计划树或其中一部分。前端状态管理 在features/workout-planning/model/目录下我们使用 React Context useReducer或 Zustand 这样的轻量级状态库来管理计划创建时的复杂状态。为什么不用 Redux对于这个特定功能其状态逻辑相对独立且复杂一个专用的 Store 比全局 Redux 更清晰。状态需要处理当前正在编辑的计划草稿、临时添加的动作、对组数次数等的实时修改、以及保存前的验证。UI/UX 实现 计划创建界面大量使用了dnd-kit库来实现拖拽排序——用户可以直接拖动PlanDay来调整周内顺序拖动PlanItem来调整当天动作顺序。每个可编辑的字段如次数、休息时间都使用shadcn/ui的Input或Slider组件并配有即时验证和防抖保存提供流畅的编辑体验。3.3 训练记录与进度追踪计划制定了执行才是关键。训练记录模块负责记录每一次训练的实际完成情况。数据模型扩展 在PlanItem的基础上我们创建了WorkoutSession和WorkoutSet模型。WorkoutSession: 记录一次训练会话关联到具体的PlanDay和用户包含训练开始结束时间、主观感受笔记等。WorkoutSet: 记录PlanItem中每一组的实际完成数据。包括setNumber第几组、actualReps实际次数、actualWeight实际重量、isCompleted是否完成。这里的设计允许用户记录与计划不符的情况比如只做了 8 次而不是计划的 10 次。前端交互逻辑 训练执行界面需要极度简洁和高效因为用户可能在健身房手指可能沾着镁粉。我们设计了一个全屏或接近全屏的视图以大字体、大按钮显示当前动作、目标组数次数、以及记录实际数据的输入框。通常使用数字键盘或大号“/-”按钮来快速输入重量和次数。完成一组后自动启动休息计时器并高亮提示下一组。进度可视化 这是体现平台价值的地方。在features/progress-tracking/ui/中我们使用recharts或visx库来绘制图表。例如力量进度图针对某个特定动作如卧推以时间为 X 轴以每次训练最佳一组重量 x 次数换算成的估算 1RM为 Y 轴绘制折线图清晰展示力量增长趋势。训练量追踪计算每周的总训练量重量 x 组数 x 次数用柱状图展示帮助用户管理疲劳和恢复。个人记录 (PR) 看板自动检测并高亮显示用户在各动作上的历史最佳成绩。这些数据的背后是服务端聚合查询。Prisma 的聚合函数如groupBy,_max,_avg在这里发挥了巨大作用可以高效地从海量的WorkoutSet记录中提取出用户关心的摘要信息。4. 本地开发与自部署全指南4.1 基于 Docker 的一键开发环境为了让任何贡献者都能在几分钟内启动项目我们提供了完善的 Docker 开发环境。Makefile和docker-compose.yml是这里的功臣。环境配置详解复制环境变量cp .env.example .env。这个.env文件是配置的核心。你需要关注几个关键变量DATABASE_URL: Docker 环境下通常为postgresql://postgres:passwordpostgres:5432/workout_cool。注意主机名是postgres服务名不是localhost。NEXTAUTH_SECRET: 用于加密会话的密钥运行openssl rand -base64 32生成一个。NEXTAUTH_URL: 开发环境设为http://localhost:3000。启动服务运行make dev。这个命令背后依次执行了docker-compose up -d postgres: 启动 PostgreSQL 容器。pnpm prisma migrate dev: 在数据库容器中运行 Prisma 迁移创建所有表结构。pnpm prisma db seed: 执行种子脚本填充必要的初始数据如管理员用户、基础动作分类。pnpm dev: 启动 Next.js 开发服务器并开启了代码热重载。现在访问http://localhost:3000你应该能看到运行中的应用。这种将所有依赖数据库容器化的方式彻底解决了“在我机器上能跑”的环境问题。避坑技巧Docker 网络与数据库连接最常见的 Docker 开发问题是应用容器无法连接数据库容器。确保你的DATABASE_URL中的主机名与docker-compose.yml中定义的服务名一致。此外如果修改了docker-compose.yml中 PostgreSQL 的端口映射如5432:5432要确保应用连接的是容器内部端口第二个5432而不是你随意映射到宿主机的端口。使用docker-compose logs postgres可以查看数据库容器的启动日志确认它正在监听正确的端口。4.2 生产环境自部署方案将 Workout.cool 部署到你自己的 VPS 或云服务上完全掌控你的数据。方案一使用 Docker Compose推荐这是最简单的方式。你需要一个安装了 Docker 和 Docker Compose 的 Linux 服务器如 Ubuntu 22.04。上传文件将项目根目录下的docker-compose.prod.yml需要从示例文件复制并调整、.env.production和Dockerfile上传到服务器。配置生产环境变量编辑.env.production必须修改DATABASE_URL: 指向你的生产数据库。强烈建议使用云数据库服务如 AWS RDS, Supabase或独立管理的 PostgreSQL 实例而不是在同一个容器内运行数据库用于生产因为这关系到数据持久性和备份。NEXTAUTH_URL: 设置为你的公网域名如https://your-workout-domain.com。设置强密码并禁用不必要的服务。构建与运行# 拉取最新代码如果你通过 Git 管理 git pull origin main # 使用生产配置启动 docker-compose -f docker-compose.prod.yml up -d --build-d代表后台运行--build会重新构建镜像。Docker Compose 会处理网络、依赖顺序等所有事情。方案二传统服务器部署如果你更熟悉传统的部署方式可以不用 Docker。服务器准备在服务器上安装 Node.js (v18)、pnpm、PostgreSQL。克隆项目并安装依赖git clone https://github.com/Snouzy/workout-cool.git cd workout-cool pnpm install --prod # 仅安装生产依赖构建应用pnpm build这会生成一个优化的生产版本在.next目录下。运行数据库迁移export DATABASE_URL你的生产数据库URL npx prisma migrate deploy注意这里用的是deploy而不是dev它适用于生产环境的无锁迁移。启动服务可以使用 PM2 来管理进程确保应用崩溃后自动重启。npm install -g pm2 pm2 start pnpm --name workout-cool -- start pm2 save pm2 startup # 设置开机自启关键生产优化静态资源托管将public/文件夹下的静态文件如图标、LOGO通过 Nginx 或 CDN 直接提供减轻 Next.js 服务器负担。反向代理使用 Nginx 或 Caddy 作为反向代理处理 SSL 证书HTTPS、Gzip 压缩、静态文件缓存等。数据库备份必须设置定期的 PostgreSQL 数据库备份策略。可以使用pg_dump命令配合 cron 任务或者使用云数据库的自动备份功能。监控与日志配置 PM2 或 Docker 的日志轮转避免日志占满磁盘。使用简单的监控如 Uptime Kuma来确保服务在线。5. 常见问题排查与社区贡献5.1 开发与部署问题速查表问题现象可能原因解决方案pnpm install失败网络错误网络问题或 pnpm 镜像源问题1. 检查网络连接。2. 切换 npm/pnpm 镜像源pnpm config set registry https://registry.npmmirror.com。3. 删除node_modules和pnpm-lock.yaml后重试。开发服务器启动后页面显示“数据库连接错误”1..env文件未配置或DATABASE_URL错误。2. PostgreSQL 服务未启动。3. 数据库未创建。1. 确认已复制.env.example到.env并正确填写。2. 运行docker-compose ps检查 postgres 容器状态。3. 运行make db-reset谨慎会清空数据或手动检查数据库。运行prisma migrate时出现版本冲突多人协作时本地数据库迁移版本与远程不同步。1. 拉取最新代码。2. 运行npx prisma migrate resolve解决冲突或备份数据后运行npx prisma migrate reset。生产环境访问慢图片加载延迟未配置 CDN 或反向代理缓存。1. 配置 Nginx 对/_next/static和public/目录设置长期缓存。2. 考虑将用户上传的图片如头像存储到对象存储如 S3并通过 CDN 分发。上传文件如用户头像功能报错服务器文件系统权限不足或未配置正确的文件存储路径。1. 检查应用运行用户对上传目录的读写权限。2. 在生产环境强烈建议集成云存储服务而不是使用本地文件系统。邮件发送功能如重置密码失效未配置 SMTP 环境变量。检查.env.production中EMAIL_SERVER_HOST,EMAIL_SERVER_PORT,EMAIL_SERVER_USER,EMAIL_SERVER_PASSWORD等变量是否正确配置。可以使用 SendGrid、Mailgun 等第三方服务。5.2 如何有效参与开源贡献Workout.cool 是一个社区驱动的项目我非常欢迎并依赖社区的贡献。为了让你的贡献过程更顺畅这里有一些建议第一步寻找切入点查看 Issues项目 GitHub 仓库的 Issues 页面列出了已知的 Bug、功能请求和“good first issue”适合新手的任务。这是最好的起点。从小处着手修复一个错别字、改进一个组件的文档、优化一个小的样式问题。这能帮助你熟悉项目的代码提交流程。沟通先行如果你打算解决一个较大的 Issue 或实现一个新功能最好先在 Issue 下留言说明你的计划。这可以避免重复劳动也能获得维护者的设计指导。第二步遵循开发流程Fork 仓库在 GitHub 上点击 Fork 按钮创建你个人账户下的副本。克隆你的仓库git clone https://github.com/你的用户名/workout-cool.git创建功能分支永远不要在main分支上直接开发。使用git checkout -b feat/your-feature-name或fix/issue-123这样的分支名。安装依赖并启动按照前文的“快速开始”指南在本地启动项目。进行修改遵循项目的代码风格使用 Prettier 和 ESLint 进行格式化检查。提交更改使用清晰的提交信息。推荐使用 Conventional Commits 格式如feat: 添加深色主题支持、fix(ui): 修复移动端侧边栏滚动问题。推送并创建 Pull Request (PR)将你的分支推送到你的 Fork然后在原仓库的 GitHub 页面上创建 PR。在 PR 描述中清晰地说明你做了什么、为什么这么做以及如何测试。代码风格与架构遵守遵循 FSD将新组件或逻辑放在正确的层级目录下。如果不确定可以在 PR 描述中提问。使用 TypeScript为所有新代码添加明确的类型定义。组件设计优先使用shadcn/ui的现有组件进行组合。如果需要全新的组件将其添加到shared/ui目录下并确保其可复用性。测试虽然项目目前单元测试覆盖不全但鼓励为复杂的工具函数或 hooks 添加测试。可以在__tests__目录下创建测试文件。维护一个活跃健康的开源项目离不开每一位贡献者的热情和严谨。每一次代码提交、每一次问题反馈、甚至每一次 Star都是让 Workout.cool 变得更好的动力。这个项目始于一个“拯救”的念头而它的未来则掌握在每一个相信开源健身工具价值的社区成员手中。