使用Go开发前端应用(三)

2,978 阅读9分钟

前言

在写这个系列第一篇文章的时候,有些朋友可能不知道wasm能够使用在什么场景,虽然在评论里面有提及,但是没有上具体的例子,这篇文章,使用一个具体的例子来示范下wasm的使用场景。这篇文章主要会讲以下几个方面的东西:

  • md5简单介绍
  • 使用Go计算文件md5
  • 使用js计算文件md5
  • 性能对比
  • 文件秒传什么实现的?

md5的简单介绍

md5 简单的说就是它是一种散列算法,在以前有很多网站或者系统,都是使用 md5 来加密的,md5 的特征是,任意长度的输入,它都可以给你生成一个128位的结果出来,而且只要输入不一样,输出的结果肯定不一样(现在据说会有hash碰撞,不过我们这里不讨论)。128位使用16进制的数字表示就是32位了。不过,在现在的系统中,应该是没有再使用md5来加密密码的了,因为 md5,使用现在的硬件或者机器的话,是可以被破解出来的。

关于 md5 的具体算法可以参考: zh.wikipedia.org/wiki/MD5

文件 md5 获取工具

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

md5 文件路径

在linux下可以使用 md5sum 命令来获取:

md5sum 文件路径

使用 Go 计算文件 md5

下面来看下,在 Go 里面,怎么计算文件的 md5 值,直接来看下代码:

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)))
	
}

目录结构:

运行结果:

在上面的代码中,我们计算了1.mp4这个文件的 md5 的值,并且输出了计算 md5 值的耗时,可以看到耗时为55ms。这个文件的大小看下:

文件的大小为37M的样子。

这里其实是一个 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…