大家好,前陣子我開源了一個爲 34 鍵分體鍵盤(Ferris Sweep)量身打造的粵拼並擊輸入法(宮保粵拼),並做了一個可視化的 Web 模擬器(基於 Rust + Leptos)。上一期發佈後,視覺效果不錯,但在底層架構上,我遇到了一個經典的 Rust 難題:如何讓編譯期就固定的靜態數據,根據前端的 UI 狀態進行動態切換?

❌ 痛點與挑戰

在 Rime 引擎的架構思維中,輸入方案(包含幾百條正則拼寫運算規則)通常是非常重的,所以我們習慣將其放在 lazy_static! 裏,擁有 'static 生命週期。

但我的需求是:當用戶在前端下拉選單切換「實體鍵盤佈局」(如標準直列 -> 縱向錯列)時,底層的方案定義必須動態替換對應的指法規則。如果直接把前端的 AppState 塞進底層,會造成嚴重的耦合;如果每次切換都重新實例化整個方案,效能開銷又太大。

✅ 解法:依賴注入 (DI) + 函數指針 + Leptos 細粒度響應式追蹤

經過一番折騰,我找到了一條非常優雅且符合 Rust 哲學的路徑:

  1. 輕量級環境上下文

我沒有把整個 AppState 傳給底層,而是抽取了一個全都是 Copy 屬性、包裝着 Leptos Signal 的環境結構體:

#[derive(Clone, Copy)]
pub struct 輸入方案環境 {
    pub 已選配列: Signal<Option<配列>>,
}
  1. 將靜態實例改爲生成函數

我把 lazy_static 裏原本寫死的方案實例,改成了接收環境參數的函數指針 fn(輸入方案環境) -> 輸入方案定義<'static>。 這完全沒有破壞 'static 的生命週期,只是把「組裝」的過程延遲到了執行期。

lazy_static! {
    pub static ref 方案選單: Vec<(方案選項, fn(輸入方案環境) -> 輸入方案定義<'static>)> = vec![
        (方案選項::宮保粵拼, 宮保粵拼輸入方案), // 傳入函數指針
        // ...
    ];
}
  1. 在具體方案中攔截狀態,動態裝配

外層的派發器變得極度「無腦」,只負責透傳 環境。只有真正關心硬件佈局的 宮保粵拼 模組,纔會在內部 讀取 Signal 的值(觸發依賴收集)。

這樣一來,外層的 theory.rs 只是在無腦透傳上下文,完全不與 UI 狀態耦合;而當用戶在前端切換佈局時,Leptos 的響應式系統會精準定位到這個具體的方案生成函數,自動觸發重新求值,完成動態裝配。

pub fn 宮保粵拼輸入方案(環境: 輸入方案環境) -> 輸入方案定義<'static> {
    let 是縱版 = matches!(環境.已選配列(), Some(配列::縱向錯列分體));
    // ... 動態回傳組裝好的結構體
}

🎉 成果

這套重構完美實現了「開閉原則(OCP)」。外層框架徹底與硬件佈局解耦,而前端 UI 只要一切換佈局,Leptos 的細粒度響應式系統就會自動觸發重新求值,指法提示瞬間切換!

這是我在 Rust 與前端框架結合上一次非常痛快的體驗,分享給各位。📺 開發實錄

如果你也對人體工學鍵盤或輸入法演算法感興趣,歡迎來看看這個專案:

💻 在線體驗:https://rime.io/typewriter/

📦 Rime 配方包:https://github.com/lotem/rime-combo-jyutping