文件上传(二)利用 web worker 计算文件 hash

这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

上一篇文章讲了判断图片的类型,这一篇就来搞一搞 vue3input 上传,以及如何获取文件的 hash 值,来保证这个文件是唯一的

我们先来看看常规的文件上传

<div ref="dragDom" id="drag">
  <input type="file" name="file" @change="handleFileChange" />
</div>

import { defineComponent, ref, onMounted } from 'vue'
import { bindEvents, upload } from './upload'
 export default defineComponent({
    setup(props) {
      const dragDom = ref(null)
      onMounted(() => {
        bindEvents(dragDom, upload().file)
      })

      return {
        dragDom,
      }
    }
  })
复制代码

需要注意的是 dom 里面获取标签实例的 ref 是不需要用 :的,但是必须用 setup 里面定义的响应式数据 我们接着给文件添加一个拖拽的功能,这其实很简单,添加一些监听事件

export const bindEvents = (dragDom, file) => {
  const drag = dragDom.value
  drag.addEventListener('dragover', (e) => {
    drag.style.borderColor = 'red'
    e.preventDefault()
  })
  drag.addEventListener('dragleave', (e) => {
    drag.style.borderColor = '#eee'
    e.preventDefault()
  })
  drag.addEventListener('drop', (e) => {
    const fileList = e.dataTransfer.files
    drag.style.borderColor = '#eee'
    file.value = fileList[0]
    e.preventDefault()
  })
}
复制代码

由于我们使用的是 vue3 提供的 ref 函数,因此在使用的时候需要用到 dragDom.value 的值, 我们做一些简单的交互,拖拽的时候添加一些颜色的变化

ref API 文档点这里

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

接着我们在上传的时候需要拿到上传的文件信息, 我们先做一个文件上传的

let file = ref<any>('')
const handleFileChange = (e) => {
  const firstFile = e.target.files[0]
  if (!firstFile) return
  file.value = firstFile
}
复制代码

到这里我们上传文件的信息也已经拿到了,下一步是不是就该上传文件,调用后端接口上传了,嗯是的,没错

  const form = new FormData()
  form.append('name', 'file')
  form.append('files', file.value)
  http.post('api/uploadFile', form)
复制代码

这样子貌似就大功告成了,但是哪里又有一点不对,感觉文章的内容和标题没啥关系,那我们就接着往下看

应该有细心的同学可能已经发现了我们 form 表单中上传的参数 name 似乎有什不妥, 文件名称怎么可以在前端写死呢,那我们用 file 中的 name 可以吗?

1642577502(1).jpg

可以是可以,但是呢就需要我们的后端同学辛苦一下啦?为什么这么讲,平时做项目我们可能传的就是文件的名字,甚至呢只传了一个 file,那在数据库中是怎么保证他是唯一一份的,如果一个文件已经上传过了我们再次上传是不是还需要等待呢?

我们先看第一个问题,怎么保证他是唯一的呢,我们一般会采用的就是用 hash 去标识一个文件 我们都知道 js 是单线程工作的,如果遇到文件比较大,那会不会把浏览器卡崩了呢,或者是用户在使用的时候会失去耐心,遇到文件比较大的时候我们又该怎么办呢

Worker() API 点这里 可能有的小伙伴会想到 web Worker 这个方法,web worker 相当于在浏览器中开了新的线程,可以理解为他有一个分身,单独做自己的事情。

在这项目中 new Worker 中用的是 vite 搭建的,因此 new Worker() 加载的资源用了 @ 的时候会 404, 这里呢就吧文件放到了 public 文件下面

export const calculateHashWorker = async (chunks: Array<chunksType>, hashProgress: any) => {
  return new Promise((resolve) => {
    // 这里使用 @ 会找不到资源文件
    const worker = new Worker('/hash/index.js')
    worker.postMessage({ chunks })
    worker.onmessage = (e) => {
      const { progress, hash } = e.data
      hashProgress.value = Number(progress.toFixed(2))
      if (hash) {
        resolve(hash)
      }
    }
  })
}
复制代码

我们新建一个 calculateHash.jsworker 去执行 new FileReader() 这个方法在上一篇文章中已经描述过了,不记得的同学可以再复习一下

这里我们使用了 spark-md5

// 引入spark-md5
// 将一个或多个脚本同步导入当前文件。
self.importScripts('/lib/spark-md5.min.js')

self.onmessage = e => {
  // 接受主线程传递的数据
  const { chunks } = e.data
  const spark = new self.SparkMD5.ArrayBuffer()

  let progress = 0 // 进度信息
  let count = 0

  const loadNext = index => {
    const reader = new FileReader()
    reader.readAsArrayBuffer(chunks[index].file)
    reader.onload = e => {
      count++
      spark.append(e.target.result)

      if (count == chunks.length) {
        self.postMessage({
          progress: 100,
          hash: spark.end()
        })
      } else {
        progress += 100 / chunks.length
        self.postMessage({
          progress
        })
        loadNext(count)
      }
    }
  }
  loadNext(0)
}
复制代码

写到这里似乎忘记了 calculateHashWorker 方法中的 chunks 是哪里来的了, 那就把这点代码也贴一下吧

let file: any = ref('')
const CHUNK_SIZE = 0.01 * 1024 * 1024 // 0.01 M
const createFileChunk = (file: any, size: number = CHUNK_SIZE) => {
  const chunks = []
  let cur = 0
  while (cur < file.value.size) {
    chunks.push({ index: cur, file: file.value.slice(cur, cur + size) })
    cur += size
  }
  return chunks
}
复制代码

关于文件过大上传怎么处理这个坑我们后续在填吧。

总结: 主要介绍了 worker() 函数的使用

  1. self.importScripts 导入文件
  2. self.onmessage 当 MessageEvent 类型的事件冒泡到 worker 时,事件监听函数 EventListener 被调用
  3. self.postMessage 发送一条消息到最近的外层对象,消息可由任何 JavaScript 对象组成。

おすすめ

転載: juejin.im/post/7054835731885195277