项目初始化
wasm-pack new react-single-rust-wasm # 搭建rust项目
cd react-single-rust-wasm && npm init -y # 创建前端环境
在package.json中加入,然后pnpm i
"scripts": {
"build": "webpack --progress --config webpack/webpack.config.js",
"dev": "webpack serve --config webpack/webpack.config.js"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "1.5.0",
"html-webpack-plugin": "^5.3.2",
"text-encoding": "^0.7.0",
"webpack": "^5.49.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2"
},
然后创建web和webpack文件夹,安装React和TS依赖
pnpm add --save-dev typescript ts-loader source-map-loader
pnpm add --save react react-dom @types/react @types/react-dom
配置tsconfig.json文件
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"esModuleInterop": true,
"target": "es5",
"jsx": "react-jsx"
},
"include": ["./web/**/*"],
"exclude": ["./node_modules/**/*"]
}
web下建一个components文件夹,写一个Hello.tsx
interface Props {
msg: string;
}
export default function Hello(props: Props) {
const { msg } = props;
return <h1 style={{ color: 'pink' }}>{msg}</h1>;
}
在web目录下建立index.html和index.tsx,index.tsx内容如下:
import { createRoot } from 'react-dom/client';
import Hello from './components/Hello';
const root = createRoot(document.getElementById('root'));
root.render(<Hello msg={'Hello World'} />);
接下来可以配置webpack文件夹下的webpack.config.js了
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); // 赋予 webpack 处理 wasm 能力的插件
/**
* @type import('webpack').Configuration
*/
module.exports = {
entry: path.resolve(__dirname, '../web/index.tsx'), // 入口文件(相对于config文件的位置)
devServer: {
port: '3042',
progress: true, //打包进度条
open: true, //打包完成自动打开浏览器
compress: false, //启用压缩
},
devtool: 'inline-source-map',
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'index.js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../web/index.html'), //需要放打包文件的html模板路径
filename: 'index.html', //打包完成后的这个模板叫什么名字
}),
// 这里暂时注释
// new WasmPackPlugin({
// crateDirectory: path.resolve(__dirname, '.'),
// }),
// Have this example work in Edge which doesn't ship `TextEncoder` or
// `TextDecoder` at this time. 处理浏览器兼容问题
new webpack.ProvidePlugin({
TextDecoder: ['text-encoding', 'TextDecoder'],
TextEncoder: ['text-encoding', 'TextEncoder'],
}),
],
mode: 'development',
experiments: {
asyncWebAssembly: true, // 打开异步 WASM 功能
},
};
现在运行pnpm run dev命令,发现可以正常启动网页,就说明之前的配置没啥问题。注意,此时我们并没有使用rust,因为webpack.config.js里面暂时注释了WasmPackPlugin,现在对它进行配置
new WasmPackPlugin({
// 这里是crate的路径,这是根目录,所以要上去一层,其实不上一层也找得到(不知道为什么),但是改动rust将不会触发热更新
crateDirectory: path.resolve(__dirname, '..'),
outDir: path.resolve(__dirname, '../web/rwasm'), // 输出文件夹路径,默认是'pkg'
outName: 'index', // 默认文件以index开头
}),
然后运行pnpm run dev,如果没有意外,你的web目录下是这样的
现在我们删掉src目录下的utils.rs,更改一下lib.rs的内容
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, react-singe-rust-wasm!");
}
#[wasm_bindgen]
pub fn test(s: &str) -> String {
return format!("Get {s} !");
}
如果你还开着pnpm run dev,那么会自动重新编译
然后我们在Hello组件中去使用wasm
import { test, greet } from '../rwasm';
interface Props {
msg: string;
}
export default function Hello(props: Props) {
const { msg } = props;
return (
<>
<h1 style={{ color: 'pink' }}>{msg}</h1>
<h1>Rust Get: {test('this is js')}</h1>
<button onClick={() => greet()}>Greet</button>
</>
);
}
效果如下,成功调用RUST函数
看一下pnpm run build的产物,里面也有wasm包,可以serve dist启动服务看一下,也是正常的。
后续开发就和正常的网页开发一样啦,rwasm当成一个Npm包用就可以了
关于异步加载WASM
为什么上方例子和网上的部分教程不一致,不需要调用import init, {greet} from 'rwasm'这种方式导入,而是可以直接同步使用?
看一下rwasm/index.d.ts文件
根本没有默认导出的函数,而且可以启动项目,查看network,发现wasm文件是首屏直接加载的。
WasmPackPlugin其实还是用的wasm-pack build去打包,在默认情况下执行命令为:wasm-pack build --target bundler,具体区别如下:
现在我们指定target参数为web,去修改一下WasmPackPlugin插件参数
new WasmPackPlugin({
......
extraArgs: '--target web',
}),
再次运行pnpm run dev,不出意外的话,会报错🤔,因为Hello组件的代码没有修改。再看一下rwasm/index.d.ts文件,不出意外会多了很多东西,以及有一个默认导出的函数,所以我们修改一下Hello组件的代码。
import { useEffect, useState } from 'react';
import init, { test, greet } from '../rwasm';
interface Props {
msg: string;
}
export default function Hello(props: Props) {
const { msg } = props;
const [txt, setTxt] = useState('');
useEffect(() => {
(async () => {
await init();
setTxt(test(msg));
})();
}, []);
return (
<>
<h1 style={{ color: 'pink' }}>{msg}</h1>
<h1>Rust Get: {txt}</h1>
<button onClick={() => greet()}>Greet</button>
</>
);
}
注意不要先于init()之前调用test!!不能直接在返回的html里面直接调用wasm的导出函数,根据react的运行机制,那样相当于在useEffect之前调用,会直接报错。
这样的好处是,wasm会在init函数调用完成后再加载,在更复杂的场景里,按需加载可以有更好的体验。
关于wasm-pack build --target bundler和web的更多讲解:Building Performant Web Apps with Rust, WebAssembly, and Webpack