尝试使用 rust 开发一个简单的 web 页面。使用 yew 框架,在 Chrome 运行 WebAssembly,并能在 IE 11 上降级到 JS 运行。

环境准备

  • rust、rustup、cargo
  • cago-generate, more detail
    • $ cargo install cargo-generate
  • wasm-pack, more detail
    • $curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
  • wasm2js, more detail
    • $ brew install binaryen
    • 速度不行的话,自行切换至 中科大 或 清北 的镜像,推荐中科大

初始化

参考 Rust and WebAssembly 的 HelloWorld 样例,使用以下命令进行初始化:

$ cargo generate --git https://github.com/rustwasm/wasm-pack-template

输入名称:hello-rust-wasm

初始化完成后,目录结构如下:

.
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── src
│   ├── lib.rs
│   └── utils.rs
└── tests
    └── web.rs

编辑页面

由于 rust 初始化时,是按模块初始化的,所以主要代码写在 lib.rs中(正常初始化为入口为 main.rs),再由外部 JS 引入之后,启动执行。 为了更贴近实际开发,我们使用 yew 框架进行开发,整体代码参考如下:

  • 添加 yew 依赖

[dependencies]

yew = { version = "0.20.0", features = ["csr"] }
  • 页面内容
mod utils;

use wasm_bindgen::prelude::*;
use yew::prelude::*;

#[wasm_bindgen]
extern {
    fn alert(s: &str);

    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[function_component]
fn App() -> Html {

    html! {
        <div>
            <h1>{"Hello Rust"}</h1>
        </div>
    }
}

#[wasm_bindgen]
pub fn start() {
    log("Hello Rust!");
    yew::Renderer::<App>::new().render();
}
  • 测试构建,成功之后,会于根路径产生 ./pkg/*.js./pkg/*.wasm
 $ wasm-pack build
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: found wasm-opt at "/opt/homebrew/bin/wasm-opt"
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: xxx
[INFO]:    Done in 1.46s
[INFO]: 📦   Your wasm pkg is ready to publish at xxx
$ tree ./pkg
./pkg
├── README.md
├── hello_rust_wasm.d.ts
├── hello_rust_wasm.js
├── hello_rust_wasm_bg.js
├── hello_rust_wasm_bg.wasm
├── hello_rust_wasm_bg.wasm.d.ts
└── package.json

0 directories, 7 files

渲染页面

参考 Rust and WebAssembly ,使用以下命令在项目下初始化 web 应用。

#  初始化
$ npm init wasm-app www

$ cd www

# 使用上层我们刚刚构建的 pkg 产物作为我们的业务依赖
# ⚠️ 此处阿里内部同学不要使用 tnpm 等,必须使用 npm,否则指向会有问题
$ npm install hello-rust-wasm@file:../pkg -S

修改 wwww/index.js文件

import * as wasm from "hello-rust-wasm";

wasm.start();

启动测试并预览:

$ npm start
$ open http://localhost:8080

image

WASM 兼容 IE11

参考 wasm2js,对于不支持 WebAssembly 的浏览器,可以通过 wasm2js 工具将 WebAssembly 编译到 JS 进行执行。

# 临时编译文件
$ wasm2js ./pkg/hello_rust_wasm_bg.wasm -o ./pkg/fallback-temp.wasm.js
$ cp ./pkg/hello_rust_wasm_bg.js ./pkg/fallback-temp.js

# 修改入口和引用
$ sed 's/hello_rust_wasm_bg/fallback/g' pkg/fallback-temp.wasm.js > pkg/fallback.wasm.js
$ sed 's/hello_rust_wasm_bg.wasm/fallback.wasm.js/' pkg/fallback-temp.js > pkg/fallback.js

# 于是,可以得到 fallback 在 IE11 的降级逻辑代码
$ tree ./pkg
./pkg
├── README.md
├── fallback-temp.js
├── fallback-temp.wasm.js
├── fallback.js
├── fallback.wasm.js
....
└── package.json

渲染兼容 IE 11

添加依赖

$ cd www
$ npm i @babel/core @babel/preset-env babel-loader@8 -D
$ npm i core-js text-encoding -S

添加 fallback.js

import * as wasm from "hello-rust-wasm/fallback.js";

wasm.start();

添加 polyfill.js,也可以先使用 https://polyfill.io/v3/url-builder/ 来做简单的 polyfill 测试。

import "core-js/modules/es.promise.js";
import "core-js/modules/es.array.fill.js";
import "core-js/modules/es.math.imul.js";
import "core-js/modules/es.math.clz32.js";
import TextEncodingPolyfill from 'text-encoding';

if (typeof window['TextEncoder'] !== 'function') {
  window['TextEncoder'] = TextEncodingPolyfill.TextEncoder;
  window['TextDecoder'] = TextEncodingPolyfill.TextDecoder;
}

修改 HTML 模板,添加 polyfill 依赖,并使其适配 fallback 的逻辑

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
  </head>
  <body>
    <script src="./polyfill.js"></script>
    <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
    <script>
      if (typeof WebAssembly === 'object' && !/wasm=0/.test(location.href)) {
        document.write('<script src="./bootstrap.js"><\/script>');
      } else {
        document.write('<script src="./fallback.js"><\/script>');
      }
    </script>
  </body>
</html>

修改 webpack 打包逻辑

const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');

module.exports = {
  entry: {
    bootstrap: './bootstrap.js',
    fallback: './fallback.js',
    polyfill: './polyfill.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  targets: { chrome: '50', ie: '11' },
                },
              ],
            ],
          },
        },
      },
    ],
  },
  plugins: [new CopyWebpackPlugin(['index.html'])]
};

为了方便上述的命令一次性执行,可以添加一个 build.sh文件

#!/bin/bash

# build WebAssembly
rm -rf ./pkg
wasm-pack build --release

# temp files
wasm2js ./pkg/hello_rust_wasm_bg.wasm -o ./pkg/fallback-temp.wasm.js
cp ./pkg/hello_rust_wasm_bg.js ./pkg/fallback-temp.js

sed 's/hello_rust_wasm_bg/fallback/g' pkg/fallback-temp.wasm.js > pkg/fallback.wasm.js
sed 's/hello_rust_wasm_bg.wasm/fallback.wasm.js/' pkg/fallback-temp.js > pkg/fallback.js

# build web app
cd www
rm -rf ./dist
npm run build

最终效果

在线预览地址:https://unpkg.com/hello-rust-wasm@0.2.0/dist/index.html

Chrome 下使用 wasm 运行(当然,你可以判断一下环境,来优化一下 polyfill 的加载)

image

IE 11 下降级到 JS 运行

image

小结

WebAssembly 可以在业务场景下进行小规模试验,目前初步具备整体的生态工具链,包括 rust 打包、WebAssembly 打包、以及 wasm2js 的降级兼容处理等等。

Rust 作为前端领域最具未来投资价值的语言,将前端的上限做了极大的提升。也希望通过本文,能引起更多前端同学的关注和尝试,引导前端做更多的 Rust 实践。通过实践,一定需要通过实践(落地一个业务场景)来更加强力地驱动 rust 的落地(包括但不限于 操作系统、WebApp、WAVM for non-Browser、WebContainer for Desktop-App、WebVM for Serverless 等等),并实现自身更广阔的职业发展空间。

参考