SSR 上下文

什么是 SSR 上下文

SSR 上下文是一个普通 JavaScript 对象,在服务端渲染时创建,用于:

  1. 服务端:组件在渲染过程中向上下文写入数据(如预取的数据)
  2. 客户端:水合时从上下文中恢复数据,避免重复请求

你可以把上下文理解为服务端和客户端之间的"数据桥梁"。

useSSRContext

useSSRContext 用于在组件中获取当前 SSR 上下文对象。

tsx
import { useSSRContext } from 'vitarx'

function MyComponent() {
  const ctx = useSSRContext()

  if (ctx) {
    // 在服务端渲染或水合阶段,ctx 不为 null
    console.log(ctx.message)
  }
}

函数签名

typescript
declare function useSSRContext<T = Record<string, any>>(): SSRContext<T> | null
类型参数说明
T上下文数据的类型,默认为 Record<string, any>

返回值SSRContext<T> | null — 如果在 SSR 环境中返回上下文对象,否则返回 null

INFO

当组件不在 SSR 应用中运行时(比如纯客户端渲染的应用),useSSRContext 返回 null。使用时务必做空值判断。

泛型用法

你可以通过泛型参数指定上下文的数据结构,获得类型提示:

tsx
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__ 是一个编译时常量,构建工具会在打包时将其替换为 truefalse,从而支持摇树优化——未使用的分支代码会被完全移除:

tsx
function MyComponent() {
  if (__VITARX_SSR__) {
    // 服务端特定逻辑——在客户端构建产物中会被完全移除
  } else {
    // 客户端特定逻辑——在服务端构建产物中会被完全移除
  }
}

isSSR()(不推荐)

如果你确实需要运行时判断,可以使用 isSSR()

tsx
import { isSSR } from 'vitarx'

function MyComponent() {
  if (isSSR()) {
    // 服务端特定逻辑
  } else {
    // 客户端特定逻辑
  }
}

函数签名

typescript
declare function isSSR(): boolean

返回值boolean — 在服务端渲染环境中返回 true,否则返回 false

判断逻辑

isSSR 的判断依据是:

  1. 当前是否在 Node.js 环境中(检查 process.versions.node
  2. SSR 上下文是否存在且不在水合阶段

也就是说,isSSR() 在以下情况返回 true

  • 服务端渲染过程中(renderToStringrenderToStream 执行时)

isSSR() 在以下情况返回 false

  • 客户端正常运行时
  • 客户端水合阶段
  • 不在 Node.js 环境中

isHydrating

isHydrating 用于判断当前是否在客户端水合阶段。

tsx
import { isHydrating } from 'vitarx'

function MyComponent() {
  if (isHydrating()) {
    // 水合阶段的特定逻辑
  }
}

函数签名

typescript
declare function isHydrating(): boolean

返回值boolean — 在水合阶段返回 true,否则返回 false

isSSR vs isHydrating

场景isSSR()isHydrating()
服务端渲染中truefalse
客户端水合阶段falsetrue
客户端正常运行falsefalse

服务端数据预取模式

SSR 上下文最常见的用途是数据预取:在服务端获取数据,写入上下文,客户端水合时从上下文恢复,避免重复请求。

基本模式如下:

  1. 服务端渲染前,预取数据并写入上下文
  2. 组件中通过 useSSRContext 读取上下文数据
  3. 服务端渲染完成后,将上下文序列化到 HTML 中
  4. 客户端水合时,从 HTML 中恢复上下文数据
tsx
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>
  )
}

状态注入与恢复

服务端注入

在服务端,将预取的数据通过 renderToStringcontext 参数传入,组件中通过 useSSRContext 读取:

tsx
// 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>
  `)
})

客户端恢复

客户端水合时,将服务端序列化的上下文数据传入 hydratemount

tsx
// 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
  • 客户端水合时:$isHydratingtrue
  • 水合完成后:$isHydrating 恢复为 false

isSSR()isHydrating() 就是基于这个属性来判断的。请不要手动修改 $isHydrating

完整示例

下面是一个完整的数据预取和状态恢复示例。

数据获取模块 fetch.ts

tsx
// 模拟数据获取
export async function fetchArticles() {
  // 实际项目中这里会调用数据库或外部 API
  return ['文章一', '文章二', '文章三']
}

根组件 App.tsx

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

tsx
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

tsx
import { createSSRApp, hydrate } from 'vitarx'
import App from './App'

const app = createSSRApp(App)

// 恢复服务端上下文
await hydrate(app, '#app', window.__INITIAL_STATE__)

下一步