You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 17:55:23