8 个版本 (4 个重大更新)
0.6.0 | 2023年11月24日 |
---|---|
0.5.0 | 2023年9月23日 |
0.4.0 |
|
0.3.2 | 2022年7月3日 |
0.1.1 | 2022年6月11日 |
#3 在 #wasm-js
每月142次下载
115KB
2.5K SLoC
wasm-react 🦀⚛️
为 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(LICENSE-APACHE 或 https://www.apache.org/licenses/LICENSE-2.0)
- MIT许可证(LICENSE-MIT 或 https://opensource.org/licenses/MIT)
任选其一。
贡献
除非您明确声明,否则根据Apache-2.0许可证定义的您有意提交以包含在作品中的任何贡献,将按上述方式双许可,无需任何额外条款或条件。
依赖
~7–9.5MB
~176K SLoC