项目结构
理解 ProductReady 的架构 - 代码在哪里,为什么这样组织
项目结构
ProductReady 遵循清晰、合理的结构。本指南帮助你理解代码的组织方式和添加新功能的位置。
刚接触 Next.js? 本项目遵循 Next.js 15+ App Router 约定,并增加了可扩展性的组织结构。
概览
apps/productready/
├── src/ # 所有应用代码
│ ├── app/ # Next.js 页面与路由
│ ├── components/ # React 组件
│ ├── db/ # 数据库 schema 与查询
│ ├── lib/ # 工具与配置
│ └── server/ # 服务端代码 (tRPC)
├── content/ # 文档 (MDX)
├── public/ # 静态资源
├── spec/ # 产品规格与设计文档
└── tests/ # 测试文件关键目录详解
src/app/ - Next.js 页面
这是你的页面和路由所在位置。Next.js 使用基于文件的路由。
文件夹结构 = URL 结构:
app/(home)/page.tsx→/(首页)app/dashboard/page.tsx→/dashboardapp/dashboard/tasks/page.tsx→/dashboard/tasksapp/api/health/route.ts→/api/health(API 端点)
路由组: (home) - 括号不影响 URL,只用于组织
特殊文件:
page.tsx- 实际页面组件layout.tsx- 共享布局包装器loading.tsx- 加载状态error.tsx- 错误边界route.ts- API 端点
新功能添加位置:
- 新页面 → 创建文件夹并添加
page.tsx - API 端点 → 在
app/api/中创建route.ts
Wouter 单页应用路由
ProductReady 使用 Wouter 在仪表盘页面内实现客户端 SPA(单页应用)路由。
架构模式:
- Next.js App Router 处理顶级路由并提供 SSR (
/dashboard,/agent,/kadmin) - Wouter 处理每个页面内的子路由,实现无刷新的 SPA 导航
示例 (app/dashboard/page.tsx):
import { Route, Switch } from 'wouter';
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';
export default function DashboardPage() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
{/* Wouter 路由实现 SPA 导航 */}
<Switch>
<Route path="/analytics">
<AnalyticsView />
</Route>
<Route path="/reports">
<ReportsView />
</Route>
<Route path="/">
<DashboardOverview />
</Route>
</Switch>
</SidebarInset>
</SidebarProvider>
);
}优势:
- ✅ 快速导航 - 子路由之间无需整页刷新
- ✅ 更好的用户体验 - 平滑过渡,保留侧边栏状态
- ✅ SEO 友好 - Next.js 处理初始 SSR,Wouter 处理客户端
- ✅ 独立模块 - 每个仪表盘页面有自己的 wouter 实例
使用场景:
- ✅ 带有多个视图的仪表盘部分 (
/dashboard,/agent,/kadmin) - ✅ 任何有标签页/导航且子路由无需 SSR 的页面
- ❌ 需要每个子路由都有 SEO 的公开页面(使用 Next.js App Router)
src/components/ - React 组件
按类别组织的可复用 UI 组件。
组织方式:
components/
├── ui/ # 通用 UI 组件 (Button, Card, Dialog)
├── auth/ # 认证相关 (LoginForm, AuthButton)
├── dashboard/ # 仪表盘专用组件
└── shared/ # 跨所有页面共享的组件何时创建组件:
- ✅ 在 2 个以上地方使用
- ✅ 复杂逻辑会让页面混乱
- ✅ 可复用的模式(表单、卡片、弹窗)
何时不创建:
- ❌ 一次性 UI(直接放在 page.tsx)
- ❌ 过度细粒度(不要提取每个
<div>)
KUI 库: 不要直接修改 packages/kui!在 src/components/ui/ 中创建包装组件。
src/db/ - 数据库层
所有数据库相关内容:schemas、migrations、查询。
关键文件:
schema/*.ts- 表定义(Drizzle ORM)index.ts- 数据库客户端导出seed.ts- 开发环境示例数据
示例 schema (schema/tasks.ts):
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const tasks = pgTable('tasks', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
description: text('description'),
status: text('status', {
enum: ['pending', 'in_progress', 'completed']
}).default('pending'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});新功能添加位置:
- 新表 → 创建
schema/your-table.ts - 查询 → 添加到 tRPC router(见
/server/routers/)
src/server/ - 服务端代码
后端逻辑:tRPC routers、procedures、middleware。
结构:
server/
├── routers/ # tRPC 路由(每个领域一个)
│ ├── tasks.ts # tasks.list, tasks.create 等
│ ├── users.ts # users.me, users.update 等
│ └── index.ts # 合并所有路由
├── trpc.ts # tRPC 配置与 procedures
└── index.ts # 主 tRPC app 导出示例 router (routers/tasks.ts):
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const tasksRouter = createTRPCRouter({
list: protectedProcedure
.input(z.object({ limit: z.number().default(20) }))
.query(async ({ ctx, input }) => {
return ctx.db.select().from(tasks).limit(input.limit);
}),
create: protectedProcedure
.input(z.object({ title: z.string() }))
.mutation(async ({ ctx, input }) => {
return ctx.db.insert(tasks).values(input);
}),
});更多参考
完整的文件结构和最佳实践,请参考英文版文档。