尝尝你的优乐美 2025-01-23 08:31 重庆
点击关注公众号,“技术干货” 及时达!
点击关注公众号,“技术干货” 及时达!
一. 前言
以前一直有一些高性能的渲染问题困扰着我,比如canvas渲染百万、千万级数据,以及一些图片像素的操作等。虽然现在的js也给了很多优化的方案(比如web workers)。但是说到底还是有语言方面的局限性,直到我看到了wasm(WebAssembly),可以在客户端处理二进制程序。
二. wasm介绍
2.1 什么是wasm
简单来说是一种为网络而生的新型代码格式,旨在提供一种比传统 JavaScript 更快的执行速度,可以大大的提升网络的性能,这个提升不是传统意义上的优化,而是真正意义上的从根本上完成代码运行质的飞跃。
2.2 wasm的优点
提高性能 wasm允许浏览器中运行高性能代码,其运行速度接近原生。当我们前端需要进行处理大型密集的计算工作,以及实时图片处理等。
跨语言支持 wasm并不是什么特定的编译语言,而是一种平台。开发者可以使用C,C++,Rust,go 等编写代码,然后转为wasm,最后在浏览器中运行。
与js功能互补 wasm出现并不是为了取代js,而是为了更好的为js提供服务。在一些特定的环境(比如可视化等)js可以做为交互,wasm可以做为密集计算。
优化资源占用 wasm的设计是为了占用较少的内存,这使得它非常适合资源有限的设备,如手机等移动设备。通过减少内存占用,Wasm可以帮助前端应用在各种类型的设备上都能顺利运行,提升用户体验。
2.3 如何在前端中使用
现代浏览器是支持直接兼容.wasm文件的。所以我们就是通过go、Rust 等语言去编写wasm文件,然后通过编译器转为为.wasm文件在前端调用。
三. Rust的使用
简单的介绍一下Rust的使用吧。
3.1 rust安装
按照官网的安装流程来就好。
curl https://sh.rustup.rs -sSf | sh
安装完成之后,我们在终端输入:
rustc --version
cargo --version
出现这两个版本,表示安装成功!
这里面要注意一下:rustc和cargo都是Rust的编程工具,但是功能不同。
rustc 是Rust的源代码编译器,把Rust源码编译成可执行文件或库。
cargo 是Rust包管理工具(可以理解为npm)。主要目的就是管理Rust的项目依赖,自动生成构建脚本等。
3.2 rust项目结构分析
完成安装之后,我们可以试一下新建一个Rust项目。
cargo new my_project --lib
--lib 是 cargo 命令行工具的一个选项,用于指示Cargo创建一个新的库(library)项目。
我们看一下文件目录:
主要核心是这两个lib.rs、Cargo.toml。
lib.ts 源码的入口地方。
Cargo.toml 项目的依赖管理文件。
四. Rust转为wasm
简单了解了一下Rust之后,就有个疑问?Rust如何编译为可以执行的wasm文件?答案就是:wasm-bindgen
4.1 wasm-bindgen
是一个强大的工具链,旨在简化WebAssembly(WASM)模块与JavaScript之间的交互。主要目的是将Rust的性能优势引入Web开发中,并实现与JavaScript的无缝集成。
主要功能就是自动生成可以必要的绑定和胶水代码,确保Rust和js之间可以正常的平滑通信。
简单来说,wasm-bindgen就是一个桥梁,沟通Rust和js之间运行的桥梁。
通过wasm-bindgen编译的代码可以在js中使用。
4.2 wasm-bindgen使用
如何在rust中使用呢?这就要用到我们之前说的Cargo.toml文件了。把wasm-bindgen的依赖声明到对应的文件中:
[package]name = "my_wasm_project"version = "0.1.0"edition = "els"[lib]crate-type = ["cdylib"][dependencies]wasm-bindgen = "0.2"
[dependencies] 指定依赖和依赖的版本。 比如: wasm-bindgen的版本为0.2。
[package] 生成的这个包的一些基本信息。
[crate-type] 是一个配置项,用于指定当你构建项目时生成的输出类型 。
五. 使用Rust生成一个canvas文件
5.1 代码
简单介绍了Rust的一些基本使用,那下面我们用一个案例来看一下Rust如何生成wasm文件的。 这里用一个简单的例子,通过传入一个canvas实例,然后绘制一个圆。
我们新建一个项目,然后先安装依赖,回到Cargo.toml文件中:
[package]name = "my_wasm_project"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib"][dependencies]wasm-bindgen = "0.2"web-sys = { version = "0.3", features = ["Window", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d"] }
主要是看一下dependencies。
wasm-bindgen 就是前面说的编译Rust代码的桥梁。
web-sys 是一个Rust的标准库,它提供了对Web API的绑定。包括 DOM、HTML、CSS、XMLHttpRequest、Fetch API、WebSocket 等等。 使得你可以使用 Rust 语言来编写与浏览器环境交互的代码。
features 字段指定了你要启用的 Web API 特性。web-sys 默认并不包含所有的 Web API,你需要显式地指定你想要使用的那些特性。比如这里我们需要用canvas绘图,就需要显示的指定Document,HtmlCanvasElement,CanvasRenderingContext2d等canvs的特性。
完成配置之后,我们回到lib.rs:编写一段代码。
use wasm_bindgen::prelude::*;use web_sys::{window, Document, HtmlCanvasElement, CanvasRenderingContext2d};#[wasm_bindgen]pub fn draw_circle(canvas_id: &str) -> Result<(), JsValue> {// 获取全局window对象let window = window().expect("no global `window` exists");// 获取documentlet document = window.document().expect("window should have a document");// 通过 ID 获取 canvas 元素let canvas = document.get_element_by_id(canvas_id).and_then(|e| e.dyn_into::<HtmlCanvasElement>().ok()).expect("canvas element not found");// 设置 canvas 尺寸canvas.set_width(500);canvas.set_height(500);// 获取 2D 渲染上下文let context = canvas.get_context("2d").expect("failed to get context").unwrap().dyn_into::<CanvasRenderingContext2d>().expect("context is not of type 2d");// 开始路径context.begin_path();// 绘制圆context.arc(250.0, 250.0, 100.0, 0.0, std::f64::consts::PI * 2.0)?;// 设置填充颜色context.set_fill_style(&JsValue::from_str("blue"));// 填充圆context.fill();// 设置描边颜色context.set_stroke_style(&JsValue::from_str("black"));// 描边圆context.stroke();Ok(())}
实现了一个draw_circle方法,然后可以提供在web端使用。
5.2 代码解析
看起来上面代码有点懵逼,没关系我们可以一行一行分析一下,确实很难!!!!
5.2.1 use关键字
use是把模块,项,或者路径导入当前的目录中。这使得你可以在代码中直接使用这些导入的名称,而不需要每次都写完整的路径。
use std::io;这个意思就是把std下面的io模块导出,就可以直接使用了。
use co::*;这个意思就是把co模块下的全部方法、定义等导出。
use wasm_bindgen::prelude::*; 意思就是从【wasm_bindgen】的模块【prelude】中导入所有的内容。
5.2.2 ::操作符
在rust中::是一个很重要的操作符,主要用于访问模块、结构体、枚举、函数、常量等的命名空间中的成员。
如下例子:
引用模块中的函数:
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
let result = math::add(1, 2); // 使用 :: 引用模块中的函数
println("{}", result);
}
// 引用枚举中的类型
enum Color {
Red = 'red',
Blue ='blue'
}
fn main() {
let color = Color::Red; // 使用 :: 引用枚举中的类型
}
use web_sys::{window, Document, HtmlCanvasElement, CanvasRenderingContext2d} 意思就是:引用web_sys模块中的window, Document, HtmlCanvasElement, CanvasRenderingContext2d使用。
5.2.3 pub关键字
pub 是访问修饰符,表示“公共”的意思,fn 是函数的关键字。通过 pub fn 定义的函数可以在模块外部被访问和调用。
pub fn draw_circle声明的函数就可以在外部引入使用。
5.2.4 #[wasm_bindgen]
使用#[wasm_bindgen]定义的函数,可以使得Rust 代码能够与 JavaScript 无缝交互。通过使用这个属性,你可以轻松地将 Rust 函数导出给 JavaScript 使用,或者从 Rust 代码中调用 JavaScript 函数。
5.2.5 #[wasm_bindgen(start)]
表示当前的函数在js中导入使用是自动执行的。
5.2.6 JsValue类型
JsValue 是 wasm-bindgen 库中的一个类型,用于表示JavaScript中的值。它可以表示任何JavaScript值,如数字、字符串、对象等。在Wasm环境中,JsValue 用于在Rust和JavaScript之间传递数据。
5.2.7 dyn_into
将 JavaScript 对象转换为特定的 Rust 类型。这个函数通常用于处理从 JavaScript 传递过来的对象,这些对象可能需要被转换成更具体的 Rust 类型,以便你可以调用该类型特有的方法或访问其属性。
由于 WebAssembly 和 JavaScript 之间的交互是通过接口定义来进行的,有时候你从 JavaScript 接收到的对象可能是通用的类型(如 JsValue),但你需要将其转换为特定的 Rust 类型(如 HtmlElement 或 Document)来使用。dyn_into 函数提供了这种转换能力,并且它会进行类型检查,确保转换是安全的。
// 尝试将 Node 转换为 HtmlElementlet element: HtmlElement = node.dyn_into().map_err(|_| {console_error!("Failed to convert Node to HtmlElement");}).unwrap();
5.2.8 expect
通常用于 Option 和 Result 类型。它的主要作用是当值为 None 或 Err 时提供自定义的错误消息,并触发 panic! 宏,从而终止程序。说白了,就是Rust的错误处理机制。这也是Rust为什么很安全的原因,每一句话都会有对应的错误处理。
OK,大概了解了一下上面的语法之后,再去看这个方法就很简单了,其实就是一个简单的canvas绘制,只不过函数的写法和一些异常处理变多了。
5.3 项目编译
完成代码之后,我们把Rust代码编译为wasm。
wasm-pack build --target pkg
我们会发现在文件夹中多出了几个包:
这个pkg就是在client的运行内容。
六. 在项目中使用
我们准备把Rust生成的wasm文件放到vue3.0项目中使用。
新建一个vue3.0项目:
npm init vite
然后我们把Rust这个包放到对应的文件夹下面。
完成之后,我们可以在node_modules里面关联一下my_wasm_project下面的pkg包。
pnpm i ./my_wasm_project/pkg
就可以在node_modules完成关联。
为了可以自动编译,然后同步更新node_modules里面内容,我们在package.json里面写一个脚步执行。
"scripts": {
"wasm": "cd ./my_wasm_project && wasm-pack build --target web && cd .. && pnpm install ./my_wasm_project/pkg"
},
cd ./my_wasm_project 进入到my_wasm_project包中。
wasm-pack build --target web 编译一下。
cd .. && pnpm install ./my_wasm_project/pkg 回到根目录,然后install一下对应的包。
然后在组件里面使用一下:
import init, { draw_circle } from 'my_wasm_project/my_wasm_project'
onMounted(async () => {
await init();
draw_circle('my_canvas')
})
这里要注意一下,需要把引入一个init函数先执行,这个目的是为了先构建wasm的运行环境。
然后执行一下draw_circle方法。
ok,完成啦,一个最简单的canvas在页面中使用。
点击关注公众号,“技术干货” 及时达!
