Jan 26, 2025
3 min read
Rust,
WASM,
npm,

使用 Rust + WASM 开发浏览器可用的 npm 库

通过 Rust + WASM 进行 npm 库开发是一个非常不错的选择。

我们都知道,Rust 中有一个叫napi的项目,用于实现Rust 开发 nodejs 平台上的 npm 库。napi 本质上是 nodejs 的C-API FFI 的绑定。但是napi开发的库不能直接在纯浏览器环境中运行,因为 napi是 Node.js 的原生插件 API,依赖于 Node.js 的运行时环境。浏览器并没有 Node.js 的运行时。

对于纯浏览器环境,我们需要借助 WASM 来实现纯浏览器可用的功能。

幸运的是,Rust 在 WASM 领域也是一个热门领域,有比较成熟的 WASM 生态。我们可以使用wasm-pack 来简化我们的开发体验。

安装与使用

我们可以通过下面四种方式中的任意一种方式进行安装:

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh # 方式1

cargo install wasm-pack # 方式2

npm install -g wasm-pack # 方式3

pnpm add -g wasm-pack # 方式4

我们以实现 png 图片转换成 jpeg 的功能为例子。创建项目:

wasm-pack new imagejs

打开项目,默认lib.rs 已经有了一些代码示例。

wasm-bindgen

#[wasm_bindgen] 属性表示它下面的函数可以在 JavaScript 和 Rust 中访问。

示例中有一段代码:

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

上面的extern 块将外部 JavaScript 函数 alert 导入到 Rust 中。从 Rust 调用 alert 需要此声明。通过以这种方式声明它,wasm-bindgen 将为 alert 创建 JavaScript 存根,允许我们在 Rust 和 JavaScript 之间来回传递字符串。

这样我们就可以直接在 Rust 代码里调用这个 javascript 里的 alert 函数。当然我们调用 alert 的函数也必须声明#[wasm_bindgen] 属性,否则无法互相访问。

针对 png 图片转换成 jpeg 这个功能,我们的函数签名可以是这样的:

#[wasm_bindgen]
pub fn png_to_jpg(buf: &[u8]) -> Result<Vec<u8>, JsError> {
	todo!()
}

函数接收一个 png 的字节组,转换后并返回 jpeg 的字节组。

为了实现图片格式转换,我们可以使用image 这个库:

cargo add image

需要注意的是,由于我们的运行环境是 wasm ,在 rust 里,并不是所有的 crate 都支持 wasm 环境的,使用一些 crate 前我们需要仔细查看是否支持 wasm。

image的使用比较简单:

#[wasm_bindgen]
pub fn png_to_jpg(buf: &[u8]) -> Result<Vec<u8>, JsError> {
    let format = image::ImageFormat::Png;
    let img = image::load_from_memory_with_format(buf, format)?;

    let mut bytes: Vec<u8> = Vec::new();

    img.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Jpeg)?;

    Ok(bytes)
}

我们运行下面的命令:

wasm-pack build --target web

就能生成符合浏览器可用的 npm 标准包。生成内容如下:

pkg
├── imagejs_bg.wasm
├── imagejs_bg.wasm.d.ts
├── imagejs.d.ts
├── imagejs.js
├── package.json
└── README.md

前端的验证

我们创建一个标准 vue 项目来验证一下:

pnpm create vue@latest

这里需要注意2点:

1. 引入本地包

  {
  "dependencies": {
    "imagejs": "file:../pkg",
    "vue": "^3.5.13"
  }
}

file 后面跟上路径。添加后需要运行一次 pnpm install

2. vite 需要知道如何处理 wasm

使用 vite-plugin-wasm:

pnpm add -g vite-plugin-wasm

然后在vite.config.ts 配置:

import wasm from "vite-plugin-wasm";

export default defineConfig({
  plugins: [
    wasm(),
    vue(),
    vueDevTools(),
  ],
  optimizeDeps:{
    exclude: ['imagejs']
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    },
  },
})

功能验证

使用前必须初始化:

import init, {png_to_jpg} from 'imagejs'

onMounted(async () => {
  await init()
})

创建一个表单,用于上传 png 图片,然后转换成jpg,并显示到浏览器页面上。

<template>
  <div>
    <h1>convert png to jpeg</h1>
    <div>
      <input type="file" @change="changeHandle">
      <img :src="showUrl" alt=""></img>
    </div>
  </div>
</template>

实现逻辑如下:

// 用于存储并展示转换后的图片URL的响应式变量
const showUrl = ref('')

async function changeHandle(e: Event) {
  // 将事件目标转换为文件输入元素(假设是input[type="file"]元素)
  let target: HTMLInputElement = e.target as HTMLInputElement
  
  // 检查是否有选择的文件
  if (target.files) {
    // 获取用户选择的第一个文件
    let file = target.files[0]
    
    // 将文件读取为ArrayBuffer(二进制数据)
    let buf = await file.arrayBuffer()
    
    // 将PNG文件转换为JPEG格式的二进制数据(假设png_to_jpg是自定义转换函数)
    let buf2 = png_to_jpg(new Uint8Array(buf))
    
    // 将处理后的二进制数据转换为可展示的URL:
    // 1. 创建包含JPEG数据的Blob对象(指定MIME类型为image/jpeg)
    // 2. 生成可用于<img>标签的Blob URL
    let url = URL.createObjectURL(new Blob([buf2], { type: 'image/jpeg' }))
    
    // 更新响应式变量,触发界面更新
    showUrl.value = url
  }
}

发布到 npm 官方注册表

我们可以使用 wasm-pack publish 来发布我们的 npm 包,前提是我们需要有对应的 npm 帐户以及登录:

wasm-pack login