Hydration utilities
把 Rho build-time 输出的"带 marker class 的静态 HTML"在浏览器里激活成可交互组件。
什么需要 hydration
| DSL | 需要 hydration |
|---|---|
| interact(slider / input / select / toggle / button / template / computed / timer / vega / svg) | ✅ |
| tabs(点击切换) | ✅ |
| stepper(Prev/Next) | ✅ |
| modal(点击展开) | ✅ |
| ─────────────────────────────── | |
| Callout | ❌ 静态 HTML |
| Layout grid + cards | ❌ CSS Grid |
| Annotate | ❌ CSS hover |
| Timeline | ❌ CSS layout |
| STL syntax highlight | ❌ highlight.js 自动 |
hydrateAll(container) —— 一键激活
最常用 API。扫 container 内所有 Rho marker class,依次接管。
import { hydrateAll } from '@rho/md';
const container = document.getElementById('root');
container.innerHTML = await renderRho(markdown);
hydrateAll(container);
幂等——同一 container 多次调用安全。每个 hydrated 块会标 data-rho-hydrated="true",后续调用跳过。
等价于:
renderInteractBlocks(container);
renderTabsBlocks(container);
renderStepperBlocks(container);
renderModalBlocks(container);
单独 hydrate
需要精细控制时(lazy load / 性能调优 / 部分接管):
import {
renderInteractBlocks,
renderTabsBlocks,
renderStepperBlocks,
renderModalBlocks,
} from '@rho/md';
// 只 hydrate tabs(structural),暂缓 interact(重型)
renderTabsBlocks(container);
// 后续在 tab 切换时再 hydrate active panel 内的 interact
container.querySelector('.rho-tab-btn').addEventListener('click', () => {
const activePanel = container.querySelector('.rho-tab-panel:not([hidden])');
renderInteractBlocks(activePanel);
});
Hydration 时机
Pattern 1:渲染立即 hydrate(默认)
container.innerHTML = html;
hydrateAll(container);
适合:内容不大,所有 interact 都立刻可见。
Pattern 2:滚动到视口才 hydrate(lazy)
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
hydrateAll(entry.target);
observer.unobserve(entry.target);
}
});
});
container.querySelectorAll('.rho-interact, .rho-tabs, .rho-stepper').forEach((el) => {
observer.observe(el);
});
适合:长文档含很多 chart——避免首屏卡顿。
Pattern 3:用户主动触发才 hydrate
container.innerHTML = html;
const button = document.getElementById('activate-demos');
button.addEventListener('click', () => {
hydrateAll(container);
button.remove();
});
适合:极端性能优化场景,明确告诉用户"点击启用 demo"。
Hydration options
部分 hydrate 函数支持 options(注意:option 类型可能因版本演化而扩展):
import { hydrateAll } from '@rho/md';
import type { HydrationOptions } from '@rho/md';
const options: HydrationOptions = {
// hydrate 完成 callback(每块)
onBlockHydrated: (blockEl: HTMLElement, type: string) => {
console.log(`Hydrated ${type}`);
},
// hydrate 失败时的 fallback UI
errorBoundary: (blockEl: HTMLElement, error: Error) => {
blockEl.innerHTML = `<p style="color: red">Failed to hydrate: ${error.message}</p>`;
},
// 跳过特定 marker class(默认全 hydrate)
skip: ['rho-modal'],
};
hydrateAll(container, options);
重新 hydrate(内容变了)
如果 markdown 源改变 → 重新 process → 重新塞进 container:
async function update(newMarkdown: string) {
container.innerHTML = await renderRho(newMarkdown);
hydrateAll(container);
}
为什么 hydrateAll 还是安全:旧 hydrated DOM 已被 innerHTML = ... 替换;新 DOM 没有 data-rho-hydrated,会被正常 hydrate。
⚠️ 替换 DOM 时之前的 interact state(slider 拖到的位置 / namespace 内的 computed 当前值)会丢失——hydrate 是 "static HTML → live",不是 "live → live"。需要保留 state 用 React/Vue 的 component 化思路(在框架里管理 state)。
嵌套块的 hydrate 顺序
hydrateAll 默认外层先 hydrate。例:
<div class="rho-tabs"> ← 1. 先 hydrate tabs
<div class="rho-tab-panel">
<div class="rho-interact"> ← 2. 再 hydrate interact
<div class="rho-modal-trigger"> ← 3. 最后 hydrate modal
</div>
</div>
</div>
通常你不需要关心——hydrate 是声明式,结果一致。但调试 hydrate 顺序问题时知道这点有用。
Server-side rendering (SSR) + hydration
// === SSR (Node.js) ===
const html = String(await processor.process(markdown));
const fullPage = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/@rho/md/css/stl-md.css">
</head>
<body>
<div id="root" class="markdown-body">${html}</div>
<script type="module" src="/client.js"></script>
</body>
</html>
`;
return fullPage;
// === client.js (browser) ===
import { hydrateAll } from '@rho/md';
hydrateAll(document.getElementById('root')!);
关键:SSR 只输出 HTML(含 marker class);hydrate 必须在浏览器跑。
React / Vue / Svelte 集成
React (useEffect)
import { useEffect, useRef } from 'react';
import { hydrateAll } from '@rho/md';
function RhoMarkdown({ source }: { source: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
(async () => {
ref.current!.innerHTML = await renderRho(source);
hydrateAll(ref.current!);
})();
}, [source]);
return <div ref={ref} className="markdown-body" />;
}
Vue 3
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { hydrateAll } from '@rho/md';
const props = defineProps<{ source: string }>();
const root = ref<HTMLElement>();
const update = async () => {
if (!root.value) return;
root.value.innerHTML = await renderRho(props.source);
hydrateAll(root.value);
};
onMounted(update);
watch(() => props.source, update);
</script>
<template>
<div ref="root" class="markdown-body" />
</template>
Svelte
<script lang="ts">
import { onMount } from 'svelte';
import { hydrateAll } from '@rho/md';
export let source: string;
let root: HTMLDivElement;
onMount(async () => {
root.innerHTML = await renderRho(source);
hydrateAll(root);
});
</script>
<div bind:this={root} class="markdown-body" />
性能 tips
- 单页 hydrate 20+ interact 块 → 考虑 lazy hydrate
- 首屏只 hydrate 视口内可见的 → 滚动时按需 hydrate 后续
- 大型 chart(vega-lite)首屏卡 → 让用户点击 trigger 才 hydrate
- 重复同 namespace 的 interact 块共享 state → reactive 更新只算一次
See also
- Quick start — 标准集成 pattern
- API reference
- Remark plugins — build-time 输出哪些 marker class
- Inner processor — 嵌套 hydration