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

前言

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

Rust 和 WebAssembly

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

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

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

src/lib.rs

rust 复制代码
#![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

rust 复制代码
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

rust 复制代码
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 文件,以便了解如何调用。

typescript 复制代码
/* 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 项目。

shell 复制代码
yarn create vue@latest
cd demo
yarn install

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

App.vue

javascript 复制代码
<script setup>
function sum(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/

修改 package.json

json 复制代码
{
    "dependencies": {
      "vue": "^3.4.15",
      "hash-wasm": "file:./wasm"
    },
}

执行 yarn install

好了,wasm 库这就装好了。

WebWorker

即使 wasm 再快,如果直接在主线程计算 sha256 也会卡渲染,所以我们需要使用 WebWorker 创建一个子线程来调用 wasm。接下来创建一个 src/webworker.js 文件。

javascript 复制代码
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 < 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 才能正确地处理它,否则会报错。

js 复制代码
import ShaWorker from "@/webworker.js?worker"

这个时候运行将会得到一个错误:

kotlin 复制代码
"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。

安装后配置如下:

js 复制代码
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

html 复制代码
<script setup>
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});
    }
  }
}
</script>

<template>
  <div style="display: flex;flex-direction: column;width: 100%">
    <input id="file" type="file">
    <div style="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>
    <div v-for="info in infos" class="card">
      <p v-if="info.result">
        计算完成,耗时{{ info.useTime }}ms<br>
        {{ info.type }}:{{ info.result }}
      </p>
      <p v-else-if="info.currentChunk > 0">正在计算{{ info.type }}...({{ info.currentChunk }}/{{ info.chunkNum }})</p>
      <p v-else>正在准备中...</p>
    </div>
  </div>
</template>

<style scoped>
.card {
  background: #eeeeee;
  margin: 10px 0;
  border: 2px solid #ffffff;
  padding: 0 10px;
  line-height: 2;
}
</style>

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

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

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

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

再看看 sha1 。

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

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

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

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

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

相关推荐
GIS程序媛—椰子31 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落1 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
麦麦大数据2 小时前
基于vue+neo4j 的中药方剂知识图谱可视化系统
vue.js·知识图谱·neo4j
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
理想不理想v2 小时前
vue经典前端面试题
前端·javascript·vue.js
小阮的学习笔记2 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜2 小时前
Vue实现登录功能
前端·javascript·vue.js
杨荧2 小时前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka