Vue3利用組合式函數(shù)和Shared Worker實(shí)現(xiàn)后臺(tái)分片上傳
1.背景
最近項(xiàng)目需求里有個(gè)文件上傳功能,而客戶需求里的文件基本上是比較大的,基本上得有 1 GiB 以上的大小,而上傳大文件尤其是讀大文件,可能會(huì)造成卡 UI 或者說點(diǎn)不動(dòng)的問題。而用后臺(tái)的 Worker 去實(shí)現(xiàn)是一個(gè)比較不錯(cuò)的解決辦法。
2.原理講解
2.1Shared Worker
Shared Worker 的好處是可以從幾個(gè)瀏覽上下文中訪問,例如幾個(gè)窗口、iframe 或其他 worker。這樣我們可以保證全局的頁面上傳任務(wù)都在我們的控制之下,甚至可以防止重復(fù)提交等功能。
2.2組合式函數(shù)
組合式函數(shù)的好處是在 Vue 3 是可以在任何 *.vue
文件中使用,并且是響應(yīng)式方法,可以偵聽 pinia 內(nèi) token 等的變化,傳遞給 Worker
2.3簡單流程設(shè)計(jì)
3.代碼
upload-worker.ts
代碼
import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex as toHex } from '@noble/hashes/utils'; interface SharedWorkerGlobalScope { onconnect: (event: MessageEvent<any>) => void; } const _self: SharedWorkerGlobalScope = self as any; /** * 分片大小 */ const pieceSize = 1024 * 1024; /** * 消息參數(shù) */ interface MessageArg<T> { /** * 函數(shù)名 */ func: string; /** * 參數(shù) */ arg: T; } /** * 上傳任務(wù)信息 */ interface UploadTaskInfo { /** * 文件名 */ fileName: string; /** * 上傳路徑 */ uploadPath: string; /** * 任務(wù) id */ id: string; /** * 文件大小 */ size: number; /** * 上傳進(jìn)度 */ progress: number; /** * 上傳速度 */ speed?: number; /** * 任務(wù)狀態(tài) */ status: 'uploading' | 'paused' | 'canceled' | 'done' | 'error' | 'waiting'; /** * 開始時(shí)間 */ startTime?: Date; /** * 結(jié)束時(shí)間 */ endTime?: Date; /** * 錯(cuò)誤信息 */ errorMessage?: string; } /** * 上傳任務(wù) */ interface UploadTask extends UploadTaskInfo { file: File; pieces: Array<boolean>; abort?: AbortController; } /** * 任務(wù)/哈希值映射 */ const hashs = new Map(); /** * 上傳任務(wù)列表 */ const uploadTasks: Array<UploadTask> = []; /** * 狀態(tài)接收器 */ const statusReceivers = new Map<string, MessagePort>(); /** * token 倉庫 */ const tokenStore = { /** * token */ BearerToken: '', }; /** * 返回上傳狀態(tài) * @param task 上傳任務(wù) */ const updateStatus = (task: UploadTaskInfo) => { const taskInfo: UploadTaskInfo = { fileName: task.fileName, uploadPath: task.uploadPath, id: task.id, size: task.size, progress: task.progress, speed: task.speed, status: task.status, startTime: task.startTime, endTime: task.endTime, errorMessage: task.errorMessage, }; statusReceivers.forEach((item) => { item.postMessage(taskInfo); }); }; /** * 運(yùn)行上傳任務(wù) * @param task 上傳任務(wù) */ const runUpload = async (task: UploadTask) => { task.status = 'uploading'; const hash = hashs.get(task.id) || sha256.create(); hashs.set(task.id, hash); let retryCount = 0; const abort = new AbortController(); task.abort = abort; while (task.status === 'uploading') { const startTime = Date.now(); const index = task.pieces.findIndex((item) => !item); if (index === -1) { try { const response: { code: number; message: string } = await fetch( '/api/File/Upload', { method: 'PUT', headers: { Authorization: tokenStore.BearerToken, 'Content-Type': 'application/json', }, body: JSON.stringify({ id: task.id, fileHash: toHex(hash.digest()), filePath: task.uploadPath, }), } ).then((res) => res.json()); if (response.code !== 200) { throw new Error(response.message); } task.status = 'done'; task.endTime = new Date(); updateStatus(task); } catch (e: any) { task.status = 'error'; task.errorMessage = e.toString(); task.endTime = new Date(); deleteUpload(task.id); updateStatus(task); } break; } const start = index * pieceSize; const end = start + pieceSize >= task.size ? task.size : start + pieceSize; const buffer = task.file.slice(index * pieceSize, end); hash.update(new Uint8Array(await buffer.arrayBuffer())); const form = new FormData(); form.append('file', buffer); let isTimeout = false; try { const timer = setTimeout(() => { isTimeout = true; abort.abort(); }, 8000); const response: { code: number; message: string } = await fetch( `/api/File/Upload?id=${task.id}&offset=${start}`, { method: 'POST', body: form, headers: { Authorization: tokenStore.BearerToken, }, signal: abort.signal, } ).then((res) => res.json()); clearTimeout(timer); if (response.code !== 200) { throw new Error(response.message); } task.pieces[index] = true; task.progress = task.pieces.filter((item) => item).length / task.pieces.length; task.speed = (pieceSize / (Date.now() - startTime)) * 1000; updateStatus(task); } catch (e: any) { retryCount++; if (retryCount > 3) { task.status = 'error'; if (isTimeout) { task.errorMessage = 'UploadTimeout'; } else { task.errorMessage = e.toString(); } task.endTime = new Date(); deleteUpload(task.id); updateStatus(task); } } runNextUpload(); } }; /** * 運(yùn)行下一個(gè)上傳任務(wù) */ const runNextUpload = async () => { if (uploadTasks.filter((item) => item.status === 'uploading').length > 3) { return; } const task = uploadTasks.find((item) => item.status === 'waiting'); if (task) { await runUpload(task); } }; /** * 排隊(duì)上傳 * @param e 消息事件 */ const queueUpload = async ( e: MessageEvent< MessageArg<{ id: string; file: File; uploadPath: string; }> > ) => { uploadTasks.push({ file: e.data.arg.file, fileName: e.data.arg.file.name, id: e.data.arg.id, uploadPath: e.data.arg.uploadPath, size: e.data.arg.file.size, progress: 0, speed: 0, status: 'waiting', pieces: new Array(Math.ceil(e.data.arg.file.size / pieceSize)).fill(false), errorMessage: undefined, }); updateStatus(uploadTasks[uploadTasks.length - 1]); await runNextUpload(); }; /** * 注冊(cè)狀態(tài)接收器 * @param e 消息事件 * @param sender 發(fā)送者 */ const registerStatusReceiver = ( e: MessageEvent<MessageArg<string>>, sender?: MessagePort ) => { if (sender) statusReceivers.set(e.data.arg, sender); }; /** * 注銷狀態(tài)接收器 * @param e 消息事件 */ const unregisterStatusReceiver = (e: MessageEvent<MessageArg<string>>) => { statusReceivers.delete(e.data.arg); }; /** * 更新 token * @param e 消息事件 */ const updateToken = (e: MessageEvent<MessageArg<string>>) => { tokenStore.BearerToken = 'Bearer ' + e.data.arg; }; /** * 暫停上傳 * @param e 消息事件 */ const pauseUpload = (e: MessageEvent<MessageArg<string>>) => { const task = uploadTasks.find((item) => item.id === e.data.arg); if (task) { task.status = 'paused'; if (task.abort) { task.abort.abort(); } updateStatus(task); } }; /** * 取消上傳 * @param e 消息事件 */ const cancelUpload = (e: MessageEvent<MessageArg<string>>) => { const task = uploadTasks.find((item) => item.id === e.data.arg); if (task) { task.status = 'canceled'; if (task.abort) { task.abort.abort(); } deleteUpload(task.id); updateStatus(task); } }; /** * 刪除上傳 * @param id 任務(wù) id */ const deleteUpload = async (id: string) => { uploadTasks.splice( uploadTasks.findIndex((item) => item.id === id), 1 ); hashs.delete(id); await fetch(`/api/File/Upload?id=${id}`, { method: 'DELETE', headers: { Authorization: tokenStore.BearerToken, }, }).then((res) => res.json()); }; /** * 消息路由 */ const messageRoute = new Map< string, (e: MessageEvent<MessageArg<any>>, sender?: MessagePort) => void >([ ['queueUpload', queueUpload], ['registerStatusReceiver', registerStatusReceiver], ['updateToken', updateToken], ['pauseUpload', pauseUpload], ['cancelUpload', cancelUpload], ['unregisterStatusReceiver', unregisterStatusReceiver], ]); // 監(jiān)聽連接 _self.onconnect = (e) => { const port = e.ports[0]; port.onmessage = async (e) => { // 調(diào)用函數(shù) const func = messageRoute.get(e.data.func); if (func) { func(e, port); } }; port.start(); };
upload-service.ts
代碼
import UploadWorker from './upload-worker?sharedworker'; import { onUnmounted, ref, watch } from 'vue'; import { storeToRefs } from 'pinia'; import { useAuthStore } from 'src/stores/auth'; /** * 上傳任務(wù)信息 */ interface UploadTaskInfo { /** * 文件名 */ fileName: string; /** * 上傳路徑 */ uploadPath: string; /** * 任務(wù) id */ id: string; /** * 文件大小 */ size: number; /** * 上傳進(jìn)度 */ progress: number; /** * 上傳速度 */ speed?: number; /** * 任務(wù)狀態(tài) */ status: 'uploading' | 'paused' | 'canceled' | 'done' | 'error' | 'waiting'; /** * 開始時(shí)間 */ startTime?: Date; /** * 結(jié)束時(shí)間 */ endTime?: Date; /** * 錯(cuò)誤信息 */ errorMessage?: string; } /** * 上傳服務(wù) */ export const useUploadService = () => { const store = storeToRefs(useAuthStore()); // 創(chuàng)建共享 worker const worker = new UploadWorker(); /** * 上傳任務(wù)列表 */ const uploadTasks = ref<Map<string, UploadTaskInfo>>( new Map<string, UploadTaskInfo>() ); // 是否已注冊(cè)狀態(tài)接收器 const isRegistered = ref(false); // 服務(wù) id const serviceId = crypto.randomUUID(); // 監(jiān)聽上傳任務(wù)列表變化(只有在注冊(cè)狀態(tài)接收器后才會(huì)收到消息) worker.port.onmessage = (e: MessageEvent<UploadTaskInfo>) => { uploadTasks.value.set(e.data.id, e.data); }; // 更新 token worker.port.postMessage({ func: 'updateToken', arg: store.token.value, }); watch(store.token, (token) => { worker.port.postMessage({ func: 'updateToken', arg: token, }); }); /** * 排隊(duì)上傳 * @param file 文件 * @param uploadPath 上傳路徑 */ const queueUpload = (file: File, uploadPath: string) => { worker.port.postMessage({ func: 'queueUpload', arg: { id: crypto.randomUUID(), file: file, uploadPath: uploadPath, }, }); }; /** * 暫停上傳 * @param id 任務(wù) id */ const pauseUpload = (id: string) => { worker.port.postMessage({ func: 'pauseUpload', arg: id, }); }; /** * 取消上傳 * @param id 任務(wù) id */ const cancelUpload = (id: string) => { worker.port.postMessage({ func: 'cancelUpload', arg: id, }); }; /** * 注冊(cè)狀態(tài)接收器 */ const registerStatusReceiver = () => { worker.port.postMessage({ func: 'registerStatusReceiver', arg: serviceId, }); isRegistered.value = true; }; /** * 注銷狀態(tài)接收器 */ const unregisterStatusReceiver = () => { worker.port.postMessage({ func: 'unregisterStatusReceiver', arg: serviceId, }); isRegistered.value = false; }; onUnmounted(() => { unregisterStatusReceiver(); worker.port.close(); }); return { uploadTasks, queueUpload, pauseUpload, cancelUpload, registerStatusReceiver, unregisterStatusReceiver, }; };
4.用法
// 引入組合式函數(shù) const uploadService = useUploadService(); // 注冊(cè)狀態(tài)接收器 uploadService.registerStatusReceiver(); // 表單綁定上傳方法 const upload = (file: File, filePath: string) => { uploadService.queueUpload(file, filePath); } // 監(jiān)聽上傳進(jìn)度,當(dāng)然也可以直接展示在界面,畢竟是 Ref watch(uploadService.uploadTasks, console.log);
以上就是Vue3利用組合式函數(shù)和Shared Worker實(shí)現(xiàn)后臺(tái)分片上傳的詳細(xì)內(nèi)容,更多關(guān)于Vue3分片上傳的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Vue2.0結(jié)合webuploader實(shí)現(xiàn)文件分片上傳功能
- Vue.Js及Java實(shí)現(xiàn)文件分片上傳代碼實(shí)例
- vue實(shí)現(xiàn)大文件分片上傳與斷點(diǎn)續(xù)傳到七牛云
- 使用Vue3+ElementPlus前端實(shí)現(xiàn)分片上傳的全過程
- 利用Vue3+Element-plus實(shí)現(xiàn)大文件分片上傳組件
- vue大文件分片上傳之simple-uploader.js的使用
- Java實(shí)現(xiàn)大文件的分片上傳與下載(springboot+vue3)
- vue3+element 分片上傳與分片下載功能實(shí)現(xiàn)方法詳解
- vue分片上傳視頻并轉(zhuǎn)換為m3u8文件播放的實(shí)現(xiàn)示例
相關(guān)文章
vue keep-alive實(shí)現(xiàn)多組件嵌套中個(gè)別組件存活不銷毀的操作
這篇文章主要介紹了vue keep-alive實(shí)現(xiàn)多組件嵌套中個(gè)別組件存活不銷毀的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10基于vue.js實(shí)現(xiàn)側(cè)邊菜單欄
這篇文章主要為大家詳細(xì)介紹了基于vue.js實(shí)現(xiàn)側(cè)邊菜單欄的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03vue后臺(tái)管理如何配置動(dòng)態(tài)路由菜單
這篇文章主要介紹了vue后臺(tái)管理如何配置動(dòng)態(tài)路由菜單,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04在Vue使用$attrs實(shí)現(xiàn)構(gòu)建高級(jí)組件
本文我們主要來看下Vue3中的$attrs屬性。首先,我們會(huì)介紹它的用途以及它的實(shí)現(xiàn)與Vue2有哪些不兩同點(diǎn),并通過事例來加深對(duì)它的理解2022-09-09vue-cli 自定義指令directive 添加驗(yàn)證滑塊示例
本篇文章主要介紹了vue-cli 自定義指令directive 添加驗(yàn)證滑塊示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10vue項(xiàng)目配置element-ui容易遇到的坑及解決
這篇文章主要介紹了vue項(xiàng)目配置element-ui容易遇到的坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07