学习理解 webAssembly 概念知识,使用 API 进行 web 前端开发。
概念
是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它有一种紧凑的二进制格式,使其能够以接近原生性能的速度运行。C/C++、 C#、Rust
等语言可以编译为 webAssembly 执行。
- 快速、高效、可移植。不同平台中能够接近本地速度运行。
- 可读、可调式。是一门低阶语言。
- 保持安全。被限制运行在一个沙箱中,遵循浏览器的同源策略和授权策略。
- 不破坏网络。向后兼容
以前无法以此方式运行的客户端软件都将可以运行在 Web 中。
WebAssembly 是一门不同于 JavaScript 的语言,它不是用来取代 JavaScript 的。相反,可以和 JavaScript 一起协同工作,从而使得网络开发者能够利用两种语言的优势
关键概念
- 模块(module) - 表示一个已经被浏览器编译为可执行机器码的 WebAssembly 二进制代码
- 内存(memory) - ArrayBuffer,大小可变。
- 表格(table) - 带类型数组,大小可变
- 实例(instance) - 一个模块及其在运行时使用的所有状态,包括内存、表格和一系列导入值
js 可以控制 WebAssembly 代码如何下载、编译运行。所以前端可以将 WebAssembly 作为一种高性能函数进行调用。
编写代码并应用
- 构建完整基于 rust 的 web 应用,比如使用
yew
。 - 构建部分应用功能,比如提供一些方法,用于复杂的计算。
使用wasm-pack
生成 webAssembly 代码
rust 环境参考另一篇文章rust 基础知识
wasm-pack
可以构建生成与 js、浏览器、node 进行互操作的代码。可以将这些功能包发布到 npm 上。
安装,等待安装完成,可以通过wasm-pack -V
查看版本,是否安装成功
sh
$> cargo install wasm-pack
创建一个 rust 项目js-utils
,初始化为一个 lib 库cargo init --lib
配置Cargo.toml
,添加wasm-binggen
作为依赖,它是 wasm 与 js 之间交互的工具;添加crate-type
设置为cdylib
,指定该库是一个 C 兼容的动态库,这是针对 rust 编译时设置的一个标志。
添加依赖可以使用命令cargo add
,在package
字段中添加一些仓库、个人信息,比如authors/description/license/repository
,在打包编译时,会被添加到package.json
文件中
toml
[package]
name = "js-utils"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ['cdylib']
[dependencies]
wasm-bindgen = "0.2.92"
开始写一些 rust 代码,在/src/lib.rs
:
-
use wasm_bindgen::prelude::*;
导入所有wasm_bindgen
的核心功能。以便后续直接调用
#[wasm_bindgen]
表明了下面的代码可以在 js 和 rust 中访问。 -
extern
将 js 函数导入 rust 中以供调用。代码中将alert
函数声明后,在 rust 代码方法greet
进行调用
rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
let str = format!("hello, World {name}");
alert(&str);
}
为了方便测试,我们直接将这个发布为一个 npm 包
首先打包,--scope
定义包命名空间
sh
$> wasm-pack build --scope hboot
通过wasm-pack login
登录你的 npm 账号,登录完成后,进入打包的目录下/pkg
下进行发布
sh
$> npm publish --access=public
发布完成后,就可以找一个前端项目测试安装发布的包。
sh
$> npm i @hboot/js-utils
引入,调用greet
函数,并传入自己的名称,启动服务,可以看到浏览器的弹窗。
在这我是采用本地软连接的方式导入到依赖中。
js
import * as jsUtils from "js-utils";
jsUtils.greet("hboot");
官方提供了一个前端项目模板wasm-app
模板,可以通过 npm 创建项目。
sh
$> npm init wasm-app rust-lib-web
官方提供了一个基于 webpack 的混合应用程序模板rust-webpack
,可以编写 rust 代码,实时测试效果
sh
$> npm init rust-webpack rust-webpack-web
这个混合应用是通过配置@wasm-tool/wasm-pack-plugin
插件,在运行时编译 rust 代码,加载编译包呈现在页面。
具体的代码可以查看仓库rust-web-lib; 😀😀😀
初次编译时间都非常久,下一次编译时就会快很多。
配置wasm-bindgen-test
进行单元测试
每次发布的包都是需要测试的,测试完成后才能进行发布。使用wasm-bindgen-test
用于测试 wasm-pack 编译为 wasm 的 rust 程序。
添加依赖
sh
$> cargo add wasm-bindgen-test
编写测试,创建目录tests/web.rs
来存储一些测试案例。
在无头浏览器中测试,增加代码,通过执行wasm-pack test --firefox
发起测试
rs
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
wasm-pack test --chrome
执行测试提示chromedriver binaries are unavailable for this target
估计是版本太新不匹配。
使用了 firefox 执行测试,运行成功,此时我们没有写任何的测试用例,会提示`no tests to run!。
和 rust 一样的测试书写方式,可以查看文章怎么写测试用例rust 自动化测试 ,区别就是把#[test]
改为#[wasm_bindgen_test]
rs
use js_utils;
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn alert_name() {
js_utils::greet("hboot");
js_utils::hello();
}
wasm_bindgen_test_configure!(run_in_browser);
这里我们引入了当前项目作为依赖模块进行使用use js_utils
,这里要注意的是 rust-wasm 和普通的 rust crate 不一样,我们需要指定crate-type = ["rlib"]
才可以确保我们的库进行单元测试。
修改Cargo.toml
:
toml
[lib]
crate-type = ['cdylib', 'rlib']
好了,可以执行测试了wasm-pack test --firefox
,看到如下输出:
我们运行在浏览器中,测试启动一个服务将我们的代码运行在浏览器中,打开服务http://127.0.0.1:8000
可以看到页面弹出的 alert
关闭弹窗,可以看到测试用例的运行情况:
默认情况下的wasm-pack test
是在 node 环境下运行测试。
console_error_panic_hook
将错误打印输出在浏览器中
在 rust 中代码出现逻辑错误,会panic!
输出在控制台中,那我们调试前端 web 程序,肯定希望错误展现在浏览器中,方便查看。
安装依赖:
rs
$> cargo add console_error_panic_hook
可以先看一下再没有使用console_error_panic_hook
时,我们panic!
信息,打包加载到我们测试项目中
再配置使用console_error_panic_hook
,有两种方式使用,我们使用第一种,可以去查看依赖文档了解。
rs
use std::panic;
#[wasm_bindgen]
pub fn greet(name: &str) {
panic::set_hook(Box::new(console_error_panic_hook::hook));
// ...
panic!("测试错误信息!");
}
在函数最顶部调用一次panic hook
,再次打包测试。可以看到效果,这极大的方便了开发测试,定位问题。
所有辅助的包都会有一个问题,就是会占用空间。所以在开发测试时需要加上,而正式发布包时则不需要。那就需要一个配置
toml
[dependencies]
- console_error_panic_hook = "0.1.7"
+ console_error_panic_hook = { version = "0.1.7", optional = true }
首先通过optional
指定了这个依赖是可选的。然后通过命令参数--features
指定启用哪些包,也可以配置包启用哪些特性。
当然,也可以使用Cargo.toml
配置字段features
来指定默认开启哪些依赖、特性。
toml
[features]
default = []
此时,代码里就可以开启条件编译,通过#[cfg(feature = "console_error_panic_hook")]
来标识
rs
use std::panic;
#[wasm_bindgen]
pub fn greet(name: &str) {
#[cfg(feature = "console_error_panic_hook")]
panic::set_hook(Box::new(console_error_panic_hook::hook));
// ...
panic!("测试错误信息!");
}
再次打包则需要通过--features
来指定开启哪些。
--all-features
- 全部开启;--no-default-features
- 不开启,默认不设置时则不开启。
可以在此打包测试下wasm-pack build
,可以看到控制台已经没有具体的错误信息所在的文件、行数、调用栈。我们再次加上参数wasm-pack build --features 'console_error_panic_hook'
打包测试,可以看到有具体的错误信息了。
wee_alloc
内存分配器
针对 wasm 内存分配,它可以生成更小的.wasm
代码体积。适用于那些需要少量初始化动态大小的内存分配。它没有全局的默认分配性能好,但是代码占用空间小。
安装依赖:
sh
$> cargo add wee_alloc
需要在全局引用,代替默认的内存的分配器:
rs
extern crate wee_alloc;
// Use `wee_alloc` as the global allocator.
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
配置前的打包体积大概是54kb
,配置后整个包的体积为45kb
。我们测试包仅仅只有一个文件,看起来差别不大,还是可以看出一点。
它也可以作为可选依赖,配置后可手动指定开启。
在需要性能优先的代码库中,则不推荐使用。
认识WebAssembly
在上面重点介绍了 rust 如何编译成 wasm,并引入使用。这边也要了解 WebAssembly 真正的是什么。
可以看到我们之前的测试库打包后生成了.wasm
文件,我们直接点击打开是不行的,提示此文件是二进制文件或使用了不受支持的文本编码,所以无法在文本编辑器中显示。
通过 vscode 的插件WebAssembly
可以查看内部的wasm
内容。在插件市场查找并安装
完全看不懂 🫠 先来一个简单的示例测试测试。可以使用后缀为.wat
格式的文件书写wasm
代码
wat
(module
(func (export "add") (param $p1 i32)(param $p2 i32) (result i32)
local.get $p1
local.get $p2
i32.add))
上面的代码就是一个简单的add
两数之和。
func
定义函数,(export "add")
定义这个函数被导出,可以在 js 中调用。params
定义参数,$p1
定义参数的别名,在下面需要读取。result
定义返回结果。local.get
读取参数。没有别名时,指定参数下标local.get 0
标识读取第一个参数。i32.add
以上参数之和并返回。
利用插件将.wat
转换为.wasm
存储,准备引入使用。
在index.html
利用 fetch 加载 wasm 文件
js
fetch("./index.wasm")
.then((res) => res.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes))
.then((res) => {
console.log(res);
});
查看最后的处理完的值,可看到res
对象中存在instance
对象包含了add
方法
这样我们就可以直接调用add
方法了。
js
// ...
let num = res.instance.exports.add(35, 55);
alert(num);
可以看到执行效果了。这是一个简单的webAssembly
示例,语法分析。
通过冒泡排序验证性能
要有强有力的验证说明,才能看到实际会提升多大的效果。rust 和 js 都使用自定义的冒泡排序算法,来执行上万条数据的排序, 看看执行时间做一个对比。
数据量少时,由于需要加载 wasm 所需的时间也会影响计算时间。在随着数据量变大,差距越来越大。
通过语言自带的排序算法可能由于内部各自实现的差异,执行效率受到其实现的影响。所以我们就自定义冒泡排序来测试
rs
#[wasm_bindgen]
pub fn bublle_srot(arr: &mut [i32]) {
let len = arr.len();
for i in 0..len {
for j in i+1..len {
if arr[i] > arr[j] {
arr.swap(i, j)
}
}
}
}
在 js 中实现同样逻辑的算法,先用了一万条数据测试 js 比 wasm 快 4ms;数据增加大三万条时,js 明显就慢了,时间比 wasm 多了 100ms
数据增大到五万条时,js 执行时间已经 wasm 慢了好几倍不止。
浏览器对数据排序应该是做了什么优化,第一次执行时间多出 wasm 好几倍;再次执行则多出几百毫秒。
通过 wasm 的加持,浏览器可以在更多领域施展拳脚。包括游戏、数据可视化、机器学习模型等。