目录
前言
最近开发上线了xhair.pro, 其核心的数据是通过爬虫下载.dem文件并使用一些开源的解析库如demoinfocs-golang,demoparser解析获取最终的数据

在本地下载解析上传可以随意配置环境,但浏览器无法直接运行Golang或Rust的代码。想要将解析的能力制作成web应用,需要使用WebAssembly技术。
WASM
WebAssembly 是一种新的编码方式,可以在现代的 Web 浏览器中运行——它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C/C++、C# 和 Rust 等语言提供编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。
我们可以把Rust、Go等语言编译成.wasm后缀的二进制文件,浏览器可以通过WebAssembly的API加载该模块,并调用相关能力
实践
不同编程语言需要通过不同的方式编译至wasm,我尝试了Go与Rust。
Golang实践
-
首先需要配置Go的环境 go 安装
-
初始化项目
mkdir wasm-examplecd wasm-examplego mod init wasm-example
- 编写代码
创建main.go文件,与普通go代码区别在于需要引用”syscall/js”包,示例代码如下
package main
import ( "fmt" "syscall/js" dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events")
func main() { c := make(chan struct{}, 0) dem.DefaultParserConfig = dem.ParserConfig{ MsgQueueBufferSize: msgQueueBufferSize, } // 注册后,将下面的parse函数全局注册,浏览器可以调用parseDemo函数方法 js.Global().Set("parseDemo", js.FuncOf(parse)) fmt.Println("WASM Go Initialized") <-c}
func parse(){ // 调用demoinfocs包解析数据 ......}
- 编译至wasm
需要设置环境变量GOOS=js GOARCH=wasm
GOOS=js GOARCH=wasm go build -o main.wasm main.go
- 前端调用
- 浏览器需先加载wasm_exec.js
所有编译至wasm的Go应用通用,代码可见 wasm_exec.js
- 加载wasm模块
// wasm_exec.js内声明的Go类,在此实例化const go = new Go();// 调用WebAssembly Api加载wasm模块WebAssembly.instantiateStreaming( fetch("http://example.com/main.wasm"), go.importObject,) .then((result) => { console.log("WASM loaded"); go.run(result.instance); }) .catch((err) => { console.error("WASM load failed:", err); });
// parseDemo函数已在全局注册,可以直接调用window.parseDemo()
wasm-example/├── go.mod (约为package.json)├── main.go (约为index.js)├── main.wasm (编译产物,浏览器下载导入该模块)├── wasm_exec.js (js和Go代码的桥梁,需加载执行后才可导入wasm模块)├── index.html (前端代码)
Rust实践
类似Go,借助wasm_bindgen和wasm-pack
简要描述下~
- 安装Rust环境
- 使用cargo(类似npm)创建项目并安装依赖
- 编写rust代码
导入wasm_bindgen并在需要的函数上添加#[wasm_bindgen]
use wasm_bindgen::prelude::*;
#[wasm_bindgen]pub fn hello() -> String { "Hello, WebAssembly!".to_string()}
- 编译产物
wasm-pack build --target web --release
- 前端使用
Go使用的wasm_exec.js代码是固定的,需要手动处理一些绑定,而Rust则是每次生成,因此使用更便捷一些
<script type="module"> import init, { hello } from './pkg/wasm_example.js'; async function run() { await init(); hello(); } run();</script>
效果
我开发的工具部署在 https://www.xhair.pro/zh-CN/tools/demo
优化
上线后我注意到在调用parse函数解析.dem文件时Loading动画不正常同时鼠标无法与任何内容交互
可以将解析动作放入web worker中执行
以下示例worker.js,加载了wasm模块,并执行wasm函数并与主线程通信
self.onmessage = async (e) => { if (e.data.type === "loadWasm") { try { await import("/demoparser.js"); await self.wasm_bindgen("/demoparser.wasm"); self.postMessage({ type: "wasmLoaded", success: true }); } catch (error) { self.postMessage({ type: "wasmError", error: error.message }); } } else if (e.data.type === "callFunction") { try { const { functionName, args = [] } = e.data.data; const result = self.wasm_bindgen[functionName](...args); self.postMessage({ type: "functionResult", functionName, result }); } catch (error) { self.postMessage({ type: "functionError", functionName: e.data.functionName, error: error.message, }); } }};
需要一个Context Provider用于初始化worker并传递到子组件
export function WasmWorkerProvider({ children }: { children: ReactNode }) { const workerRef = useRef<Worker>(null);
useEffect(() => { workerRef.current = new Worker("/wasmworker.js"); workerRef.current.postMessage({ type: "loadWasm" });
return () => workerRef.current?.terminate(); }, []);
return ( <WasmWorkerContext.Provider value={{ callFunction: (data) => new Promise((resolve) => { workerRef.current.postMessage({ type: "callFunction", data }); workerRef.current.onmessage = (e) => resolve(e.data.result); }), }} > {children} </WasmWorkerContext.Provider> );}
使用wasm函数,例如
// context获取const { callFunction } = useWasmWorker();const header = await callFunction({ functionName: "parseHeader", args: [data],});
需要注意的是,在worker环境中只有self没有window,所以可能需要修改部分已有代码
// rust wasm_bindgen自动生成的window.wasm_bindgen = Object.assign(__wbg_init, { initSync }, __exports);
// 修改为self.wasm_bindgen = Object.assign(__wbg_init, { initSync }, __exports);
总结
wasm可以在浏览器运行Go、Rust等代码,虽然有些性能损耗,但对于我的应用场景,从一个几百M的文件中解析数据,依然还是会比上传文件至服务端计算快得多。
在Next.js中使用worker和wasm以及Golang和Rust编译至wasm运行都没找到什么优质的文档,也许是对于大多数场景直接请求API让服务端执行代码更常见吧。