我使用 vue3 + WebAssembly 做了个文件校验网站,性能提升600%
前言
一天我吃了网友的布道简单地学习了一下 rust,学了就想做点啥,于是我想到了一直刷到但是没学的 WebAssembly(后面简称 wasm),那干脆一起学了吧,那做个啥呢?而且这个项目还得比纯 js 的项目性能强。突然灵光一闪,不如做个 md5/sha256/sha1 的文件校验吧,js 算得肯定没 wasm 快。
Rust 和 WebAssembly
很快啊,我找到了一本电子书,# Rust 🦀 和 WebAssembly 🕸
随便学学,学会 HelloWorld 就行了,赶紧趁热创建一个项目。
嗯,就这三个文件需要简单解释下。
src/lib.rs
#![no_std] pub mod hashs; mod utils; use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { fn alert(s: &str); } #[wasm_bindgen] pub fn greet(msg: &str) { alert(msg); } #[wasm_bindgen] pub fn set_panic_hook() { utils::set_panic_hook(); }
第一行表示我们不要标准库,这样可以缩小一点 wasm 文件大小。
标注了 #[wasm_bindgen] 属性的函数表示这个函数需要导出,以便 js 调用。
pub mod hashs; 可以理解成将 hashs 模块注册到根模块,并暴露出来给外部调用。
src/utils.rs
pub fn set_panic_hook() { // When the `console_error_panic_hook` feature is enabled, we can call the // `set_panic_hook` function at least once during initialization, and then // we will get better error messages if our code ever panics. // // For more details see // https://github.com/rustwasm/console_error_panic_hook#readme #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); }
这个函数很简单,就是把崩溃信息打印到 console.error
好,现在把目光转到
src/hashs.rs
extern crate alloc; use core::fmt::Write; use md5::Md5; use sha1::Sha1; use sha2::{Digest, Sha256}; use wasm_bindgen::prelude::*; // sha256 #[wasm_bindgen] pub struct Sha256Hasher { hasher: Sha256, } #[wasm_bindgen] impl Sha256Hasher { /// 创建一个对象 pub fn new() -> Sha256Hasher { let hasher = Sha256::new(); Sha256Hasher { hasher } } /// 计算文件块 pub fn update(&mut self, data: &[u8]) { self.hasher.update(data); } /// 获取最终计算结果 pub fn digest(&mut self) -> alloc::string::String { let a = self.hasher.clone(); let result = a.finalize(); let mut text = alloc::string::String::new(); write!(text, "{:x}", result).unwrap(); text } } // md5 #[wasm_bindgen] pub struct Md5Hasher { hasher: Md5, } #[wasm_bindgen] impl Md5Hasher { pub fn new() -> Md5Hasher { let hasher = Md5::new(); Md5Hasher { hasher } } pub fn update(&mut self, data: &[u8]) { self.hasher.update(data); } pub fn digest(&mut self) -> alloc::string::String { let a = self.hasher.clone(); let result = a.finalize(); let mut text = alloc::string::String::new(); write!(text, "{:x}", result).unwrap(); text } } // sha1 #[wasm_bindgen] pub struct Sha1Hasher { hasher: Sha1, } #[wasm_bindgen] impl Sha1Hasher { pub fn new() -> Sha1Hasher { let hasher = Sha1::new(); Sha1Hasher { hasher } } pub fn update(&mut self, data: &[u8]) { self.hasher.update(data); } pub fn digest(&mut self) -> alloc::string::String { let a = self.hasher.clone(); let result = a.finalize(); let mut text = alloc::string::String::new(); write!(text, "{:x}", result).unwrap(); text } } // sha512 #[wasm_bindgen] pub struct Sha512Hasher { hasher: sha2::Sha512, } #[wasm_bindgen] impl Sha512Hasher { pub fn new() -> Sha512Hasher { let hasher = sha2::Sha512::new(); Sha512Hasher { hasher } } pub fn update(&mut self, data: &[u8]) { self.hasher.update(data); } pub fn digest(&mut self) -> alloc::string::String { let a = self.hasher.clone(); let result = a.finalize(); let mut text = alloc::string::String::new(); write!(text, "{:x}", result).unwrap(); text } }
先看 sha256 部分,这里直接调了第三方的包,创建了一个结构体来持有“加密器”,然后写了三个函数,用来创建对象、计算文件块、获取最终计算结果。其他几个其实都是一样的,只是调用了不同的包。
到这里 rust 部分就结束了,是不是还挺简单的。
最后输入命令行:wasm-pack build 打包成 npm 包。
编译结果将存放在 /pkg 目录中
我们简单看一下 hash_wasm.d.ts 文件,以便了解如何调用。
/* tslint:disable */ /* eslint-disable */ /** * @param {string} msg */ export function greet(msg: string): void; /** */ export function set_panic_hook(): void; /** */ export class Md5Hasher { free(): void; /** * @returns {Md5Hasher} */ static new(): Md5Hasher; /** * @param {Uint8Array} data */ update(data: Uint8Array): void; /** * @returns {string} */ digest(): string; } /** */ export class Sha1Hasher { free(): void; /** * @returns {Sha1Hasher} */ static new(): Sha1Hasher; /** * @param {Uint8Array} data */ update(data: Uint8Array): void; /** * @returns {string} */ digest(): string; } /** */ export class Sha256Hasher { free(): void; /** * @returns {Sha256Hasher} */ static new(): Sha256Hasher; /** * @param {Uint8Array} data */ update(data: Uint8Array): void; /** * @returns {string} */ digest(): string; } /** */ export class Sha512Hasher { free(): void; /** * @returns {Sha512Hasher} */ static new(): Sha512Hasher; /** * @param {Uint8Array} data */ update(data: Uint8Array): void; /** * Returns the final hash result as a string of hexadecimal characters. * @returns {string} */ digest(): string; }
Vue 和 WebAssembly
接下来讲一下怎么在 Vue 项目中使用刚才 build 的 hash_wasm
创建 Vue 项目
我们先简单创建一个 vue 的 demo 项目。
yarn create vue@latest cd demo yarn install
接下来清理掉 demo 中的示例代码。并加上文件选择和按钮
App.vue
function sum(type) { }计算sha256
安装刚才 build 的 hash_wasm 库
把 pkg 目录下的文件都拷贝到 demo/wasm/
修改 package.json
{ "dependencies": { "vue": "^3.4.15", "hash-wasm": "file:./wasm" }, }
执行 yarn install
好了,wasm 库这就装好了。
WebWorker
即使 wasm 再快,如果直接在主线程计算 sha256 也会卡渲染,所以我们需要使用 WebWorker 创建一个子线程来调用 wasm。接下来创建一个 src/webworker.js 文件。
import * as wasm from "hash-wasm?a=2" // 设置捕获 wasm 崩溃 wasm.set_panic_hook(); /** * 发送进度 * @param chunkNr 文件分块序号,从1开始 * @param chunks 文件分块总数 */ function sendProgress(chunkNr, chunks) { postMessage({ type: "progress", data: { chunkNr, chunks } }); } /** * 发送结果 * @param result {String} 计算结果 */ function sendResult(result) { postMessage({ type: "result", data: result }); } /** * 计算文件散列值 * @param file {File} 文件 * @param hasher {Object} hasher对象 */ function shaSum(file, hasher) { // 将文件按50M分割 const chunkSize = 50 * 1024 * 1024; // 计算文件分块总数 const chunks = Math.ceil(file.size / chunkSize); // 当前分块序号 let currentChunk = 0; // 对文件进行分块读取 let fileReader = new FileReader(); // 加载下一块 function loadNext() { const start = currentChunk * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(file.slice(start, end)); } // 读取文件完成 fileReader.onload = function (e) { hasher.update(new Uint8Array(e.target.result)); // 计算这一块的散列值 sendProgress(currentChunk + 1, chunks); // 发送进度 currentChunk++; if (currentChunk这个 worker 只做三件事,接收主线程的消息,计算散列值,发送进度和结果。
然后在 App.vue 中引用我们的 WebWorker,记得在后面加上 ?worker,这样 vite 才能正确地处理它,否则会报错。
import ShaWorker from "@/webworker.js?worker"这个时候运行将会得到一个错误:
"ESM integration proposal for Wasm" is not supported currently. Use vite-plugin-wasm or other community plugins to handle this. Alternatively, you can use `.wasm?init` or `.wasm?url`. See https://vitejs.dev/guide/features.html#webassembly for more details.这提示我们需要安装 vite-plugin-wasm 插件来支持 wasm。
安装后配置如下:
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import wasm from "vite-plugin-wasm"; export default defineConfig({ plugins: [ vue(), wasm(), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, worker: { plugins() { return [ wasm(), ] } } })要加上 worker 这段插件配置,否则在 worker 里引用 wasm 还是会报错。
再次运行就正常了,接下来处理一下点击事件。请看 App.vue
import ShaWorker from "@/webworker.js?worker" import {reactive} from "vue"; let infos = reactive([]) function sum(hashType) { let startTime = Date.now() let file = document.getElementById('file').files[0] // 创建处理item info let info = reactive({ chunkNum: 0, currentChunk: 0, result: '', useTime: 0, type: hashType, }) infos.push(info) // 创建 worker const worker = new ShaWorker() worker.onmessage = function (e) { const {data, type} = e.data; if (type === 'progress') { // 计算进度 info.currentChunk = data.chunkNr; info.chunkNum = data.chunks; } else if (type === 'result') { // 计算完成 info.result = data; let endTime = Date.now(); info.useTime = endTime - startTime; worker.terminate(); // 关闭 worker 释放资源 } else if (type === 'ready') { // worker 加载完成,把文件传给 worker 进行计算 worker.postMessage({file, type: hashType}); } } }.card { background: #eeeeee; margin: 10px 0; border: 2px solid #ffffff; padding: 0 10px; line-height: 2; }计算sha1 计算sha256 计算sha512 计算md5计算完成,耗时{{ info.useTime }}ms
{{ info.type }}:{{ info.result }}正在计算{{ info.type }}...({{ info.currentChunk }}/{{ info.chunkNum }})
正在准备中...
好的,功能代码写完了。让我们来试试速度,先下载一个 win11 的系统镜像,文件大小为 6.27G。
很快啊,四舍五入 18 秒完成 sha256 的计算。
让我们搜一个 sha256 的在线校验工具,这个网站是排名比较靠前的。
耗时 119 秒,可以看到 wasm 计算 sha256 的速度是他的 6.6 倍。(我的CPU是 i7-13700KF)
再看看 sha1 。
表现优秀啊,兄弟姐妹们。而且这玩意儿还支持多线程多个文件多个类型一起算。性能也不会受到影响,感兴趣的友友可以试试。
另外说一下,测试性能的时候不要开着控制台,会严重影响性能,不管是 js 的还是 wasm 的速度,通通变慢。
其实我写完 rust 部分的时候,发现 npm 已经有相关的包了,而且名字还和我取的名字一样,叫 hash-wasm。人家还做得更好。但这不失为一个很好的实践机会。
既然都做到这里了,我稍微完善了一下界面,把网站部署上去了。你可以打开体验一下试试,地址是:https://hash.jethro.fun/
最后,对全部源码感兴趣的友友可以移步 https://github.com/jethroHuang/fast_hash