使用 Serde 將任意資料序列化及反序列化為和從 JsValue

透過使用 Serde 進行序列化,可以將任意資料從 Rust 傳遞到 JavaScript。這可以透過 serde-wasm-bindgen crate 來完成。

新增依賴項

要使用 serde-wasm-bindgen,您必須先將其作為依賴項新增到您的 Cargo.toml 中。您也需要啟用 derive 功能的 serde crate,以允許您的類型使用 Serde 進行序列化和反序列化。

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

衍生 SerializeDeserialize Traits

#[derive(Serialize, Deserialize)] 新增到您的類型。您的類型中的所有成員也必須受到 Serde 的支援,也就是說,它們的類型也必須實作 SerializeDeserialize traits。

例如,假設我們想要將這個 struct 傳遞給 JavaScript;在 wasm-bindgen 中,由於使用了 HashMap、陣列和巢狀的 Vecs,通常無法做到這一點。這些類型都不支援直接透過 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 透過將其序列化為 JsValue 來傳遞給 JavaScript


# #![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,其成員是數字的 Arrays,而 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 和其他 crate 的某些功能後,serde_json 將最終循環依賴於 wasm-bindgen,這在 Rust 中是不合法的,並導致人們的程式碼無法編譯。因此,這些方法被提取到 gloo-utils 中,並帶有一個擴充 trait,而原始方法則被棄用。