SSR 基础

什么是服务端渲染(SSR)

传统的单页应用(SPA)在浏览器中运行 JavaScript 来生成页面内容,用户首次访问时拿到的是一个空白的 HTML 壳子,需要等 JS 加载执行后才能看到页面。

服务端渲染(SSR) 则是在服务器上提前把组件渲染成 HTML 字符串,直接发送给浏览器。用户打开页面时就能立即看到完整内容,随后 JavaScript 加载完成后再"激活"页面,使其具备交互能力。

简单来说:

  • SPA:浏览器下载 JS → 执行 JS → 生成页面 → 用户看到内容
  • SSR:服务器生成 HTML → 用户立即看到内容 → 浏览器下载 JS → 激活交互

SSR 的优势

更好的 SEO

搜索引擎爬虫通常不会等待 JavaScript 执行完毕再抓取内容。SSR 直接输出完整的 HTML,爬虫可以立即获取页面内容,提升搜索排名。

更快的首屏加载

用户不需要等待 JavaScript 下载和执行就能看到页面内容。虽然交互仍需等待 JS 加载,但视觉上页面已经呈现,体验更好。

createSSRApp

createSSRApp 用于创建一个 SSR 应用实例。它在服务端和客户端都可以使用:

  • 服务端:创建应用后配合 renderToStringrenderToStream 输出 HTML
  • 客户端:创建应用后调用 mount 进行水合,复用服务端渲染的 DOM
tsx
import { createSSRApp } from '@vitarx/runtime-ssr'

function App() {
  return <h1>Hello SSR</h1>
}

// 创建 SSR 应用实例
const app = createSSRApp(App)

函数签名

typescript
declare function createSSRApp(root: View | Component, config?: AppConfig): SSRApp
参数类型说明
rootView | Component根组件或虚拟节点
configAppConfig应用配置(可选)

SSRApp.mount

SSRAppmount 方法与普通 App 不同——它会自动检测容器中是否已有服务端渲染的内容,如果有则进行水合,否则执行正常的客户端渲染。

tsx
// 客户端:自动检测并水合
const app = createSSRApp(App)
app.mount('#app', window.__INITIAL_STATE__)
参数类型说明
containerHostContainer | Element | string挂载容器,可以是 DOM 元素或选择器字符串
SSRContextRecord<string, any>服务端渲染存储的上下文(可选)

renderToString

renderToString 将应用渲染为完整的 HTML 字符串。它会等待所有异步任务完成后,一次性输出结果。

tsx
import { createSSRApp } from '@vitarx/runtime-ssr'
import { renderToString } from '@vitarx/runtime-ssr'

function App() {
  return <h1>Hello SSR</h1>
}

const app = createSSRApp(App)
const html = await renderToString(app)
// html: '<h1>Hello SSR</h1>'

函数签名

typescript
declare function renderToString(root: SSRApp | View, context?: SSRContext): Promise<string>
参数类型说明
rootSSRApp | ViewSSR 应用实例或虚拟节点
contextSSRContextSSR 上下文对象,用于服务端记录状态(可选)

返回值Promise<string> — 渲染后的 HTML 字符串

传入上下文

renderToString 的第二个参数是 SSR 上下文对象。组件在渲染过程中可以向上下文写入数据,渲染完成后上下文中会包含这些数据,可以传递给客户端用于状态恢复。

tsx
import { createSSRApp } from '@vitarx/runtime-ssr'
import { renderToString } from '@vitarx/runtime-ssr'

const context = {}
const app = createSSRApp(App)
const html = await renderToString(app, context)

// context 中可能包含组件写入的数据
console.log(context)

服务端入口文件示例

服务端入口文件负责创建应用、渲染 HTML 并返回给客户端。

tsx
// server.js
import express from 'express'
import { createSSRApp } from '@vitarx/runtime-ssr'
import { renderToString } from '@vitarx/runtime-ssr'
import App from './App'

const server = express()

server.get('/', async (req, res) => {
  // 创建 SSR 上下文
  const context = {}

  // 创建应用并渲染
  const app = createSSRApp(App)
  const html = await renderToString(app, context)

  // 将上下文数据序列化后注入 HTML,供客户端恢复
  const serializedState = JSON.stringify(context)

  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>Vitarx SSR</title>
      </head>
      <body>
        <div id="app">#123;html}</div>
        <script>window.__INITIAL_STATE__ = #123;serializedState}</script>
        <script src="/client.js"></script>
      </body>
    </html>
  `)
})

server.listen(3000, () => {
  console.log('服务运行在 http://localhost:3000')
})

客户端入口文件示例

客户端入口文件负责创建应用并挂载到服务端渲染的 DOM 上,完成水合。

tsx
// client.js
import { createSSRApp } from '@vitarx/runtime-ssr'
import App from './App'

// 创建应用
const app = createSSRApp(App)

// 挂载并传入服务端上下文数据
app.mount('#app', window.__INITIAL_STATE__)

SSRApp.mount 会自动检测 #app 容器中是否已有服务端渲染的 HTML 内容,如果有则进行水合复用 DOM,否则执行正常的客户端渲染。

完整的最小 SSR 项目示例

下面是一个完整的最小 SSR 项目,包含根组件、服务端入口和客户端入口。

根组件 App.tsx

tsx
import { ref } from 'vitarx'

export default function App() {
  const count = ref(0)

  return (
    <div>
      <h1>Vitarx SSR 示例</h1>
      <p>当前计数:{count.value}</p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  )
}

服务端入口 server.ts

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

const server = express()

server.get('/', async (req, res) => {
  const context = {}
  const app = createSSRApp(App)
  const html = await renderToString(app, context)

  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>Vitarx 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.use(express.static('dist'))

server.listen(3000, () => {
  console.log('服务运行在 http://localhost:3000')
})

客户端入口 client.ts

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

const app = createSSRApp(App)
app.mount('#app', window.__INITIAL_STATE__)

运行流程

  1. 用户访问页面,服务端创建 SSRApp 并调用 renderToString 生成 HTML
  2. 服务端将 HTML 和上下文数据一起发送给浏览器
  3. 浏览器立即显示 HTML 内容(用户已经能看到页面)
  4. 浏览器加载客户端 JS,创建 SSRApp 并调用 mount 进行水合
  5. 水合完成后,页面具备完整的交互能力

下一步