数据获取共置
数据获取共置让真正消费数据的组件或模块靠近对应数据请求,同时保留 Repository、Service、API Client 等领域与访问层边界。
[!info] related notes
- 所属 MOC:
- 前置概念:
- 并列概念:
- 易混淆概念:
- 关系笔记:
数据获取共置
一句话定义
数据获取共置(Data Fetching Colocation)是指:让数据请求靠近真正消费这份数据的组件或模块,而不是把所有请求机械集中在最顶层 Page;但访问数据库、调用服务和领域规则仍应保留清晰的数据层/服务层抽象。
为什么需要它
传统页面很容易写成:
DashboardPage
├── fetch profile
├── fetch assessments
├── fetch notifications
├── fetch ai insight
│
├── ProfilePanel
├── AssessmentPanel
├── NotificationPanel
└── AIInsightPanel
顶层组件同时负责:
- 所有数据获取
- 所有 loading / error 判断
- 大量 props 下发
- 所有请求的执行时序
随着页面增大,它会成为数据编排瓶颈。
共置后可以变成:
DashboardPage
├── ProfilePanel
│ └── getProfile()
├── AssessmentPanel
│ └── getAssessments()
├── NotificationPanel
│ └── getNotifications()
└── AIInsightPanel
└── getAIInsight()
谁真正依赖数据,谁更接近数据请求。
Next.js Server Components 为什么适合共置
Server Component 可以直接 await:
async function AssessmentPanel() {
const assessments = await getAssessments()
return <AssessmentList assessments={assessments} />
}
因此数据请求不必全部提升到 Page:
export default function DashboardPage() {
return (
<>
<ProfilePanel />
<AssessmentPanel />
<NotificationPanel />
</>
)
}
这可以让组件依赖更局部,也更容易配合 Suspense 定义独立等待边界。
共置不等于“在 JSX 里到处写 SQL”
错误理解:
async function GoalCard() {
const result = await db.$queryRaw`...`
// 大量 SQL / 事务 / 权限规则
}
如果每个 UI 组件直接操作底层数据源,会造成:
- SQL/ORM 细节散落
- 权限逻辑重复
- 难以测试
- UI 与 schema 强耦合
- 领域规则难以复用
更稳的结构是:
Server Component
↓
query / data function
↓
Repository / API Client
↓
Owning Service / DB
例如:
// data/assessments.ts
export async function getAssessmentsForUser(userId: string) {
return assessmentApi.listForUser(userId)
}
组件只表达它需要什么:
const assessments = await getAssessmentsForUser(userId)
UI 共置与 Domain Ownership 是两条轴
共置回答:
哪个组件最接近这份数据的消费点?
而 数据所有权与服务边界 回答:
谁有资格提供和修改这份数据?
因此:
Fetch near consumer
≠
Bypass owning service
即使 AssessmentPanel 自己发起数据获取,它也可以:
AssessmentPanel
→ assessmentApi.get(...)
→ Go Backend
而不是直接查 Go 服务数据库。
与 Props Drilling 的关系
没有共置时:
Page fetches A
↓
Container receives A
↓
Section receives A
↓
Leaf finally uses A
很多中间组件只是转发数据。
共置可以减少不必要的数据 props 链:
Leaf Server Component
→ own server-side data dependency
但如果多个兄弟组件必须共享同一个一致性快照,或者请求本来就应该在统一聚合层完成,也不必为了“共置”强行拆开。
与 Request Waterfall 的关系
顶层集中请求不一定慢;如果独立请求使用 Promise.all,它也可以很高效。
共置的价值更多在于:
- 依赖局部化
- 组件边界清晰
- 更自然地配合 Suspense
- 避免一个慢模块阻塞整个页面
与 Suspense / Streaming 的关系
假设 AI Insight 慢 5 秒:
<ProfilePanel />
<AssessmentPanel />
<Suspense fallback={<AIInsightSkeleton />}>
<AIInsightPanel />
</Suspense>
AIInsightPanel 自己获取数据,就能形成清晰的:
consumer boundary
+
data boundary
+
loading boundary
这比 Page 先统一等待所有数据,再把结果下发更利于局部 Streaming。
什么时候不应该下沉
多个组件需要完全相同的聚合结果
如果真正的业务对象就是“Dashboard Snapshot”,由后端一次性返回一个一致性视图,那么把它拆成 8 个独立请求可能反而破坏一致性和性能。
数据获取本质属于上层编排
例如:
先取得当前 workspace
→ 根据 workspace policy 决定后续哪些模块可见
上层本来就承担协调职责,此时不应该机械地下沉全部逻辑。
会造成 N+1
如果每个列表 Item 都自己向后端查询关联数据:
100 GoalCard
→ 100 requests
这不是健康的共置,而是请求粒度失控。
共置的真正边界
好的共置通常是:
UI/feature boundary
≈
data consumption boundary
但不是:
每个小组件
=
一个独立 HTTP/SQL 请求
最短记忆
数据靠近消费者,
访问逻辑留在 data/service layer,
业务 authority 留在 owning service。