SSR 上下文
什么是 SSR 上下文
SSR 上下文是一个普通 JavaScript 对象,在服务端渲染时创建,用于:
- 服务端:组件在渲染过程中向上下文写入数据(如预取的数据)
- 客户端:水合时从上下文中恢复数据,避免重复请求
你可以把上下文理解为服务端和客户端之间的"数据桥梁"。
useSSRContext
useSSRContext 用于在组件中获取当前 SSR 上下文对象。
import { useSSRContext } from 'vitarx'
function MyComponent() {
const ctx = useSSRContext()
if (ctx) {
// 在服务端渲染或水合阶段,ctx 不为 null
console.log(ctx.message)
}
}函数签名
declare function useSSRContext<T = Record<string, any>>(): SSRContext<T> | null| 类型参数 | 说明 |
|---|---|
| T | 上下文数据的类型,默认为 Record<string, any> |
返回值:SSRContext<T> | null — 如果在 SSR 环境中返回上下文对象,否则返回 null
INFO
当组件不在 SSR 应用中运行时(比如纯客户端渲染的应用),useSSRContext 返回 null。使用时务必做空值判断。
泛型用法
你可以通过泛型参数指定上下文的数据结构,获得类型提示:
import { useSSRContext } from 'vitarx'
interface MyContext {
message: string
count: number
}
function MyComponent() {
const ctx = useSSRContext<MyContext>()
if (ctx) {
// ctx.message 和 ctx.count 有完整的类型提示
console.log(ctx.message)
console.log(ctx.count)
}
}isSSR
isSSR 用于判断当前是否在服务端渲染环境中。
WARNING
不推荐使用 isSSR()——它是运行时判断,不支持摇树优化。即使在纯客户端项目中,服务端相关的代码也会被打包进去。推荐使用 __VITARX_SSR__ 代替。
推荐:使用 __VITARX_SSR__
__VITARX_SSR__ 是一个编译时常量,构建工具会在打包时将其替换为 true 或 false,从而支持摇树优化——未使用的分支代码会被完全移除:
function MyComponent() {
if (__VITARX_SSR__) {
// 服务端特定逻辑——在客户端构建产物中会被完全移除
} else {
// 客户端特定逻辑——在服务端构建产物中会被完全移除
}
}isSSR()(不推荐)
如果你确实需要运行时判断,可以使用 isSSR():
import { isSSR } from 'vitarx'
function MyComponent() {
if (isSSR()) {
// 服务端特定逻辑
} else {
// 客户端特定逻辑
}
}函数签名
declare function isSSR(): boolean返回值:boolean — 在服务端渲染环境中返回 true,否则返回 false
判断逻辑
isSSR 的判断依据是:
- 当前是否在 Node.js 环境中(检查
process.versions.node) - SSR 上下文是否存在且不在水合阶段
也就是说,isSSR() 在以下情况返回 true:
- 服务端渲染过程中(
renderToString或renderToStream执行时)
isSSR() 在以下情况返回 false:
- 客户端正常运行时
- 客户端水合阶段
- 不在 Node.js 环境中
isHydrating
isHydrating 用于判断当前是否在客户端水合阶段。
import { isHydrating } from 'vitarx'
function MyComponent() {
if (isHydrating()) {
// 水合阶段的特定逻辑
}
}函数签名
declare function isHydrating(): boolean返回值:boolean — 在水合阶段返回 true,否则返回 false
isSSR vs isHydrating
| 场景 | isSSR() | isHydrating() |
|---|---|---|
| 服务端渲染中 | true | false |
| 客户端水合阶段 | false | true |
| 客户端正常运行 | false | false |
服务端数据预取模式
SSR 上下文最常见的用途是数据预取:在服务端获取数据,写入上下文,客户端水合时从上下文恢复,避免重复请求。
基本模式如下:
- 服务端渲染前,预取数据并写入上下文
- 组件中通过
useSSRContext读取上下文数据 - 服务端渲染完成后,将上下文序列化到 HTML 中
- 客户端水合时,从 HTML 中恢复上下文数据
import { ref, onInit, useSSRContext, isSSR } from 'vitarx'
function ArticleList() {
const ctx = useSSRContext<{ articles: string[] }>()
const articles = ref(ctx?.articles ?? [])
onInit(async () => {
// 仅在服务端发起请求
if (__VITARX_SSR__) {
const res = await fetch('/api/articles')
articles.value = await res.json()
}
})
return (
<ul>
{articles.value.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}状态注入与恢复
服务端注入
在服务端,将预取的数据通过 renderToString 的 context 参数传入,组件中通过 useSSRContext 读取:
// server.ts
import { createSSRApp, renderToString } from 'vitarx'
import App from './App'
server.get('/', async (req, res) => {
// 预取数据
const articles = await db.getArticles()
// 创建上下文并写入数据
const context = { articles }
// 渲染,context 会被注入到应用中
const app = createSSRApp(App)
const html = await renderToString(app, context)
// 将上下文序列化到 HTML
res.send(`
<div id="app">#123;html}</div>
<script>window.__INITIAL_STATE__ = #123;JSON.stringify(context)}</script>
<script src="/client.js"></script>
`)
})客户端恢复
客户端水合时,将服务端序列化的上下文数据传入 hydrate 或 mount:
// client.ts
import { createSSRApp, hydrate } from 'vitarx'
import App from './App'
const app = createSSRApp(App)
// 传入服务端的上下文数据,组件中 useSSRContext() 即可读取
await hydrate(app, '#app', window.__INITIAL_STATE__)上下文内部属性
SSR 上下文有一个内部属性 $isHydrating,由框架自动管理:
- 服务端渲染时:
$isHydrating不存在或为false - 客户端水合时:
$isHydrating为true - 水合完成后:
$isHydrating恢复为false
isSSR() 和 isHydrating() 就是基于这个属性来判断的。请不要手动修改 $isHydrating。
完整示例
下面是一个完整的数据预取和状态恢复示例。
数据获取模块 fetch.ts
// 模拟数据获取
export async function fetchArticles() {
// 实际项目中这里会调用数据库或外部 API
return ['文章一', '文章二', '文章三']
}根组件 App.tsx
import { ref, onMounted, useSSRContext, isSSR } from 'vitarx'
interface AppContext {
articles: string[]
title: string
}
export default function App() {
// 从上下文中获取服务端预取的数据
const ctx = useSSRContext<AppContext>()
const articles = ref<string[]>(ctx?.articles ?? [])
const title = ref(ctx?.title ?? '文章列表')
// 客户端刷新数据
const refresh = async () => {
const res = await fetch('/api/articles')
articles.value = await res.json()
}
return (
<div>
<h1>{title.value}</h1>
<ul>
{articles.value.map((article) => (
<li key={article}>{article}</li>
))}
</ul>
<button onClick={refresh}>刷新</button>
</div>
)
}服务端入口 server.ts
import express from 'express'
import { createSSRApp, renderToString } from 'vitarx '
import App from './App'
import { fetchArticles } from './fetch'
const server = express()
server.get('/', async (req, res) => {
// 预取数据
const articles = await fetchArticles()
// 创建上下文
const context = { articles, title: '文章列表' }
// 渲染
const app = createSSRApp(App)
const html = await renderToString(app, context)
res.send(`
<!DOCTYPE html>
<html>
<head><meta charset="utf-8" /><title>SSR 上下文示例</title></head>
<body>
<div id="app">#123;html}</div>
<script>window.__INITIAL_STATE__ = #123;JSON.stringify(context)}</script>
<script src="/client.js"></script>
</body>
</html>
`)
})
server.get('/api/articles', async (req, res) => {
const articles = await fetchArticles()
res.json(articles)
})
server.listen(3000)客户端入口 client.ts
import { createSSRApp, hydrate } from 'vitarx'
import App from './App'
const app = createSSRApp(App)
// 恢复服务端上下文
await hydrate(app, '#app', window.__INITIAL_STATE__)