Rust状态管理库hydration_context的使用:高效处理组件上下文与数据水合作用

<picture> <source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_pref_dark_RGB.svg" media="none" data-media="(prefers-color-scheme: dark)"> Leptos Logo </picture>

Leptos是一个全栈、同构的Rust Web框架,利用细粒度响应式系统构建声明式用户界面。

这意味着什么?

  • 全栈:Leptos可用于构建在浏览器中运行的应用程序(客户端渲染)、在服务器上运行的应用程序(服务器端渲染),或通过在服务器上渲染HTML然后在浏览器中添加交互性(带水合作用的服务器端渲染)。这包括支持HTTP流式传输数据(资源)和HTML(无序或有序流式传输Suspense组件)。
  • 同构:Leptos提供了编写同构服务器函数的原语,即可以在客户端或服务器上以"相同形状"调用的函数,但仅在服务器上运行。这意味着您可以将仅服务器逻辑(数据库请求、身份验证等)与将使用它的客户端组件一起编写,并像在浏览器中运行一样调用服务器函数,而无需创建和维护单独的REST或其他API。
  • Web:Leptos构建在Web平台和Web标准之上。路由器设计为使用Web基础(如链接和表单)并在此基础上构建,而不是试图替换它们。
  • 框架:Leptos提供了构建现代Web应用程序所需的大部分内容:响应式系统、模板库以及在服务器和客户端都能工作的路由器。
  • 细粒度响应式:整个框架构建自响应式原语。这允许以最小开销实现极其高性能的代码:当响应式信号的值更改时,它可以更新单个文本节点、切换单个类或从DOM中移除元素,而无需运行任何其他代码。(因此,没有虚拟DOM开销!)
  • 声明式:告诉Leptos您希望页面看起来如何,让框架告诉浏览器如何实现。

了解更多

以下是学习Leptos的更多资源:

  • 书(进行中)
  • 示例
  • API文档
  • 常见错误(以及如何修复它们!)

夜间版本说明

大多数示例假设您使用Rust的夜间版本和Leptos的夜间功能。要使用夜间Rust,您可以在全局或每个项目基础上设置工具链。

要为所有项目设置夜间作为默认工具链(并添加将Rust编译为WebAssembly的能力,如果尚未安装):

rustup toolchain install nightly
rustup default nightly
rustup target add wasm32-unknown-unknown

如果您只想在Leptos项目中使用夜间版本,请添加具有以下内容的rust-toolchain.toml文件:

[toolchain]
channel = "nightly"
targets = ["wasm32-unknown-unknown"]

夜间功能启用了访问和设置信号的函数调用语法,而不是.get()和.set()。这导致了一个一致的心智模型,其中访问任何类型的响应式值(信号、备忘录或派生信号)始终表示为函数调用。这只有在夜间Rust和夜间功能下才可能。

cargo-leptos

cargo-leptos是一个构建工具,旨在使构建在客户端和服务器上运行的应用程序变得容易,并具有无缝集成。目前开始一个真实Leptos项目的最佳方式是使用cargo-leptos以及我们为Actix或Axum的启动模板。

cargo install cargo-leptos
cargo leptos new --git https://github.com/leptos-rs/start
cd [您的项目名称]
cargo leptos watch

打开浏览器访问http://localhost:3000/。

常见问题解答

名称有什么含义?

Leptos(λεπτός)是一个古希腊词,意思是"薄、轻、精致、细粒度"。对我(一位古典学者,不是狗主人)来说,它唤起了驱动框架的轻量级响应式系统。我后来得知同一个词是医学术语"钩端螺旋体病"的词根,这是一种影响人类和动物的血液感染……我的错。在创建此框架时没有伤害任何狗。

它是否可用于生产?

人们通常通过这个问题意指三件事之一。

  1. API是否稳定? 即,我是否必须从Leptos 0.1重写整个应用程序到0.2到0.3到0.4,或者我现在可以编写它并随着新版本的发布受益于新功能和更新?

API基本上已经确定。我们正在添加新功能,但我们对类型系统和模式的落地位置非常满意。我不期望为了适应未来版本而在架构上对您的代码进行重大破坏性更改。

  1. 是否有错误?

是的,我确定有。您可以从我们的问题跟踪器的状态随时间变化看出,没有那么多错误,并且它们通常很快得到解决。但肯定的是,有时您可能会遇到需要在框架级别修复的问题,这可能不会立即解决。

  1. 我是消费者还是贡献者?

这可能是重要的一点:"生产就绪"意味着对库的某种定位:您基本上可以使用它,而无需任何对其内部结构的特殊知识或贡献能力。每个人在堆栈的某个级别都有这种情况:例如,我(@gbj)目前没有能力或知识来为像wasm-bindgen这样的东西做出贡献:我只是依赖它工作。

社区中有几个人现在在工作中为内部应用程序使用Leptos,他们也成为了重要的贡献者。我认为这是目前生产使用的正确水平。可能会有您需要的缺失功能,您最终可能会构建它们!但对于内部应用程序,如果您愿意沿途构建和贡献缺失的部分,该框架现在肯定可用。

我可以将此用于本机GUI吗?

当然!显然,视图宏用于生成DOM节点,但您可以使用响应式系统轻松驱动任何使用与DOM相同类型的面向对象、基于事件回调的框架的本机GUI工具包。原则是相同的:

  • 使用信号、派生信号和备忘录创建您的响应式系统
  • 创建GUI小部件
  • 使用事件监听器更新信号
  • 创建效果以更新UI

0.7更新最初旨在创建一种"通用渲染"方法,允许我们重用大部分相同的视图逻辑来完成上述所有操作。不幸的是,由于Rust编译器在使用此要求在整个代码库中传播的泛型数量构建大规模应用程序时遇到的困难,这不得不暂时搁置。这是我期待在未来再次探索的方法;如果您对这类工作感兴趣,请随时联系。

这与Yew有何不同?

Yew是用于Rust Web UI开发的最常用库,但Yew和Leptos在哲学、方法和性能方面存在一些差异。

  • VDOM与细粒度: Yew构建在虚拟DOM(VDOM)模型上:状态更改导致组件重新渲染,生成新的虚拟DOM树。Yew将其与先前的VDOM进行差异比较,并将这些补丁应用于实际DOM。状态更改时组件函数重新运行。Leptos采用完全不同的方法。组件运行一次,创建(并返回)实际DOM节点,并设置响应式系统以更新这些DOM节点。
  • 性能: 这对性能有巨大影响:Leptos在创建和更新UI方面都比Yew快得多。
  • 服务器集成: Yew创建于浏览器渲染的单页面应用程序(SPA)占主导地位的时代。虽然Leptos支持客户端渲染,但它还通过服务器函数和多种提供HTML的模式(包括无序流式传输)专注于与应用程序的服务器端集成。

这与Dioxus有何不同?

与Leptos一样,Dioxus是一个使用Web技术构建UI的框架。然而,在方法和功能上存在显著差异。

  • VDOM与细粒度: 虽然Dioxus有一个高性能的虚拟DOM(VDOM),但它仍然使用粗粒度/组件范围的响应式:更改状态值会重新运行组件函数,并将旧UI与新UI进行差异比较。Leptos组件使用不同的心智模型,创建(并返回)实际DOM节点,并设置响应式系统以更新这些DOM节点。
  • Web与桌面优先级: Dioxus在其全栈模式中使用Leptos服务器函数,但不具有相同的基于Suspense的支持,例如流式HTML渲染,或共享对整体Web性能的相同关注。Leptos倾向于优先考虑整体Web性能(流式HTML渲染、更小的WASM二进制大小等),而Dioxus在构建桌面应用程序时具有无与伦比的体验,因为您的应用程序逻辑作为本机Rust二进制文件运行。

这与Sycamore有何不同?

Sycamore和Leptos都深受SolidJS的影响。此时,Leptos拥有更大的社区和生态系统,并且开发更活跃。其他差异:

  • 模板DSL: Sycamore为其视图使用自定义模板语言,而Leptos使用类似JSX的模板格式。
  • ’static信号: Leptos的主要创新之一是创建了Copy + 'static信号,具有出色的易用性。Sycamore正在采用相同的模式,但尚未发布。
  • Perseus与服务器函数: Perseus元框架提供了一种构建包含服务器功能的Sycamore应用程序的固执己见的方式。Leptos相反在框架核心提供像服务器函数这样的原语。

示例代码

use leptos::*;

#[component]
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
    // 使用初始值创建一个响应式信号
    let (value, set_value) = signal(initial_value);

    // 为我们的按钮创建事件处理程序
    // 注意 `value` 和 `set_value` 是 `Copy`,所以很容易将它们移入闭包
    let clear = move |_| set_value(0);
    let decrement = move |_| set_value.update(|value| *value -= 1);
    let increment = move |_| set_value.update(|value| *value += 1);

    // 使用声明式 `view!` 宏创建用户界面
    view! {
        <div>
            <button on:click=clear>Clear</button>
            <button on:click=decrement>-1</button>
            // 文本节点可以加引号或不加引号
            <span>"Value: " {value} "!"</span>
            <button on:click=increment>+1</button>
        </div>
    }
}

// 我们也支持构建器语法,而不是类似JSX的 `view` 宏
#[component]
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
    use leptos::html::*;

    let (value, set_value) = signal(initial_value);
    let clear = move |_| set_value(0);
    let decrement = move |_| set_value.update(|value| *value -= 1);
    let increment = move |_| set_value.update(|value| *value += 1);

    // 上面的 `view` 宏扩展为此构建器语法
    div().child((
        button().on(ev::click, clear).child("Clear"),
        button().on(ev::click, decrement).child("-1"),
        span().child(("Value: ", value, "!")),
        button().on(ev::click, increment).child("+1")
    ))
}

// 易于与Trunk (trunkrs.dev) 或简单的wasm-bindgen设置一起使用
pub fn main() {
    mount_to_body(|| view! {
        <SimpleCounter initial_value=3 />
    })
}

完整示例代码

use leptos::*;

// 定义一个简单的计数器组件
#[component]
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
    // 创建响应式信号
    let (value, set_value) = signal(initial_value);
    
    // 事件处理函数
    let clear = move |_| set_value(0);
    let decrement = move |_| set_value.update(|value| *value -= 1);
    let increment = move |_| set_value.update(|value| *value += 1);

    // 返回视图
    view! {
        <div class="counter">
            <button on:click=clear class="btn-clear">清除</button>
            <button on:click=decrement class="btn-decrement">-1</button>
            <span class="value">"当前值: " {value}</span>
            <button on:click=increment class="btn-increment">+1</button>
        </div>
    }
}

// 主函数
pub fn main() {
    // 挂载组件到body
    mount_to_body(|| view! {
        <div class="app">
            <h1>"Leptos 计数器示例"</h1>
            <SimpleCounter initial_value=0/>
        </div>
    })
}

// 样式可以通过CSS文件添加,这里使用内联样式示例
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
    // 水合函数,用于客户端渲染
    leptos::mount_to_body(|| view! {
        <div class="app">
            <h1>"Leptos 计数器示例"</h1>
            <SimpleCounter initial_value=0/>
        </div>
    });
}

1 回复

Rust状态管理库hydration_context的使用指南

介绍

hydration_context是一个专门为Rust设计的轻量级状态管理库,专注于处理组件上下文和数据水合作用。该库通过提供高效的数据同步机制和上下文管理功能,帮助开发者构建响应式应用程序。

核心特性

  • 高效数据同步:自动处理组件间的数据流动
  • 上下文管理:简化组件层级间的状态共享
  • 类型安全:充分利用Rust的类型系统保证安全性
  • 零成本抽象:运行时开销极小

安装方法

在Cargo.toml中添加依赖:

[dependencies]
hydration_context = "0.3.0"

基本使用方法

1. 创建上下文提供者

use hydration_context::{create_context, ContextProvider, use_context};

#[derive(Clone)]
struct AppState {
    count: i32,
    user: String,
}

fn main() {
    let initial_state = AppState {
        count: 0,
        user: "John".to_string(),
    };
    
    let context = create_context(initial_state);
    
    yew::start_app_with_props::<ContextProvider<AppState>>(context);
}

2. 在组件中使用上下文

use hydration_context::use_context;
use yew::prelude::*;

#[function_component(Counter)]
fn counter() -> Html {
    let state = use_context::<AppState>().unwrap();
    
    html! {
        <div>
            <p>{ format!("Count: {}", state.count) }</p>
            <p>{ format!("User: {}", state.user) }</p>
        </div>
    }
}

3. 更新上下文状态

use hydration_context::{use_context, ContextUpdater};

#[function_component(UpdateButton)]
fn update_button() -> Html {
    let mut updater = ContextUpdater::<AppState>::new();
    
    let onclick = Callback::from(move |_| {
        updater.update(|state| {
            state.count += 1;
        });
    });
    
    html! {
        <button {onclick}>{ "Increment" }</button>
    }
}

高级用法示例

数据水合作用

use hydration_context::{hydrate, dehydrate};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone)]
struct HydratedData {
    preferences: Vec<String>,
    session_data: Option<String>,
}

fn handle_hydration() {
    // 从存储中获取脱水数据
    let dehydrated = localStorage::get("app_state");
    
    // 水合数据
    let hydrated_data: HydratedData = hydrate(&dehydrated).unwrap();
    
    // 使用水合后的数据
    let context = create_context(hydrated_data);
}

选择性重渲染

use hydration_context::{use_context, should_update};

#[function_component(OptimizedComponent)]
fn optimized_component() -> Html {
    let state = use_context::<AppState>().unwrap();
    
    // 只有当count变化时才重渲染
    if !should_update(|| state.count) {
        return html! {};
    }
    
    html! {
        <div>{ state.count }</div>
    }
}

完整示例demo

use hydration_context::{create_context, ContextProvider, use_context, ContextUpdater, hydrate, dehydrate, should_update};
use yew::prelude::*;
use serde::{Deserialize, Serialize};
use web_sys::window;

// 定义应用状态结构体
#[derive(Clone, Serialize, Deserialize)]
struct AppState {
    count: i32,
    user: String,
    preferences: Vec<String>,
}

// 主应用组件
#[function_component(App)]
fn app() -> Html {
    // 初始化应用状态
    let initial_state = AppState {
        count: 0,
        user: "John Doe".to_string(),
        preferences: vec!["dark_mode".to_string(), "notifications".to_string()],
    };
    
    // 创建上下文
    let context = create_context(initial_state);
    
    html! {
        <ContextProvider<AppState> context={context}>
            <div class="app">
                <Header />
                <Counter />
                <UpdateButton />
                <Preferences />
            </div>
        </ContextProvider<AppState>>
    }
}

// 头部组件
#[function_component(Header)]
fn header() -> Html {
    let state = use_context::<AppState>().unwrap();
    
    html! {
        <header>
            <h1>{ "Hydration Context Demo" }</h1>
            <p>{ format!("Welcome, {}!", state.user) }</p>
        </header>
    }
}

// 计数器组件
#[function_component(Counter)]
fn counter() -> Html {
    let state = use_context::<AppState>().unwrap();
    
    html! {
        <div class="counter">
            <h2>{ "Counter" }</h2>
            <p>{ format!("Current count: {}", state.count) }</p>
        </div>
    }
}

// 更新按钮组件
#[function_component(UpdateButton)]
fn update_button() -> Html {
    let mut updater = ContextUpdater::<AppState>::new();
    
    let increment = {
        let mut updater = updater.clone();
        Callback::from(move |_| {
            updater.update(|state| {
                state.count += 1;
            });
        })
    };
    
    let decrement = Callback::from(move |_| {
        updater.update(|state| {
            state.count -= 1;
        });
    });
    
    html! {
        <div class="buttons">
            <button onclick={increment}>{ "Increment" }</button>
            <button onclick={decrement}>{ "Decrement" }</button>
        </div>
    }
}

// 偏好设置组件(使用选择性重渲染)
#[function_component(Preferences)]
fn preferences() -> Html {
    let state = use_context::<AppState>().unwrap();
    let mut updater = ContextUpdater::<AppState>::new();
    
    // 只有当preferences变化时才重渲染
    if !should_update(|| state.preferences.clone()) {
        return html! {};
    }
    
    let toggle_preference = |pref: String| {
        let mut updater = updater.clone();
        Callback::from(move |_| {
            updater.update(|state| {
                if state.preferences.contains(&pref) {
                    state.preferences.retain(|p| p != &pref);
                } else {
                    state.preferences.push(pref.clone());
                }
            });
        })
    };
    
    html! {
        <div class="preferences">
            <h2>{ "Preferences" }</h2>
            <div>
                <label>
                    <input 
                        type="checkbox" 
                        checked={state.preferences.contains(&"dark_mode".to_string())}
                        onchange={toggle_preference("dark_mode".to_string())}
                    />
                    { "Dark Mode" }
                </label>
            </div>
            <div>
                <label>
                    <input 
                        type="checkbox" 
                        checked={state.preferences.contains(&"notifications".to_string())}
                        onchange={toggle_preference("notifications".to_string())}
                    />
                    { "Notifications" }
                </label>
            </div>
        </div>
    }
}

// 数据水合功能示例
fn hydration_example() {
    // 模拟从localStorage获取数据
    let saved_state = r#"
        {
            "count": 42,
            "user": "Hydrated User",
            "preferences": ["dark_mode"]
        }
    "#;
    
    // 水合数据
    match hydrate::<AppState>(saved_state) {
        Ok(hydrated_state) => {
            println!("Successfully hydrated state: {:?}", hydrated_state);
            
            // 创建水合后的上下文
            let context = create_context(hydrated_state);
            
            // 可以在这里启动应用或更新现有上下文
        }
        Err(e) => {
            eprintln!("Hydration failed: {}", e);
        }
    }
}

// 脱水功能示例
fn dehydration_example(state: &AppState) {
    match dehydrate(state) {
        Ok(dehydrated) => {
            println!("Dehydrated state: {}", dehydrated);
            
            // 可以保存到localStorage或其他存储
            if let Some(window) = window() {
                if let Ok(Some(local_storage)) = window.local_storage() {
                    let _ = local_storage.set_item("app_state", &dehydrated);
                }
            }
        }
        Err(e) => {
            eprintln!("Dehydration failed: {}", e);
        }
    }
}

fn main() {
    // 启动Yew应用
    yew::start_app::<App>();
    
    // 演示水合功能
    hydration_example();
}

最佳实践

  1. 合理划分上下文:根据功能模块创建不同的上下文
  2. 使用选择性更新:避免不必要的重渲染
  3. 错误处理:妥善处理上下文未找到的情况
  4. 类型安全:充分利用Rust的类型系统

性能优化建议

  • 使用should_update进行选择性重渲染
  • 避免在上下文中存储过大对象
  • 合理使用数据水合,避免频繁的序列化/反序列化操作

这个库特别适合构建复杂的Web应用程序,能够有效管理组件状态和数据流,同时保持代码的清晰和可维护性。

回到顶部