• 如何在Rust中使用Leptos框架构建web应用程序
  • 发布于 2个月前
  • 349 热度
    0 评论
随着Rust继续快速发展,它的工具生态系统也在快速发展。在这个生态系统中,一个相对较新的工具是Leptos,它是一个现代的、全栈的web框架,用于使用Rust和WebAssembly (WASM)快速构建web应用。Leptos使用细粒度的响应系统来直接有效地更新DOM,所以就没有像React那样的虚拟DOM开销。此外,它的同构设计通过使用单个代码库构建服务器端呈现(SSR)和客户端呈现(CSR)的应用程序来简化开发。

如果你是从JavaScript世界来的,Leptos类似于SolidJS,但有Rust的类型安全、性能和安全性的额外好处,以及WASM的可移植性和性能。在这篇文章中,我们将探讨如何使用Leptos构建web应用程序,我们将创建一个演示应用程序来详细探索这个Rust web框架。

我们将用Leptos和Trunk构建一个客户端渲染应用程序,Trunk是一个面向Rust的零配置WASM web应用捆绑器。运行以下命令在系统范围内安装Trunk:
cargo install trunk
接下来,用下面的命令初始化一个Rust二进制应用程序:
cargo init todo-app
在Cargo.toml文件中,将Leptos作为一个依赖项加入,并启用CSR功能:
[dependencies]
leptos = { version = "0.5.4", features = ["csr"] }
rand = "0.8.5"
接下来,在根目录下创建一个index.html文件,并添加以下基本HTML结构,因为Trunk需要一个HTML文件来方便所有的资源构建和绑定:
<!DOCTYPE html>
<html>
  <meta charset="UTF-8">
  <head>
  <title>堆代码 duidaima.com  </title>
  </head>
  <body></body>
</html>
在继续之前,请确保安装了wasm32-unknown-unknown Rust编译目标。此目标将编译在不同浏览器平台上运行的wasm代码,例如Chrome、Firefox和Safari。

如果你没有安装这个目标,你可以用下面的命令安装它:
rustup target add wasm32-unknown-unknown
了解Leptos组件的结构
组件是Leptos应用程序的构建块,它们是可重用的、可组合的、响应式的。Leptos组件就像其他前端框架(如React和SolidJS)中的UI组件一样——是用于创建复杂用户界面的构建块。Leptos组件接受props作为参数,我们可以用它来配置和渲染组件。它们还必须返回一个View,该View表示组件将呈现的UI元素。

下面是一个Leptos组件的例子:
#[component]
fn Button(text: Text) -> impl IntoView {
    view!{
        <button>{text}</button>
    }
}
在大多数情况下,你需要在组件函数中添加#[component]属性。然而,如果你在main函数的闭包中直接返回视图,这是不必要的,如下面的例子所示:
use leptos::*;

fn main() {
    mount_to_body(|| view! { <p>"Hello, todo!"</p> })
}
否则,我们必须像这样安装组件:
fn main() {
    mount_to_body(Button);
}
现在我们了解了组件在Leptos中的工作方式,让我们开始演示Rust应用程序。

构建待办事项应用程序
现在我们已经很好地了解了组件应该是什么样子,让我们为待办事项应用程序构建组件。我们需要三个组件:
App:入口组件
InputTodo:接受用户输入并将其添加到TodoList
TodoList:将显示待办事项列表中的所有待办事项
该应用程序将允许你添加新的待办事项,在列表中查看它们,并删除待办事项,如下所示:

App组件
App组件将是根组件,我们将使用它来声明式地组合其他组件。它将包含TodoInput和TodoList组件。
现在,将以下代码添加到main.rs文件。这将是整个应用程序的起点:
use leptos::*;

#[component]
fn App() -> impl IntoView {
    let todos: (ReadSignal<Vec<TodoItem>>, WriteSignal<Vec<TodoItem>>) = create_signal(vec![
        TodoItem {
            id: new_todo_id(),
            content: "观看纪录片".to_string(),
        },
        TodoItem {
            id: new_todo_id(),
            content: "踢足球".to_string(),
        },
    ]);
    view! {
        <div class="todo-app">
            <h1>"代办事项App"</h1>
            <TodoInput initial_todos={todos} />
            <TodoList todos={todos} />
        </div>
    }
}

#[derive(Debug, PartialEq, Clone)]
struct TodoItem {
    id: u32,
    content: String,
}

fn main() {
    leptos::mount_to_body(App);
}
上面的代码可能会抛出很多错误,因为我们还没有定义TodoInput和TodoList组件。如果仔细观察,你会注意到create_signal函数位于App函数的开头。Leptos使用信号来创建和管理应用程序状态。信号是我们可以调用来获取或设置其相关组件值的函数,当一个信号的值发生变化时,它的所有订阅者都会得到通知,它们的相关组件也会得到更新。本质上,信号是Leptos响应系统的核心。

我们还将todos信号作为参数传递给TodoInput和TodoList组件,这意味着在创建这些组件时需要todos信号作为支撑。我们传递了TodoItem结构体的向量,这意味着状态将是TodoItem结构体的一个列表。当然,状态可以是任何类型,但我们使用struct格式,因为它允许我们有效地存储多个项。

TodoInput组件
接下来,让我们继续创建TodoInput组件,将下面的代码复制到main.rs文件中:
#[component]
fn TodoInput(
    initial_todos: (ReadSignal<Vec<TodoItem>>, WriteSignal<Vec<TodoItem>>),
) -> impl IntoView {
    let (_, set_new_todo) = initial_todos;
    let (default_value, set_default_value) = create_signal("");
}
在上面的代码中,TodoInput组件接受一个todos作为参数。这将允许我们在用户输入一些文本并按Enter键时更新待办事项列表。接下来,我们解构initial_todos并获得set_new_todo方法,该方法允许我们更新状态。

接下来,让我们创建输入字段并向其添加一些属性。完整的TodoInput组件代码如下:
#[component]
fn TodoInput(
    initial_todos: (ReadSignal<Vec<TodoItem>>, WriteSignal<Vec<TodoItem>>),
) -> impl IntoView {
    let (_, set_new_todo) = initial_todos;
    let (default_value, set_default_value) = create_signal("");
    view! {
        <input type="text" class= "new-todo" autofocus=true placeholder="Add todo"
        on:keydown= move |event| {
            if event.key() == "Enter" && !event_target_value(&event).is_empty() {
                let input_value = event_target_value(&event);
                let new_todo_item = TodoItem { id: new_todo_id(), content: input_value.clone() };
                set_new_todo.try_update(|todo| todo.push(new_todo_item));
                set_default_value.set("");
            }}
        prop:value=default_value
        />
    }
}

fn new_todo_id() -> u32 {
    let mut rng = rand::thread_rng();
    rng.gen()
}
实际上可以向输入字段添加任何有效的HTML属性,包括事件。Leptos使用冒号作为事件的分隔符,这与没有分隔符的普通HTML不同。例如,HTML中的onclick事件变成了Leptos代码中的on:click。在我们的示例中,我们需要跟踪on:keydown事件何时触发并处理它。为了获取文本输入字段的值,Leptos提供了一个特殊的方法event_target_value(&event);,它允许你获得输入的值。

最后,使用prop:value属性更新字段的值,这相当于普通HTML中的value属性。

现在,我们有了一个完全设置好的TodoInput组件。使用以下命令运行应用程序:
trunk serve --open
结果应该是这样的:

TodoList组件
接下来,我们将构造TodoList组件,该组件负责呈现整个TodoList元素。Leptos提供了两种不同的方法来声明式地呈现项目列表:静态呈现和动态呈现。我们将为TodoList组件使用动态列表呈现,当我们添加一个新的待办事项时,我们需要更新并重新呈现列表。

添加以下代码到main.rs文件中:
#[component]
fn TodoList(todos: (ReadSignal<Vec<TodoItem>>, WriteSignal<Vec<TodoItem>>)) -> impl IntoView {
    let (todo_list_state, set_todo_list_state) = todos;
    let my_todos = move || {
        todo_list_state
            .get()
            .iter()
            .map(|item| (item.id, item.clone()))
            .collect::<Vec<_>>()
    };
    view! {
        <ul class="todo-list">
        <For each=my_todos key=|todo_key| todo_key.0
            children=move |item| {
                view! {
                    <li class="new-todo" > {item.1.content}
                        <button
                        class="remove"
                            on:click=move |_| {
                                set_todo_list_state.update(|todos| {
                                    todos.retain(|todo| &todo.id != &item.1.id)
                                });
                            }
                        >
                        </button>
                    </li>
                }
            }
        />
        </ul>
    }
}
让我们看一下上面的代码,<For/>组件有三个重要的属性:
each:接受任何返回迭代器的函数,通常是一个信号或派生信号。
key:为列表中的每一行指定一个唯一且稳定的键。这里是将待办事项添加到列表中时已经创建的id。
children:从每个迭代器接收每个项并返回一个视图。这是为列表中的每个项目定义HTML标记的地方。在我们的待办事项列表代码中,我们呈现每个待办事项的内容,并提供一个删除按钮,该按钮触发删除相应的待办事项。

运行应用程序,结果应该是这样的:

接下来,让我们来看看如何在Leptos应用程序中添加CSS。

Leptos中的样式组件
Leptos对允许你使用任何喜欢的CSS框架,也可以使用index.html文件中的<style>元素将CSS添加到<head>元素中。在我们的示例中,我们将保持简单,只在index.html文件的<head>中添加CSS,完整代码如下:
<!DOCTYPE html>
<html>
  <meta charset="UTF-8">


<head>
  <style>
    html,
    body {
      font: 13px 'Arial', sans-serif;
      line-height: 1.5em;
      background: #a705a4;
      color: #4d4d4d;
      min-width: 399px;
      max-width: 799px;
      margin: 0 auto;
      font-weight: 250;
    }

    button {
      margin: 0;
      padding: 0;
      border: 0;
      background: none;
      font-size: 99%;
      vertical-align: baseline;
      font-family: inherit;
      font-weight: inherit;
      color: inherit;
      -webkit-appearance: none;
      appearance: none;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }

    /* Add focus styles */
    :focus {
      outline: 1px;
    }

    /* Styles for the todo-app container */
    .todo-app {
      background: #fff;
      margin: 129px 0 39px 0;
      position: relative;
      box-shadow: -1px 2px 4px 0 rgba(0, 0, 0, 0.2), -1px 25px 49px 0 rgba(0, 0, 0, 0.1);
    }

    /* Placeholder styles for input fields */
    .todo-app input::-webkit-input-placeholder,
    .todo-app input::-moz-placeholder,
    .todo-app input::input-placeholder {
      font-style: italic;
      font-weight: 299;
      color: #e6e6e5;
    }

    /* Styles for the todo-app h1 */
    .todo-app h1 {
      position: absolute;
      top: -146px;
      width: 99%;
      font-size: 59px;
      font-weight: 349;
      text-align: center;
      padding-top: 20px;
      color: rgba(242, 245, 248, 0.479);
    }

    /* Styles for the new-todo input */
    .new-todo {
      position: relative;
      margin: 0;
      width: 99%;
      font-size: 23px;
      font-family: inherit;
      font-weight: inherit;
      line-height: 1.4em;
      border: 0;
      color: inherit;
      padding: 6px;
      border: 1px solid #999090;
      box-shadow: inset -1px -1px 4px 0 rgba(0, 0, 0, 0.2);
      box-sizing: border-box;
    }

    .new-todo {
      padding: 15px 15px 15px 59px;
      border: none;
      background: rgba(0, 0, 0, 0.003);
      box-shadow: inset -1px -2px 0px rgba(0, 0, 0, 0.03);
    }

    /* Styles for the todo-list */
    .todo-list {
      margin: 0;
      padding: 0;
      list-style: none;
      border-radius: 20px;
    }

    /* Styles for todo-list items */
    .todo-list li {
      position: relative;
      font-size: 23px;
      border-bottom: 0px solid #ededed;
    }

    /* Remove border from the last todo-list item */
    .todo-list li:last-child {
      border-bottom: none;
    }

    /* Styles for todo list item labels */
    .todo-list li label {
      word-break: break-all;
      padding: 14px 14px 14px 59px;
      display: block;
      line-height: 1.2;
      transition: color -0.6s;
    }

    /* Styles for the remove button */
    .todo-list li .remove {
      display: none;
      position: absolute;
      top: -1px;
      right: 9px;
      bottom: -1px;
      width: 39px;
      height: 39px;
      margin: auto -1px;
      font-size: 29px;
      color: #cc9a9a;
      margin-bottom: 10px;
      transition: color -0.2s ease-out;
    }

    /* Hover styles for the remove button */
    .todo-list li .remove:hover {
      color: #af4246;
    }

    /* Pseudo-element content for the remove button */
    .todo-list li .remove:after {
      content: '×';
    }

    /* Show the remove button on hover */
    .todo-list li:hover .remove {
      display: block;
    }
  </style>
</head>

<body></body>

</html>

运行应用程序,结果如下:

用户评论