文章同步发布于本人博客,戳这儿

布景

WebAssembly(WASM)是一个简略的机器模型和可履行格局,具有广泛的标准。它被设计为便携、紧凑,代码履行可以达到挨近本机原生指令的履行速度。

作为一种编程言语,WebAssembly 由两种格局组成,它们以不同的办法表明相同的结构:

  • 后缀为 .wat 的文本格局(称为“WebAssembly Text”),可以被人类理解,运用 S-表达式。
  • 后缀为 .wasm二进制格局是较低等级的,人无法读懂,它旨在供 wasm 虚拟机直接运用。

作为参考,下面是一个在 JS 中调用两数求和 WASM 函数的例子:

const wasmInstance = new WebAssembly.Instance(wasmModule, {});
const { addTwo } = wasmInstance.exports;
for (let i = 0; i < 5; i++) {
  console.log(addTwo(i, i));
}
/**
 * output:
 * 0
 * 2
 * 4
 * 6
 * 8
 **/

addTwo 函数本身是由其他言语编写而成的,并且被编译成了 .wat 格局。以下是这个 addTwo 求和函数的 .wat 文件:

(module
  (func (export "addTwo") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

可以经过这个网站将 .wat 生成对应的二进制 .wasm 文件:wat2wasm demo

环境装备

  1. 装置 rust 东西链(rustuprustccargo
  2. 装置 wasm-pack,一个构建、测验和发布 WASM 的 Rust CLI 东西,咱们将运用 wasm-pack 相关的指令来构建 WASM 二进制内容。
  3. npm,JS 包管理器

如果不装置 wasm-pack,运用打包东西 webpack 加上 @wasm-tool/wasm-pack-plugin 插件也能构建 WASM,后文会详细介绍。

Tips:装置 cargo-generate,可以运用现有的 git 库房生成一个新的 Rust 项目: cargo install cargo-generate

快速入门

项目初始化

首先咱们履行 cargo new wasm-demo 初始化 Rust 项目,新建一个名为 wasm-demo 的文件夹(也可以选一个你喜爱的文件夹名),主动生成装备文件 Cargo.toml,结构如下。

手把手教育,运用 Rust + WASM 进行 Web 开发

装备包文件

咱们可以在 Cargo.toml 文件中加上下列代码并保存,保存之后 Cargo 会主动下载依靠。

  • cdylib 用来指明库的类型。
  • wasm-bindgen 是一个简化 Rust WASM 与 JS 之间交互的库。
    • 它可以将如 DOM 操作、console.log 和 performance 等 JS 相关 API 露出给 Rust 运用
    • 它可以将 Rust 功用导出到 JS 中,如类、函数等
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.83"

编写代码

接着开始编写一些简略的 Rust 代码。将模板文件中的 src/main.rs 改成 src/lib.rs,里面写上一个求斐波那契数列的 Rust 函数。需求加上#[wasm_bindgen]标示告诉 wasm-pack 需求将这个函数编译成 wasm 可履行文件。

use wasm_bindgen::prelude::*; // 用于加载 Prelude(预导入)模块
#[wasm_bindgen]
pub fn fib(n: u32) -> u32 {
    if n == 0 || n == 1 {
        return 1;
    }
    fib(n - 1) + fib(n - 2)
}

当时目录应该长这样:

手把手教育,运用 Rust + WASM 进行 Web 开发

Rust 中包管理体系将 crate 包分为二进制包(Binary)和库包(Library)两种,二者可以在同一个项目中同时存在。

二进制包:

  • main.rs 是二进制项目的进口
  • 二进制项目可直接履行
  • 一个项目中二进制包可以有多个,所以在 Cargo.toml 中经过双方括号标识 [[bin]]

库包:

  • lib.rs 是库包的进口。
  • 库项目不行直接履行,通常用来作为一个模块被其他项目引用。
  • 一个项目中库包仅有1个,在 Cargo.toml 中经过单独括号标识 [lib]

由于咱们这儿期望将 WASM 转为一个可以在 JS 项目中运用的模块,所以需求运用库包 lib.rs 的命名。

履行编译

只需求履行咱们之前装置的 wasm-pack 即可将刚刚的 Rust 代码转换成可以被 JS 导入的模块。

wasm-pack build

编译完成后,咱们会发现根目录下多了一个 pkg/ 文件夹,里面便是咱们的 WASM 产品地点的 npm 包了。

手把手教育,运用 Rust + WASM 进行 Web 开发

包的进口文件是不带 _bg.js 文件,即 wasm_demo2.js

wasm_demo2.js 的内容如下:

import * as wasm from "./wasm_demo2_bg.wasm";
export * from "./wasm_demo2_bg.js";

wasm_demo2_bg.js 的内容如下:

import * as wasm from './wasm_demo2_bg.wasm';
/**
* @param {number} n
* @returns {number}
*/
export function fib(n) {
    const ret = wasm.fib(n);
    return ret >>> 0;
}

wasm_demo2.d.ts 的内容如下:

/* tslint:disable */
/* eslint-disable */
/**
* @param {number} n
* @returns {number}
*/
export function fib(n: number): number;

可以看到,wasm-pack 打包不只输出一个 ESM 标准的模块,并且还支持主动生成 d.ts 文件,对模块的运用者十分友爱。

运用 WASM 包

下面咱们就新建一个 html 页面去调用刚刚生成的模块。在根目录下生成 index.html,并输入以下内容。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rust WASM demo</title>
    <script>
        /**
         * 1. 经过运用 instantiateStreaming 调用流式实例化
         **/
        WebAssembly.instantiateStreaming(fetch("./pkg/wasm_demo2.wasm")).then((obj) => {
            const fib = obj.instance.exports.fib;
            const out = fib(20);
            console.log("rust output: ", out);
        })
        /**
         * 2. 不经过流式调用,直接读取二进制文件并对字节进行实例化
         **/
        fetch("./pkg/wasm_demo2.wasm")
        .then(res => res.arrayBuffer())
        .then(bytes => WebAssembly.instantiate(bytes))
        .then(results => {
            const fib = results.instance.exports.fib;
            const out = fib(20);
            console.log("rust output: ", out);
        })
    </script>
</head>
<body>
</body>
</html>

如上所示,可以经过流式与非流式两种原生 JS API 办法进行 .wasm 二进制文件的模块实例化。

接下来编写一个简略的服务 server.js

const http = require('http');
const fs = require('fs');
const reqListener = function(req, res) {
    f = req.url === '/' ? 'index.html' : './pkg/wasm_demo2_bg.wasm';
    if (f === './pkg/wasm_demo2_bg.wasm') {
        res.setHeader('Content-type', 'application/wasm')
      }
      res.writeHead(200)
      return fs.createReadStream(f).pipe(res)
}
const server = http.createServer(reqListener);
server.listen(8081);
console.log('listening: http://localhost:8081')

开启服务:

node server.js

翻开 html 页面 http://localhost:8081/ ,在控制台可看到两份 fib(20) 的成果被打印出来了。

手把手教育,运用 Rust + WASM 进行 Web 开发

进阶用法

配合 Webpack 运用

运用 Webpack + wasm-pack 插件的办法构建和测验,可以直接经过 npm scripts 运转代码,像前端开发相同调试。

npm init -y 新建一个项目,在 package.json 中新增如下代码:

...
  "scripts": {
    "build": "webpack",
    "serve": "webpack serve"
  },
  "devDependencies": {
    "@wasm-tool/wasm-pack-plugin": "1.5.0",
    "html-webpack-plugin": "^5.3.2",
    "text-encoding": "^0.7.0",
    "webpack": "^5.49.0",
    "webpack-cli": "^4.7.2",
    "webpack-dev-server": "^3.11.2"
  },
  ...

履行 npm i 装置依靠。

新建 index.js 文件,作为 WASM 模块的履行文件。在里面写入如下内容:

// 由于 webpack 的 bug(webpack/webpack#6615),这儿暂时只能运用动态导入 import
const rust = import('./pkg');
rust.then(m => {
    const out = m.fib(20);
    console.log("rust output: ", out);
}).catch(console.error)

新建 webpack.config.js 文件,并进行如下装备:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); // 赋予 webpack 处理 wasm 才能的插件
/**
 * @type import('webpack').Configuration
 */
module.exports = {
    entry: './index.js',
    devServer: {
        port: '8082'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'index.js',
    },
    plugins: [
        new HtmlWebpackPlugin(),
        new WasmPackPlugin({
            crateDirectory: path.resolve(__dirname, ".")
        }),
        // Have this example work in Edge which doesn't ship `TextEncoder` or
        // `TextDecoder` at this time. 处理浏览器兼容问题
        new webpack.ProvidePlugin({
          TextDecoder: ['text-encoding', 'TextDecoder'],
          TextEncoder: ['text-encoding', 'TextEncoder']
        })
    ],
    mode: 'development',
    experiments: {
        asyncWebAssembly: true // 翻开异步 WASM 功用
   }
};

履行 npm run build 构建出 WASM 二进制产品。

手把手教育,运用 Rust + WASM 进行 Web 开发

履行 npm run serve 即可进行开发,在浏览器的控制台中实时看到对应 fib(20) 的成果。咱们可以在 index.js 中更改传入的参数,并查看控制台的新输出成果。

手把手教育,运用 Rust + WASM 进行 Web 开发

总结便是,运用 webpack 可以协助可以更加高效地进行 Rust WASM 运用的开发和调试。

这一块凭借了 webpack-dev-server 的 HMR 模块实现热更新:

  1. 打包时将一块 webpack 脚本代码(JSONP 脚本)打包到客户端运用中。
  2. 当本地 lib.rs 文件发生变化时,服务端 webpack-dev-server 经过 websocket 告诉客户端运用代码中的 webpack 脚本代码,客户端向服务端请求最新编译好的 wasm 模块
  3. 新的 WASM 模块以 JSONP 的办法从服务端传输到客户端
  4. 经过 webpack 重写的 __webpack_require__ 办法获取到新模块并加载、包裹、运转和缓存,实现模块的热替换。

Rust 操纵 DOM

实现一个求斐波那契数的运用,如下所示:

手把手教育,运用 Rust + WASM 进行 Web 开发

需求先装置一个叫 web-sys 的 Crate,它为 Rust 提供了控制 DOM 的才能,在 Cargo.toml 中新增依靠:

[dependencies.web-sys]
version = "0.3.4"
features = [ 'Document', 'Element', 'HtmlElement', 'Node', 'Window', 'HtmlInputElement']

features 需求开发者手动地声明需求运用到的模块,这样做的好处有二:一是防止不同模块下的 features 名字发生冲突;二是条件编译各个依靠中的特性,对不运用的 features 不编译。

lib.rs 函数中新增 init() 函数,用于生成 DOM 节点、挂载监听器并挂载 DOM 节点。


// start 标识 init() 在 WASM 加载时主动履行
#[wasm_bindgen(start)]
pub fn init() -> Result<(), JsValue> {
    // 运用 web_sys 的 window 大局目标
    let window = web_sys::window().expect("不存在大局 window 目标");
    let document = window.document().expect("需求在 window 上存在 document");
    let body = document.body().expect("document 中需求存在一个 body");
    // 生成 dom 元素
    let input = document
        .create_element("input")?
        .dyn_into::<web_sys::HtmlInputElement>()?;
    let btn = document.create_element("button")?;
    btn.set_text_content(Some(&"点击核算斐波那契数"));
    let out = document.create_element("h3")?;
    let input = Rc::new(input); // 为了不违背“一个变量只能有一个所有者”的规矩,需求运用 Rc 包裹 input 元素,方便在闭包中拿到并运用它的值
    let out = Rc::new(RefCell::new(out)); // 由于需求改动 out 元素的 textContent,需求运用 RefCell 包裹方便去在闭包中把它作为可变变量来改动它
    {
        let out = out.clone(); // 仿制一份智能指针
        let input = input.clone();
        // 运用到 wasm_bindgen::closure::Closure,它的作用是打通 Rust 中的闭包和 JS 中的闭包
        let closure = Closure::<dyn Fn()>::new(move || {
            let val = input.value();
            let num = val.parse::<u32>().unwrap();
            let res = fib(num);
            out.borrow_mut()
                .set_text_content(Some(res.to_string().as_str())); // 在这儿运用 borrow_mut 把 out 作为可变变量获取出来,并设置 textContent
        });
        btn.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?; // 挂载事件监听器,将闭包函数先转换为 JS 值,再越过类型判别,再作为回调函数传给 btn
        closure.forget(); // 开释 Rust 对这片堆内存的管理,交给 JS 的 GC 去收回
    }
    body.append_child(&input)?;
    body.append_child(&btn)?;
    body.append_child(&out.borrow())?; // 挂载 DOM 元素节点
    Ok(())
}

上述 init() 添加了 #[wasm_bindgen(start)] 宏标示,init() 函数会在 WASM 模块初始化时主动履行。因此不再需求更改 index.js 文件。

直接运转服务:

npm run serve

翻开 http://localhost:8082/ ,成功!

翻开查看,咱们可以发现 body 上正确地挂载了 DOM 元素。

手把手教育,运用 Rust + WASM 进行 Web 开发

功能指标

咱们先在 lib.rs 中加上以下代码,答应 Rust 代码中调用 console.log

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str); // 将 js 命名空间中的 console.log 办法界说在 Rust 中
}
// 界说 console.log! 宏
macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

接着,在刚刚的求斐波那契数的运用中加上 performance API 相关的代码。

Cargo.toml 中加上 Performance 的 features:

[dependencies.web-sys]
version = "0.3.4"
features = [
  'Document',
  'Element',
  'HtmlElement',
  'Node',
  'Window',
  'Performance', // 加上这一行
  'HtmlInputElement'
]

将求斐波那契数运用中的 closure 办法进行如下重写:

let closure = Closure::<dyn Fn()>::new(move || {
    let performance = window
        .performance()
        .expect("performance should be available"); // 获取 window.performance
    let val = input.value();
    let num = val.parse::<u32>().unwrap();
    let start = performance.now(); // 调用 performance.now() 获取当时时刻
    console_log!("the start time (in ms) is {}", start);
    let res = fib(num);
    let end = performance.now();
    console_log!("the end time (in ms) is {}", end);
    console_log!("delta (in ms) is {}", end-start);
    out.borrow_mut()
        .set_text_content(Some(res.to_string().as_str()));
});

运转服务器:

npm run serve

现在在控制台就可以看到履行运算的耗时了。

手把手教育,运用 Rust + WASM 进行 Web 开发

履行运算的录屏

功能比较:

fib(n) 10 20 30 35 40 45
wasm 0.30 0.90 69.90 726.59 8018 90918.79
js 0.19 0.80 67.70 753.20 8061 91794.70

可以看到,在 n 不大的场景下,WASM 的耗时比纯 JS 还要长,这是由于浏览器需求在 VM 容器中对 WASM 模块进行实例化,可能这一部分会耗费适当的时刻,导致功能不如纯 JS 的履行。随着运算规划变大,WASM 的优化越来越显着。

总结

WASM 从 2017 年 3 月推出以来,已然成了 Web 开发的未来发展趋势之一。

本文不只介绍了 WASM 的布景、环境装备、Rust 项目初始化、编译和运用 WASM 等基本用法,还经过一个简略的运用介绍了 WASM 与 webpack 配合开发、与 DOM 之间交互以及功能指标剖析等进阶用法。

参考资料

  • Rust and WebAssembly
  • hello-world
  • Using the WebAssembly JavaScript API
  • Closure in wasm_bindgen::closure – Rust (rustwasm.github.io)
  • start – The wasm-bindgen Guide (rustwasm.github.io)
  • Make a Combined Library and Binary Project in Rust – DEV Community ‍‍