客户端水合
什么是水合(Hydration)
服务端渲染输出了完整的 HTML,但这个 HTML 是"静态"的——按钮点击没有反应,输入框无法交互。水合就是让客户端 JavaScript 接管这些已有的 DOM 节点,绑定事件监听器和响应式状态,使页面"活"过来。
水合的核心思路是:不重新创建 DOM,而是复用服务端已经渲染好的 DOM 节点,只补充事件绑定和响应式关联。这样可以避免页面闪烁,也能提升性能。
hydrate 函数
hydrate 是 Vitarx 提供的水合函数,将服务端渲染的 HTML 与客户端应用关联起来。
tsx
import { createSSRApp, hydrate } from 'vitarx'
import App from './App'
const app = createSSRApp(App)
// 水合到服务端渲染的 HTML
await hydrate(app, '#app', window.__INITIAL_STATE__)函数签名
typescript
declare function hydrate(
root: SSRApp | View,
container: HostContainer | string,
context?: SSRContext
): Promise<void>| 参数 | 类型 | 说明 |
|---|---|---|
| root | SSRApp | View | SSR 应用实例或虚拟节点 |
| container | HostContainer | string | 挂载容器,可以是 DOM 元素或选择器字符串 |
| context | SSRContext | SSR 上下文对象,用于恢复服务端状态(可选) |
返回值:Promise<void> — 水合完成后 resolve
hydrate vs SSRApp.mount
SSRApp.mount 内部就是调用 hydrate,两者效果相同。区别在于:
app.mount('#app', context)— 同步返回,不等待水合完成await hydrate(app, '#app', context)— 异步等待水合完成后才继续执行
如果你需要在确保水合完成后再执行某些操作(比如启动路由),使用 hydrate 更合适。
tsx
// 方式一:mount(不等待水合完成)
const app1 = createSSRApp(App)
app1.mount('#app', window.__INITIAL_STATE__)
// 方式二:hydrate(等待水合完成)
const app2 = createSSRApp(App)
await hydrate(app2, '#app', window.__INITIAL_STATE__)
// 水合已完成,可以安全地执行后续操作自动检测水合模式
hydrate 会自动检测容器中是否包含服务端渲染的内容:
- 容器有子节点:执行水合,逐节点匹配并复用 DOM
- 容器为空:跳过水合,直接执行正常的客户端渲染
这意味着即使服务端渲染失败或未启用 SSR,客户端代码也能正常工作。
tsx
// 无论服务端是否渲染了内容,客户端代码都一样
const app = createSSRApp(App)
await hydrate(app, '#app', window.__INITIAL_STATE__)水合失败时的降级策略
水合过程中可能遇到服务端和客户端渲染结果不一致的情况,例如:
- 找不到对应的 DOM 节点
- 节点类型不匹配(期望
<div>但找到了<span>)
遇到这些情况时,Vitarx 会采取以下策略:
- 节点缺失:创建新的 DOM 节点并插入到容器中,输出警告日志
- 节点不匹配:用新创建的节点替换不匹配的节点,输出警告日志
- 整体失败:如果水合过程抛出异常,会清空容器并回退到完整的客户端渲染
tsx
// hydrate 内部的降级逻辑(简化版)
try {
// 尝试逐节点水合
await hydrateNode(rootView, containerEl)
activate(rootView, containerEl)
} catch (err) {
// 水合失败,清空容器,回退到客户端渲染
containerEl.innerHTML = ''
rootView.mount(containerEl)
}这种降级策略保证了即使水合出现问题,页面仍然可以正常工作,只是失去了 SSR 的性能优势。
客户端激活(activate)流程
水合分为两个阶段:
- 水合阶段(hydrateNode):遍历虚拟节点树,逐个与服务端渲染的 DOM 节点匹配,将 DOM 引用关联到虚拟节点上
- 激活阶段(activate):调用视图的
mount方法,绑定事件监听器、建立响应式关联,使页面具备交互能力
activate 是一个内部函数,由 hydrate 在水合匹配完成后自动调用,通常不需要直接使用。
激活阶段的一个关键优化是:防止重复插入 DOM。激活时会临时重写渲染器的 append 方法,如果发现节点已经在正确的父容器中,就跳过插入操作。
tsx
// activate 的核心逻辑(简化版)
function activate(view, container) {
const renderer = getRenderer()
const originalAppend = renderer.append.bind(renderer)
// 重写 append,跳过已在正确位置的节点
renderer.append = (child, parent) => {
if (getParentNode(child) === parent) return
originalAppend(child, parent)
}
// 执行挂载(绑定事件、建立响应式关联)
view.mount(container)
// 恢复原始 append
renderer.append = originalAppend
}完整示例
下面是一个包含异步数据获取的水合示例,展示服务端和客户端如何协作。
根组件 App.tsx
tsx
import { ref, onMounted, useSSRContext, isSSR } from 'vitarx'
export default function App() {
const message = ref('加载中...')
onMounted(async () => {
// 仅在客户端且非水合阶段时获取数据
// 服务端渲染的数据已通过上下文恢复
if (!isSSR()) {
const res = await fetch('/api/message')
message.value = await res.text()
}
})
return (
<div>
<h1>SSR 水合示例</h1>
<p>{message.value}</p>
</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 message = await fetch('http://localhost:3000/api/message').then((r) => r.text())
context.message = message
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/message', (req, res) => {
res.send('来自服务端的数据')
})
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__)