这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战
上一篇文章讲了判断图片的类型,这一篇就来搞一搞 vue3
的 input
上传,以及如何获取文件的 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 对象。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
可以吗?
可以是可以,但是呢就需要我们的后端同学辛苦一下啦?为什么这么讲,平时做项目我们可能传的就是文件的名字,甚至呢只传了一个 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.js
让 worker
去执行 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() 函数的使用
- self.importScripts 导入文件
- self.onmessage 当 MessageEvent 类型的事件冒泡到 worker 时,事件监听函数 EventListener 被调用
- self.postMessage 发送一条消息到最近的外层对象,消息可由任何 JavaScript 对象组成。