先记住这个答案
onServerPrefetch 是服务端渲染专用生命周期钩子。在 setup 中同步注册后,回调可以返回 Promise;服务端渲染器会等待它完成,再继续产出该组件对应的 HTML。因此它和 onMounted 的方向不同:onServerPrefetch 只在服务端调用,onMounted 只在客户端挂载后调用。组件通常在预取中填充响应式状态,并把数据序列化给客户端复用,避免 hydration 后重复请求。失败策略必须明确,是让渲染失败、展示可降级状态,还是由上层缓存返回旧数据。
- 服务端渲染器等待返回的 Promise
- onMounted 不参与服务端首份 HTML
- 预取结果应序列化以避免客户端重复请求
它解决首份 HTML 缺少异步内容的问题
若服务端只同步渲染 loading,再由浏览器 mounted 后请求,搜索引擎和用户收到的首份 HTML 都没有主体数据。onServerPrefetch 让组件声明服务端必须完成的任务,数据写入 ref 后,随后生成的 HTML 就能包含内容。
代价是请求进入服务端响应关键路径。需要为数据源设置超时、缓存和失败策略,不能让单个下游永久阻塞整页。列表页还要避免每个子组件独立请求同一资源造成瀑布或重复访问。
把加载器注入组件以验证状态转换
下例不把网络细节写死在组件中。onServerPrefetch 返回 load 的 Promise,并在成功后写入 message;真正的 SSR 测试可传入受控 Promise,确认 renderToString 在解析前未完成,解析后 HTML 包含结果。
客户端直接挂载这段组件时 onServerPrefetch 不运行,因此还需从服务端注入已有状态,或者在 onMounted 中仅对缺失数据补载。两端共用加载函数时要用稳定缓存键去重。
import { h, onServerPrefetch, ref } from 'vue'
function createSsrMessage(load: () => Promise<string>) {
return {
setup() {
const message = ref('loading')
onServerPrefetch(async () => {
message.value = await load()
})
return () => h('p', message.value)
}
}
}服务端传入解析为 hello 的加载器时,渲染器等待后输出含 hello 的段落;普通客户端挂载不会调用该钩子,初态来源和补载规则必须由应用补全。
一致性比“服务端请求成功”多一步
服务端得到的数据若没有嵌入页面或状态容器,客户端 hydration 时仍可能从 loading 开始或重复请求,造成文本不一致和额外流量。序列化内容还要过滤私密字段,并安全转义后再写入文档。
使用 Nuxt 等上层框架时,优先采用框架的数据获取与缓存原语,因为它们通常已经连接服务端等待、载荷传输和客户端复用。直接用 onServerPrefetch 更适合解释底层语义或构建自定义 SSR 集成。
容易答错的地方
- 在 onServerPrefetch 中只启动任务却不返回 Promise
- 渲染器必须拿到可等待的 Promise 才知道任务何时结束。async 回调会自动返回 Promise;手写回调则应显式 return 加载链。
- 把浏览器专用对象放进预取逻辑
- 该钩子在服务端运行,没有 window、document 或浏览器存储。请求身份和区域信息应由 SSR 请求上下文显式传入,避免模块级共享导致跨请求串数据。
面试官还会怎么问?
onServerPrefetch 会在客户端路由切换时运行吗?
不会,它是服务端专用钩子。客户端后续导航的数据获取应由路由框架、缓存层或 mounted 前后的客户端流程负责,并与服务端缓存键保持一致。
预取失败一定要让整页返回 500 吗?
不一定。关键数据可把错误交给上层终止渲染,非关键区域可以记录错误并写入可降级状态。选择要和 HTTP 状态、缓存策略与页面是否仍有用保持一致。
怎样证明服务端真的等待了?
在 SSR 集成测试中传入受控 Promise,先启动 renderToString 并确认未完成,再解析 Promise,最后断言 HTML 包含数据。只测试组件状态函数不能证明渲染器的等待行为。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。