曾经复杂计算的需求,多数聚集在后端,随着的技术和业务的日益发展,在前端也出现了越来越多的频繁复杂计算和高性能的需求。此时通过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常用命令
如果你对Rust
和React
没有相关的知识储备,请先参照官方文档。
Rust: https://www.rust-lang.org/
React: https://reactjs.org/
环境准备:
注: 教程的环境为mac系统。
我们需要npm
和cargo
。用来构建react应用和rust-wasm。
如果你对两者没有了解。请先参照官方教程。
cargo new
创建Rust应用cargo-generate
创建Rust-Wasm应用如果你已经使用React脚手架,那么你应该会有npx
命令,这里创建的React应用的方式和React
官方文档一致。就不赘述。
mkdir react-wasm
cd react-wasm
npx create-react-app react-client
当创建好以后我们得到目录结构如下
➜ 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
npm run eject
命令完成后,结构如下:
➜ react-client git:(master) ✗ ls
README.md config node_modules package-lock.json package.json public scripts src
测试一下React应用是否正常
react-client
目录下运行npm start
浏览器中访问http://localhost:3000/
,看到以下的结果,证明React应用正常。
回到react-wasm
目录下。
➜ react-wasm ls
react-client
cargo
来创建一个新的文件夹cargo new wasm
,结果如下➜ react-wasm ls
react-client wasm
wasm
目录下➜ wasm git:(master) ✗ ls
Cargo.lock Cargo.toml src target
Cargo.toml
[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"
src
目录下 将main.rs
重命名或者删除之后创建 lib.rs
➜ 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) ✗
cargo build
看下是否能正常编译➜ 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
lib.rs
内容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/
我们已经完成了定义Rust函数并告诉wasm-bindgen编译内容的工作。wasm-pack帮助我们创建Javascript和Typescript接口。
cargo
安装wasm-pack
cargo install wasm-pack
命令由于本机环境已经安装过,所以得到以下结果
➜ 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/
wasm-pack
已经安装完成wasm-pack
命令,得到以下结果➜ 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!
wasm-pack build
命令编译wasm文件--out-dir
来指定文件输出位置wasm-pack build
命令➜ 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
目录下生成了一个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应用
/react-client/package.json
{
"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"
]
}
}
dependencies
中增加依赖项 "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"
]
}
}
wasm/pkg
下编辑package.json
{
"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
}
安装wasm-loader
在react-client
目录下运行npm install --save-dev wasm-loader
,结果如下
➜ 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
react-client/config
目录下的webpack.config.js
文件,在rules
中添加以下内容: //如果存在这行,则不需要
{ 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: {}
}],
},
file-loader
,如下: {
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]',
},
},
file-loader
中添加/\.wasm$/
{
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
里的内容。
/react-client/src
中的App.js
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
App.js
,添加import('wasm').then(module => { console.log(module) })
import logo from './logo.svg';
import './App.css';
function App() {
import('wasm').then(module => {
console.log(module)
})
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
npm i
保证我们的依赖和配置生效➜ 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方法
App.js
,调用我们的wasm中的hello方法import logo from './logo.svg';
import './App.css';
function App() {
import('wasm').then(({hello}) => {
hello();
})
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
*恭喜你~~~~!!!,我们成功的调用了在rust应用中定义的hello方法,并把它集成在react应用中了*
cargo-generate
创建Rust-Wasm应用如果你阅读到此处还是不能够理解整个流程中我们做了什么的话,建议你重新阅读或者思考一下整个流程。
接下来的内容会需要你对以上内容有一定理解之后阅读起来会比较轻松。接下来的内容,是在上文中的扩展。
cargo-generate
cargo install cargo-generate
如果没有openssl则使用
cargo install cargo-generate --features vendored-openssl
用例:cargo generate --git https://github.com/githubusername/mytemplate.git
wasm-pack
cargo install wasm-pack
wasm-bindgen
cargo install wasm-bindgen-cli --force
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) ✗
wasm-pack build
"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