引言

最近有一个技术需求,把gif图片高性能解码后渲染到canvas上。经过技术调研,决定选用Rust+Yaged+WebAssembly的技术方案。于是开始了解Rust和WebAssembly,尝试发布一个npm的包,有一些踩坑,记录一下。

什么是WebAssembly

WebAssembly是一种可以在现在网络浏览器中运行的代码类型——他是一种类似于汇编语言的低级语言,具有紧凑的二进制格式,以接近原生性能运行,并为C/C++,C#和Rust等语言提供编译目标,以便他们可以在Web中运行。它还设计为与JavaScript一起运行,协同工作。

简单来说,一些高性能的编译型语言如Rust等,可以将其代码编译为WebAssembly格式后在Web浏览器中运行,极大地提高了运行性能。

为什么需要WebAssembly

最主要的原因就是为了提升性能。由于JS只是一个解释型的脚本语言,所以性能上难以和编译型语言编译后的可执行文件相比。即使V8引擎使用解释执行和JIT结合等技术也难以弥补JS在性能上的先天不足。

WebAssembly的出现可以让我们用Rust等高性能语言来解决复杂计算,图形渲染等问题,然后利用WebAssembly打包成npm包供Web应用的JS调用。

如何使用WebAssembly

安装Rust工具链

这里以Rust为例,介绍如何将Rust编译为WebAssembly。

首先安装Rust,推荐使用rustup的方式,在终端执行以下命令

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

默认会将bin目录添加到环境变量,重启终端即可。

安装wasm-pack

wasm-pack是一个Rust的生态的第三方库,能够将代码编译成WebAssembly并生成适合在浏览器中使用的正确打包格式。

使用cargo来安装wasm-pack。cargo是Rust生态的包管理器,类似于我们的npm。

1
cargo install wasm-pack

但是与npm不同的是,cargo的install命令会直接全局安装该包,如果是在项目中安装,要使用add命令。

构建Rust包

1
cargo new --lib hello-wasm

这会在名为hello-wasm的子目录中创建一个新的库,目录结构如下

1
2
3
├── Cargo.toml
└── src
└── lib.rs

Cargo.toml是Rust的配置文件,类似于我们的package.json

进入src/lib.rs,替换为以下代码

1
2
3
4
5
6
7
8
9
10
11
use wasm_bindgen::prelude::*;

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

#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}

简单解释一下代码意思。

wasm-bindgen是用来在Rust和JavaScript之间进行通信的一个Rust的第三方库。

Rust中的库被称为”crate”,意思是集装箱。而包管理器Cargo是货轮的意思。也就是货轮装着集装箱。

第一行的use命令,类似于我们JS的import,用于将第三方库的代码导入到我们的代码中。

#[]用来包裹“属性”。属性会以某种方式修改下面的语句。我们的例子中,下一句是一个extern,它告诉Rust我们想要调用一些外部定义的函数。属性表示“wasm-bindgen”知道如何找到这些函数。

接下来是一个用Rust编写的函数签名,它说“alert函数接受一个名为s的字符串参数”。

这就是JavaScript提供的alert函数。

接下来是生成Rust函数,供JavaScript调用。

我们同样使用了#[wasm_bindgen]属性。在这种情况下,它不是修改一个extern块,而是一个fn;这意味着我们希望这个Rust函数能够被JavaScript调用。他与extern相反:这些不是我们需要的函数,而是我们希望暴露给外部的函数。

这个函数名为greet,接受一个参数,一个字符串name。然后它调用上面extern块中的alert函数。传递一个对format!宏的调用,该宏允许我们连接字符串。

format!宏在这个情况下接受两个参数:一个格式字符串和一个要放入其中的变量。格式字符串是 "Hello, {}!" 部分。它包含 {}占位符,变量将被插入其中。我们传递的变量是 name,即函数的参数,因此如果我们调用 greet("Steve") ,我们应该看到 "Hello, Steve!".

这种格式字符串的语法功能相当于我们JS的模板字符串,但是写法上更类似于C或者是Python。

接下来需要将我们的代码编译为WebAssembly。

Cargo.toml进行配置,内容类似下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[package]
name = "hello-wasm"
version = "0.1.1"
authors = ["yourname <your email>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
repository = "https://github.com/your github name/hello-wasm"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.100"

然后就可以构建包了。

1
wasm-pack build --target web

这里做了以下几件事:

  1. 将Rust代码编译成WebAssembly
  2. 在WebAssembly上运行wasm-bindgen,生成一个将WebAssembly文件封装成浏览器能理解的模块的JavaScript文件。
  3. 创建一个pkg目录,并将JavaScript文件和WebAssembly代码移动到该目录中
  4. 读取Cargo.toml并生成等效的package.json文件。
  5. 如果有README.md文件的话,复制到pkg目录下。

通过网页直接使用

现在我们已经编译了一个wasm模块,我们可以在浏览器中运行它。

首先,在根目录下创建一个index.html文件。项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── Cargo.lock
├── Cargo.toml
├── index.html <-- new index.html file
├── pkg
│ ├── hello_wasm.d.ts
│ ├── hello_wasm.js
│ ├── hello_wasm_bg.wasm
│ ├── hello_wasm_bg.wasm.d.ts
│ └── package.json
├── src
│ └── lib.rs
└── target
├── CACHEDIR.TAG
├── release
└── wasm32-unknown-unknown

将一下内容放入index.html文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init().then(() => {
greet("WebAssembly");
});
</script>
</body>
</html>

利于live-server插件启动本地服务器来打开这个index.html文件。

注意:不能直接在浏览器中打开该文件,因为直接打开使用的是file协议,会存在浏览器的跨域限制无法加载脚本,要使用live-server等启动本地开发服务器用http协议来访问。

20250502122522

可以看到,已经可以在网页上成功弹出消息。

通过构建工具使用

刚才我们打包成web形式,方便直接在html文件中导入,快速验证。我们要用构建工具使用需要打包成bundler形式。

1
wasm-pack build --target bundler

在当前目录下新建一个名为site的目录,然后本地直接安装该包。

1
2
mkdir site && cd site
npm i ../pkg

安装webpack开发依赖项目

1
npm i -D webapck webpack-cli webpack-dev-server copy-webpack-plugin

然后在创建webpack.config.js文件,并在其中加入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const CopyPlugin = require("copy-webpack-plugin");
const path = require("path");

module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
},
mode: "devlopment",
experiments: {
asyncWebAssembly: true,
},
plugins: [
new CopyPlugin({
patterns: [{ from: "index.html" }],
}),
]
}

package.json文件中添加buildserve脚本来运行webpack。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package.json
{
"scripts": {
"build": "webpack --config webpack.config.js",
"serve": "webpack serve --config webpack.config.js --open"
},
"dependencies": {
"hello-wasm": "file:../pkg"
},
"devDependencies": {
"copy-webpack-plugin": "^12.0.2",
"webpack": "^5.97.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
}
}

然后创建一个index.js的文件,并写入以下内容:

1
2
3
4
import * as wasm from "hello-wasm";

wasm.greet("WebAssembly with npm");

最后添加一个HTML文件来加载JavaScript。

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>

hello-wasm/site的目录结构应该如下面所示

1
2
3
4
5
6
├── node_modules
├── index.html
├── index.js
├── package-lock.json
├── package.json
└── webpack.config.js

启动开发服务器

1
npm run serve

20250502124934

可以看到在项目中能够成功显示。

发布npm包

首先是注册npm账户并登录,这块不再赘述,可自行百度。

然后是发布npm包,这里有个坑,MDN上直接让我们运行打包命令然后就发布了。

1
wasm-pack pack
1
wasm-pack publish

然后会让我们认证身份或输入密码,认证成功后还是会报错。

20250502130334

提示我们没有权限发布。这里是因为,npm要求所有的包名字必须独一无二,所以我们的包名和别人冲突了,我们想把自己的包发布到别人那里,当然没有权限。怎么办呢?

20250502130647

可以使用命名空间,在包名前加@自己在npm的账户名,然后再发布,才可以发布到自己的仓库包中。

所以在构建的时候就要带上当前作用域。

1
wasm-pack build --target bundler --scope 你的npm用户名

然后我们再来查看我们的pkg目录下的package.json文件,发现包名称已经加上了命名空间,限定在了我们账户的作用域内。

20250502130838

然后再次发布即可。

1
wasm-pack publish --access public

这里切记一定要带上 —access public命令参数,因为npm对于带有作用域的包名默认发布为restricted私有包,而发布私有包是付费功能。所以带上之后才可以成功发布。

20250502131003

进入npm查看,我们的包已经成功发布

20250502131114

使用远程安装包构建项目

刚才我们使用的是安装本地包npm i ../pkg的方式来构建项目,现在我们已经成功将包发布到npm

进入我们刚才的site目录。

首先卸载我们刚才安装的本地包。

1
npm uninstall hello-wasm

然后通过远程安装刚才发布的包

1
npm i @你的npm账户名/hello-wasm

然后修改site目录下的index.js文件中的导入,改为导入我们安装的在线包。

1
2
3
4
// index.js
import * as wasm from "@你的npm账户名/hello-wasm";

wasm.greet("WebAssembly with npm");

启动服务

1
npm run serve

启动成功!

20250502132156

总结

本文首先介绍了WebAssembly的概念,然后分析了其常见的使用情景,接着详细介绍了如何使用Rust编译WebAssembly并构建为npm包进行发布,以及导入我们自己构建的wasm的npm包进行测试。

如果觉得本文有所帮助,欢迎点赞转发👍

由于本人水平有限,难免有疏漏之处,欢迎各位大佬在评论区指正。