文章目錄
- 獲取文件對象
- 文件上傳(秒傳、分片上傳、斷點續傳、重傳)
- 優化
獲取文件對象
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. 如何上傳多文件:multiple2. 如何上傳文件夾:為了兼顧各瀏覽器兼容性,需設置三個屬性:webkitdirectory mozdirectory odirectory3. 如何實現拖拽上傳: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 * 1024const 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.itemsfor (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 = 0const 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>