文章目录
- 获取文件对象
- 文件上传(秒传、分片上传、断点续传、重传)
- 优化
获取文件对象
input标签的onchange方法接收到的参数就是用户上传的所有文件
<html lang="en">
<head>
<title>文件上传</title>
<style>
#inputFile,#inputDirectory {
display: none;
}
#dragarea{
width: 100%;
height: 100px;
border: 2px dashed #ccc;
}
.dragenter{
background-color: #ccc;
}
</style>
</head>
<body>
<!--
1. 如何上传多文件:multiple
2. 如何上传文件夹:为了兼顾各浏览器兼容性,需设置三个属性:webkitdirectory mozdirectory odirectory
3. 如何实现拖拽上传:input默认是有拖拽性质的,但是由于浏览器兼容性问题,开发一般不使用,一般使用div阻止默认事件以及通过拖拽api实现
4. 如何获取选择的所有文件
-->
<div id="dragarea"></div>
<input id="inputFile" type="file" multiple>
<!-- 如果不想用input自带的上传文件的样式,可以通过button的click触发input的点击事件来上传文件 -->
<button id="buttonFile">上传文件</button>
<input id="inputDirectory" type="file" multiple webkitdirectory mozdirectory odirectory>
<button id="buttonDirectory">上传文件夹</button>
<ul class="fileList"></ul>
<script>
const inputFile = document.getElementById("inputFile")
const buttonFile = document.getElementById("buttonFile")
const inputDirectory = document.getElementById("inputDirectory")
const buttonDirectory = document.getElementById("buttonDirectory")
const dragarea = document.getElementById("dragarea")
const fileList = document.getElementById("fileList")
const appendFile = (fileList) => {
for(const file in fileList){
const li = document.getElementById("li")
li.innerText = `${file.name}-${file.name.split(".")[1]}-${file.size}`
fileList.appendChild(li)
}
}
const traverseFile = (entry) => {
if(entry.isFile){
entry.file((file) => {
const li = document.getElementById("li")
li.innerText = `${file.name}-${file.name.split(".")[1]}-${file.size}`
fileList.appendChild(li)
})
}else if(entry.isDirectory){
traverseDirectory(entry)
}
}
const traverseDirectory = (directory) => {
const reader = directory.createReader()// 创建读取器读取文件夹
reader.readEntries((entries) => {
for(const entry of entries) {
traverseFile(entry)
}
})
}
buttonFile.onclick = () => {
inputFile.click()
}
inputFile.onchange = (e) => {
const files = e.target.files// 获得用户上传的所有文件
appendFile(files)
}
inputDirectory.onchange = (e) => {
console.log(e.target.files)
const files = e.target.files// 获得用户上传的所有文件
appendFile(files)
}
buttonDirectory.onclick = () => {
inputDirectory.click()
}
dragarea.ondragenter = (e) => {
e.preventDefault();
console.log("拖拽进入区域")
dragarea.classList.add("dragenter")
}
dragarea.ondragover = (e) => {
e.preventDefault();
console.log("拖拽着悬浮在区域上方")
dragarea.classList.add("dragenter")
}
dragarea.ondragleave = (e) => {
e.preventDefault();
console.log("拖拽离开")
dragarea.classList.remove("dragenter")
}
// 拖拽放开
dragarea.ondrop = (e) => {
e.preventDefault();
dragarea.classList.remove("dragenter")
const items = e.dataTransfer.items// 拖拽进来的所有文件
for(const item of items){
const entry = item.webkitGetAsEntry()
traverseFile(entry)
}
}
</script>
</body>
</html>
文件上传(秒传、分片上传、断点续传、重传)
秒传:调用后端的接口,将md5值传过去,后端判断如果这个md5值对应的文件是否已经合并,如果已经合并,则返回文件上传成功
分片上传:每片大小chunk_size为1m,假如文件1.5m,那么会被分成2片,使用file.slice截取[0,1),再截取[1,1.5)
断点续传:文件上传前会调用后端的接口,将md5值传过去,后端判断如果这个md5值对应的文件是否已经合并,如果没有合并,会返回这个md5值已经上传的切片的索引,前端重新上传剩余索引的片
并发控制:假如我们把文件切成了100片,如果一下子把这100片全传给后端,会给后端造成并发压力,所以在发送前可以在前端进行并发控制一下,我们将所有的请求都放在队列里,每次从队列里弹出几个请求来发送
明明浏览器可以控制请求并发,为什么前端还要自己控制并发请求?
- 避免浏览器并发限制:浏览器对同一域名的并发请求数量是有限制的(通常是 6-8 个,具体取决于浏览器和协议)。如果前端不控制并发请求,可能会导致大量请求堆积,超出浏览器的并发限制,从而阻塞其他重要请求(如关键 API 或资源加载),
- 提升用户体验:如果一次性发送过多请求,可能会导致网络带宽被占满,影响页面其他资源的加载(如图片、CSS、JS 等),并且可能会导致部分请求超时或失败,从而浪费网络资源和用户流量。
- 错误处理和重试机制:手动控制并发可以更好地实现错误处理和重试机制。
例如,某个请求失败后,可以立即重试,而不是等待所有请求完成后再处理错误。- 优先级控制:手动控制并发可以实现请求的优先级管理。例如,某些关键请求可以优先发送,而低优先级的请求可以稍后处理。
- 兼容性和稳定性:不同浏览器对并发请求的处理方式可能不同,手动控制并发可以确保应用在各种浏览器中表现一致。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>文件上传</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.7.2/axios.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.js"></script>
<style>
#inputFile,
#inputDirectory {
display: none;
}
#dragarea {
width: 100%;
height: 100px;
border: 2px dashed #ccc;
}
.dragenter {
background-color: #ccc;
}
</style>
</head>
<body>
<div class="dragarea"></div>
<input class="inputFile" type="file" multiple>
<!-- 如果不想用input自带的上传文件的样式,可以通过button的click触发input的点击事件来上传文件 -->
<button class="buttonFile">上传文件</button>
<input class="inputDirectory" type="file" multiple webkitdirectory mozdirectory odirectory>
<button class="buttonDirectory">上传文件夹</button>
<button class="buttonUpload">点击上传</button>
<ul class="fileListElement"></ul>
<script>
// 文件交互相关
const fileList = []
const chunk_size = 1 * 1024 * 1024
const requestQueue = []
const maxRequest = 2// 最大请求数量
let currentRequest = 0// 当前请求数
const inputFile = document.getElementsByClassName("inputFile")[0]
const buttonFile = document.getElementsByClassName("buttonFile")[0]
const inputDirectory = document.getElementsByClassName("inputDirectory")[0]
const buttonDirectory = document.getElementsByClassName("buttonDirectory")[0]
const dragarea = document.getElementsByClassName("dragarea")[0]
const fileListElement = document.getElementsByClassName("fileListElement")[0]
const buttonUpload = document.getElementsByClassName("buttonUpload")[0]
// 将上传的文件展示在按钮下方
const showFileList = (files) => {
for (const file in files) {
const li = document.getElementById("li")
li.innerText = `${file.name}-${file.name.split(".")[1]}-${file.size}`
fileListElement.appendChild(li)
fileList.push(file)
}
}
const traverseFile = (entry) => {
// 拖拽进来的如果是文件,直接展示在按钮下方
if (entry.isFile) {
entry.file((file) => {
const li = document.getElementById("li")
li.innerText = `${file.name}-${file.name.split(".")[1]}-${file.size}`
fileList.appendChild(li)
})
} else if (entry.isDirectory) {
// 拖拽进来的如果是文件夹,读文件夹,获得文件夹里面的文件
traverseDirectory(entry)
}
}
const traverseDirectory = (directory) => {
const reader = directory.createReader()
reader.readEntries((entries) => {
for (const entry of entries) {
traverseFile(entry)
}
})
}
buttonFile.onclick = () => {
inputFile.click()
}
inputFile.onchange = (e) => {
const files = e.target.files // 获得用户上传的所有文件
showFileList(files)
}
inputDirectory.onchange = (e) => {
console.log(e.target.files)
const files = e.target.files // 获得用户上传的所有文件
showFileList(files)
}
buttonDirectory.onclick = () => {
inputDirectory.click()
}
dragarea.ondragenter = (e) => {
e.preventDefault();
console.log("拖拽进入区域")
dragarea.classList.add("dragenter")
}
dragarea.ondragover = (e) => {
e.preventDefault();
console.log("拖拽着悬浮在区域上方")
dragarea.classList.add("dragenter")
}
dragarea.ondragleave = (e) => {
e.preventDefault();
console.log("拖拽离开")
dragarea.classList.remove("dragenter")
}
// 拖拽放开
dragarea.ondrop = (e) => {
e.preventDefault();
dragarea.classList.remove("dragenter")
const items = e.dataTransfer.items
for (const item of items) {
const entry = item.webkitGetAsEntry()
traverseFile(entry)
}
}
// 文件上传
buttonUpload.onclick = () => {
for (const file of fileList) {
if (file.size <= chunk_size) {
uploadSingleFile(file)
} else {
uploadLargeFile(file)
}
}
}
// 单文件一整个文件上传
// 文件上传通过formData传输,因为formData是前后端都认识的格式,file是只有前端才认识的格式(后端不认识)
const uploadSingleFile = (file) => {
const formData = new FormData()
formData.append("file", file) // 通过append往formData身上添加对象,如果formData身上已有file对象,会覆盖
try {
axios.post("http://127.0.0.1:3001/upload", formData, {
headers: {
"content-type": "multipart/form-data"
}
})
} catch (error) {
throw error
}
}
// 大文件上传
const uploadLargeFile = async (file) => {
// 创建文件hash。创建整个文件的hash即可,每个片不用创建hash,因为每片是调用后端的方法上传的,返回成功即上传成功
const md5 = await createFileMd5(file)
// 大文件分片
const chunksList = createChunkFile(file)
// 创建文件分片对象
const chunkListObj = createChunkFileObj(chunksList, file, md5)
// 将md5值传给后端接口,判断文件是否在服务器上存在,如果存在,后端返回isExistObj.isExists为true,则秒传成功,
// 如果不存在,后端会返回给你此md5值上传了哪些片,已上传的片的索引放在chunkIds中
const isExistObj = await juedgeFileExist(file, md5)
if (isExistObj && isExistObj.isExists) {
alert('文件已秒传成功!')
return
}
// 文件上传
await asyncPool(chunkListObj, isExistObj.chunkIds) // chunkIds:后端返回的,已上传的分片的索引
// await Promise.all(promises)
concatChunkFile(file, md5)// 文件上传完毕,调用后端合并文件的接口
}
// 创建文件的md5值
const createFileMd5 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
// reader.readAsArrayBuffer(file)读取完毕后会调用onload,读取失败调用onerror,读取到的内容在e.target.result中
reader.onload = (e) => {
const md5 = SparkMD5.ArrayBuffer.hash(e.target.result)
resolve(md5)
}
reader.onerror = () => {
reject(error)
}
reader.readAsArrayBuffer(file)
})
}
// 创建文件分片:每片大小chunk_size为1m,假如文件1.5m,那么会被分成2片,使用file.slice截取[0,1),再截取[1,1.5)
const createChunkFile = (file) => {
let current = 0
const chunkList = []
while (current < file.size) {
chunkList.push(file.slice(current, Math.min(current + chunk_size, file.size)))
current += chunk_size
}
return chunkList
}
// 创建文件分片对象。将文件的md5、文件名、本片在整个文件中的索引,都传入这个对象,调用后端接口上传时会用到的数据都可以封装进来
const createChunkFileObj = (chunkList, file, md5) => {
return chunkList.map((chunk, index) => {
return {
file: chunk,
md5,
name: file.name,
index: index,
}
})
}
// 文件分片上传
const uploadChunkFile = (chunkListObj, chunkIds) => {
return chunkListObj.filter((item,index) => (!chunkIds.includes(index)))// 过滤掉已经上传的切片,让已经上传的切片没有下面那个函数
.map((chunk, index) => {
return () => {
const formData = new FormData()
formData.append("file", chunk.file, `${chunk.md5}-${chunk.index}`)
formData.append("name", chunk.name)
formData.append("timestamp", Date.now().toString()) // 防止走缓存
try {
axios.post("http://127.0.0.1:3001/upload/large", formData, {
headers: {
"content-type": "multipart/form-data"
}
})
} catch (error) {
return Promise.reject(error)
throw error
}
}
})
}
// 判断文件是否存在
const juedgeFileExist = async (file, md5) => {
try {
const response = await axios.post("http://127.0.0.1:3001/upload/exists", formData, {
params: {
"name": file.nam,
md5,
}
})
return response.data.data
} catch (error) {
return {}
throw error
}
}
// 合并请求
const concatChunkFile = (file, md5) => {
try {
axios.post("http://127.0.0.1:3001/upload/concatFiles", {
"name": file.nam,
md5,
})
} catch (error) {
throw error
}
}
// 把要发送的函数放在队列里,每次从头部取一个函数调用,这样就可以控制并发数量
const asyncPool = (chunkListObj, chunkIds) => {
return new Promise((resolve,reject) => {
requestQueue.push(...uploadChunkFile(chunkListObj, chunkIds))
run(resolve,reject)
})
}
const run = (resolve,reject) => {
while(currentRequest < maxRequest && requestQueue.length > 0){
const task = requestQueue.shift()
currentRequest++
task().then().finally(() => {
currentRequest--
run(resolve,reject)
})
}
if(currentRequest === 0 && requestQueue.length === 0) {
resolve()
}
}
</script>
</body>
</html>