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 应用实例。它在服务端和客户端都可以使用:
- 服务端:创建应用后配合
renderToString或renderToStream输出 HTML - 客户端:创建应用后调用
mount进行水合,复用服务端渲染的 DOM
import { createSSRApp } from '@vitarx/runtime-ssr'
function App() {
return <h1>Hello SSR</h1>
}
// 创建 SSR 应用实例
const app = createSSRApp(App)函数签名
declare function createSSRApp(root: View | Component, config?: AppConfig): SSRApp| 参数 | 类型 | 说明 |
|---|---|---|
| root | View | Component | 根组件或虚拟节点 |
| config | AppConfig | 应用配置(可选) |
SSRApp.mount
SSRApp 的 mount 方法与普通 App 不同——它会自动检测容器中是否已有服务端渲染的内容,如果有则进行水合,否则执行正常的客户端渲染。
// 客户端:自动检测并水合
const app = createSSRApp(App)
app.mount('#app', window.__INITIAL_STATE__)| 参数 | 类型 | 说明 |
|---|---|---|
| container | HostContainer | Element | string | 挂载容器,可以是 DOM 元素或选择器字符串 |
| SSRContext | Record<string, any> | 服务端渲染存储的上下文(可选) |
renderToString
renderToString 将应用渲染为完整的 HTML 字符串。它会等待所有异步任务完成后,一次性输出结果。
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>'函数签名
declare function renderToString(root: SSRApp | View, context?: SSRContext): Promise<string>| 参数 | 类型 | 说明 |
|---|---|---|
| root | SSRApp | View | SSR 应用实例或虚拟节点 |
| context | SSRContext | SSR 上下文对象,用于服务端记录状态(可选) |
返回值:Promise<string> — 渲染后的 HTML 字符串
传入上下文
renderToString 的第二个参数是 SSR 上下文对象。组件在渲染过程中可以向上下文写入数据,渲染完成后上下文中会包含这些数据,可以传递给客户端用于状态恢复。
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 并返回给客户端。
// 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 上,完成水合。
// 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
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
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
import { createSSRApp } from 'vitarx'
import App from './App'
const app = createSSRApp(App)
app.mount('#app', window.__INITIAL_STATE__)运行流程
- 用户访问页面,服务端创建
SSRApp并调用renderToString生成 HTML - 服务端将 HTML 和上下文数据一起发送给浏览器
- 浏览器立即显示 HTML 内容(用户已经能看到页面)
- 浏览器加载客户端 JS,创建
SSRApp并调用mount进行水合 - 水合完成后,页面具备完整的交互能力