Common pitfalls

跨所有 Rho 能力的常见陷阱总览——按类型分组。每一条带"症状 / 原因 / 修复"。 写完文档渲染没出来 / 行为怪异,先来这里查。

单个能力的 pitfalls 在各自文档里(Callout / Layout / 等)。本页只列跨能力 / 高频 / 高影响 的。


A. Markdown 语法层

A1. fenced code 嵌套反引号冲突

症状:嵌套块解析失败,外层 ``` 提前 close。

原因:内层 fenced block 反引号数量跟外层一样,markdown 提前 close 外层。

修复:内外层反引号数不同:

\````layout              ← 4 反引号外层
:::card
\```interact            ← 3 反引号内层
\```
:::
\````

A2. directive ::: 没关闭

症状:整个 block 不渲染,错误连锁影响后面的内容。

原因::::card / :::tab / :::step / :::event 缺对应 ::: 关闭。

修复:每个 :::xxx 开必有 ::: 关,保持配对。

A3. callout 中段被空行打断

症状:callout 只渲染了第一段。

原因:blockquote 内空行(不带 >)会断开 callout。

修复:段落间用只含 > 的空引用行:

> [!INFO]
> 第一段。
>
> 第二段(仍在 callout 内)。

B. DSL 选错

B1. 用 callout 做 layout

症状:3-5 个 callout 并排显示,垂直堆叠占满页面。

原因:callout 是单段强调,不是布局工具。

修复:用 Layout grid + cards。

B2. 用 layout 做严格序列

症状:3 张卡片并排,但用户必须按 1→2→3 顺序看。

原因:layout 是平级展示,不暗示顺序。

修复:用 Stepper。

B3. 用 modal 藏核心内容

症状:80% 的读者看不到关键解释,因为不点 modal。

原因:modal 是"按需深度",藏核心 = 等于没写。

修复:核心内容直接写在正文;modal 只藏可选深度。

B4. 用 stepper 当 timeline

症状:每个"step"其实是历史事件,不是教程步骤。

原因:stepper 暗示"按顺序学",timeline 暗示"时间线展示"。

修复:历史 / 里程碑用 Timeline;教程 / 配置用 stepper。

B5. 用 tabs 让用户必看全部

症状:tabs 切换显示,但每个 tab 都是必读 → 用户错过 2-3 个。

原因:tabs 把内容藏起来——读者只看默认那个。

修复:必读全部用 Layout grid 同时显示;tabs 适合"等价多视图选一个"。


C. interact / 状态层

C1. 跨块期待状态共享但忘 namespace

症状:第 2 块 template 显示 {x} 字面量或抛 "undefined"。

原因:默认每个 interact 块独立——状态不共享。

修复:两个块都加同一 namespace:

\```interact namespace foo
slider x 0 10 5 1
\```

\```interact namespace foo
template stl: [{x}]
\```

详见 Shared state.

C2. 同 namespace 重复声明同名变量

症状:行为未定义(可能覆盖、可能错乱、可能保留第一次)。

原因:namespace 内每个变量只能声明一次。

修复:第一块声明所有 sliders / computed,后续块只引用,不重复声明。

C3. variable 名含特殊字符

症状:解析失败或行为意外。

原因:变量名只允许 [A-Za-z0-9_]。

修复:用驼峰 myVar 或下划线 my_var,不要空格 / 连字符 / 特殊字符。

C4. button value 含表达式但忘加引号

症状:解析失败或 button 行为不对。

原因:value 含 {expr} 时必须包在引号里。

修复:

button inc "+1" "{counter+1}"      ← ✅
button inc "+1" {counter+1}        ← ❌ 缺引号

C5. input 数值要做数学运算

症状:{age + 1} 输出 "301"(字符串拼接)而不是 31。

原因:input 永远返回字符串。

修复:数值输入用 slider;非要用 input 就 cast:{parseFloat(age) + 1}。


D. mini DSL 表达式

D1. 引用未声明变量

症状:template 显示 {y} 字面量或 "undefined" 错。

原因:所有 template 引用的变量必须先用 slider/input/select/toggle/computed 声明。

修复:检查变量是否在同 namespace(或同块)声明。

D2. 表达式优先级混淆

症状:BMI / 复利等公式算错。

原因:mini DSL 优先级跟数学 / JS 一致,但不显式括号容易踩坑。

修复:用括号显式表达:

computed bmi = weight / (height * height)    ← ✅
computed bmi = weight / height ** 2          ← ⚠️ 优先级容易错

D3. 用了 mini DSL 不支持的函数

症状:表达式抛错或返回 undefined。

原因:mini DSL 不是 JS——Math.X / console.log / Date 等不存在。

修复:参考 Computed 内置函数列表。

D4. computed 循环依赖

症状:所有 computed 报错或显示 NaN。

原因:a = b + 1 + b = a + 1 形成环。

修复:computed 之间不能形成环。要"前次值"模式用 button self-reference。

D5. 三元嵌套太深

症状:表达式难读、易错、难维护。

原因:3 层以上三元嵌套人脑难解析。

修复:拆成多个 computed,或用多行格式:

computed grade = score >= 90 ? "A"
              : score >= 80 ? "B"
              : score >= 70 ? "C"
              :               "F"

E. Vega-Lite chart

E1. JSON 语法错

症状:chart 不渲染,控制台 JSON parse error。

原因:vega-lite spec 是严格 JSON——双引号 key、value 后逗号、严格格式。

修复:用 Vega-Lite Editor 验证 spec。

E2. {var} 占位符位置错

症状:chart 不渲染或行为意外。

原因:占位符替换发生在 JSON parse 之前——数字字段不带引号、字符串字段带引号。

修复:

  • 数字:"domain": [-{maxY}, {maxY}]
  • 字符串:"mark": "{showArea ? "area" : "line"}"

E3. data.sequence 范围太大

症状:浏览器卡死。

原因:超过 5000 数据点 vega-lite 渲染压力极大。

修复:控制点数 < 5000;曲线 < 500 点已经看着平滑。

E4. transform.calculate 用 mini DSL syntax

症状:calculate 表达式不工作。

原因:vega-lite 用 vega expression,跟 mini DSL 不完全兼容。

修复:参考 vega expression 文档。


F. SVG scenes

F1. 缺 viewBox

症状:SVG 缩放行为不可预期。

原因:缺 viewBox 内部坐标系不定。

修复:总是设 viewBox:<svg viewBox="0 0 100 100">。

F2. 写了 <script> / <foreignObject> / on* 事件

症状:这些元素被剥离,对应功能不工作。

原因:Rho SVG sanitizer 故意阻止——安全设计。

修复:用纯声明式 SVG(标准形状 + animate 标签 + 占位符),不靠 JS。

F3. mini DSL 表达式用 Math.X

症状:Math.sin 等抛错。

原因:mini DSL 不是 JS,没 Math 全局。

修复:直接用 sin(x) / cos(x) / pow(a,b) / 等。

F4. SVG 元素过多

症状:动画卡顿。

原因:SVG 是 DOM——元素 > 200 后渲染压力大。

修复:减少元素 / 简化形状 / 改用 chart。


G. Timer / 动画

G1. step 太小

症状:浏览器卡。

原因:(max - min) / step > 数千帧。

修复:保持总帧数 100-1000 范围。

G2. timer 在 chart 但 calculate 没引用 timer 变量

症状:chart 静止,timer "白跑"。

原因:calculate 表达式不依赖 {t}。

修复:在 vega 表达式里显式引用 {t}:"sin(datum.x + {t})"。

G3. 多 timer 期待严格同步

症状:两个 timer 显示出 phase 不同步。

原因:每个 timer 独立调度——可能微小漂移。

修复:用 1 个 timer + 多个 computed 派生:

timer t 0 10 0.1 loop
computed t1 = t
computed t2 = (t + 5) % 10

H. 嵌套 / 复杂度

H1. 嵌套 4 层以上

症状:plain-text reader 看不懂结构;Rho 渲染性能差。

原因:每嵌套一层 inner processor 跑一次。

修复:保持 ≤ 3 层;重新设计页面结构。

H2. 同类型嵌套(tabs 套 tabs / stepper 套 stepper)

症状:UI 混乱,读者分不清外层内层。

原因:两层进度条 / 两组 tab bar 视觉冲突。

修复:把内层换成不同容器(modal / accordion / 列表)。

H3. interact 嵌入 modal

症状:modal 关再开 slider 状态丢失。

原因:modal hidden 时 hydration state 通常被清。

修复:核心 demo 别藏 modal——直接放正文。


I. Plain-text fallback

I1. 用 raw HTML 替代 Rho DSL

症状:在 Rho 里渲染 OK,但 GitHub / 其他 reader 不渲染 CSS。

原因:raw HTML 在多 reader 不一致;GitHub 剥 style。

修复:用 Rho DSL(Layout / Modal / 等)替代。

I2. 把 plain-text fallback 当安全机制

症状:试图用 modal 藏敏感信息——但 plain-text 全都看得到。

原因:fallback 是"读者依然能读",不是"对方看不到"。

修复:敏感数据不要进 markdown——根本不应该在文档里。

I3. 用没 label 的 emoji 当结构信号

症状:plain-text reader 看到一堆 emoji 没法理解结构。

原因:emoji 视觉上有意义,纯文本里只是字符。

修复:用 Callout 等结构化容器。


J. 性能

J1. 单页 interact 块过多

症状:page 加载慢、操作有延迟。

原因:每个 interact 块独立 hydrate,hydration 累加。

修复:合并相关 interact 用 namespace 共享;或拆到多页。

J2. chart 数据点过多 + timer 高频

症状:拖滑块 / timer 跳帧时浏览器卡。

原因:每帧重渲染 N 个数据点。

修复:减点数 + 增 step(降帧率)。


K. AI / LLM 生成

K1. LLM 把 mini DSL 当 JS

症状:生成的 computed 有 Math.sin / if/else / function。

原因:LLM 默认认 JS。

修复:在 prompt 里明确:"use Rho mini DSL — NOT JavaScript. Functions: pow, sqrt, sin, cos, abs, ... See [Rho LLM Protocol]."

K2. LLM 忘加 namespace

症状:多个 interact 块期待共享但实际隔离。

原因:LLM 可能不知 namespace 必填。

修复:在 prompt 里给 namespace 例子。详见 Rho AI / LLM Guide.

K3. LLM 写非法 directive 顺序

症状::::card 没在 layout 里 / :::step 没在 stepper 里。

原因:LLM 不知 directive 必须配父容器。

修复:prompt 强调 "directives must be inside their proper container"。


See also