目录

基于 Rust 的 WebAssembly 工程开发过程小记

初始化工程

1
2
3
4
$ npm init rust-webpack web_assembly_demo
npx: 18 安装成功,用时 3.989 秒
 Rust +  WebAssembly + Webpack = ️
Installed dependencies

安装 Web 依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ yarn
yarn install v1.19.1
warning package.json: No license field
info No lockfile found.
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
warning rust-webpack-template@0.1.0: No license field
[1/4]   Resolving packages...
warning @wasm-tool/wasm-pack-plugin > watchpack > chokidar > fsevents@1.2.9: One of your dependencies needs to upgrade to fsevents v2: 1) Proper nodejs v10+ support 2) No more fetching binaries from AWS, smaller package size
[2/4]   Fetching packages...
[3/4]   Linking dependencies...
[4/4]   Building fresh packages...
success Saved lockfile.
  Done in 17.87s.

修改 Cargo.toml 为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# You must change these to your own details.
[package]
name = "web_assembly_demo"
description = "My super awesome Rust, WebAssembly, and Webpack project!"
version = "0.1.0"
authors = ["guzhongren <guzhoongren@live.cn>"]
categories = ["wasm"]
readme = "README.md"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[profile.release]
# This makes the compiled code faster and smaller, but it makes compiling slower,
# so it's only enabled in release mode.
lto = true

[features]
# If you uncomment this line, it will enable `wee_alloc`:
#default = ["wee_alloc"]

[dependencies]
# The `wasm-bindgen` crate provides the bare minimum functionality needed
# to interact with JavaScript.
wasm-bindgen = "0.2.45"

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. However, it is slower than the default
# allocator, so it's not enabled by default.
wee_alloc = { version = "0.4.2", optional = true }

# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.
[dependencies.web-sys]
version = "0.3.22"
features = ["console"]

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so it's only enabled
# in debug mode.
[target."cfg(debug_assertions)".dependencies]
console_error_panic_hook = "0.1.5"

# These crates are used for running unit tests.
[dev-dependencies]
wasm-bindgen-test = "0.2.45"
futures = "0.1.27"
js-sys = "0.3.22"
wasm-bindgen-futures = "0.3.22"

Rust 的依赖会在启动 Web 程序的时候自动安装。

启动程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$ yarn start
yarn run v1.19.1
warning package.json: No license field
$ rimraf dist pkg && webpack-dev-server --open -d
🧐  Checking for wasm-pack...

  wasm-pack is installed. 

ℹ️  Compiling your crate in development mode...

ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/c4/Desktop/Personal/01.Project/web_assembly/web_assembly_demo/dist
[INFO]:   Checking for the Wasm target...
[INFO]:   Compiling to Wasm...
ℹ 「wdm」: wait until bundle finished: /
   Compiling proc-macro2 v1.0.6
   Compiling unicode-xid v0.2.0
...
 3 assets
Entrypoint index = index.js
[./pkg/index.js] 4.41 KiB {0} [built]
[./pkg/index_bg.wasm] 145 KiB {0} [built]
    + 33 hidden modules
ℹ 「wdm」: Compiled successfully.
ℹ️  Compiling your crate in development mode...

[INFO]:   Checking for the Wasm target...
[INFO]:   Compiling to Wasm...
   Compiling rust-webpack-template v0.1.0 (/Users/c4/Desktop/Personal/01.Project/web_assembly/web_assembly_demo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.62s
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: Optional fields missing from Cargo.toml: 'repository', 'license'. These are not necessary, but recommended
[INFO]:    Done in 0.77s
[INFO]:    Your wasm pkg is ready to publish at ./pkg.
  Your crate has been correctly compiled

ℹ 「wdm」: Compiling...
ℹ 「wdm」: Hash: d4e8a3c57ad23f847707
Version: webpack 4.41.2
Time: 411ms
Built at: 2019-11-23 20:16:55
                           Asset     Size  Chunks                         Chunk Names
                            0.js   17 KiB       0  [emitted]              
beee557fb69dcfa0df60.module.wasm  161 KiB       0  [emitted] [immutable]  
                        index.js  897 KiB   index  [emitted]              index
Entrypoint index = index.js
[./pkg/index.js] 4.93 KiB {0} [built]
[./pkg/index_bg.wasm] 161 KiB {0} [built]
    + 33 hidden modules
ℹ 「wdm」: Compiled successfully.
ℹ 「wdm」: Compiling...
ℹ 「wdm」: Hash: 3e1681b9b4c4c940722e
Version: webpack 4.41.2
Time: 16ms
Built at: 2019-11-23 20:17:14
   Asset     Size  Chunks             Chunk Names
index.js  897 KiB   index  [emitted]  index
 + 2 hidden assets

在 src 中新建 parse.rs 并编写处理 markdown 的 rust 代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
struct Parser {
  pos: usize,
  input: String,
}

pub fn parse(source: String) -> String {
  Parser {
    pos: 0,
    input: source,
  }.parse_lines()
}

impl Parser {
  fn parse_lines(&mut self) -> String {
    let mut result = String::new();
    loop {
      self.consume_whitespace();
      if self.end_of_line() {
        break;
      }
      result.push_str(&self.parse_line());
    }
    result
  }

  fn parse_line(&mut self) -> String {
    match self.next_char() {
      '#' => self.parse_title(),
      '-' => {
        if char::is_whitespace(self.input[self.pos + 1..].chars().next().unwrap()) {
          self.parse_list()
        } else {
          self.parse_text()
        }
      }
      _ => self.parse_text(),
    }
  }

  fn parse_list(&mut self) -> String {
    self.consume_char();
    self.consume_whitespace();
    let text = self.parse_text();
    create_html_element("li".to_string(), text)
  }

  fn parse_title(&mut self) -> String {
    let pound = self.consume_while(|c| c == '#');
    self.consume_whitespace();
    let text = self.parse_text();

    create_html_element(format!("h{}", pound.len()), text)
  }

  fn parse_text(&mut self) -> String {
    self.consume_while(|c| !is_new_line(c))
  }

  fn end_of_line(&self) -> bool {
    self.pos >= self.input.len()
  }

  // fn starts_with(&self, s: &str) -> bool {
  //   self.input[self.pos..].starts_with(s)
  // }

  fn next_char(&self) -> char {
    self.input[self.pos..].chars().next().unwrap()
  }

  fn consume_char(&mut self) -> char {
    let mut iter = self.input[self.pos..].char_indices();
    let (_, cur_char) = iter.next().unwrap();
    let (next_pos, _) = iter.next().unwrap_or((1, ' '));
    self.pos += next_pos;
    cur_char
  }

  fn consume_while<F>(&mut self, cond: F) -> String
  where
    F: Fn(char) -> bool,
  {
    let mut result = String::new();
    while !self.end_of_line() && cond(self.next_char()) {
      result.push(self.consume_char());
    }
    result
  }

  fn consume_whitespace(&mut self) {
    self.consume_while(char::is_whitespace);
  }
}

fn create_html_element(tag_name: String, text: String) -> String {
  format!("<{}>{}</{}>", tag_name, text, tag_name)
}

fn is_new_line(c: char) -> bool {
  c == '\n'
}

在 src/lib.rs 中引入 parse mod, 并编写向外暴露的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mod parser;

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn parse(input: &str) -> String {
  let result = parser::parse(input.to_string());
  result
}

在 js/index.js 中使用 Web Assembly ,修改结果如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
document.body.onload = addElement;
function addElement() {
  const markdown = document.createElement('textarea')
  markdown.id = 'markdown'
  markdown.style = "height: 300px; width: 400px ;"

  document.body.appendChild(markdown)
  const parseBtn = document.createElement('button')
  parseBtn.id = 'parse'
  parseBtn.innerHTML = '解析 markdown'
  document.body.appendChild(parseBtn)

  const previewArea = document.createElement('div')
  previewArea.id = 'preview'
  document.body.appendChild(previewArea)

  const rust = import('../pkg/index.js')

  rust.then(module => {
    const btn = document.getElementById('parse')
    const previewArea = document.getElementById('preview')

    btn.addEventListener('click', () => {
      const input = document.getElementById('markdown').value
      previewArea.innerHTML = module.parse(input)
    })
  })
}

// import("../pkg/index.js").then(module => {
//   const input = '1233'
//     previewArea.innerHTML = module.parse(input)
// }).catch(console.error);

效果图

https://i.loli.net/2020/06/27/PabvnjXR1MouQcz.png

在线体验

Markdown editor: https://andorlab.github.io/WebAssembly/

后续

后面会在这个基础上继续开发,并将 WASM 中的一些概念配置也写成文档共享在博客中。

仓库地址:https://github.com/AndorLab/web_assembly/tree/based_rust

Refs

Disclaimer

本文仅代表个人观点,与 Thoughtworks 公司无任何关系。


https://cdn.staticaly.com/gh/guzhongren/data-hosting@master/20210819/wechat.ae9zxgscqcg.png