NextJS 13 App Router:服务端组件取数后更新子客户端组件
解决方案:NextJS 13 App Router 实现MongoDB实时排行榜更新
你的核心问题在于:服务端组件是一次性渲染的,渲染完成后User.watch()的回调在服务端运行,但无法直接更新客户端组件的状态;同时客户端不能直接调用MongoDB的watch(安全+兼容性问题),服务端也没法访问客户端Context。
推荐用**Server-Sent Events(SSE)**实现服务端到客户端的实时推送,这是最适合单向实时更新场景的方案,步骤如下:
1. 创建SSE API路由
新建app/api/leaderboard-updates/route.ts,在这个服务端路由里建立MongoDB的change stream,通过SSE把更新推给客户端:
import { connectToDatabase, User } from '@/lib/db'; import { NextResponse } from 'next/server'; export async function GET() { await connectToDatabase(); const stream = new ReadableStream({ async start(controller) { // 先发送初始用户数据 const initialUsers = await User.find({}).lean(); const formattedInitial = initialUsers.map(u => ({ uuid: u._id.toString(), name: u.name, money: u.money, online: u.online, })); controller.enqueue(`data: ${JSON.stringify({ type: 'initial', data: formattedInitial })}\n\n`); // 监听MongoDB数据变化 const changeStream = User.watch(); changeStream.on('change', async () => { // 获取最新用户列表(也可以根据change事件做局部更新优化性能) const latestUsers = await User.find({}).lean(); const formatted = latestUsers.map(u => ({ uuid: u._id.toString(), name: u.name, money: u.money, online: u.online, })); controller.enqueue(`data: ${JSON.stringify({ type: 'update', data: formatted })}\n\n`); }); // 连接关闭时清理资源 return () => { changeStream.close(); controller.close(); }; }, }); return new NextResponse(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }); }
2. 修改客户端Leaderboard组件
在客户端组件里通过EventSource监听SSE连接,收到更新后同步本地状态:
"use client"; import { useState, useEffect } from 'react'; import { UserDAO } from '@/models/user'; interface Props { initialUsers: UserDAO[]; } export default function Leaderboard(props: Props) { const [users, setUsers] = useState(props.initialUsers.filter(it => !it.vip)); useEffect(() => { const eventSource = new EventSource('/api/leaderboard-updates'); eventSource.onmessage = (event) => { const payload = JSON.parse(event.data); // 处理初始数据或更新数据 if (payload.type === 'initial' || payload.type === 'update') { const filteredUsers = payload.data.filter((u: UserDAO) => !u.vip); setUsers(filteredUsers); } }; eventSource.onerror = (error) => { console.error('SSE连接出错:', error); eventSource.close(); }; // 组件卸载时关闭连接 return () => { eventSource.close(); }; }, []); return ( <div className="block max-w-full p-4 m-0 border rounded-lg shadow bg-gray-800 border-gray-700"> <div className={`grid grid-cols-1 grid-rows-${users.length} gap-16 gap-y-6 grid-flow-row`}> { // 注意:先拷贝数组再排序,避免修改原数组 [...users].sort((a, b) => b.money - a.money).map((user, index) => ( <LeaderboardEntry key={user.uuid} user={user} place={index + 1} /> )) } </div> </div> ); }
3. 调整服务端Home组件
服务端组件只需要获取初始数据传给客户端,不需要再处理watch逻辑:
export default async function Home() { await connectToDatabase(); const initialUsers = await getUsers(); return ( <Leaderboard initialUsers={initialUsers}/> ); } async function getUsers(): Promise<UserDAO[]> { const users = await User.find({}).lean(); return users.map(it => ({ uuid: it._id.toString(), name: it.name, money: it.money, online: it.online, })); }
为什么之前的方法行不通?
- 服务端组件是一次性渲染的,渲染完成后
User.watch()的回调在服务端运行,但服务端没有能力直接更新客户端组件的状态。 - React Context的
useContext只能在客户端组件中使用,服务端组件无法访问客户端的Context状态,所以没法通过Context传递更新。 - 客户端直接调用Mongoose的
watch()会暴露数据库连接信息等敏感内容,而且Mongoose的schema在客户端环境下会有兼容性问题,本身就不适合在客户端运行。
内容的提问来源于stack exchange,提问作者skyelander
相关产品推荐
相关产品推荐

