文章来源于code秘密花园 ,作者ConardLi

Wasm 为 Web 开发带来无限可能

Google 开发者大会 (Google Developer Summit) 是 Google 面向开发者和科技爱好者展示最新产品和平台的年度盛会。2021 年,Google 开发者大会以 “Develop as One” 为主题,携手开发者与合作伙伴共创机遇,共谋发展!

什么是 WebAssembly


WebAssembly 是一种二进制指令格式,简称为 Wsam,它可以运行在适用于堆栈的虚拟机上。
WebAssembly 存在的意义就是成为编程语言的可移植编译目标,让在 Web 上部署客户端和服务端应用成为可能。

WebAssembly 可以为我们带来什么

可移植性

如果你的网站现在想用一个能力,但是这个能力还没有被任何的 JavaScript 库实现,但是在其他编程领域里已经有了解决方案。
这时,你就可以借助 WebAssembly 将所需要的库编译为可以在 Web 上运行的二进制格式,在某些情况下甚至你还可以编译整个应用。一旦编译到 WebAssembly ,代码就可以在任何装有网络浏览器的设备上运行了,例如 PC、手机、平板电脑等等。

安全性

WebAssembly 需要在沙盒中运行,在沙盒中,除了初始化时程序主动提供给它的内容,它无法访问其他主机的内存和函数。
这意味着, WebAssembly ,在你没有给它下发命令的情况下,永远不会损坏你的主机进程内存,也无法随意访问文件系统或与其他设备通信。这就让它与运行在虚拟机和容器中的应用有相同的优势

高效

JavaScript 等人类可读的语言相比, WebAssembly 的字节码可以用更少的字节表示相同的指令,并且在 WebAssembly 模块依然处于下载期间就可以被编译。
因为编译器已经事先完成了优化工作,在 WebAssembly 中可以更轻松的获取到可预测的性能

WebAssembly 的开源应用

Squoosh

image.png
Squoosh 是一个超强的图像压缩Web应用程序,可让你深入研究各种图像压缩器提供的高级选项,例如比较视觉差异和文件大小以及下载优化后的图片版本。
https://squoosh.app/

它借助 WebAssembly 纳入了非常多的图片编解码器,这些编解码器可能来源于 C、C++、Rust 等等,在浏览器的标签页旧可以直接执行它们,不需要服务端做任何额外的处理。这让 Squoosh 可以处理很多旧的图片格式(例如 JPEG、PNG),也可以处理很多新的图片格式(例如 AVIF、JPEG-XL)。

FFMpeg

FFmpeg 是视频处理最常用的开源软件,它功能强大,用途广泛,大量用于视频网站和商业软件(比如 YoutubeiTunes),也是许多音频和视频格式的标准编码/解码实现。

借助 WebAssembly 的能力,它现在有了一个 Web 版本:FFMPEG.WASM,让你可以在浏览器里处理视频,你可以到下面这个网址上去体验一下:
https://ffmpegwasm.netlify.app/

MediaPipe

MediaPipe 是一款由 Google 开发并开源的数据流处理机器学习应用开发框架。它是一个基于图的数据处理管线,用于构建使用了多种形式的数据源,如视频、音频、传感器数据以及任何时间序列数据。
https://google.github.io/mediapipe/
它支持多个平台,融入了 WebAssemblyWebGL 的强大能力,可以通过 JavaScriptWeb 上提供机器学习模型。

WebAssembly 用法

如果你现在有一个想要移植到 WebAssembly 的库,该怎么用呢?
image.png

实际上, WebAssembly 的官网 webassembly.org 是一个很好的开始,上面对于各种语言的教程都是比较全的,在这些教程里你可以学到怎么去用相应的工具链,怎么向 WebAssembly 构建代码,以及如何利用到 Web 上,下面我们看几个最常用的工具链。

Emscripten

image.png
Emscripten 是一个开源的编译器,可以将 C/C++ 的代码编译成高度优化的 JavaScript 并且高效运行在现代浏览器上面,它推出的时间甚至比 WebAssembly 还要早。
现在,它可以将相同的 C/C++ 代码编译到 WebAssembly ,并提供各种各样的工具和绑定关系帮助你将生成的代码继承到 Web 中。
例如,Emscripten 提供 SDL 实现,可以用于在画布上绘制内容以及播放 Web 中的音频,来转换对 WebGL 的调用。

SDL(简单直接媒体层)是一个跨平台的开源开发库,旨在提供对输入和图形硬件的低级访问,用 C 语言编写,视频播放软件、模拟器和许多流行游戏都使用它。

Embind

不同语言都拥有不同的类型和内存表示法,JavaScript 和 C++ 也不例外,当你编译成 WebAssembly 也是一样的情况,所以仅仅通过编译是无法解决这个问题的。
想要使用这些库中的结果,还需要一些中间层来转换双向传递的值。
Emscripten 中实现这点最简单的方法,是使用一个叫 Embind 的功能,下面是一个示例:

  1. // quick_example.cpp
  2. #include <emscripten/bind.h>
  3. using namespace emscripten;
  4. float lerp(float a, float b, float t) {
  5. return (1 - t) * a + t * b;
  6. }
  7. EMSCRIPTEN_BINDINGS(my_module) {
  8. function("lerp", &lerp);
  9. }

通过 EMSCRIPTEN_BINDINGS 块,就可以以 JavaScript 函数形式声明对外开放的 API,以及转换作为实参传递到 C++ 函数的值或者从 C++ 返回的值。这样一来,你就可以将现有任何的 C++ 库封装到一个对 JavaScript 友好的 API 中。
最后你可以同时编译 API 封装容器和之前构建的依赖项,并传递一个 —bind 参数来启用 Embind

  1. emcc --bind -o quick_example.js quick_example.cpp

如果将其编译为 扩展项,它会生成一个 ES6 兼容模块,然后你就可以从 JavaScript 代码导入它,异步初始化这个模块。

  1. import initModule from './mylib.mjs';
  2. const Module = await initModule();
  3. Module.lerp(1,2,3);

然后你就可以使用之前从 EMSCRIPTEN_BINDINGS 块声明的所有 API。

wasm-bindgen

如果你熟悉 Rust ,就知道它在 WebAssembly 领域的贡献是非常大的。
image.png
Rust 提供了 wasm-bindgen 这个工具来支持为任何 Web API 生成绑定关系,以及将你自己的 Rust 函数导出为 JavaScript
感兴趣你可以看一下下面这个在线教程:https://rustwasm.github.io/
教程中有将 Rust 函数导出为 JavaScript 的详细指引,以及一些示例,和 Embind 一样,它也负责在语言之间的双向类型转换,参考下面这段代码:

  1. use wasm_bindgen::prelude::*;
  2. #[wasm_bindgen]
  3. extern "C" {
  4. fn alert(s: &str);
  5. }
  6. #[wasm_bindgen]
  7. pub fn greet(name: &str) {
  8. alert(&format!("Hello, {}!", name));
  9. }

当你在一个 extern 块上应用 wasm_bindgen 属性时,就可以导入指定的 API,当你在自己的类型和函数上应用 wasm_bindgen 属性时,系统会导出相应的类型和函数。
在每种情况下,工具链都负责在后台为库生成类型转换, 以及 JavaScript 封装容器,甚至是 TypeScript 定义,声明 API 后,就可以编译库生成一个 ES6 模块。

  1. const rust = import('./pkg');
  2. rust
  3. .then(m => m.greet('World!'))
  4. .catch(console.error);

Emscripten 的示例类似,也需要异步将其初始化一次,然后就可以作为常规的 JavaScript 模块进行调用。

将 JS/TS 编译成 WebAssembly

那么,JavaScript、TypeScript 能不能编译成 WebAssembly 呢?
答案是否定的,因为 JavaScript 是高度动态的语言,而 WebAssembly 属于静态类型语言,不过我们可以借助 AssemblyScript 来帮助我们模拟实现这一点。
image.png
AssemblyScript 是一个 TypeScriptWebAssembly 的编译器,你可以到 https://www.assemblyscript.org/ 去了解它的详细用法。

未来

WebAssembly 现在已经处于稳定阶段了,几年前就被所有主流浏览器所支持,但是它仍在不断发展,探索新的能力。
image.png
在这些探索中,有一些改进了与 JavaScriptWeb 的集成,有一些缩减了代码体积、,还有一些进一步提升了性能,想了解更多,可以到下面的网址进行查看:https://webassembly.org/roadmap/

现代浏览器的功能早已不局限在简单的页面呈现,这就是为什么 WebAssembly 会诞生的重要原因之一。为了将沉重的任务性能提升到一个新的水平,在 JavaScript 和机器代码之间搭建了一座桥梁,由此才有了 WebAssembly


使用 Rust 编写更快的 React 组件

Wasm

WebAssembly 是一种二进制指令格式,简称为 Wasm,它可以运行在适用于堆栈的虚拟机上。
WebAssembly 存在的意义就是成为编程语言的可移植编译目标,让在 Web 上部署客户端和服务端应用成为可能。
Wasm 具有紧凑的二进制格式,可为我们提供近乎原生的网络性能。随着它变得越来越流行,许多语言都编写了编译成 Web 程序集的绑定工具。

为什么是 Rust

Rust 是一个快速、可靠二期又节约内存的编程语言。在过去六年的 stackoverflow 的最受喜爱的编程语言中,它一直蝉联榜首的位置,主要还是这个语言本身拥有众多的优点,比如:

  • 内存安全
  • 类型安全
  • 消除数据竞争
  • 使用前编译
  • 建立(并且鼓励)在零抽象之上
  • 最小的运行时(无停止世界的垃圾搜集器,无 JIT 编译器,无 VM
  • 低内存占用(程序可以运行在资源受限的环境,比如小的微控制器)
  • 针对裸机(比如,写一个 OS 内核或者设备驱动,把 Rust 当一个 ‘高层’汇编器使用)”

另外,RustWebAssembly 领域的贡献非常大的,使用 Rust 编写 WebAssembly 非常简单。
但是,Rust 存在的目的不是为了替代 JavaScript 而是和他形成互补,因为 Rust 语言的学习曲线是非常陡峭的,用它去完全替代 Web 开发几乎是不可能的。
所以,我们一般会在 Web 开发的工具链,或者前端页面中一些非常大量的数据计算中的操作用到它。

前置知识

在开始开发之前,你需要了解一些前置知识,React 相关的就不多说了,我们来看看 Rust 相关的几个重要概念。

cargo

cargorust 的代码组织和包管理工具,你可以将它类比为 node.js 中的 npm
cargo 提供了一系列强大的功能,从项目的建立、构建到测试、运行直至部署,为 rust 项目的管理提供尽可能完整的手段。同时,它也与 rust 语言及其编译器 rustc 本身的各种特性紧密结合。

rustup

rustupRust 的安装和工具链管理工具,并且官网推荐使用 rustup 安装 Rust
rustuprustc(rust编译器) 和 cargo 等工具安装在 Cargobin 目录,但这些工具只是 Rust 工具链中组件的代理,真正工作的是工具链中的组件。通过 rustup 的命令可以指定使用不同版本的工具链。

wasm-bindgen

image.png
wasm-bindgen 提供了 JSRust 类型之间的桥梁,它允许 JS 使用字符串调用 Rust API,或者使用 Rust 函数来捕获 JS 异常。
wasm-bindgen 的核心是促进 javascriptRust 之间使用 wasm 进行通信。它允许开发者直接使用 Rust 的结构体、javascript的类、字符串等类型,而不仅仅是 wasm 支持的整数或浮点数类型。

wasm-pack

wasm-packRust / Wasm 工作组开发维护,是现在最为活跃的 WebAssembly 应用开发工具。
wasm-pack 支持将代码打包成 npm 模块,并且附带 Webpack 插件(wasm-pack-plugin),借助它,我们可以轻松的将 Rust 与已有的 JavaScript 应用结合。

wasm32-unknown-unknown

通过 rustuptarget 命令可以指定编译的目标平台,也就是编译后的程序在哪种操作系统上运行。
wasm-pack 使用 wasm32-unknown-unknown 目标编译代码。
好了,了解了 Rust 相关的一些知识,我们一起来完成这个 Demo 吧。

一起来做个 Demo

在开始之前,要确保你的电脑上已经安装了 NodeRust,可以在命令行分别输入 npmrustup 看看能否找到命令,如果没安装的话自己先安装一下。

初始化一个简单 React 程序

首先,我们来初始化一个 React 项目,命令行执行 npm init

  1. npm init

然后,我们安装一些开发项目必备的包:

  1. npm i react react-dom
  2. npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
  3. npm i -D babel-core babel-loader @babel/preset-env @babel/preset-react

然后,我们在项目中创建一些常用的文件夹:srcpagepublicbuild、和 dist
我们在 page 文件夹中创建一个 index.jsx,编写一些测试代码:

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. ReactDOM.render(<h1>code秘密花园 Hello, world!</h1>, document.getElementById('root'));

然后,我们为 babelwebpack 创建两个配置文件:
.babelrc

  1. {
  2. "presets": [
  3. "@babel/preset-env",
  4. "@babel/preset-react"
  5. ]
  6. }

webpack.config.js

  1. const HtmlWebpackPlugin = require('html-webpack-plugin');
  2. const path = require('path');
  3. module.exports = {
  4. entry: './page/index.jsx',
  5. output: {
  6. path: path.resolve(__dirname, 'dist'),
  7. filename: 'bundle.[hash].js',
  8. },
  9. devServer: {
  10. compress: true,
  11. port: 8080,
  12. hot: true,
  13. static: './dist',
  14. historyApiFallback: true,
  15. open: true,
  16. },
  17. module: {
  18. rules: [
  19. {
  20. test: /.(js|jsx)$/,
  21. exclude: /node_modules/,
  22. use: {
  23. loader: 'babel-loader',
  24. },
  25. },
  26. ],
  27. },
  28. plugins: [
  29. new HtmlWebpackPlugin({
  30. template: `${__dirname }/public/index.html`,
  31. filename: 'index.html',
  32. }),
  33. ],
  34. mode: 'development',
  35. devtool: 'inline-source-map',
  36. };

然后,在 public 下创建一个 index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>code秘密花园</title>
  7. </head>
  8. <body>
  9. <div id="root"></div>
  10. </body>
  11. </html>

下面检查下你的 package.json,看看和我的是不是一样:

  1. {
  2. "name": "react-wasm",
  3. "version": "1.0.0",
  4. "description": "一个 Rust 编写 React 组件的 Demo",
  5. "main": "src/index.jsx",
  6. "scripts": {
  7. "dev": "webpack server"
  8. },
  9. "keywords": [],
  10. "author": "ConardLi",
  11. "license": "MIT",
  12. "dependencies": {
  13. "react": "^17.0.2",
  14. "react-dom": "^17.0.2"
  15. },
  16. "devDependencies": {
  17. "@babel/core": "^7.16.0",
  18. "@babel/preset-env": "^7.16.4",
  19. "@babel/preset-react": "^7.16.0",
  20. "babel-loader": "^8.2.3",
  21. "html-webpack-plugin": "^5.5.0",
  22. "webpack": "^5.64.2",
  23. "webpack-cli": "^4.9.1",
  24. "webpack-dev-server": "^4.5.0"
  25. }
  26. }

下面,执行 npm install,然后 npm run dev,你就可以跑起来一个非常简单的 React 应用:
image.png

引入 Rust

好了,下面我们来编写我们的 Rust 组件(别忘了回顾下上面提到的 Rust 前置知识),首先我们使用 Rust 的包管理工具 cargo 来初始化一个简单的 Rust 应用程序:

  1. cargo init --lib .

执行完之后,会创建一个 Cargo.toml 和一个 src/lib.rc 文件。
然后,我们在 Cargo.toml 中引入 wasm-bindgen 这个包,另外我们还需要告诉编译器这个包是一个 cdylib

  1. [package]
  2. name = "react-wasm"
  3. version = "1.0.0"
  4. edition = "2021"
  5. [lib]
  6. crate-type = ["cdylib"]
  7. [dependencies]
  8. wasm-bindgen = "0.2"

现在,你可以先尝试执行下 cargo build
image.png

第一次执行可能会比较慢,可以 Google 搜一下怎么将 cargo 配置为国内源。

好了,上面只是测试一下构建,它现在还派不上用场,我们下面还要执行一下编译目标,执行:

  1. rustup target add wasm32-unknown-unknown

指定好 wasm32-unknown-unknown 这个编译目标,我们才能把它应用到我们的 React 程序中,下面我们给我们的 src/lib.rs 写两个简单的函数:

  1. use wasm_bindgen::prelude::*;
  2. #[wasm_bindgen]
  3. extern "C" {
  4. fn alert(s: &str);
  5. }
  6. #[wasm_bindgen]
  7. pub fn big_computation() {
  8. alert("这个是一个超级耗时的复杂计算逻辑");
  9. }
  10. #[wasm_bindgen]
  11. pub fn welcome(name: &str) {
  12. alert(&format!("Hi 我是 {} ,我在 code秘密花园 !", name));
  13. }

为了确保我们的 Rust 应用程序正常工作,我们重新用 wasm32-unknown-unknown 编译一下:

  1. cargo build --target wasm32-unknown-unknown

然后我们安装一下 wasm-bindgen-cli 这个命令行工具,以便我们能利用我们创建的 WebAssembly 代码:

  1. cargo install -f wasm-bindgen-cli

安装后,我们可以使用 Rust 生成的 WebAssembly 给我们的 React 代码创建一个包:

  1. wasm-bindgen target/wasm32-unknown-unknown/debug/react_wasm.wasm --out-dir build

执行完成后,编译好的 JavaScript 包和优化好的 Wasm 代码会保存到我们的 build 目录中,以供 React 程序使用。

在 React 程序中应用 Wasm

下面,我们尝试一下在我们的 React 程序中用上这些 Wasm 代码,我们现在 package.json 中添加一些常用的 npm 脚本:

  1. "build:wasm": "cargo build --target wasm32-unknown-unknown",
  2. "build:bindgen": "wasm-bindgen target/wasm32-unknown-unknown/debug/rusty_react.wasm --out-dir build",
  3. "build": "npm run build:wasm && npm run build:bindgen && npx webpack",

然后我们执行 npm run build 就可以打包所有代码啦。
下面,我们还需要安装一下上面我们提到的 wasm-packWebpack 插件,它可以帮助我们把 Wasm 代码打包成 NPM 模块:

  1. npm i -D @wasm-tool/wasm-pack-plugin

最后更新一下我们的 webpack.config.js,添加下面的配置:

  1. const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
  2. ...
  3. plugins: [
  4. ...
  5. new WasmPackPlugin({
  6. crateDirectory: path.resolve(__dirname, ".")
  7. }),
  8. ],
  9. ...
  10. experiments: {
  11. asyncWebAssembly: true
  12. }

下面,执行一下这几个命令:npm run build:wasm、npm run build:bindgen、npm run build,应该都不会报错。
image.png
最后,我们在我们的 React 组件中调用一下我们刚刚生成的 Wasm 模块:

  1. import React, { useState } from "react";
  2. import ReactDOM from "react-dom";
  3. const wasm = import("../build/rusty_react");
  4. wasm.then(m => {
  5. const App = () => {
  6. const [name, setName] = useState("");
  7. const handleChange = (e) => {
  8. setName(e.target.value);
  9. }
  10. const handleClick = () => {
  11. m.welcome(name);
  12. }
  13. return (
  14. <>
  15. <div>
  16. <h1>Hi there</h1>
  17. <button onClick={m.big_computation}>Run Computation</button>
  18. </div>
  19. <div>
  20. <input type="text" onChange={handleChange} />
  21. <button onClick={handleClick}>Say hello!</button>
  22. </div>
  23. </>
  24. );
  25. };
  26. ReactDOM.render(<App />, document.getElementById("root"));
  27. });

下面,你就可以在 React 组件中愉快的使用 Rust 了!
image.png

参考