WebAssembly 初学者第 4 部分:WebAssembly 和 JavaScript 伙伴关系

在 WebAssembly 初学者指南的第四部分,我们将深入探讨 WebAssembly 与 JavaScript 之间的合作关系。

本文将教你如何在 JavaScript 中运用 WebAssembly 技术。 此外,我们还将探索 WebAssembly JavaScript API 的功能。

WebAssembly 是一种开放的二进制格式标准,它使开发者能够在浏览器上以接近原生的性能运行应用程序。 如果您是初次接触此概念,我们建议您先阅读本指南的前面部分。

让我们开始吧。

WebAssembly 与 JavaScript 的协同使用

在我们的 WebAssembly 系列教程的第一部分中,我们阐述了 WASM 的工作原理。 若要为您的 Web 应用程序编写高效的代码,您必须熟练掌握 WASM API 及在 JavaScript 中的函数调用。 我们还讨论了 JavaScript 框架是如何利用 WASM 来创建高性能应用的。

然而,您目前还不能像加载 ES6 模块那样直接使用 <script type=”module”> 加载 WASM 模块。 这正是 JavaScript 的用武之地。它可以帮助在浏览器中加载和编译 WASM。 具体步骤如下:

  • 将 .wasm 字节码加载到 ArrayBuffer 或类型数组中。
  • 使用 WebAssembly.Module 编译字节码。
  • 现在,通过导入来实例化 WebAssembly.Module,从而获得可调用的导出项。

因此,您需要从预编译的 WASM 模块开始入手。 在这里,您有很多选择。 您可以使用 Rust、C/C++、AssemblyScript,甚至 TinyGo (Go) 来编写代码,然后将其转化为 .wasm 模块。

从技术角度来看,WebAssembly 是多种编程语言的编译目标。 这意味着您需要先使用您选择的语言编写代码,然后在应用程序(无论是 Web 应用还是非 Web 应用)中使用生成的二进制代码。 此外,如果您计划在服务器端使用 WASM,则需要使用 WASI 与系统进行交互。

由于 WebAssembly 使用可扩展数组来实现线性内存管理,JavaScript 和 WASM 都可以同步访问它,这使得您能够开发出功能丰富且运行迅速的应用程序。

WebAssembly 和 JavaScript 的实际应用示例

让我们通过一些实际例子,来了解如何将 WASM 与 JavaScript 结合使用。

如前所述,您需要一个预编译的 WASM 模块。 在此示例中,我们将使用 Emscripten (C/C++) 工具链。 由于 WASM 提供了一种高性能的二进制格式,我们可以将生成的代码与 JavaScript 或其他语言一起运行。

工具设置

因为我们使用的是 Emscripten,所以首先需要安装 emsdk 工具。 它允许您将 C/C++ 代码编译为 .wasm 代码。

只需在您的终端中执行以下命令。 如果您尚未安装 GIT,请参考我们的开源 101 指南:版本控制系统和 Git 安装指南进行安装。

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
# 输出

[email protected]:~/Projects/WASM2$ git clone https://github.com/emscripten-core/emsdk.git
Cloning into 'emsdk'...
remote: Enumerating objects: 3566, done.
remote: Counting objects: 100% (62/62), done.
remote: Compressing objects: 100% (49/49), done.
remote: Total 3566 (delta 31), reused 38 (delta 13), pack-reused 3504
Receiving objects: 100% (3566/3566), 2.09 MiB | 2.24 MiB/s, done.
Resolving deltas: 100% (2334/2334), done.
[email protected]:~/Projects/WASM2$ cd emsdk
[email protected]:~/Projects/WASM2/emsdk$

进入 emsdk 文件夹后,我们执行另一个命令来获取最新的可用 Emscripten 构建版本。

为此,您需要运行以下命令。

./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
# 输出

[email protected]:~/Projects/WASM2/emsdk$ ./emsdk install latest
Resolving SDK alias 'latest' to '3.1.31'
Resolving SDK version '3.1.31' to 'sdk-releases-1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-64bit'
Installing SDK 'sdk-releases-1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-64bit'..
Installing tool 'node-14.18.2-64bit'..
Downloading: /home/nitt/Projects/WASM2/emsdk/zips/node-v14.18.2-linux-x64.tar.xz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v14.18.2-linux-x64.tar.xz, 21848416 Bytes
Unpacking '/home/nitt/Projects/WASM2/emsdk/zips/node-v14.18.2-linux-x64.tar.xz' to '/home/nitt/Projects/WASM2/emsdk/node/14.18.2_64bit'
Done installing tool 'node-14.18.2-64bit'.
Installing tool 'releases-1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-64bit'..
Downloading: /home/nitt/Projects/WASM2/emsdk/zips/1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-wasm-binaries.tbz2 from https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/1eec24930cb2f56f6d9cd10ffcb031e27ea4157a/wasm-binaries.tbz2, 349224945 Bytes
Unpacking '/home/nitt/Projects/WASM2/emsdk/zips/1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-wasm-binaries.tbz2' to '/home/nitt/Projects/WASM2/emsdk/upstream'
Done installing tool 'releases-1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-64bit'.
Done installing SDK 'sdk-releases-1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-64bit'.
[email protected]:~/Projects/WASM2/emsdk$ ./emsdk activate latest
Resolving SDK alias 'latest' to '3.1.31'
Resolving SDK version '3.1.31' to 'sdk-releases-1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-64bit'
Setting the following tools as active:
   node-14.18.2-64bit
   releases-1eec24930cb2f56f6d9cd10ffcb031e27ea4157a-64bit

Next steps:
- To conveniently access emsdk tools from the command line,
  consider adding the following directories to your PATH:
    /home/nitt/Projects/WASM2/emsdk
    /home/nitt/Projects/WASM2/emsdk/node/14.18.2_64bit/bin
    /home/nitt/Projects/WASM2/emsdk/upstream/emscripten
- This can be done for the current shell by running:
    source "/home/nitt/Projects/WASM2/emsdk/emsdk_env.sh"
- Configure emsdk in your shell startup scripts by running:
    echo 'source "/home/nitt/Projects/WASM2/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile

最后一条命令,”source ./emsdk_env.sh”,用于确保设置 emcc Emscripten 编译器工具的路径,以便您可以利用它来编译代码。

# 输出

[email protected]:~/Projects/WASM2/emsdk$ source ./emsdk_env.sh
Setting up EMSDK environment (suppress these messages with EMSDK_QUIET=1)
Adding directories to PATH:
PATH += /home/nitt/Projects/WASM2/emsdk
PATH += /home/nitt/Projects/WASM2/emsdk/upstream/emscripten
PATH += /home/nitt/Projects/WASM2/emsdk/node/14.18.2_64bit/bin

Setting environment variables:
PATH = /home/nitt/Projects/WASM2/emsdk:/home/nitt/Projects/WASM2/emsdk/upstream/emscripten:/home/nitt/Projects/WASM2/emsdk/node/14.18.2_64bit/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
EMSDK = /home/nitt/Projects/WASM2/emsdk
EMSDK_NODE = /home/nitt/Projects/WASM2/emsdk/node/14.18.2_64bit/bin/node
[email protected]:~/Projects/WASM2/emsdk$

现在,我们需要通过运行以下命令来生成 wasm 代码。

emcc hello-techblik.com.c -o hello-techblik.com.js
# 输出

[email protected]:~/Projects/WASM2$ emcc hello-techblik.com.c -o hello-techblik.com.js
shared:INFO: (Emscripten: Running sanity checks)
cache:INFO: generating system asset: symbol_lists/1c683af19e290d0b5ca7a8747d74a76f63dcb362.txt... (this will be cached in "/home/nitt/Projects/WASM2/emsdk/upstream/emscripten/cache/symbol_lists/1c683af19e290d0b5ca7a8747d74a76f63dcb362.txt" for subsequent builds)
cache:INFO:  - ok
[email protected]:~/Projects/WASM2$ dir
emsdk  hello-techblik.com.c  hello-techblik.com.js  hello-techblik.com.wasm
[email protected]:~/Projects/WASM2$

正如您所看到的,您将获得 “hello-techblik.com.js” 和 hello-techblik.com.wasm 的输出文件。 您可以通过在项目目录中运行 dir 命令来查看这些文件。

这两个文件都是必不可少的。 hello-techblik.com.wasm 文件包含了编译后的代码。 另一方面,hello-techblik.com.js 文件则提供了运行该 WASM 代码所必需的 JavaScript 代码。 由于 Emscripten 支持 Web 和 Node.js 执行环境,我们可以使用 Node 来进行测试。

node hello-techblik.com.js
# 输出

[email protected]p:~/Projects/WASM2$ node hello-techblik.com.js
Hello, techblik.com!
[email protected]:~/Projects/WASM2$

如果您希望在 Web 环境中运行它,可以使用 Emscripten 生成 HTML 文件。 为此,请运行以下命令。

emcc hello-techblik.com.c -o hello-techblik.com.html
# 输出

[email protected]:~/Projects/WASM2$ emcc hello-techblik.com.c -o hello-techblik.com.html
[email protected]:~/Projects/WASM2$

若要运行 HTML 文件,可以使用 Python 3 HTTPServer,只需运行以下命令即可。

python3 -m http.server 8000

现在,在浏览器中访问 http://localhost:8000/hello-techblik.com.html 查看输出。

注意:大多数系统中都预装了 Python。 如果没有,您可以在尝试运行 Python3 服务器之前轻松安装它。

使用 JavaScript API 与 WASM 协同工作

本节将深入研究 JavaScript WASM API。 通过它,我们将学习如何加载 WASM 代码并执行它。 但首先,让我们看下面的代码。

fetch('hello-techblik.com.wasm').then(response =>
    response.arrayBuffer())
    .then(bytes =>
      WebAssembly.instantiate(bytes))
    .then(result =>
      alert(result.instance.exports.answer()))

上述代码使用了以下 JavaScript API:

  • fetch() 浏览器 API
  • WebAssembly.instantiate

除此之外,还有其他一些值得关注的 API,包括:

  • WebAssembly.compile
  • WebAssembly.Instance
  • WebAssembly.instantiate
  • WebAssembly.instantiateStreaming

fetch() 浏览器 API

fetch() API 用于加载 .wasm 网络资源。 如果您尝试在本地加载它,则需要禁用跨域资源共享 (CORS) 以加载网络资源。 或者,您可以使用节点服务器为您完成此操作。 要安装和运行节点服务器,请执行以下命令。

sudo apt install npm

接下来,运行以下命令来启动服务器。

npx http-server -o
# 输出

http-server version: 14.1.1

http-server settings:
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

Available on:
  http://127.0.0.1:8080
  http://192.168.0.107:8080
Hit CTRL-C to stop the server
Open: http://127.0.0.1:8080

[2023-01-28T19:22:21.137Z]  "GET /" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70"
(node:37919) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
(Use `node --trace-deprecation ...` to show where the warning was created)
[2023-01-28T19:22:21.369Z]  "GET /favicon.ico" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70"
[2023-01-28T19:22:21.369Z]  "GET /favicon.ico" Error (404): "Not found"

它将启动 Web 浏览器,您可以在其中查看项目中的所有文件。

现在,打开 hello-techblik.html 并启用 Web 开发工具。 在那里,打开控制台并输入以下命令。

fetch(“hello-techblik.com.wasm”);

它将返回以下 Promise。

# 输出

Promise {}
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Response
body: (...)
bodyUsed: false
headers: Headers {}
ok: true
redirected: false
status: 200
statusText: "OK"
type: "basic"
url: "http://127.0.0.1:8080/hello-techblik.com.wasm"
[[Prototype]]: Response

您还可以编写以下脚本并通过 HTML 运行它。

要在服务器上运行 wasm 模块,需要在 Node.js 中使用以下代码。

const fs = require('fs');
const run = async() => {
    const buffer = fs.readFileSync("./hello-techblik.com.wasm");
    const result = await WebAssembly.instantiate(buffer);
    console.log(result.instance.exports.answer());
};

run();

我们建议查阅 WebAssembly JavaScript API 文档以了解更多信息。

JavaScript 与 WASM 的比较

为了更好地理解 WASM 和 JavaScript 之间的协作关系,我们需要对它们进行比较。 WASM 的核心优势是速度更快,并且具有用于目标编译的二进制格式,而 JavaScript 则是一种高级语言。 WASM 的二进制代码使其学习难度较大,但有一些方法可以高效地使用 WASM。

WASM 和 JavaScript 之间的主要区别包括:

  • WASM 是一种编译型语言,而 JS 是一种解释型语言。 浏览器需要在运行时下载并解析 JavaScript,而 WASM 代码由于是预编译的,因此可以立即执行。
  • WebAssembly 是一种底层语言。 相比之下,JavaScript 是一种高级语言。 作为一种高级语言,JS 更易于使用。 然而,WASM 由于是低级别的,因此执行速度比 JavaScript 更快。
  • 最后,JavaScript 得益于其庞大的社区。 因此,如果您正在寻求更佳的开发体验,那么 JS 是一个明显的选择。 另一方面,WebAssembly 相对较新,因此资源较少。

作为开发人员,您不必为选择其中一个而纠结。 这是因为 JS 和 WASM 是协同工作的,而不是相互竞争的关系。

因此,如果您正在开发高性能的应用程序,您可能希望使用 WebAssembly 仅对那些需要高性能的部分进行编码。 JavaScript API 可以帮助您直接从 JavaScript 代码中获取和使用 WASM 模块。

总结

总而言之,WebAssembly 是 JavaScript 的绝佳伴侣。 它鼓励开发者在 Web 和非 Web 环境中创建高性能的应用程序。 此外,它并不试图取代 JavaScript。

然而,WASM 是否会发展成一个完整的工具包并最终取代 JavaScript 呢? 考虑到 WebAssembly 的设计目标,这似乎不太可能。 但是,关于 WebAssembly 在未来取代 JavaScript 的设想并非完全不可理喻。

接下来,请阅读关于构建现代应用程序的最佳 JavaScript (JS) UI 库的文章。