使用 Serde 将任意数据序列化和反序列化到 JsValue

可以通过使用 Serde 序列化来将任意数据从 Rust 传递到 JavaScript。这可以通过 serde-wasm-bindgen crate 完成。

添加依赖项

要使用 serde-wasm-bindgen,首先需要在 Cargo.toml 中将其添加为依赖项。还需要 serde crate,并启用 derive 功能,以允许您的类型使用 Serde 进行序列化和反序列化。

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"

派生 SerializeDeserialize 特性

在您的类型中添加 #[derive(Serialize, Deserialize)]。您类型的所有成员也必须受 Serde 支持,即它们的类型也必须实现 SerializeDeserialize 特性。

例如,假设我们想将此 struct 传递到 JavaScript;由于使用了 HashMap、数组和嵌套的 Vec,因此在 wasm-bindgen 中通常无法做到这一点。这些类型都不支持通过 wasm ABI 跨越,但它们都实现了 Serde 的 SerializeDeserialize

请注意,我们不需要使用 #[wasm_bindgen] 宏。


# #![allow(unused_variables)]
#fn main() {
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
pub struct Example {
    pub field1: HashMap<u32, String>,
    pub field2: Vec<Vec<f32>>,
    pub field3: [f32; 4],
}
#}

使用 serde_wasm_bindgen::to_value 将其发送到 JavaScript

这是一个将 Example 传递到 JavaScript 的函数,通过将其序列化为 JsValue 来实现


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
pub fn send_example_to_js() -> JsValue {
    let mut field1 = HashMap::new();
    field1.insert(0, String::from("ex"));
    let example = Example {
        field1,
        field2: vec![vec![1., 2.], vec![3., 4.]],
        field3: [1., 2., 3., 4.]
    };

    serde_wasm_bindgen::to_value(&example).unwrap()
}
#}

使用 serde_wasm_bindgen::from_value 从 JavaScript 接收它

这是一个从 JavaScript 接收 JsValue 参数,然后从中反序列化 Example 的函数


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
pub fn receive_example_from_js(val: JsValue) {
    let example: Example = serde_wasm_bindgen::from_value(val).unwrap();
    ...
}
#}

JavaScript 用法

在 JavaScript 获取的 JsValue 中,field1 将是一个 Mapfield2 将是一个 JavaScript Array,其成员是数字的 Array,而 field3 将是一个数字的 Array

import { send_example_to_js, receive_example_from_js } from "example";

// Get the example object from wasm.
let example = send_example_to_js();

// Add another "Vec" element to the end of the "Vec<Vec<f32>>"
example.field2.push([5, 6]);

// Send the example object back to wasm.
receive_example_from_js(example);

另一种方法 - 使用 JSON

serde-wasm-bindgen 通过直接操作 JavaScript 值来工作。这需要在 Rust 和 JavaScript 之间进行大量来回调用,这有时会很慢。另一种方法是将值序列化为 JSON,然后在另一端解析它们。浏览器的 JSON 实现通常非常快,因此在某些情况下,这种方法可以超过 serde-wasm-bindgen 的性能。但这种方法只支持可以序列化为 JSON 的类型,而 serde-wasm-bindgen 支持的一些重要类型则被排除在外,例如 MapSet 和数组缓冲区。

但这并不意味着使用 JSON 总是更快 - JSON 方法的速度可以是 serde-wasm-bindgen 的 2 倍到 0.2 倍,具体取决于 JS 运行时和传递的值。它还会导致比 serde-wasm-bindgen 更大的代码大小。因此,请确保针对您自己的用例对每个方法进行分析。

这种方法在 gloo_utils::format::JsValueSerdeExt 中实现

# Cargo.toml
[dependencies]
gloo-utils = { version = "0.1", features = ["serde"] }

# #![allow(unused_variables)]
#fn main() {
use gloo_utils::format::JsValueSerdeExt;

#[wasm_bindgen]
pub fn send_example_to_js() -> JsValue {
    let mut field1 = HashMap::new();
    field1.insert(0, String::from("ex"));
    let example = Example {
        field1,
        field2: vec![vec![1., 2.], vec![3., 4.]],
        field3: [1., 2., 3., 4.]
    };

    JsValue::from_serde(&example).unwrap()
}

#[wasm_bindgen]
pub fn receive_example_from_js(val: JsValue) {
    let example: Example = val.into_serde().unwrap();
    ...
}
#}

历史

在之前的 wasm-bindgen 版本中,gloo-utils 的基于 JSON 的 Serde 支持(JsValue::from_serdeJsValue::into_serde)是内置在 wasm-bindgen 本身中的。然而,这需要依赖 serde_json,而 serde_json 存在一个问题:当启用 serde_json 和其他 crate 的某些功能时,serde_json 会最终形成一个对 wasm-bindgen 的循环依赖,这在 Rust 中是非法的,会导致用户代码无法编译。因此,这些方法被提取到 gloo-utils 中,并使用扩展 trait,而原始方法则被弃用。