使用Go開發前端應用(三)

前言

在寫這個系列第一篇文章的時候,有些朋友可能不知道wasm可以使用在什麼場景,雖然在評論裏面有說起,可是沒有上具體的例子,這篇文章,使用一個具體的例子來示範下wasm的使用場景。這篇文章主要會講如下幾個方面的東西:html

  • md5簡單介紹
  • 使用Go計算文件md5
  • 使用js計算文件md5
  • 性能對比
  • 文件秒傳什麼實現的?

md5的簡單介紹

md5 簡單的說就是它是一種散列算法,在之前有不少網站或者系統,都是使用 md5 來加密的,md5 的特徵是,任意長度的輸入,它均可以給你生成一個128位的結果出來,並且只要輸入不同,輸出的結果確定不同(如今聽說會有hash碰撞,不過咱們這裏不討論)。128位使用16進制的數字表示就是32位了。不過,在如今的系統中,應該是沒有再使用md5來加密密碼的了,由於 md5,使用如今的硬件或者機器的話,是能夠被破解出來的。前端

關於 md5 的具體算法能夠參考: zh.wikipedia.org/wiki/MD5linux

文件 md5 獲取工具

在mac下,可使用 md5 的命令獲取指定文件的md5值:git

md5 文件路徑
複製代碼

在linux下可使用 md5sum 命令來獲取:github

md5sum 文件路徑
複製代碼

使用 Go 計算文件 md5

下面來看下,在 Go 裏面,怎麼計算文件的 md5 值,直接來看下代碼:golang

func main() {

	s := time.Now();
	// 打開文件
	f, err := os.Open("1.mp4")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	// 實例化一個Hash對象
	h := md5.New()
	if _, err := io.Copy(h, f); err != nil {
		log.Fatal(err)
	}
	e := time.Now();

	fmt.Println(e.Sub(s));
	// 計算,而且輸出給定文件的md5結果
	fmt.Println(fmt.Sprintf("%x",h.Sum(nil)))
	
}
複製代碼

目錄結構:web

運行結果:算法

在上面的代碼中,咱們計算了1.mp4這個文件的 md5 的值,而且輸出了計算 md5 值的耗時,能夠看到耗時爲55ms。這個文件的大小看下:bash

文件的大小爲37M的樣子。app

這裏實際上是一個 Go 計算文件 md5 的 demo,後面會用來編譯成 wasm,跟前端交互。這裏暫且到這裏先。

使用 js 計算文件 md5

咱們知道,js 也能夠計算文件的 md5 值,這裏咱們使用一個比較成熟的開源庫來作實例。

開源庫的github地址是: github.com/satazor/js-…

但這裏爲了方便,咱們直接使用cdn上的庫:

<!DOCTYPE html>
<html lang="en">
<body>
    <form method="POST" enctype="multipart/form-data" onsubmit="return false;"><input id="file" type="file" placeholder="select a file"></form>
</body>
<script src="//cdn.rawgit.com/satazor/SparkMD5/master/spark-md5.min.js"></script>
<script>
    var log=document.getElementById("log");
    document.getElementById("file").addEventListener("change", function() {
        // 記錄計算開始時間
        const s = Date.now();
        var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
            file = this.files[0],
            chunkSize = 2097152, // read in chunks of 2MB
            chunks = Math.ceil(file.size / chunkSize),
            currentChunk = 0,
            spark = new SparkMD5.ArrayBuffer(),
            frOnload = function(e){
                spark.append(e.target.result); // append array buffer
                currentChunk++;
                if (currentChunk < chunks)
                    loadNext();
                else {
                    // 沒有下一個chunk了,輸出耗時
                    console.log('time:',  Date.now() - s);
                }
                    
            },
            frOnerror = function () {
            };
        function loadNext() {
            var fileReader = new FileReader();
            fileReader.onload = frOnload;
            fileReader.onerror = frOnerror;
            var start = currentChunk * chunkSize,
                end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
            fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
        };
        loadNext();
    });
</script>
</html>
複製代碼

這裏的例子實際上是這個開源項目給的例子,我改了一下,加了一個耗時的輸出,去掉了其餘的提示,官方的demo地址爲: 9px.ir/demo/increm…

使用上面的代碼,咱們一樣計算下剛剛那個1.mp4文件的md5,看下耗時:

從耗時來看,咱們發現,使用js來計算文件 md5 的話,比使用 Go 來計算文件的 md5 值,耗時多了差很少8倍。這對於C端的應用來講,確定是沒法接受的。因此,這也是爲何,我能須要使用 wasm 的緣由。

文件秒傳什麼實現的?

若是你們用過百度網盤或者其餘的相似的網盤,確定有秒傳這樣的功能。那網盤的秒傳功能是怎麼樣實現的呢?難道真的是上傳速度快,實現的嗎?確定不是,由於可能你上傳一個幾十G的文件,也能夠實現秒傳。

秒傳的實現其實很簡單,就是利用文件的md5來跟雲端的文件的md5作對比,若是相同,說明你要上傳的這個文件,雲端已經存在了,那麼這個時候,就不須要上傳了,直接標識上傳完成就行,後面若是你須要下載,就提供雲端的文件給你下載就行了。是否是很簡單?

下面咱們來作一個實例,利用Go來計算文件的md5,經過前端頁面來選擇文件,而後將文件給到Go編譯成的wasm去計算,算完以後,返回給到js使用。注意,這裏並無要實現文件秒傳的完整功能,由於須要和服務端交互,後面獲取到md5以後,發送給服務端,服務端校驗文件是否在雲端存在,這些步驟,就再也不這篇文章說了,由於後面的內容,在前端這裏,好像也沒有什麼能夠再細說的了。若是有特殊的問題,能夠再問我。

大概的實現流程

簡單畫了一個草圖,js端用來選擇文件,而後將文件傳遞給Go端,Go端計算好文件的md5值以後,再返回給js端,js端拿到結果以後,想幹嗎就隨意了。

js代碼實現

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form method="POST" enctype="multipart/form-data" onsubmit="return false;"><input id="file" type="file" placeholder="select a file"></form>
</body>
<script src="./wasm_exec.js"></script>
<script>
    // 全局的target對象,供go端訪問
    var target = {
        // go端會調用該方法來傳遞計算的結果
        callback(md5) {
            // 打印結果到控制檯
            console.log('文件的md5爲:', md5);
        }
    }
    document.getElementById("file").addEventListener("change", function() {
        const file = this.files[0];
        const go = new Go()
        WebAssembly.instantiateStreaming(fetch('main.wasm'), go.importObject)
            .then(async result => {
                go.run(result.instance);
                // 獲取文件的ArrayBuffer對象
                const buffer = await file.arrayBuffer();
                // 轉換爲Uint8Array
                const bytes = new Uint8Array(buffer)
                // 調用target對象上的calcMd5方法(這個方法由Go提供,掛載到target上)
                target.calcMd5(bytes);
            });
    });
</script>
</html>
複製代碼

在上面的代碼中,咱們監聽了input的change事件,而且在這個事件的回調函數中,能夠經過this.files訪問到選擇的文件對象,這裏咱們直接取了files[0],表示獲取第一個文件對象,若是你須要獲取多個文件對象,能夠本身改一下。下面的Go的wasm初始化代碼,以前在第一篇文章的時候說過,這裏就再也不說明了。 在下面的:

// 獲取文件的ArrayBuffer對象
const buffer = await file.arrayBuffer();
// 轉換爲Uint8Array
const bytes = new Uint8Array(buffer)
複製代碼

這裏爲何須要將ArrayBuffer對象轉換爲Uint8Array對象呢?由於在Go接收js傳遞給它的數據的時候,咱們須要經過一個叫作CopyBytesToGo的方法,來拷貝數據到go的對象中,這樣在go裏面纔可使用js的數據,這個方法要求的咱們必須傳遞Uint8Array類型的數據過去,不然會報下面的錯誤:

這個錯誤仍是比較清楚的,對不對。

最後一行代碼:

// 調用target對象上的calcMd5方法(這個方法由Go提供,掛載到target上)
target.calcMd5(bytes);
複製代碼

這裏咱們調用了target對象上的calcMd5方法,而後將bytes做爲第一個參數傳遞過去,注意,這裏的calcMd5方法,是在Go裏面聲明的,而且掛載到了target對象上面,你能夠看到咱們的js代碼,並無任何地方給target對象聲明一個calcMd5方法。

上面的代碼是js端的實現,代碼比較簡單,可是若是你對Go的wasm不太熟悉的話,就很容易掉坑裏。

下面再來看下Go端的代碼:

Go代碼實現

package main

import (
	"crypto/md5"
	"fmt"
	"syscall/js"
)

func main() {

	// 聲明一個函數,用來導出到js端,供js端調用
	calcMd5 := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		// 聲明一個和文件大小同樣的切片
		buffer := make([]byte, args[0].Length())
		// 將文件的bytes數據複製到切片中,這裏傳進來的是一個Uint8Array類型
		js.CopyBytesToGo(buffer, args[0])
		// 計算md5的值
		res := md5.Sum(buffer)
		// 調用js端的方法,將結構返回給js端
		js.Global().Get("target").Get("callback").Invoke(fmt.Sprintf("%x", res))
		return nil
	})

	// 獲取js端全局對象中的target對象,設置target的calcMd5方法爲上面的calcMd5實現
	js.Global().Get("target").Set("calcMd5", calcMd5)

	// 阻止go程序退出,由於退出了,js端就不能再調用了
	signal := make(chan int)
	<-signal
}

複製代碼

上面的代碼中,首先咱們聲明瞭一個calcMd5函數,注意這裏的函數跟普通的go函數不太同樣,須要使用js.FuncOf包裹一下,在函數中:

buffer := make([]byte, args[0].Length())
複製代碼

這行代碼,咱們使用make函數來建立了一個byte類型的切片,make函數是go語言內置的一個函數,不瞭解的能夠直接看go的官方文檔。經過make函數,建立了一個叫作buffer的切片,而後切片的長度爲js端傳進來的Uint8Array的長度大小,還記得上面的js代碼嗎,裏面調用calcMd5函數,傳的第一個參數就是一個Uint8Array的數據,咱們經過arg[0]來獲取第一個參數。

js.CopyBytesToGo(buffer, args[0])
複製代碼

這行代碼,將js端傳遞進來的Uint8Array的數據,複製到了咱們建立的buffer切片中,這樣在後面的go代碼中,纔可以使用,否則是沒有辦法直接使用的。 既然有CopyBytesToGo方法,那有沒有CopyBytesToJS方法呢?那確定有,能夠看這裏: golang.org/pkg/syscall…

// 計算md5的值
res := md5.Sum(buffer)
// 調用js端的方法,將結構返回給js端
js.Global().Get("target").Get("callback").Invoke(fmt.Sprintf("%x", res))
複製代碼

上面的代碼中,首先計算md5的值,而後使用fmt.Sprintf("%x", res)來將res轉換爲16進制的字符串數據,自己md5.Sum方法返回的是一個byte類型的切片。 下面的一行代碼,調用上面js端聲明的target對象中的callback方法,將md5的值做爲參數傳遞過去。這樣在js端就可以拿到計算的md5的結果,去作後續的事情了。

最後,來看下執行結果:

對比下上面咱們經過md5命令執行的結果,是同樣的,說明沒有問題。

總結

在這篇文章中,主要簡單介紹了下md5是什麼,而後經過實現一個demo,經過這個demo,你們應該就知道,文件秒傳是如何實現的了,而且,咱們也看到了,使用wasm來計算文具的md5的速度,是要比js計算快不少的。這也是爲何在以前的文章中,我說wasm通常用在文件上傳,計算等場景下,大部分場景用不上的緣由。

好了,這篇文章就到這裏,後面若是有須要,可能還會繼續寫一篇關於使用wasm來作計算的文章,好比使用wasm來實如今線excel的函數計算等場景。

文章中若有不對的地方,歡迎指正,🙏。

參考連接:

golang.org/pkg/syscall…

stackoverflow.com/questions/3…

相關文章
相關標籤/搜索