08-wasm-bindgen
15.4.1 wasm-bindgen
wasm-bindgen是由GitHub上的rust-wasm团队开发的一款软件包。它支持Rust代码调用 JavaScript 代码,反之亦然。基于该软件包,已经构建了很多其他更高级的程序库,例如web-sys和js-sys软件包。
JavaScript本身就是欧洲计算机制造商协会(European Computer Manufacturers Association,ECMA)标准定义的内容,但相关标准没有规定它在Web上的工作方式。JavaScript可以支持多种宿主,Web恰好是其中之一。web-sys软件包允许访问Web上的所有JavaScript API,即DOM API,例如Window、Navigator及EventListener等。js-sys软件包提供ECMA标准规范中指定的所有基本的JavaScript对象,即函数、对象及数字等。
由于WebAssembly仅支持数字类型,因此wasm-bindgen软件包生成适配元素以便用户能够在JavaScript中使用原生的Rust类型。例如,Rust中的结构体表示为JavaScript端的对象,而Promise对象可以在Rust端作为Future访问。它通过在函数定义上使用#[wasm-bindgen]属性来完成所有这些操作。
为了探索wasm-bindgen以及它如何与JavaScript交互,我们将构建一些实用的程序。接下来将构建一个在线markdown编辑器应用程序,它允许用户编写markdown并预览经过渲染后的HTML页面。不过在正式开始之前,需要安装为我们生成适配元素的wasm-bindgen-cli工具,从而允许我们方便地使用其中公开的Rust函数。我们可以通过运行如下命令安装它:
cargo install wasm-bindgen-cli
接下来,让我们通过运行cargo new livemd命令创建一个项目,相关的Cargo.toml文件内容如下所示:
[package]
name = "livemd"
version = "0.1.0"
authors = ["Rahul Sharma <[email protected]>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.29"
comrak = "0.4.0"
我们将软件包命名为livemd,程序库是cdylib类型,并且公开了一个C语言接口,因为WebAssembly接收一个目标宽泛的动态C程序库接口,大多数语言都可以编译到该接口。接下来将在我们的项目根目录下创建一个run.sh脚本,以便构建和运行我们的项目,并在每次使用cargo-watch检测到代码发生任何更改时重新运行它。以下是run.sh文件的内容:
#!/bin/sh
set -ex
cargo build --target wasm32-unknown-unknown
wasm-bindgen target/wasm32-unknown-unknown/debug/livemd.wasm --out-dir app
cd app
yarn install
yarn run serve
接下来是lib.rs中的markdown转换代码的实现,完整内容如下所示:
// livemd/src/lib.rs
use wasm_bindgen::prelude::*;
use comrak::{markdown_to_html, ComrakOptions};
#[wasm_bindgen]
pub fn parse(source: &str) -> String {
markdown_to_html(source, &ComrakOptions::default())
}
我们的livemd软件包公开了一个名为parse的函数,它从网页上的textarea标签中获取markdown文本(尚未创建),并通过调用comrak软件包中的markdown_to_html函数返回经过编译的HTML字符串。如你所见,parse函数采用了#[wasm_bindgen]属性进行注释。此属性为各种底层转换生成代码,并且需要将此函数公开给JavaScript。使用此方法,我们不必关心parse函数接收何种类型的字符串。JavaScript中的字符串与Rust中的字符串有所不同。#[wasm_bindgen]属性负责处理这种差异,以及在接收&str 类型字符串之前从JavaScript端转换字符串的底层细节。在撰写本书时,有些类型是wasm-bindgen无法转换的,例如引用或带有生命周期注释的类型定义。
然后我们需要为该软件包生成wasm文件。但在此之前,我们先对应用程序进行一些设置。在同一目录下,我们将创建一个名为app/的目录,并通过运行yarn init命令来初始化项目:
yarn init会创建我们的package.json文件。除了普通的字段之外,我们还会指定脚本(scripts)和开发依赖项(dev-dependencies):
{
"name": "livemd",
"version": "1.0.0",
"description": "A live markdown editor",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "webpack",
"serve": "webpack-dev-server"
},
"devDependencies": {
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.28.3",
"webpack-cli": "^3.2.0",
"webpack-dev-server": "^3.1.0"
}
}
我们将使用webpack来启动开发环境下的Web服务器。webpack是一个模块捆绑器。它会接收多个JavaScript源文件,并将它们打包到一个文件中,从而缩小其体积以便在Web上使用。要让webpack能够捆绑JavaScript和wasm生成的代码,我们将在名为web.pack. config.js的文件中创建一个webpack配置文件:
// livemd/app/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin({
template: "index.html"
})
],
mode: 'development'
};
接下来在同一app/目录中,我们将创建3个文件。
- index.html——这包含应用程序的UI:
<!--livemd/app/index.html-->
<!DOCTYPE html>
<html>
<head>
<title>Livemd: Realtime markdown editor</title>
<link
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap
.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Aleo"
rel="stylesheet">
<link href="styles.css" rel="stylesheet">
</head>
<body class="container-fluid">
<section class="row">
<textarea class="col-md-6 container" id="editor">_Write
your text here.._</textarea>
<div class="col-md-6 container" id="preview"></div>
</section>
<script src="index.js" async defer></script>
</body>
</html>
我们已经声明了一个带有编辑器ID的HTML元素