web worker计算md5实践及遇到的坑

最近项目要实现大文件的分块上传及断点续传,其中文件的md5是判断文件或文件块是否已被上传的重要依据。

1. 阶段一

编码初期,直接在公共方法中写了一个传入file返回md5的函数,供文件上传模块使用。关键代码如下:

/**
 * 获取文件的md5
 * @param {*} file 文件对象
 */
import SparkMD5 from 'spark-md5'
export async function getFileMd5(file) {
  return new Promise((resolve) => {
    const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
    const chunkSize = 1024 * 1024 * 5
    const chunks = Math.ceil(file.size / chunkSize)
    let currentChunk = 0
    const spark = new SparkMD5.ArrayBuffer()
    const fileReader = new FileReader()

    fileReader.onload = function(e) {
      spark.append(e.target.result)
      currentChunk++
      if (currentChunk < chunks) {
        loadNext()
      } else {
        resolve(spark.end())
      }
    }

    fileReader.onerror = function(error) {
      console.error(error)
      resolve()
    }

    function loadNext() {
      const start = currentChunk * chunkSize
      const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
      fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
    }
    loadNext()
  })
}

2. 阶段二

考虑到计算md5涉及大量的计算,因此可以利用Web Worker技术,将计算的任务交给其它线程使用,避免主线程的阻塞导致的页面卡顿。改造步骤如下:

  1. 新建md5.worker.js文件,将计算md5得逻辑移动到该文件中。
import SparkMD5 from 'spark-md5'
addEventListener('message', function(event) {
  getFileMd5(event.data)
}, false)
function getFileMd5(file) {
  const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
  const chunkSize = 1024 * 1024 * 5
  const chunks = Math.ceil(file.size / chunkSize)
  let currentChunk = 0
  const spark = new SparkMD5.ArrayBuffer()
  const fileReader = new FileReader()

  fileReader.onload = function(e) {
    spark.append(e.target.result)
    currentChunk++
    if (currentChunk < chunks) {
      loadNext()
    } else {
      postMessage(spark.end())
    }
  }

  fileReader.onerror = function(error) {
    console.error(error)
    postMessage()
  }

  function loadNext() {
    const start = currentChunk * chunkSize
    const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
    fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
  }
  loadNext()
}
  1. 改造之前为分块上传模块提供的函数
/**
 * 获取文件的md5
 * @param {*} file 文件对象
 */

import Md5Worker from 'worker-loader!../workers/md5.worker'
export async function getFileMd5(file) {
  return new Promise((resolve) => {
    const md5Worker = new Md5Worker()
    md5Worker.onerror = () => {
      md5Worker.terminate()
      resolve()
    }
    md5Worker.onmessage = (event) => {
      md5Worker.terminate()
      resolve(event.data)
    }
    md5Worker.postMessage(file)
  })
}
  1. 安装worker-loader,npm i -D worker-loader
  2. 安装完成后运行打包命令(例如npm run build),可以看到在dist目录下输出了对应的[hash].worker.js文件。

3. 遇到的坑

最开始是参考官网在vue.config.js中配置loader,配置如下

{
  module: {
    rules: [
      {
        test: /\.worker\.js$/,
        use: { loader: 'worker-loader' }
      }
    ]
  }
}

运行打包命令,提示Cannot read property 'createChildCompiler' of undefined,定位到需要设置parallel: false,该配置是是否启用多核打包(当时寻思代价不小啊)。
设置该配置之后,可以正常打包了,dist目录下出现了.worker.js文件。然而当我以为大功告成的时候,再次运行打包命令(配置文件没有任何变化),神奇的事情出现了,dist目录下没有.worker.js文件了!!!但只要我修改vue.config.js文件,哪怕在任何地方输出log,运行打包命令.worker.js文件又会出现。因此最终使用了以上的解决方案。

同时,在此期间,还调研了worker-plugin,然而在打包的时候,总是会对md5.worker.jsESLint相关的错误,即便我关闭了对该文件的代码检测。唯一的区别是,关闭代码检测之后,错误变少了。