React SSR场景下如何仅发起一次API请求?
这确实是SSR开发里非常头疼的一个问题——两次相同的慢请求完全是在浪费资源和用户时间。好在有不少成熟的方案可以实现只发起一次请求,服务器端和客户端复用同一份数据,下面给你详细拆解:
一、通用方案:手动把服务器数据注入到HTML(无框架依赖)
如果是你自己搭建的SSR服务(比如用Express + ReactDOMServer),最直接的方式就是把服务器端获取到的数据序列化后,通过<script>标签挂载到全局对象上,客户端初始化时直接取用这份数据,跳过API请求。
服务器端代码示例:
// 1. 服务器端发起API请求获取数据 const fetchProjects = async () => { const res = await fetch('https://api.example.com/projects'); return res.json(); }; const projects = await fetchProjects(); // 2. 渲染React组件并把数据注入到HTML模板 const appHtml = ReactDOMServer.renderToString(<App projects={projects} />); const finalHtml = ` <!DOCTYPE html> <html> <head> <title>我的项目列表</title> </head> <body> <div id="root">${appHtml}</div> <!-- 把数据挂载到全局window对象 --> <script> window.__SSR_INITIAL_DATA__ = ${JSON.stringify(projects)}; </script> <!-- 加载客户端打包文件 --> <script src="/client.bundle.js"></script> </body> </html> `; // 返回HTML给客户端 res.send(finalHtml);
客户端代码示例:
// 1. 优先读取全局对象中的初始数据 const initialProjects = window.__SSR_INITIAL_DATA__; // 2. Hydrate时直接传入数据,无需再次请求 ReactDOM.hydrate( <App projects={initialProjects} />, document.getElementById('root') ); // 3. 清理全局变量,避免污染 delete window.__SSR_INITIAL_DATA__;
这个方案的好处是完全不依赖任何框架,自定义SSR场景下非常好用,唯一需要注意的是JSON.stringify处理数据时的兼容性(比如不能包含函数、循环引用等)。
二、框架自带方案:利用SSR框架的数据预取能力
如果你用的是Next.js、Nuxt.js这类成熟的SSR框架,它们已经内置了解决这个问题的机制,不用手动操作全局对象,框架会帮你自动完成数据的传递和复用。
以Next.js为例,使用getServerSideProps方法:
// 页面组件中导出getServerSideProps,仅在服务器端执行 export async function getServerSideProps() { // 服务器端发起API请求 const res = await fetch('https://api.example.com/projects'); const projects = await res.json(); // 返回的数据会作为props自动传递给页面组件 return { props: { projects } }; } // 页面组件直接接收props中的数据,客户端hydrate时不会重复请求 export default function ProjectsPage({ projects }) { return ( <div className="project-list"> {projects.map(project => ( <div key={project.id} className="project-item"> <h3>{project.name}</h3> <p>{project.description}</p> </div> ))} </div> ); }
框架会在服务器端执行getServerSideProps获取数据,渲染页面后把数据序列化注入到HTML中,客户端初始化时自动读取这些数据作为组件props,全程不需要你手动处理数据传递,非常省心。
三、状态管理方案:复用服务器端的Store状态
如果你的项目用了Redux、MobX这类状态管理库,可以在服务器端把数据填充到Store中,然后把Store的状态序列化后传递给客户端,客户端用这个初始状态初始化Store,组件从Store取数据时直接复用服务器端的结果。
服务器端代码示例(Redux):
// 1. 创建服务器端Store并填充数据 const store = configureStore(); await store.dispatch(fetchProjectsAction()); // 这个action内部发起API请求 // 2. 获取Store的初始状态并序列化 const initialState = JSON.stringify(store.getState()); // 3. 渲染组件并注入状态 const appHtml = ReactDOMServer.renderToString( <Provider store={store}> <App /> </Provider> ); const finalHtml = ` <!DOCTYPE html> <html> <head><title>我的项目列表</title></head> <body> <div id="root">${appHtml}</div> <script> window.__INITIAL_REDUX_STATE__ = ${initialState}; </script> <script src="/client.bundle.js"></script> </body> </html> `; res.send(finalHtml);
客户端代码示例(Redux):
// 1. 读取全局中的初始状态,创建客户端Store const initialState = window.__INITIAL_REDUX_STATE__; const store = configureStore(initialState); // 2. Hydrate时使用这个Store,组件从Store取数据时直接复用服务器端数据 ReactDOM.hydrate( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); delete window.__INITIAL_REDUX_STATE__;
这个方案适合状态复杂的大型项目,能确保服务器端和客户端的状态完全一致,避免hydrate时出现不匹配的问题。
核心思路其实都是服务器端获取数据后,把数据传递给客户端,让客户端直接复用,而不是重新发起请求。具体选哪种方案,完全取决于你的项目技术栈和复杂度:
- 自定义SSR场景:选方案一或方案三
- 用现成SSR框架:直接用框架自带的方案(比如Next.js的
getServerSideProps、Nuxt.js的asyncData)最省事
内容的提问来源于stack exchange,提问作者Philip Fabianek

