8 个版本 (4 个重大更新)

0.6.0 2023年11月24日
0.5.0 2023年9月23日
0.4.0 2022年8月10日
0.3.2 2022年7月3日
0.1.1 2022年6月11日

#3#wasm-js

Download history 41/week @ 2024-04-23 2/week @ 2024-05-21 7/week @ 2024-05-28 4/week @ 2024-06-04 3/week @ 2024-06-11 1/week @ 2024-06-18 6/week @ 2024-06-25 6/week @ 2024-07-02 2/week @ 2024-07-16 140/week @ 2024-07-30

每月142次下载

MIT/Apache

115KB
2.5K SLoC

wasm-react 🦀⚛️

GitHub crates.io CI docs.rs

React 提供的 WASM 绑定。

简介

此库允许您在 Rust 中编写和使用 React 组件,然后可以导出为 JS 以供重用或渲染。

为何选择 React?

React 是最受欢迎的 JS UI 框架之一,拥有繁荣的社区和大量为其编写的库。站在巨人的肩膀上,您将能够使用 Rust 编写复杂的客户端应用程序。

目标

  • 尽可能接近原始 API,为 react 的公共 API 提供 Rust 绑定。
  • 提供编写组件的直观方式。
  • 提供与用 JS 编写的组件交互的方法。

非目标

  • 为除 react 之外的任何库提供绑定,例如 react-dom
  • 重实现协调算法或运行时。
  • 注重性能。

入门指南

请确保您已安装 Rust 和 Cargo。您可以使用 cargo 安装 wasm-react。此外,如果您想将您的 Rust 组件暴露给 JS,您还需要安装 wasm-bindgen 并安装 wasm-pack

$ cargo add wasm-react
$ cargo add [email protected]

创建组件

首先,您需要为组件的 props 定义一个结构体。要定义渲染函数,您需要为您的结构体实现 Component 特性。

use wasm_react::{h, Component, VNode};

struct Counter {
  counter: i32,
}

impl Component for Counter {
  fn render(&self) -> VNode {
    h!(div)
      .build((
        h!(p).build(("Counter: ", self.counter)),
        h!(button).build("Increment"),
      ))
  }
}

添加状态

您可以使用 use_state() 钩子使您的组件有状态。

use wasm_react::{h, Component, VNode};
use wasm_react::hooks::use_state;

struct Counter {
  initial_counter: i32,
}

impl Component for Counter {
  fn render(&self) -> VNode {
    let counter = use_state(|| self.initial_counter);

    let result = h!(div)
      .build((
        h!(p).build(("Counter: ", *counter.value())),
        h!(button).build("Increment"),
      ));
    result
  }
}

请注意,根据 Rust 的常规规则,状态将在渲染函数返回时被丢弃。 use_state() 通过将状态的生命周期与组件的生命周期绑定来防止这种情况,因此 持久化 状态通过组件的整个生命周期。

添加事件处理器

要创建一个事件处理器,您需要传递一个由Rust闭包创建的 Callback。您可以使用辅助宏 clones! 来更方便地克隆捕获环境。

use wasm_react::{h, clones, Component, Callback, VNode};
use wasm_react::hooks::{use_state, Deps};

struct Counter {
  initial_counter: i32,
}

impl Component for Counter {
  fn render(&self) -> VNode {
    let message = use_state(|| "Hello World!");
    let counter = use_state(|| self.initial_counter);

    let result = h!(div)
      .build((
        h!(p).build(("Counter: ", *counter.value())),

        h!(button)
          .on_click(&Callback::new({
            clones!(message, mut counter);

            move |_| {
              println!("{}", message.value());
              counter.set(|c| c + 1);
            }
          }))
          .build("Increment"),

        h!(button)
          .on_click(&Callback::new({
            clones!(mut counter);

            move |_| counter.set(|c| c - 1)
          }))
          .build("Decrement"),
      ));
    result
  }
}

导出组件以供JS使用

首先,您需要 wasm-pack。您可以使用 export_components! 将您的Rust组件导出以供JS使用。要求是您的组件实现 TryFrom<JsValue, Error = JsValue>

use wasm_react::{h, export_components, Component, VNode};
use wasm_bindgen::JsValue;

struct Counter {
  initial_counter: i32,
}

impl Component for Counter {
  fn render(&self) -> VNode {
    /**/
    VNode::new()
  }
}

struct App;

impl Component for App {
  fn render(&self) -> VNode {
    h!(div).build((
      Counter {
        initial_counter: 0,
      }
      .build(),
    ))
  }
}

impl TryFrom<JsValue> for App {
  type Error = JsValue;

  fn try_from(_: JsValue) -> Result<Self, Self::Error> {
    Ok(App)
  }
}

export_components! { App }

使用 wasm-pack 将您的Rust代码编译成WASM

$ wasm-pack build

根据您的JS项目结构,您可能需要指定 --target 选项,请参阅 wasm-pack 文档

假设您使用支持在ES模块中使用JSX和WASM导入的打包器,例如Webpack,您可以使用

import React from "react";
import { createRoot } from "react-dom/client";

async function main() {
  const { WasmReact, App } = await import("./path/to/pkg/project.js");
  WasmReact.useReact(React); // Tell wasm-react to use your React runtime

  const root = createRoot(document.getElementById("root"));
  root.render(<App />);
}

如果您使用纯ES模块,您可以这样做

$ wasm-pack build --target web
import "https://unpkg.com/react/umd/react.production.min.js";
import "https://unpkg.com/react-dom/umd/react-dom.production.min.js";
import init, { WasmReact, App } from "./path/to/pkg/project.js";

async function main() {
  await init(); // Need to load WASM first
  WasmReact.useReact(window.React); // Tell wasm-react to use your React runtime

  const root = ReactDOM.createRoot(document.getElementById("root"));
  root.render(React.createElement(App, {}));
}

导入组件以供Rust使用

您可以使用 import_components!wasm-bindgen 一起导入JS组件以供Rust使用。首先,准备您的JS组件

// /.dummy/myComponents.js
import "https://unpkg.com/react/umd/react.production.min.js";

export function MyComponent(props) {
  /* … */
}

确保组件使用与 wasm-react 指定相同的React运行时。之后,使用 import_components!

use wasm_react::{h, import_components, Component, VNode};
use wasm_bindgen::prelude::*;

import_components! {
  #[wasm_bindgen(module = "/.dummy/myComponents.js")]

  MyComponent
}

struct App;

impl Component for App {
  fn render(&self) -> VNode {
    h!(div).build((
      MyComponent::new()
        .attr("prop", &"Hello World!".into())
        .build(()),
    ))
  }
}

向下传递非复制属性

假设您定义了一个以下结构的组件

use std::rc::Rc;

struct TaskList {
  tasks: Vec<Rc<str>>
}

您想在容器组件 App 中包含 TaskList,其中 tasks 由状态管理

use std::rc::Rc;
use wasm_react::{h, Component, VNode};
use wasm_react::hooks::{use_state, State};

struct TaskList {
  tasks: Vec<Rc<str>>
}

impl Component for TaskList {
  fn render(&self) -> VNode {
    /**/
    VNode::default()
  }
}

struct App;

impl Component for App {
  fn render(&self) -> VNode {
    let tasks: State<Vec<Rc<str>>> = use_state(|| vec![]);

    h!(div).build((
      TaskList {
        tasks: todo!(), // Oops, `tasks.value()` does not fit the type
      }
      .build(),
    ))
  }
}

tasks 的类型改为适合 tasks.value() 不起作用,因为 tasks.value() 返回一个非 'static 引用,而组件结构体只能包含 'static 值。您可以将底层的 Vec 进行克隆,但这会引入不必要的开销。在这种情况下,您可能认为您可以将 TaskList 的类型改为 State

use std::rc::Rc;
use wasm_react::{h, Component, VNode};
use wasm_react::hooks::{use_state, State};

struct TaskList {
  tasks: State<Vec<Rc<str>>>
}

只要属性 tasks 保证来自状态,这就会起作用。但这种假设可能不成立。您可能希望将来或在其他地方将 Rc<Vec<Rc<str>>>Memo<Vec<Rc<str>>> 传递下去。为了尽可能通用,您可以使用 PropContainer

use std::rc::Rc;
use wasm_react::{h, Component, PropContainer, VNode};
use wasm_react::hooks::{use_state, State};

struct TaskList {
  tasks: PropContainer<Vec<Rc<str>>>
}

impl Component for TaskList {
  fn render(&self) -> VNode {
    /* Do something with `self.tasks.value()`… */
    VNode::default()
  }
}

struct App;

impl Component for App {
  fn render(&self) -> VNode {
    let tasks: State<Vec<Rc<str>>> = use_state(|| vec![]);

    h!(div).build((
      TaskList {
        // Cloning `State` has low cost as opposed to cloning the underlying
        // `Vec`.
        tasks: tasks.clone().into(),
      }
      .build(),
    ))
  }
}

已知注意事项

  • Rust组件不能是 StrictMode 组件子树的组成部分。

    wasm-react 使用React钩子手动管理Rust内存。 StrictMode 将运行两次钩子和它们的析构函数,这会导致双重释放。

许可证

在以下任一许可证下授权

任选其一。

贡献

除非您明确声明,否则根据Apache-2.0许可证定义的您有意提交以包含在作品中的任何贡献,将按上述方式双许可,无需任何额外条款或条件。

依赖

~7–9.5MB
~176K SLoC