Vue SSR(Vite)如何在服务端预取数据并阻塞页面渲染?
解决Vue SSR服务端数据预取问题
核心问题分析
你目前的问题在于:虽然用了<Suspense>和顶层await,但服务端渲染流程没有等待异步数据加载完成就生成了HTML,导致客户端拿到的是空页面,后续再通过客户端请求补全数据。要实现服务端预取并交付完整渲染的页面,需要从服务端渲染流程配置、状态管理、组件数据预取逻辑三个层面配合调整。
步骤1:调整服务端渲染入口,等待异步依赖
服务端入口文件(通常是src/entry-server.ts)需要确保路由就绪后,等待所有<Suspense>的异步操作完成再生成HTML。关键是要异步执行renderToString并等待结果:
import { createSSRApp } from 'vue' import { createStore } from './store' import App from './App.vue' import { createRouter } from './router' import { renderToString } from '@vue/server-renderer' export async function render(url: string) { // 每个请求创建独立的store和router实例,避免跨请求状态污染 const store = createStore() const router = createRouter() const app = createSSRApp(App) app.use(store) app.use(router) // 先跳转到目标路由,等待路由就绪 await router.push(url) await router.isReady() // 等待所有Suspense异步依赖完成,生成完整HTML const html = await renderToString(app) // 提取服务端预取的状态,后续传给客户端做hydration const initialState = store.state return { html, initialState } }
步骤2:客户端注入服务端初始状态
客户端入口(src/entry-client.ts)需要接收服务端传递的初始状态,在挂载应用前注入到store,避免客户端重复请求数据,同时保证hydration时状态一致:
import { createApp } from 'vue' import { createStore } from './store' import App from './App.vue' import { createRouter } from './router' // 从服务端渲染的HTML中获取全局初始状态 const initialState = (window as any).__INITIAL_STATE__ const store = createStore(initialState) const router = createRouter() const app = createApp(App) app.use(store) app.use(router) // 等待路由就绪后再挂载 await router.isReady() app.mount('#app')
同时,在服务端返回的HTML模板中,需要把初始状态注入为全局变量:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Vue SSR App</title> </head> <body> <div id="app"><!--vue-ssr-outlet--></div> <!-- 注入服务端初始状态 --> <script> window.__INITIAL_STATE__ = JSON.parse('{{ JSON.stringify(initialState) }}') </script> <script type="module" src="/src/entry-client.ts"></script> </body> </html>
步骤3:优化组件数据预取逻辑
组件中的顶层await可以保留,但要加入状态存在性检查,避免客户端hydration时重复请求数据:
<template> <ul> <li v-for="item in activeItems" :key="item.id">{{ item.title }}</li> </ul> </template> <script setup lang="ts"> import { useStore } from '../store'; import { createClient } from '../api'; const api = createClient(); const store = useStore(); // 仅当store中无数据时才发起请求,服务端和客户端通用 if (!store.state.items.length) { const res = await api.getItems(); res.data.forEach(item => store.commit('addItem', item)); } const activeItems = computed(() => store.state.items.filter(x => x.active)); </script>
关键注意事项
- 避免跨请求状态污染:每个请求必须创建独立的store、router实例,不能复用单例。
- API请求环境适配:服务端发起API请求时可能需要使用绝对路径(比如
http://api.example.com),客户端可以用相对路径,需要在createClient中根据环境区分。 - 确保
<Suspense>包裹正确:必须在路由视图外层用<Suspense>包裹,确保所有路由组件的异步依赖都被捕获。
内容的提问来源于stack exchange,提问作者Sam
相关产品推荐
相关产品推荐

