Featured image of post 使用Rust和WebAssembly整花活儿(二)——DOM和类型转换

使用Rust和WebAssembly整花活儿(二)——DOM和类型转换

WebAssembly可以使用多种语言开发, 如果你更注重性能和安全性, 那么使用Rust整花活儿是个更好的选择~

前言

在上一篇文章《使用Rust和WebAssembly整花活儿(一)——快速开始》中,描述了如何创建项目和快速生成wasm并在前端中使用,迈出了整花活儿的第一步。

在开发 Web 应用程序时,使用 Rust 编写的 Wasm 模块可以提供更高的性能和更好的安全性。但是,为了与现有的 JavaScript 代码集成,必须实现 Rust 与 JS 之间的交互。Rust 与 JS 交互的主要目的是将两种语言的优势结合起来,以实现更好的 Web 应用程序。

本篇文章中,将基于上一篇文章中创建的项目来继续开发。

源码:github.com/Kuari/hello-wasm

环境

  • Rust 1.70.0
  • wasm-bindgen 0.2.87
  • web-sys 0.3.64

DOM

配置依赖

要操作DOM,需要引入新的依赖web-sys,因此,可以配置Cargo.toml中依赖如下:

1
2
3
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = [] }

你或许会好奇,这个features是什么,讲真,我一开始很好奇,又没看到什么特别的说明,试错才发现,原来是要手动引入功能依赖…比如说,当你需要在Rust中使用JS的console,那么你需要在features中加入console

获取Document

在Rust中使用Document,我们需要按照上一步的说明,添加features。那么这里有一个依赖关系,首先在Rust中获取window,然后再获取document

因此,添加features后如下:

1
2
3
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["Window", "Document"] }

然后在lib.rs中创建一个函数,用来调用document

1
2
3
4
5
#[wasm_bindgen]
pub fn update_message() {
    let window = web_sys::window().expect("Failed to load window");
    let document = window.document().expect("Failed to load document");
}

那么,现在就是在Rust中解锁了document,就可以在前端为所欲为了!

操作Element

那么开始操作一波,首先得获取到Element……

是的,你没有想错,继续来添加features吧,此处要添加一个Element

1
2
3
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["Window", "Document", "Element"] }

ok,那么继续。此处设定函数传入两个参数selectormessage,然后通过selector获取element,更新值为message参数的值。完整函数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#[wasm_bindgen]
pub fn update_message(selector: &str, message: &str) {
    let window = web_sys::window().expect("Failed to load window");
    let document = window.document().expect("Failed to load document");
    let element = document.query_selector(selector).expect("Failed to load element");

    if let Some(element) = element {
        element.set_inner_html(message);
    } else {
        panic!("Failed to set inner html")
    }
}

编译

将写完的Rust项目编译成wasm:

1
wasm-pack build --target web

在html中调用

基于上一篇文章的项目中的html,此处添加一个div,id为message,添加调用wasm的update_message函数,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<body>
    <div id="message"></div>
</body>

<script type="module">
    import init, { update_message } from './pkg/hello_wasm.js'; // 引入update_message函数

    const run = async () => {
        await init();

        update_message('#message', '<h1>Hello, Rust!</h1>'); // 调用update_message函数
    }

    run();
</script>

在浏览器验证

启动一个http server,然后在浏览器查看,可以看到在页面上出现一个h1标签的Hello, Rust!

发现更多方法

按照文章来写的过程中,你应该会发现一个问题——怎么这些方法没有补全?!

是的,没错的,(至少我发现)当前web-sys并没有补全,所以只能结合开发者优秀的前端技能和丰富的官方文档来开发了。

Rust与JS的类型相互转换

对于wasm而言,性能固然是提升的,但是类型转换一直是个问题。当大量数据需要在wasm/js中进行类型转换时,这对性能来说,真的是个灾难。之前在使用go开发wasm时,就遇到过这样的问题,需要用官方的方法来进行手动类型转换,然而wasm处理的是一个很大的数据量……

不过好在Rust的类型支持真的挺丰富的!

基础类型

基础类型挺简单的,而且Rust的范性也很好地支持了很多类型。如下是基础类型映射表:

Rust类型 JavaScript类型
i8 number
i16 number
i32 number
i64 BigInt
u8 number
u16 number
u32 number
u64 BigInt
f32 number
f64 number
bool boolean
char string
&str string
String string
&[T] 例如:&[u8] [T] 例如:Uint8Array
Vec Array

基础类型转换示例

lib.rs文件中,创建一个函数,挑选几个类型作为参数传入,然后将其读取并打印:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[wasm_bindgen]
pub fn print_values(js_number: i32, js_boolean: bool, js_uint8_array: &[u8], js_number_array: Vec<i32>) {
    println!("js number: {}", js_number);
    println!("js boolean: {}", js_boolean);

    for item in js_uint8_array {
        println!("js Uint8Array item: {}", item);
    }

    for item in js_number_array {
        println!("js number array item: {}", item);
    }
}

可以看到该函数传入了JS的numberbooleanUint8ArrayArray四个类型的参数。

然后编译:

1
wasm-pack build --target web

接着,在前端中引入函数并调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<script type="module">
    import init, { print_values } from './pkg/hello_wasm.js';

    const run = async () => {
        await init();

        const jsNumber = 10;
        const jsBoolean = true;

        const jsUint8Array = new Uint8Array(3);
        jsUint8Array[0] = 1;
        jsUint8Array[1] = 2;
        jsUint8Array[2] = 3;

        const jsNumberArray = [30, 40, 50];

        print_values(jsNumber, jsBoolean, jsUint8Array, jsNumberArray);
    }

    run();
</script>

最后,启动http server并打开浏览器,在控制台可以看到…看不到?!

是的,没错,Rust的println!只会将打印的内容发送到Rust的标准输出流,而不是前端的控制台。如果想在控制台中打印,那么需要调用JS的console了。

使用新的功能,第一步就是添加featuresCargo.toml中添加console如下:

1
2
3
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["Window", "Document", "Element", "console"] }

在Rust中调用console.log()如下:

1
web_sys::console::log_1(&"Hello, Rust!".into());

此处将其封装成一个函数:

1
2
3
fn console_log(message: String) {
    web_sys::console::log_1(&message.into());
}

然后,将示例函数的println改成console_log()format!,函数代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[wasm_bindgen]
pub fn print_values(js_number: i32, js_boolean: bool, js_uint8_array: &[u8], js_number_array: Vec<i32>) {
    console_log(format!("js number: {}", js_number));
    console_log(format!("js boolean: {}", js_boolean));

    for item in js_uint8_array {
        console_log(format!("js Uint8Array item: {}", item));
    }

    for item in js_number_array {
        console_log(format!("js number array item: {}", item));
    }
}

最后,编译之后,打开浏览器,就可以在控制台看到输出:

1
2
3
4
5
6
7
8
js number: 10
js boolean: true
js Uint8Array item: 1
js Uint8Array item: 2
js Uint8Array item: 3
js number array item: 30
js number array item: 40
js number array item: 50

通用类型

Rust中提供了一个通用的类型——JsValue,可以作为任何JS类型。

这里给一个简单的案例,设置一个函数,使用JsValue作为参数传入,并打印。

创建函数:

1
2
3
4
#[wasm_bindgen]
pub fn print_js_value(val: JsValue) {
    console_log(format!("{:?}", val));
}

然后编译成wasm文件。

在html中调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script type="module">
    import init, { print_js_value } from './pkg/hello_wasm.js';

    const run = async () => {
        await init();

        const jsNumber = 10;
        const jsBoolean = true;

        const jsUint8Array = new Uint8Array(3);
        jsUint8Array[0] = 1;
        jsUint8Array[1] = 2;
        jsUint8Array[2] = 3;

        const jsNumberArray = [30, 40, 50];

        print_js_value(jsNumber);
        print_js_value(jsBoolean);
        print_js_value(jsUint8Array);
        print_js_value(jsNumberArray);
    }

    run();
</script>

在html中,传入了不同类型的参数,但是在浏览器的控制台中可以看到,将所有不同类型的参数都打印出来了:

1
2
3
4
JsValue(10)
JsValue(true)
JsValue(Uint8Array)
JsValue([30, 40, 50])

Result

Result在Rust中是一个很重要的存在,经常写Rust的话,也不想在写WebAssembly时改变开发习惯。

其实对于JS而言,Result可以直接在catch中捕获到,只是说,这里我们需要定义好参数类型。

####1 使用Result返回报错

首先来一个只返回报错的场景:

1
2
3
4
5
6
7
8
#[wasm_bindgen]
pub fn only_return_error_when_result(count: i32) -> Result<(), JsError> {
    if count > 10 {
        Ok(())
    } else {
        Err(JsError::new("count < 10"))
    }
}

这里返回类型是Result,但是仅仅返回了一个错误。值得注意的是,这里的报错使用的类型是JsError,当然,这里也可以使用JsValue

然后在html调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script type="module">
    import init, { only_return_error_when_result } from './pkg/hello_wasm.js';

    const run = async () => {
        await init();

        try {
            only_return_error_when_result(1);
            console.log('1 is ok');
        } catch(error) {
            console.log('An error is reported when the input parameter is 1: ', error);
        }

        try {
            only_return_error_when_result(100);
            console.log('100 is ok');
        } catch(error) {
            console.log('An error is reported when the input parameter is 100: ', error);
        }
    }

    run();
</script>

这里调用了两次,第一次应当是错误的,第二次应该是正确的,并且都使用了catch来捕获错误。

那么,在浏览器的控制台可以看到输出:

1
2
An error is reported when the input parameter is 1:  Error: count < 10
100 is ok

####2 使用Result返回正常值和错误

那么,如果想既返回正常值,也想返回错误呢?Rust返回一个Result是没有问题,那么JS怎么解析呢?

直接上Rust代码:

1
2
3
4
5
6
7
8
#[wasm_bindgen]
pub fn return_all_when_result(count: i32) -> Result<i32, JsError> {
    if count > 10 {
        Ok(count + 10)
    } else {
        Err(JsError::new("count < 10"))
    }
}

该函数,获取到参数后,如果满足条件,加10后返回,否则报错。

那么看看html中如何调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script type="module">
    import init, { return_all_when_result } from './pkg/hello_wasm.js';

    const run = async () => {
        await init();

        try {
            const res = return_all_when_result(1);
            console.log(`get ${res}`);
        } catch(error) {
            console.log('An error is reported when the input parameter is 1: ', error);
        }

        try {
            const res = return_all_when_result(100);
            console.log(`get ${res}`);
        } catch(error) {
            console.log('An error is reported when the input parameter is 100: ', error);
        }
    }

    run();
</script>

是的,没错,正常获取就行了……/捂脸哭

这里的调用,依然是,第一个是错误的,第二个是正确返回值的,并且都使用了catch来捕获错误。

最后,就是在浏览器的控制台中看到:

1
2
An error is reported when the input parameter is 1:  Error: count < 10
get 110                                                             

直接引入JS类型

如果你想更直接一点,那么可以直接引入JS类型!这里主要是利用js-sys这个依赖,可以在官方文档上看到很多JS的类型和函数,直接引入即可使用。当然,一定场景下,直接引入的类型,是需要手动转换类型的。

####1 配置依赖

Cargo.toml中添加js-sys依赖:

1
2
3
4
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["Window", "Document", "Element", "console"] }
js-sys = "0.3.61"

####2 Uint8Array

首先以Uint8Array举例,在lib.rs头部引入类型:

1
use js_sys::Uint8Array;

然后创建一个函数,参数和返回都是Uint8Array类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#[wasm_bindgen]
pub fn print_uint8_array(js_arr: Uint8Array) -> Uint8Array {
    // new Uint8Array
    let mut arr = Uint8Array::new_with_length(3);

    // Uint8Array -> vec
    for (index, item) in js_arr.to_vec().iter().enumerate() {
        console_log(format!("{} - the item in js_arr: {}", index, item));
    }

    // Avoid type conversion
    // Use the method of the type itself
    for index in 0..js_arr.length() {
        console_log(format!("{} - the item in js_arr: {}", index, js_arr.get_index(index)));
    }

    // vec -> Uint8Array
    let vec = vec![1, 2, 3];
    let arr2 = Uint8Array::from(vec.as_slice());
    arr = arr2.clone();

    // Use the method of the type itself
    arr.set_index(0, 100);

    arr
}

忽略该函数中无意义的逻辑和arr变量的警告,只是为了演示用法。

可以在代码中看到,直接引入的Uint8Array有自己的方法,一定场景下,需要转换类型,但是最好避免进行类型转换,而直接使用其自带的方法。

这里可以简要总结下,就是最好一定场景内全部使用直接引入的JS类型,或者直接全部使用Rust类型来代替JS类型,两者都存在场景下,手动转换类型是件很糟糕的事。

####3 Date

Date类型,在上面的篇章中都没有提及,这里可以直接引入JS的Date类型来使用。

首先是引入类型:

1
use js_sys::Date;

然后,创建一个函数,返回时间戳:

1
2
3
4
5
#[wasm_bindgen]
pub fn return_time() -> f64 {
    let date = Date::new_0();
    date.get_time()
}

接着,在html中调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script type="module">
    import init, { return_time } from './pkg/hello_wasm.js';

    const run = async () => {
        await init();

        console.log('current time: ', return_time());
    }

    run();
</script>

最后,在浏览器的控制台中,可以看到:

1
current time:  1686979932833

总结

本文中,主要讲述了如何使用Rust来实现DOM操作,读者可以根据方法自己去找到合适的方法,来实现自己的场景。其次,还讲述了Rust与JS的类型转换,从基础的各自类型的映射,到Rust独有的Result,到直接引入JS类型。当然这里需要注意的是,直接引入JS类型和Rust的基础类型映射JS类型这两种方法尽量不要混用,混用会导致需要手动类型转换,造成性能损耗。

至此,又向Rust和WebAssembly整花活儿迈进了一步~😼

Built with Hugo
Theme Stack designed by Jimmy