rust-wasm 與 react 集成

概要

爲什麼需要 Webassembly?

曾經複雜計算的需求,多數聚集在後端,隨着的技術和業務的日益發展,在前端也出現了越來越多的頻繁複雜計算和高性能的需求。此時通過 js 來和後端交互,反而優秀的 js 變成了應用的瓶頸

在這樣的情況下,WebAssembly(WASM) 提供了高性能和安全的環境,以接近本機的速度來運行計算。

WASM 是一種編譯目標,而不是一種語言,你可以通過各種語言 (Rust、Golang、C、C++...) 編譯得到。Chrome、FireFox、Safari、Edge 中得到了很好的支持。

準備

相關知識:

react node npm wasm-pack

rust rust-wasm cargo cargo-generate wasm-bindgen

linux linux常用命令

如果你對RustReact沒有相關的知識儲備,請先參照官方文檔。

環境準備:

注: 教程的環境爲 mac 系統。

我們需要npmcargo。用來構建 react 應用和 rust-wasm。

如果你對兩者沒有了解。請先參照官方教程。

步驟概覽

創建 React 應用

如果你已經使用 React 腳手架,那麼你應該會有npx命令,這裏創建的 React 應用的方式和React官方文檔一致。就不贅述。

當創建好以後我們得到目錄結構如下

➜  react-client git:(master) ls
README.md         node_modules      package-lock.json package.json      public            src

參考資料: https://stackoverflow.com/questions/49737652/what-does-eject-do-in-create-react-app

命令完成後,結構如下:

➜  react-client git:(master) ✗ ls
README.md         config            node_modules      package-lock.json package.json      public            scripts           src

測試一下 React 應用是否正常

瀏覽器中訪問http://localhost:3000/,看到以下的結果,證明 React 應用正常。

創建 Rust 應用

回到react-wasm目錄下。

➜  react-wasm ls
react-client
➜  react-wasm ls
react-client wasm
➜  wasm git:(master) ✗ ls
Cargo.lock  Cargo.toml  src  target
[package]
name = "wasm"
version = "0.1.0"
authors = ["jim <303600370@qq.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
[package]
name = "wasm"
version = "0.1.0"
authors = ["jim <303600370@qq.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] 
crate-type = ["cdylib"] 
path ="src/lib.rs"

[dependencies]
wasm-bindgen ="0.2.34"
➜  wasm git:(master) ✗ ls
Cargo.lock Cargo.toml src        target
➜  wasm git:(master) ✗ cd src 
➜  src git:(master) ✗ ls
main.rs
➜  src git:(master) ✗ mv main.rs lib.rs
➜  src git:(master) ✗ ls
lib.rs
➜  src git:(master) ✗
➜  src git:(master) ✗ cargo build
   Compiling proc-macro2 v1.0.24
   Compiling log v0.4.14
   Compiling wasm-bindgen-shared v0.2.71
   Compiling syn v1.0.60
   Compiling bumpalo v3.6.1
   Compiling quote v1.0.9
   Compiling wasm-bindgen-backend v0.2.71
   Compiling wasm-bindgen-macro-support v0.2.71
   Compiling wasm-bindgen-macro v0.2.71
   Compiling wasm-bindgen v0.2.71
   Compiling wasm v0.1.0 (/Users/jim/dev/tmp/react-wasm/wasm)
    Finished dev [unoptimized + debuginfo] target(s) in 11.70s
use::wasm_bindgen::prelude::*;

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

#[wasm_bindgen]
pub fn hello() {
    unsafe {
        alert("Hello World");
    }
}

#[wasm_bindgen]會告訴wasm-bindgen幫我們生成一個可以調用的方法

此時,你已經準備好了編譯 wasm 文件並在 react 應用中使用

關於 wasm-bindgen

可以參照官方文檔 : https://rustwasm.github.io/docs/wasm-bindgen/

編譯 WASM

我們已經完成了定義 Rust 函數並告訴 wasm-bindgen 編譯內容的工作。wasm-pack 幫助我們創建 Javascript 和 Typescript 接口。

由於本機環境已經安裝過,所以得到以下結果

➜  src git:(master) ✗ cargo install wasm-pack
    Updating `git://mirrors.ustc.edu.cn/crates.io-index` index
  Downloaded wasm-pack v0.9.1 (registry `git://mirrors.ustc.edu.cn/crates.io-index`)
  Downloaded 1 crate (422.5 KB) in 3.23s
     Ignored package `wasm-pack v0.9.1` is already installed, use --force to override
➜  src git:(master) ✗

同樣可以參考官方文檔

wasm-pack : https://rustwasm.github.io/wasm-pack/installer/

➜  src git:(master) ✗ wasm-pack
wasm-pack 0.9.1
Ashley Williams <ashley666ashley@gmail.com>
📦 ✨  pack and publish your wasm!

USAGE:
    wasm-pack [FLAGS] [OPTIONS] <SUBCOMMAND>

FLAGS:
    -h, --help       Prints help information
    -q, --quiet      No output printed to stdout
    -V, --version    Prints version information
    -v, --verbose    Log verbosity is based off the number of v used

OPTIONS:
        --log-level <log_level>    The maximum level of messages that should be logged by wasm-pack. [possible values:
                                   info, warn, error] [default: info]

SUBCOMMANDS:
    build      🏗️  build your npm package!
    help       Prints this message or the help of the given subcommand(s)
    login      👤  Add an npm registry user account! (aliases: adduser, add-user)
    new        🐑 create a new project with a template
    pack       🍱  create a tar of your npm package but don't publish!
    publish    🎆  pack up your npm package and publish!
    test       👩‍🔬  test your wasm!
➜  src git:(master) ✗ wasm-pack build
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
   Compiling proc-macro2 v1.0.24
   Compiling unicode-xid v0.2.1
   Compiling log v0.4.14
   Compiling syn v1.0.60
   Compiling wasm-bindgen-shared v0.2.71
   Compiling cfg-if v1.0.0
   Compiling bumpalo v3.6.1
   Compiling lazy_static v1.4.0
   Compiling wasm-bindgen v0.2.71
   Compiling quote v1.0.9
   Compiling wasm-bindgen-backend v0.2.71
   Compiling wasm-bindgen-macro-support v0.2.71
   Compiling wasm-bindgen-macro v0.2.71
   Compiling wasm v0.1.0 (/Users/jim/dev/tmp/react-wasm/wasm)
warning: unnecessary `unsafe` block
  --> src/lib.rs:13:5
   |
13 |     unsafe {
   |     ^^^^^^ unnecessary `unsafe` block
   |
   = note: `#[warn(unused_unsafe)]` on by default

warning: 1 warning emitted

    Finished release [optimized] target(s) in 10.75s
⚠️   [WARN]: origin crate has no README
[INFO]: ⬇️  Installing wasm-bindgen...

[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description''repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨   Done in 6m 50s
[INFO]: 📦   Your wasm pkg is ready to publish at /Users/jim/dev/tmp/react-wasm/wasm/pkg.
➜  wasm git:(master) ✗ ls
Cargo.lock Cargo.toml pkg        src        target
➜  wasm git:(master) ✗ cd pkg 
➜  pkg git:(master) ✗ ls
package.json      wasm.d.ts         wasm.js           wasm_bg.js        wasm_bg.wasm      wasm_bg.wasm.d.ts
➜  pkg git:(master) ✗

pkg目錄中*.wasm就是我們的 WASM 文件。其中還包括了 typescript 的定義*.d.ts和 js 文件*.js。另外,它還會生成一個package.json,接下來,我們將會使用這個文件配合npm成爲我們 react 的應用依賴。

在 React 應用中使用 WASM

回到我們的 React 應用

{
  "name""react-client",
  "version""0.1.0",
  "private": true,
  "dependencies"{
    "@babel/core""7.12.3",
    "@pmmmwh/react-refresh-webpack-plugin""0.4.3",
    "@svgr/webpack""5.5.0",
    "@testing-library/jest-dom""^5.11.9",
    "@testing-library/react""^11.2.5",
    "@testing-library/user-event""^12.8.1",
    "@typescript-eslint/eslint-plugin""^4.5.0",
    "@typescript-eslint/parser""^4.5.0",
    "babel-eslint""^10.1.0",
    "babel-jest""^26.6.0",
    "babel-loader""8.1.0",
    "babel-plugin-named-asset-import""^0.3.7",
    "babel-preset-react-app""^10.0.0",
    "bfj""^7.0.2",
    "camelcase""^6.1.0",
    "case-sensitive-paths-webpack-plugin""2.3.0",
    "css-loader""4.3.0",
    "dotenv""8.2.0",
    "dotenv-expand""5.1.0",
    "eslint""^7.11.0",
    "eslint-config-react-app""^6.0.0",
    "eslint-plugin-flowtype""^5.2.0",
    "eslint-plugin-import""^2.22.1",
    "eslint-plugin-jest""^24.1.0",
    "eslint-plugin-jsx-a11y""^6.3.1",
    "eslint-plugin-react""^7.21.5",
    "eslint-plugin-react-hooks""^4.2.0",
    "eslint-plugin-testing-library""^3.9.2",
    "eslint-webpack-plugin""^2.5.2",
    "file-loader""6.1.1",
    "fs-extra""^9.0.1",
    "html-webpack-plugin""4.5.0",
    "identity-obj-proxy""3.0.0",
    "jest""26.6.0",
    "jest-circus""26.6.0",
    "jest-resolve""26.6.0",
    "jest-watch-typeahead""0.6.1",
    "mini-css-extract-plugin""0.11.3",
    "optimize-css-assets-webpack-plugin""5.0.4",
    "pnp-webpack-plugin""1.6.4",
    "postcss-flexbugs-fixes""4.2.1",
    "postcss-loader""3.0.0",
    "postcss-normalize""8.0.1",
    "postcss-preset-env""6.7.0",
    "postcss-safe-parser""5.0.2",
    "prompts""2.4.0",
    "react""^17.0.1",
    "react-app-polyfill""^2.0.0",
    "react-dev-utils""^11.0.3",
    "react-dom""^17.0.1",
    "react-refresh""^0.8.3",
    "resolve""1.18.1",
    "resolve-url-loader""^3.1.2",
    "sass-loader""^10.0.5",
    "semver""7.3.2",
    "style-loader""1.3.0",
    "terser-webpack-plugin""4.2.3",
    "ts-pnp""1.2.0",
    "url-loader""4.1.1",
    "web-vitals""^1.1.0",
    "webpack""4.44.2",
    "webpack-dev-server""3.11.1",
    "webpack-manifest-plugin""2.2.0",
    "workbox-webpack-plugin""5.1.4"
  },
  "scripts"{
    "start""node scripts/start.js",
    "build""node scripts/build.js",
    "test""node scripts/test.js"
  },
  "eslintConfig"{
    "extends"[
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist"{
    "production"[
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development"[
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "jest"{
    "roots"[
      "<rootDir>/src"
    ],
    "collectCoverageFrom"[
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts"
    ],
    "setupFiles"[
      "react-app-polyfill/jsdom"
    ],
    "setupFilesAfterEnv"[
      "<rootDir>/src/setupTests.js"
    ],
    "testMatch"[
      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
      "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
    ],
    "testEnvironment""jsdom",
    "testRunner""/Users/jim/dev/tmp/react-wasm/react-client/node_modules/jest-circus/runner.js",
    "transform"{
      "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$""<rootDir>/config/jest/babelTransform.js",
      "^.+\\.css$""<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)""<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns"[
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
      "^.+\\.module\\.(css|sass|scss)$"
    ],
    "modulePaths"[],
    "moduleNameMapper"{
      "^react-native$""react-native-web",
      "^.+\\.module\\.(css|sass|scss)$""identity-obj-proxy"
    },
    "moduleFileExtensions"[
      "web.js",
      "js",
      "web.ts",
      "ts",
      "web.tsx",
      "tsx",
      "json",
      "web.jsx",
      "jsx",
      "node"
    ],
    "watchPlugins"[
      "jest-watch-typeahead/filename",
      "jest-watch-typeahead/testname"
    ],
    "resetMocks"true
  },
  "babel"{
    "presets"[
      "react-app"
    ]
  }
}
    "wasm":"file:../wasm/pkg"

結果如下

{
  "name""react-client",
  "version""0.1.0",
  "private": true,
  "dependencies"{
    "@babel/core""7.12.3",
    "@pmmmwh/react-refresh-webpack-plugin""0.4.3",
    "@svgr/webpack""5.5.0",
    "@testing-library/jest-dom""^5.11.9",
    "@testing-library/react""^11.2.5",
    "@testing-library/user-event""^12.8.1",
    "@typescript-eslint/eslint-plugin""^4.5.0",
    "@typescript-eslint/parser""^4.5.0",
    "babel-eslint""^10.1.0",
    "babel-jest""^26.6.0",
    "babel-loader""8.1.0",
    "babel-plugin-named-asset-import""^0.3.7",
    "babel-preset-react-app""^10.0.0",
    "bfj""^7.0.2",
    "camelcase""^6.1.0",
    "case-sensitive-paths-webpack-plugin""2.3.0",
    "css-loader""4.3.0",
    "dotenv""8.2.0",
    "dotenv-expand""5.1.0",
    "eslint""^7.11.0",
    "eslint-config-react-app""^6.0.0",
    "eslint-plugin-flowtype""^5.2.0",
    "eslint-plugin-import""^2.22.1",
    "eslint-plugin-jest""^24.1.0",
    "eslint-plugin-jsx-a11y""^6.3.1",
    "eslint-plugin-react""^7.21.5",
    "eslint-plugin-react-hooks""^4.2.0",
    "eslint-plugin-testing-library""^3.9.2",
    "eslint-webpack-plugin""^2.5.2",
    "file-loader""6.1.1",
    "fs-extra""^9.0.1",
    "html-webpack-plugin""4.5.0",
    "identity-obj-proxy""3.0.0",
    "jest""26.6.0",
    "jest-circus""26.6.0",
    "jest-resolve""26.6.0",
    "jest-watch-typeahead""0.6.1",
    "mini-css-extract-plugin""0.11.3",
    "optimize-css-assets-webpack-plugin""5.0.4",
    "pnp-webpack-plugin""1.6.4",
    "postcss-flexbugs-fixes""4.2.1",
    "postcss-loader""3.0.0",
    "postcss-normalize""8.0.1",
    "postcss-preset-env""6.7.0",
    "postcss-safe-parser""5.0.2",
    "prompts""2.4.0",
    "react""^17.0.1",
    "react-app-polyfill""^2.0.0",
    "react-dev-utils""^11.0.3",
    "react-dom""^17.0.1",
    "react-refresh""^0.8.3",
    "resolve""1.18.1",
    "resolve-url-loader""^3.1.2",
    "sass-loader""^10.0.5",
    "semver""7.3.2",
    "style-loader""1.3.0",
    "terser-webpack-plugin""4.2.3",
    "ts-pnp""1.2.0",
    "url-loader""4.1.1",
    "web-vitals""^1.1.0",
    "webpack""4.44.2",
    "webpack-dev-server""3.11.1",
    "webpack-manifest-plugin""2.2.0",
    "workbox-webpack-plugin""5.1.4",
    "wasm":"file:../wasm/pkg"
  },
  "scripts"{
    "start""node scripts/start.js",
    "build""node scripts/build.js",
    "test""node scripts/test.js"
  },
  "eslintConfig"{
    "extends"[
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist"{
    "production"[
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development"[
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "jest"{
    "roots"[
      "<rootDir>/src"
    ],
    "collectCoverageFrom"[
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts"
    ],
    "setupFiles"[
      "react-app-polyfill/jsdom"
    ],
    "setupFilesAfterEnv"[
      "<rootDir>/src/setupTests.js"
    ],
    "testMatch"[
      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
      "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
    ],
    "testEnvironment""jsdom",
    "testRunner""/Users/jim/dev/tmp/react-wasm/react-client/node_modules/jest-circus/runner.js",
    "transform"{
      "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$""<rootDir>/config/jest/babelTransform.js",
      "^.+\\.css$""<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)""<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns"[
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
      "^.+\\.module\\.(css|sass|scss)$"
    ],
    "modulePaths"[],
    "moduleNameMapper"{
      "^react-native$""react-native-web",
      "^.+\\.module\\.(css|sass|scss)$""identity-obj-proxy"
    },
    "moduleFileExtensions"[
      "web.js",
      "js",
      "web.ts",
      "ts",
      "web.tsx",
      "tsx",
      "json",
      "web.jsx",
      "jsx",
      "node"
    ],
    "watchPlugins"[
      "jest-watch-typeahead/filename",
      "jest-watch-typeahead/testname"
    ],
    "resetMocks"true
  },
  "babel"{
    "presets"[
      "react-app"
    ]
  }
}
{
  "name""wasm",
  "collaborators"[
    "jim <303600370@qq.com>"
  ],
  "version""0.1.0",
  "files"[
    "wasm_bg.wasm",
    "wasm.js",
    "wasm.d.ts"
  ],
  "module""wasm.js",
  "types""wasm.d.ts",
  "sideEffects"false
}
➜  react-client git:(master) ✗ npm install --save-dev wasm-loader
npm WARN tsutils@3.20.0 requires a peer of typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta but none is installed. You must install peer dependencies yourself.

+ wasm-loader@1.3.0
added 8 packages from 11 contributors and audited 1962 packages in 12.833s

132 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
   //如果存在這行,則不需要
   { parser: { requireEnsure: false } },
   { 
      test: /\.wasm$/, // only load WASM files (ending in .wasm)
      // only files in our src/ folder
      include: path.resolve(__dirname, "src"), 
      use: [{ 
         // load and use the wasm-loader dictionary
         loader: require.resolve("wasm-loader"), 
         options: {} 
      }],
   },
            {
              loader: require.resolve('file-loader'),
              // Exclude `js` files to keep "css" loader working as it injects
              // its runtime that would otherwise be processed through "file" loader.
              // Also exclude `html` and `json` extensions so they get processed
              // by webpacks internal loaders.
              exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
              options: {
                name: 'static/media/[name].[hash:8].[ext]',
              },
            },
            {
              loader: require.resolve('file-loader'),
              // Exclude `js` files to keep "css" loader working as it injects
              // its runtime that would otherwise be processed through "file" loader.
              // Also exclude `html` and `json` extensions so they get processed
              // by webpacks internal loaders.
              exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/,/\.wasm$/],
              options: {
                name: 'static/media/[name].[hash:8].[ext]',
              },
            },

恭喜你到了這一步,接下來我們要修改App.js裏的內容。

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div class>
      <header class>
        <img src={logo} class />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          class
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
import logo from './logo.svg';
import './App.css';

function App() {
  import('wasm').then(module ={
    console.log(module)
 })
  return (
    <div class>
      <header class>
        <img src={logo} class />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          class
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
➜  src git:(master) ✗ npm i    
npm WARN tsutils@3.20.0 requires a peer of typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta but none is installed. You must install peer dependencies yourself.

audited 1963 packages in 7.57s

132 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

可以看到 wasm 已經已經應用成功,還可以看到我們在 rust 應用中定義的 hello 方法

import logo from './logo.svg';
import './App.css';

function App() {
  import('wasm').then(({hello}) ={
    hello();
 })
  return (
    <div class>
      <header class>
        <img src={logo} class />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          class
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

_* 恭喜你~~~~!!!,我們成功的調用了在 rust 應用中定義的 hello 方法,並把它集成在 react 應用中了_ *

附加:使用 cargo-generate 創建 Rust-Wasm 應用

如果你閱讀到此處還是不能夠理解整個流程中我們做了什麼的話,建議你重新閱讀或者思考一下整個流程。

接下來的內容會需要你對以上內容有一定理解之後閱讀起來會比較輕鬆。接下來的內容,是在上文中的擴展。

  1. 安裝 cargo-generate
cargo install cargo-generate

如果沒有 openssl 則使用

cargo install cargo-generate --features vendored-openssl

用例:cargo generate --git https://github.com/githubusername/mytemplate.git

  1. 安裝wasm-pack
cargo install wasm-pack
  1. 安裝 wasm-bindgen
cargo install wasm-bindgen-cli --force
  1. 生成 rust 應用
cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
cd my-project

得到結果如下

➜  react-wasm cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
cd my-project
🔧   Creating project called `my-project`...
✨   Done! New project created /Users/jim/dev/tmp/react-wasm/my-project
➜  my-project git:(master) ✗

目錄結構

➜  my-project git:(master) ✗ ls 
Cargo.toml     LICENSE_APACHE LICENSE_MIT    README.md      src            tests
➜  my-project git:(master) ✗
  1. build
wasm-pack build
  1. 本地可以使用 packgejson 配置
"wasm-demo""file:../pkg",

這個工具能夠迅速的幫你生成一個 rust-wasm 應用。

實際上只是一個模板,這個也算一個生產力工具吧,仁者見仁。

更多操作參見 github

cargo-generate:  https://github.com/cargo-generate/cargo-generate.git

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/yjIcSi4s978uhufyk9aKPw