0


我使用 vue3 + WebAssembly 做了个文件校验网站,性能提升600%

前言

有一天我吃了网友的布道简单地学习了一下 rust,学了就想做点啥,于是我想到了一直刷到但是没学的 WebAssembly(后面简称 wasm),那干脆一起学了吧,那做个啥呢?而且这个项目还得比纯 js 的项目性能强。突然灵光一闪,不如做个 md5/sha256/sha1 的文件校验吧,js 算得肯定没 wasm 快。

Rust 和 WebAssembly

很快啊,我找到了一本电子书,# Rust 🦀 和 WebAssembly 🕸

随便学学,学会 HelloWorld 就行了,赶紧趁热创建一个项目。

image.png

嗯,就这三个文件需要简单解释下。

src/lib.rs

#![no_std]pubmodhashs;modutils;usewasm_bindgen::prelude::*;#[wasm_bindgen]extern"C"{fnalert(s:&str);}#[wasm_bindgen]pubfngreet(msg:&str){alert(msg);}#[wasm_bindgen]pubfnset_panic_hook(){utils::set_panic_hook();}

第一行表示我们不要标准库,这样可以缩小一点 wasm 文件大小。

标注了

#[wasm_bindgen]

属性的函数表示这个函数需要导出,以便 js 调用。

pub mod hashs;

可以理解成将 hashs 模块注册到根模块,并暴露出来给外部调用。

src/utils.rs

pubfnset_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

externcratealloc;usecore::fmt::Write;usemd5::Md5;usesha1::Sha1;usesha2::{Digest,Sha256};usewasm_bindgen::prelude::*;// sha256#[wasm_bindgen]pubstructSha256Hasher{
    hasher:Sha256,}#[wasm_bindgen]implSha256Hasher{/// 创建一个对象pubfnnew()->Sha256Hasher{let hasher =Sha256::new();Sha256Hasher{ hasher }}/// 计算文件块pubfnupdate(&mutself, data:&[u8]){self.hasher.update(data);}/// 获取最终计算结果pubfndigest(&mutself)->alloc::string::String{let a =self.hasher.clone();let result = a.finalize();letmut text =alloc::string::String::new();write!(text,"{:x}", result).unwrap();
        text
    }}// md5#[wasm_bindgen]pubstructMd5Hasher{
    hasher:Md5,}#[wasm_bindgen]implMd5Hasher{pubfnnew()->Md5Hasher{let hasher =Md5::new();Md5Hasher{ hasher }}pubfnupdate(&mutself, data:&[u8]){self.hasher.update(data);}pubfndigest(&mutself)->alloc::string::String{let a =self.hasher.clone();let result = a.finalize();letmut text =alloc::string::String::new();write!(text,"{:x}", result).unwrap();
        text
    }}// sha1#[wasm_bindgen]pubstructSha1Hasher{
    hasher:Sha1,}#[wasm_bindgen]implSha1Hasher{pubfnnew()->Sha1Hasher{let hasher =Sha1::new();Sha1Hasher{ hasher }}pubfnupdate(&mutself, data:&[u8]){self.hasher.update(data);}pubfndigest(&mutself)->alloc::string::String{let a =self.hasher.clone();let result = a.finalize();letmut text =alloc::string::String::new();write!(text,"{:x}", result).unwrap();
        text
    }}// sha512#[wasm_bindgen]pubstructSha512Hasher{
    hasher:sha2::Sha512,}#[wasm_bindgen]implSha512Hasher{pubfnnew()->Sha512Hasher{let hasher =sha2::Sha512::new();Sha512Hasher{ hasher }}pubfnupdate(&mutself, data:&[u8]){self.hasher.update(data);}pubfndigest(&mutself)->alloc::string::String{let a =self.hasher.clone();let result = a.finalize();letmut text =alloc::string::String::new();write!(text,"{:x}", result).unwrap();
        text
    }}

先看 sha256 部分,这里直接调了第三方的包,创建了一个结构体来持有“加密器”,然后写了三个函数,用来创建对象、计算文件块、获取最终计算结果。其他几个其实都是一样的,只是调用了不同的包。

到这里 rust 部分就结束了,是不是还挺简单的。

最后输入命令行:

wasm-pack build

打包成 npm 包。

编译结果将存放在 /pkg 目录中

image.png

我们简单看一下 hash_wasm.d.ts 文件,以便了解如何调用。

/* tslint:disable *//* eslint-disable *//**
 * @param {string} msg
 */exportfunctiongreet(msg:string):void;/**
 */exportfunctionset_panic_hook():void;/**
 */exportclassMd5Hasher{free():void;/**
     * @returns {Md5Hasher}
     */staticnew(): Md5Hasher;/**
     * @param {Uint8Array} data
     */update(data: Uint8Array):void;/**
     * @returns {string}
     */digest():string;}/**
 */exportclassSha1Hasher{free():void;/**
     * @returns {Sha1Hasher}
     */staticnew(): Sha1Hasher;/**
     * @param {Uint8Array} data
     */update(data: Uint8Array):void;/**
     * @returns {string}
     */digest():string;}/**
 */exportclassSha256Hasher{free():void;/**
     * @returns {Sha256Hasher}
     */staticnew(): Sha256Hasher;/**
     * @param {Uint8Array} data
     */update(data: Uint8Array):void;/**
     * @returns {string}
     */digest():string;}/**
 */exportclassSha512Hasher{free():void;/**
     * @returns {Sha512Hasher}
     */staticnew(): 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
yarninstall

接下来清理掉 demo 中的示例代码。并加上文件选择和按钮

App.vue

<script setup>functionsum(type){}</script><template><div><input id="file" type="file"><div style="margin-top: 10px"><button @click="sum('sha256')">计算sha256</button></div></div></template><style scoped></style>

安装刚才 build 的 hash_wasm 库

把 pkg 目录下的文件都拷贝到 demo/wasm/

image.png

修改 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 文件分块总数
 */functionsendProgress(chunkNr, chunks){postMessage({type:"progress",data:{
            chunkNr,
            chunks
        }});}/**
 * 发送结果
 * @param result {String} 计算结果
 */functionsendResult(result){postMessage({type:"result",data: result
    });}/**
 * 计算文件散列值
 * @param file {File} 文件
 * @param hasher {Object} hasher对象
 */functionshaSum(file, hasher){// 将文件按50M分割const chunkSize =50*1024*1024;// 计算文件分块总数const chunks = Math.ceil(file.size / chunkSize);// 当前分块序号let currentChunk =0;// 对文件进行分块读取let fileReader =newFileReader();// 加载下一块functionloadNext(){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(newUint8Array(e.target.result));// 计算这一块的散列值sendProgress(currentChunk +1, chunks);// 发送进度
        currentChunk++;if(currentChunk < chunks){loadNext();// 继续加载下一块}else{sendResult(hasher.digest());// 发送结果
            hasher.free();// 释放内存}};// 加载第一块loadNext();}// 接收主线程的消息onmessage=function(e){let{ type, file }= e.data;switch(type){case"md5":shaSum(file, wasm.Md5Hasher.new());break;case"sha256":shaSum(file, wasm.Sha256Hasher.new());break;case"sha1":shaSum(file, wasm.Sha1Hasher.new());break;case"sha512":shaSum(file, wasm.Sha512Hasher.new());break;default:
            console.error("unknow type", type);}}// 发送 ready 消息postMessage({type:"ready"})

这个 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";exportdefaultdefineConfig({plugins:[vue(),wasm(),],resolve:{alias:{'@':fileURLToPath(newURL('./src',import.meta.url))}},worker:{plugins(){return[wasm(),]}}})

要加上 worker 这段插件配置,否则在 worker 里引用 wasm 还是会报错。

再次运行就正常了,接下来处理一下点击事件。请看 App.vue

<scriptsetup>import ShaWorker from"@/webworker.js?worker"import{reactive}from"vue";let infos =reactive([])functionsum(hashType){let startTime = Date.now()let file = document.getElementById('file').files[0]// 创建处理item infolet info =reactive({chunkNum:0,currentChunk:0,result:'',useTime:0,type: hashType,})
  infos.push(info)// 创建 workerconst worker =newShaWorker()
  worker.onmessage=function(e){const{data, type}= e.data;if(type ==='progress'){// 计算进度
      info.currentChunk = data.chunkNr;
      info.chunkNum = data.chunks;}elseif(type ==='result'){// 计算完成
      info.result = data;let endTime = Date.now();
      info.useTime = endTime - startTime;
      worker.terminate();// 关闭 worker 释放资源}elseif(type ==='ready'){// worker 加载完成,把文件传给 worker 进行计算
      worker.postMessage({file,type: hashType});}}}</script><template><divstyle="display: flex;flex-direction: column;width: 100%"><inputid="file"type="file"><divstyle="margin-top: 10px;display: flex;gap: 8px"><button@click="sum('sha1')">计算sha1</button><button@click="sum('sha256')">计算sha256</button><button@click="sum('sha512')">计算sha512</button><button@click="sum('md5')">计算md5</button></div><divv-for="info in infos"class="card"><pv-if="info.result">
        计算完成,耗时{{ info.useTime }}ms<br>
        {{ info.type }}:{{ info.result }}
      </p><pv-else-if="info.currentChunk > 0">正在计算{{ info.type }}...({{ info.currentChunk }}/{{ info.chunkNum }})</p><pv-else>正在准备中...</p></div></div></template><stylescoped>.card{background: #eeeeee;margin: 10px 0;border: 2px solid #ffffff;padding: 0 10px;line-height: 2;}</style>

好的,功能代码写完了。让我们来试试速度,先下载一个 win11 的系统镜像,文件大小为 6.27G。

image.png

很快啊,四舍五入 18 秒完成 sha256 的计算。

让我们搜一个 sha256 的在线校验工具,这个网站是排名比较靠前的。

image.png

耗时 119 秒,可以看到 wasm 计算 sha256 的速度是他的 6.6 倍。(我的CPU是 i7-13700KF)

再看看 sha1 。

image.png

image.png

表现优秀啊,兄弟姐妹们。而且这玩意儿还支持多线程多个文件多个类型一起算。性能也不会受到影响,感兴趣的友友可以试试。

另外说一下,测试性能的时候不要开着控制台,会严重影响性能,不管是 js 的还是 wasm 的速度,通通变慢。

其实我写完 rust 部分的时候,发现 npm 已经有相关的包了,而且名字还和我取的名字一样,叫 hash-wasm。人家还做得更好。但这不失为一个很好的实践机会。

既然都做到这里了,我稍微完善了一下界面,把网站部署上去了。你可以打开体验一下试试,地址是:https://hash.jethro.fun/

最后,对全部源码感兴趣的友友可以移步 https://github.com/jethroHuang/fast_hash

标签: webassembly vue.js rust

本文转载自: https://blog.csdn.net/jethroHEX/article/details/136394323
版权归原作者 七分小熊猫 所有, 如有侵权,请联系我们删除。

“我使用 vue3 + WebAssembly 做了个文件校验网站,性能提升600%”的评论:

还没有评论