#reactive

maple-core-macro

一个无 VDOM、细粒度响应式的 Web 库

9 个不稳定版本 (3 个破坏性更新)

0.4.3 2021 年 4 月 1 日
0.4.2 2021 年 4 月 1 日
0.4.1 2021 年 3 月 31 日
0.3.1 2021 年 3 月 16 日
0.1.1 2021 年 3 月 7 日

#209#reactive

每月 29 次下载
用于 maple-core

MIT 许可证

16KB
384 行代码

maple

Crates.io docs.rs GitHub contributors Discord

一个无 VDOM、细粒度响应式的 Web 库。

入门

推荐的构建工具是 Trunk。首先,将 maple-core 添加到您的 Cargo.toml

maple-core = "0.4.3"

将以下内容添加到您的 src/main.rs 文件中

use maple_core::prelude::*;

fn main() {
    let root = template! {
        p {
            "Hello World!"
        }
    };

    render(|| root);
}

就这样!您已经使用 maple 创建了一个 Hello World 程序。要运行应用程序,只需运行 trunk serve --open 并在您的网络浏览器中查看结果。

template!

maple 使用 template! 宏作为创建复杂用户界面的便捷方式。

// You can create nested elements.
template! {
    div {
        p {
            span { "Hello " }
            strong { "World!" }
        }
    }
};

// Attributes (including classes and ids) can also be specified.
template! {
    p(class="my-class", id="my-paragraph", aria-label="My paragraph")
};

template! {
    button(disabled="true") {
        "My button"
    }
}

// Events are attached using the `on:*` directive.
template! {
    button(on:click=|_| { /* do something */ }) {
        "Click me"
    }
}

响应式

与依赖虚拟 DOM (VDOM) 不同,maple 使用细粒度响应式来保持 DOM 和状态同步。实际上,maple 的响应式部分可以单独使用,而不依赖于 DOM 渲染部分。

响应式基于响应式原语。以下是一个示例

use maple_core::prelude::*;
let state = Signal::new(0); // create an atom with an initial value of 0

如果您熟悉 React hooks,这会立即显得很熟悉。

现在,要访问状态,我们像这样在 state 上调用 .get() 方法

println!("The state is: {}", state.get()); // prints "The state is: 0"

要更新状态,我们在 state 上调用 .set(...) 方法

state.set(1);
println!("The state is: {}", state.get()); // should now print "The state is: 1"

这有什么用?这很有用,因为它提供了一种轻松通知任何状态变化的方法。例如,如果我们想打印出每个状态变化,可以轻松地这样完成

let state = Signal::new(0);

create_effect(cloned!((state) => move || {
    println!("The state changed. New value: {}", state.get());
}));  // prints "The state changed. New value: 0" (note that the effect is always executed at least 1 regardless of state changes)

state.set(1); // prints "The state changed. New value: 1"
state.set(2); // prints "The state changed. New value: 2"
state.set(3); // prints "The state changed. New value: 3"

如何让 create_effect(...) 函数在状态变化时执行闭包?调用 create_effect 会创建一个新的 “响应式作用域”,在这个作用域内调用 state.get() 会将自身添加为一个 依赖。现在,当调用 state.set(...) 时,它会自动调用所有依赖项,在这种情况下,调用 state 因为它是在闭包内部被调用的。

那个 cloned! 宏是做什么的?

cloned! 宏是一个用于将变量克隆到后续表达式的实用宏。之前的 create_effect 函数调用完全可以写成以下形式

create_effect({
    let state = state.clone();
    move || {
        println!("The state changed. New value: {}", state.get());
    }
}));

这最终只是一个权宜之计,直到 Rust RFC #2407 发生某些事情。

我们还可以轻松地使用 create_memo(...) 创建派生状态,它实际上是 create_effect 的一个易于使用的包装器。

let state = Signal::new(0);
let double = create_memo(cloned!((state) => move || *state.get() * 2));

assert_eq!(*double.get(), 0);

state.set(1);
assert_eq!(*double.get(), 2);

create_memo(...) 会自动在依赖项变化时重新计算派生值。

现在您已经了解了 maple 的响应式系统,我们可以看看如何使用它来更新 DOM。

使用响应式进行 DOM 更新

响应式自动集成到 template! 宏中。假设我们有以下代码

use maple_core::prelude::*;

let state = Signal::new(0);

let root = template! {
    p {
        (state.get())
    }
}

这将扩展成类似以下内容

use maple_core::prelude::*;
use maple_core::internal;

let state = Signal::new(0);

let root = {
    let element = internal::element(p);
    let text = internal::text(String::new() /* placeholder */);
    create_effect(move || {
        // update text when state changes
        text.set_text_content(Some(&state.get()));
    });

    internal::append(&element, &text);

    element
}

如果我们在我们代码的某个地方调用 state.set(...),文本内容将自动更新!

组件

maple 中的组件是简单的函数,它们返回 HtmlElement。它们通过函数参数接收它们的属性。

为了使组件能够自动响应属性变化,它们应该接受一个类型为 StateHandle<T> 的属性,并在 template! 中调用该函数来订阅状态。

Signal<T> 获取一个 StateHandle<T> 非常简单。只需调用 .handle() 方法。

这是一个简单组件的示例

// This is temporary and will later be removed.
// Currently, the template! macro assumes that all components start with an uppercase character.
#![allow(non_snake_case)]

use maple_core::prelude::*;

fn Component(value: StateHandle<i32>) -> TemplateResult {
    template! {
        div(class="my-component") {
            "Value: " (value.get())
        }
    }
}

// ...
let state = Signal::new(0);

template! {
    Component(state.handle())
}

state.set(1); // automatically updates value in Component

贡献

问题报告和 PR 受欢迎!通过 ARCHITECTURE.md 熟悉项目结构。

依赖项

~1.5MB
~34K SLoC