使用 Rust 开发 WebAssembly 初体验

cover

作为一名主营 Web 前端的开发小哥,学习 Rust 的动力之一自然是希望能将其发力于 Web 方面。个人目前所知 Rust 在前端的应用一个是各种前端工具链使用 Rust 重写,另一方面便是通过 WebAssembly 技术将一些非 JS 语言的库移植到了浏览器内运行,又或是进行一次逼格十足的用 Rust 写前端页面行为。

Rust 环境搭建

复制官网的命令安装即可:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Windows 用户请参阅这里的文档下载 exe 文件来安装

然后验证安装是否成功:

rustc --version

对于国内用户,还有个不可少的步骤便是包镜像配置,个人目前使用的是清华源,编辑 ~/.cargo/config 文件:

[source.crates-io]
replace-with = 'tuna'
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

另外不得不说一句,Rust 的工具链真的很完备,一站式包含了 Monorepo、包管理、Lint、构建、用例测试等等,完全不需要自己一个一个去配置。

初始化项目

上述配置后会提供一个叫做 cargo 的命令,它较类似于 NodeJS 中的 npm, Rust 中通过它来管理项目,可通过 cargo init project-name 来始化目录。本文里我们是会开发 WebAssembly 项目,使用 cargo 初始化目录后还需要手动配置一些其他内容,略微繁琐,由于已经有专门开发 WebAssembly 的命令行工具了,我们直接用它吧。下述会在全局安装 wasm-pack 命令(类似于 npm install some-cmd --global):

cargo install wasm-pack

接着通过它来初始化我们的项目:

wasm-pack new wasm-demo

使用 VSCode 打开该项目,然后安装下编辑器插件 rust-analyzer,这样变能正常进行 Rust 开发了。

该项目就是一个标准的 Cargo 项目(Cargo.toml 配置文件和 src 源码目录),并已经帮我们配置好了依赖并提供了一个 greet 函数。现在我们便可以根据所学学习的知识来写代码了。

代码实现

下面我们实现高亮 canvas 上的某个区域,我们将会用到 image crate(crate 在 rust 里指的就是某个包),先在 Cargo.toml 里添加 image crate(也可以使用 cargo add image 安装最近版本),然后启用 web_sys crate 要用到的一些 features(没用到的特性不开启,可以减小产物体积):

[dependencies]
image = "0.24.6"
web-sys = { version = "0.3.61", features = ["HtmlCanvasElement", "CanvasRenderingContext2d", "ImageData"] }

然后在 lib.rs 里写入下述内容:

// lib.rs

use image::{imageops, DynamicImage, ImageBuffer};
use wasm_bindgen::{prelude::*, Clamped};
use web_sys::{HtmlCanvasElement, ImageData};

fn to_image(width: u32, height: u32, vec: Vec<u8>) -> DynamicImage {
    let img_buffer = ImageBuffer::from_vec(width, height, vec).unwrap();
    DynamicImage::ImageRgba8(img_buffer)
}

fn to_canvas_image(width: u32, height: u32, vec: Vec<u8>) -> ImageData {
    ImageData::new_with_u8_clamped_array_and_sh(Clamped(&mut vec.clone()), width, height).unwrap()
}

#[wasm_bindgen]
pub fn highlight_canvas(canvas_elem: &HtmlCanvasElement) {
    let ctx = canvas_elem
        .get_context("2d")
        .unwrap()
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()
        .unwrap();

    // 绘制一个红色边框
    ctx.set_stroke_style(&"red".into());
    ctx.rect(0f64, 0f64, 50f64, 50f64);
    ctx.stroke();
    // 获取上述区域的数据
    let image_data = ctx
        .get_image_data(0f64, 0f64, 50f64, 50f64)
        .unwrap()
        .data()
        .to_vec();
    // 将上述获取的 canvas 数据转成 `image` crate 所需的格式
    let photo_image = to_image(50, 50, image_data);
    // 返回高亮后的信息
    let filtered_image = imageops::brighten(&photo_image, 100).to_vec();
    // 将 rust 数据转回 canvas 支持的数据
    let new_canvas_image = to_canvas_image(50, 50, filtered_image);
    ctx.put_image_data(&new_canvas_image, 0f64, 0f64).unwrap();
}

然后命令行运行 wasm-pack build --target web 构建项目,会生成一个 pkg 目录,里面是一个配置好的 npm 包,可以直接用在自己的前端项目。我们新建一个 index.html 写入下述内容:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>hello-wasm example</title>
  </head>
  <body>
    <canvas id="canvas" style="border: 1px solid black"></canvas>

    <script type="module">
      import init, { highlight_canvas } from "./pkg/hello_wasm.js";

      await init();

      const $canvas = document.querySelector("#canvas");
      const ctx = $canvas.getContext("2d");
      ctx.rect(0, 0, 200, 100);
      ctx.fill();

      highlight_canvas($canvas);
    </script>
  </body>
</html>

然后开启一个 http 服务器访问该文件,便可以看到效果了。