Canvas 居然还能用 Rust 实现?

大家好,我是刘布斯。

我们之前也给大家分享过 Rust 相关的文章,但大部分都是开发的一些工具,给前端的工程化提效,比如 Oxlint、Rolldown。

咱们能在页面中直接使用 Rust 吗?

答案是可以的,今天就带来一篇文章,介绍如何使用 Rust 来实现 canvas。

以下是正文:


一. 前言

以前一直有一些高性能的渲染问题困扰着我,比如canvas渲染百万、千万级数据,以及一些图片像素的操作等。虽然现在的js也给了很多优化的方案(比如web workers)。但是说到底还是有语言方面的局限性,直到我看到了wasm(WebAssembly),可以在客户端处理二进制程序。

二. wasm介绍

2.1 什么是wasm

简单来说是一种为网络而生的新型代码格式,旨在提供一种比传统 JavaScript 更快的执行速度,可以大大的提升网络的性能,这个提升不是传统意义上的优化,而是真正意义上的从根本上完成代码运行质的飞跃。

2.2 wasm的优点

  • 提高性能 wasm允许浏览器中运行高性能代码,其运行速度接近原生。当我们前端需要进行处理大型密集的计算工作,以及实时图片处理等。
  • 跨语言支持 wasm并不是什么特定的编译语言,而是一种平台。开发者可以使用CC++Rustgo 等编写代码,然后转为wasm,最后在浏览器中运行。
  • 与js功能互补 wasm出现并不是为了取代js,而是为了更好的为js提供服务。在一些特定的环境(比如可视化等)js可以做为交互,wasm可以做为密集计算。
  • 优化资源占用 wasm的设计是为了占用较少的内存,这使得它非常适合资源有限的设备,如手机等移动设备。通过减少内存占用,Wasm可以帮助前端应用在各种类型的设备上都能顺利运行,提升用户体验。

2.3 如何在前端中使用

现代浏览器是支持直接兼容.wasm文件的。所以我们就是通过goRust 等语言去编写wasm文件,然后通过编译器转为为.wasm文件在前端调用。

三. Rust的使用

简单的介绍一下Rust的使用吧。

3.1 rust安装

按照官网的安装流程来就好。

curl https://sh.rustup.rs -sSf | sh

安装完成之后,我们在终端输入:

rustc --version
cargo --version
image-20241129094157617.png

出现这两个版本,表示安装成功!

这里面要注意一下:rustccargo都是Rust的编程工具,但是功能不同。

  • rustcRust的源代码编译器,把Rust源码编译成可执行文件或库。
  • cargoRust包管理工具(可以理解为npm)。主要目的就是管理Rust的项目依赖,自动生成构建脚本等。

3.2 rust项目结构分析

完成安装之后,我们可以试一下新建一个Rust项目。

cargo new my_project --lib

--libcargo 命令行工具的一个选项,用于指示Cargo创建一个新的库(library)项目。

我们看一下文件目录:

image-20241129095548168.png

主要核心是这两个lib.rsCargo.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就是一个桥梁,沟通Rustjs之间运行的桥梁。

通过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绘图,就需要显示的指定DocumentHtmlCanvasElementCanvasRenderingContext2d等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");
   // 获取document
    let 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.0250.0100.00.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(12); // 使用 :: 引用模块中的函数
  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 类型(如 HtmlElementDocument)来使用。dyn_into 函数提供了这种转换能力,并且它会进行类型检查,确保转换是安全的。

 // 尝试将 Node 转换为 HtmlElement
    let element: HtmlElement = node.dyn_into().map_err(|_| {
        console_error!("Failed to convert Node to HtmlElement");
    }).unwrap();

5.2.8 expect

通常用于 OptionResult 类型。它的主要作用是当值为 NoneErr 时提供自定义的错误消息,并触发 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在页面中使用。

原文地址:https://juejin.cn/post/7442554317051461658

最后

还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库主打题全和更新快哦~。


有会员购买、辅导咨询的小伙伴,可以通过下面的二维码,联系我们的小助手。