11.3 迁移指南

> **本节目标**:帮助从原生 GPUI、gpui-rsx、WPF、Vue 迁移到 RML 的开发者快速建立映射。

11.3.1 从原生 GPUI 迁移

心智模型转变

原生 GPUI 是“命令式链式调用”,RML 是“声明式标记”。核心转变:

| 原生 GPUI | RML | | --------------------------------- | ---------------------------- | | `div().flex().child(...)` | `<div class="flex">...</div>` | | `cx.listener(\|this, ev, cx\| ...)` | `on-click={method}` | | `Entity<Self>` 持状态 | ViewModel 持状态,View 自动生成 | | `Render::render` 手写 | `.rml` 模板自动生成 Render |

迁移步骤

1. **识别视图边界**:每个 `impl Render` 对应一个 RML 视图 2. **提取状态到 ViewModel**:把 `render` 中读写的字段移到 ViewModel 3. **把链式调用翻译为模板**:`div().class("x")` → `<div class="x">` 4. **把监听器翻译为命令**:`on_click(cx.listener(...))` → `on-click={cmd}` 5. **保留复杂逻辑**:实在难翻译的部分可暂时用 `ElementRef` 命令式访问

代码对照

**原生 GPUI**:

fn render(&mut self, _window: &mut Window, cx: &mut ViewContext<Self>) -> impl IntoElement {
    div()
        .flex()
        .flex_col()
        .gap(px(16.0))
        .child(Label::new(format!("Count: {}", self.count)))
        .child(
            Button::new("Click me")
                .on_click(cx.listener(|this, _ev, cx| {
                    this.count += 1;
                    cx.notify();
                }))
        )
}

**RML**:

<div class="flex flex-col gap-4">
  <p>Count: {count}</p>
  <button on-click={increment}>Click me</button>
</div>
#[command]
pub fn increment(&mut self, _ev: &ClickEvent, cx: &mut ViewContext<Self>) {
    self.count += 1;
    cx.notify();
}

11.3.2 从 gpui-rsx 迁移

gpui-rsx 是 JSX 风格的宏,与 RML 的差异主要在“文件分离”和“绑定系统”。

| gpui-rsx | RML | | --------------------------------- | -------------------------------- | | `rsx! { div { ... } }` 在 Rust 内 | 独立 `.rml` 文件 | | 闭包捕获变量 | 绑定到 ViewModel 字段 | | 无双向绑定 | `value={field}` 双向绑定 | | 无计算属性 | `#[computed]` | | 无热重载 | 支持热重载 |

迁移要点:

- 把 `rsx!` 块整体移到 `.rml` 文件 - 闭包捕获的变量改为 ViewModel 字段 - 手动的 `format!` 改为插值 - 手动的事件处理改为 `#[command]`

11.3.3 从 WPF 迁移

WPF 是 RML 的主要灵感来源,迁移最自然。

| WPF | RML | | --------------------------------- | -------------------------------- | | `.xaml` + `.xaml.cs` | `.rml` + `.rml.rs` | | `{Binding Path=Name}` | `{name}` 插值 | | `Mode=TwoWay` | `value={name}` | | `ICommand` | `#[command]` | | `INotifyPropertyChanged` | `cx.notify()` | | `StaticResource` | 资源字典 | | `DataTemplate` | 组件 + 插槽 | | `Style BasedOn` | `based_on` | | `x:Name` | `ref` |

迁移要点:

- XAML 标签大多可直接对应 RML 标签 - `INotifyPropertyChanged` 的属性改为 ViewModel 字段 - `ICommand` 实现改为 `#[command]` 方法 - `ValueConverter` 改为实现 `Converter` trait

11.3.4 从 Vue 迁移

Vue 与 RML 在声明式理念上高度相似。

| Vue | RML | | --------------------------------- | -------------------------------- | | `<template>` SFC | `.rml` 文件 | | `ref()` / `reactive()` | `#[derive(IModel)]` 字段 | | `v-if` | `if={cond}` | | `v-for` | `each={item in items}` `key={item.id}` | | `v-model` | `value={field}` | | `computed` | `#[computed]` | | `methods` | `#[command]` | | `mounted` / `unmounted` | `#[on_loaded]` / `#[on_unloaded]` | | `props` + `emit` | 组件 props + 事件 | | `slot` | `<slot>` | | `provide` / `inject` | Context |

主要差异

1. **类型系统**:RML 基于 Rust,类型严格;Vue 基于 JS,运行时检查 2. **响应式**:Vue 是 Proxy 自动追踪;RML 是编译期依赖分析 + `cx.notify()` 3. **状态管理**:Vue 用 Pinia / Vuex;RML 用 Context + 全局 Model 4. **样式作用域**:Vue 的 `scoped`;RML 的组件级样式表

迁移示例

**Vue**:

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => count.value++;
</script>

**RML**:

<div>
  <p>{count}</p>
  <Button label="+1" on-click={increment} />
</div>
#[derive(IModel)]
pub struct CounterVM { pub count: i32 }

impl CounterVM {
    #[command]
    pub fn increment(&mut self, _ev: &ClickEvent, cx: &mut ViewContext<Self>) {
        self.count += 1;
        cx.notify();
    }
}

11.3.5 迁移策略

渐进式迁移(推荐)

不要一次性重写。推荐顺序:

1. **新功能用 RML**:新增视图用 RML 写 2. **叶子视图先迁**:从无依赖的简单视图开始 3. **共享状态抽 Context**:迁移过程中用 Context 桥接新旧代码 4. **逐步替换**:每迁一个视图,跑全量测试

一次性迁移

适用于小型项目或重写项目。步骤:

1. 梳理所有视图,列出迁移清单 2. 设计 Model / Service 层(与框架无关,可先做) 3. 逐个视图翻译 4. 集成测试

不可迁移的部分

- 高度定制的渲染逻辑(如自绘 canvas):保留为 GPUI 自定义元素,在 RML 中通过 `<Custom>` 标签嵌入 - 性能极敏感的动画层:保留原生 GPUI

11.3.6 迁移常见陷阱

陷阱 1:把 Vue 的“魔法”带进 RML

Vue 的响应式是自动的,RML 需要显式 `cx.notify()`。忘记 notify 是最常见 bug。

陷阱 2:在模板里写 JS 表达式

Vue 模板支持复杂 JS 表达式,RML 模板只支持简单绑定。复杂逻辑收敛到 `#[computed]`。

陷阱 3:用 ref 直接操作 DOM

Vue 的 `ref` 是 DOM 引用,RML 的 `ElementRef` 是 GPUI 元素句柄,能力不同。优先用状态驱动,`ElementRef` 只用于聚焦等少数场景。

陷阱 4:忽略类型

Vue 是动态类型,RML 是静态类型。迁移时要补全类型,否则编译失败。

下一节 → [11.4 常见问题 FAQ](./faq.md)