WEB WORKER配合FILE API,加速前端秒传读取MD5

WEB WORKER配合FILE API,加速前端秒传读取MD5

File API可以读取文件内容的功能让我们可以直接在浏览器端执行生成MD5的工作,对比传统的直接把文件发送到后台的方式,无疑节省了数据流量,在小文件的处理上,速度也应当有提升。

实现读取文件MD5的功能并不困难,但是这种方式的瓶颈在于速度:在把文件流传入Spark的时候速度过慢,而且会影响页面响应,导致较大的文件并不适合采用这种方法。而这时候可以尝试使用WebWorker

关于Web Worker

W3C 在 HTML5 的规范中提出了工作线程(Web Worker)的概念,工作线程允许开发人员编写能够长时间运行而不被用户所中断的后台程序, 去执行事务或者逻辑,并同时保证页面对用户的及时响应。

传统上的线程可以解释为轻量级进程,它和进程一样拥有独立的执行控制,一般情况下由操作系统负责调度。而在 HTML5 中的多线程是这样一种机制,它允许在 Web 程序中并发执行多个 JavaScript 脚本,每个脚本执行流都称为一个线程,彼此间互相独立,并且有浏览器中的 JavaScript 引擎负责管理。

在 HTML5 中,工作线程的出现使得在 Web 页面中进行多线程编程成为可能。众所周知,传统页面中(HTML5 之前)的 JavaScript 的运行都是以单线程的方式工作的,虽然有多种方式实现了对多线程的模拟(例如:JavaScript 中的 setinterval 方法,setTimeout 方法等),但是在本质上程序的运行仍然是由 JavaScript 引擎以单线程调度的方式进行的。在 HTML5 中引入的工作线程使得浏览器端的 JavaScript 引擎可以并发地执行 JavaScript 代码,从而实现了对浏览器端多线程编程的良好支持。

在这个例子里面,将要使用到的是Dedicated Worker(专用线程),而不涉及到Shared Worker(共享线程)。

Worker使用方式十分简单:

/*定义worker*/
var worker = new Worker('readFileAsBuffer.js');

/*向worker传递信息*/
worker.postMessge('hello worker');

/*从worker接收信息*/
worker.onmessage = function(event){
    console.log(event.data)     //'hi you'
}


//-----woker文件部分,readFileAsBuffer.js-------


onmessage = function(event){
    console.log(event.data);   //'hello worker'
}

postMessage('hi you');

其中:readFileAsBuffer.js是一个单独的文件。从代码中可以看到,在主文件中创建一个Worker对象,其参数为对应的worker文件的路径。在构造实例之后,通过调用实例的postMessge()方法即可对线程传送数据。woker文件中直接给onmessage赋值一个响应函数即可。传递给worker的内容不仅仅是字符串,还可以传递js对象等。

传送文件到Worker

既然我们现在可以很方便的通过FileAPI直接获取文件的内容了,那下一步就是把文件传送给worker线程了。

不过,在此之前,我们要先了解一下我们到底需要传递什么样格式的数据进去。

在此例子中我用到的是npm中 spark-md5 的一个module。当然,在module中的readme已经详细的告诉我们使用的方法了。通过FileReader读取文件为ArrayBuffer,然后append到spark对象上

ArrayBuffer(缓冲数组)是一种用于呈现通用、固定长度的二进制数据的类型。

关于ArrayBuffer的内容可以参阅 MDN

结合示例我们可以写出如下的代码:

const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
const file = files[0]
const fileReader = new FileReader()
const worker = new Worker('readFileAsBuffer.js')
let timeStart = 0

/*fileReader读取完成,传递给worker*/
fileReader.onload = function(e){
    worker.postMessage({
        operation:'sendArrayBuffer',
        input:e.target.result,
        threshold:0.8,
        finish:true
    },[e.target.result])
}
fileReader.onerror = function(){
    console.log('文件读取失败,请重试');
}

/*worker线程计算MD5完成并返回结果*/
worker.onmessage = function(event){
    console.log('total time: ',(new Date()).getTime()-timeStart)
    /*读取完成,获取md5*/
    const md5 = event.data
    file.md5 = md5
    /*发至后端检验md5*/
    checkMD5(md5)
}

/*使用FileReader读取文件*/
fileReader.readAsArrayBuffer(blobSlice.call(file,0,file.size))
timeStart = (new Date()).getTime()


//-----woker文件部分,readFileAsBuffer.js-------

/*在worker线程中,如果需要引入别的文件,要使用importScripts*/
importScripts('../../node_modules/spark-md5/spark-md5.js');

const spark = new SparkMD5.ArrayBuffer()
const saveArrayBuffer = []
const start = false
const finish = false

/*接收到主线程发来的文件*/
onmessage = function(event){
    spark.append(event.data.input)
    const md5 = spark.end()
    postMessage(md5)
}

概括步骤是:

  1. 通过用户上传获得文件对象
  2. 通过FileReader的readAsArrayBuffer读取文件
  3. 在FileReader的onload响应里把读取为ArrayBuffer的文件发送给worker线程
  4. 在worker线程中使用spark计算MD5后返回给主线程

在调用postMessage的时候,在第二个参数的位置传入了内容是ArrayBuffer的数组,这一个数组的元素是Transferable对象,这是一个什么鬼呢?

An optional array of Transferable objects to transfer ownership of. If the ownership of an object is transferred, it becomes unusable (neutered) in the context it was sent from and becomes available only to the worker it was sent to.

当我们传入这个参数时,这个数组里面的元素会被剪切到worker上下文中,而不是复制,意味着在传入之后ArrayBuffer在主线程将不再可用。这样做的好处是,传送速度会得到提高。Chrome48本地测试传送Transferable对象数组,90M左右的文件大概可以减少耗时800毫秒左右。

上述的例子代码还不包括Web Worker和FileAPI的兼容处理方式。其中这两者的兼容性如下:

如果浏览器不兼容Web Worker,则可以直接在主线程处理文件,要注意的是ChunkSize要设小一点以免造成页面卡顿;如果不兼容FileAPI,那还是直接把文件上传到后端处理吧。


而回到Web Worker的性能上,经过初步测试,Chrome44测试读取一个73.5M文件平均耗时3,500毫秒,如果不启动Web Worker则耗时为40,000毫秒,在这个大小的文件上耗时仅为1/10。不但减少了计算MD5的时间,而且经过初步测试,Chrome44测试读取一个73.5M文件平均耗时3,500毫秒,如果不启动Web Worker则耗时为40,000毫秒,在这个大小的文件上耗时仅为1/10。不但减少了计算MD5的时间,而且不会造成页面卡顿。

编辑于 2016-05-03

文章被以下专栏收录