Vue 3 中用组合式函数和 Shared Worker 实现后台分片上传(带哈希计算)
01. 背景
最近项目需求里有个文件上传功能,而客户需求里的文件基本上是比较大的,基本上得有 1 GiB 以上的大小,而上传大文件尤其是读大文件,可能会造成卡 UI 或者说点不动的问题。而用后台的 Worker 去实现是一个比较不错的解决办法。
02. 原理讲解
02.01. Shared Worker
Shared Worker 的好处是可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。这样我们可以保证全局的页面上传任务都在我们的控制之下,甚至可以防止重复提交等功能。
02.02. 组合式函数
组合式函数的好处是在 Vue 3 是可以在任何 *.vue 文件中使用,并且是响应式方法,可以侦听 pinia 内 token 等的变化,传递给 Worker
02.03 简单流程设计
id1[用户选择文件] --> id2[创建上传任务]
id2 --> id3[任务推送到 Worker]
id3 --> id4[上传到服务器]
id4 --> id5[Worker 返回任务状态]
id5 --> id6[组合式函数拦截状态放到 Map 里]
03. 代码
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;
/**
* 消息参数
*/
interface MessageArg<T> {
/**
* 函数名
*/
func: string;
/**
* 参数
*/
arg: T;
}
/**
* 上传任务信息
*/
interface UploadTaskInfo {
/**
* 文件名
*/
fileName: string;
/**
* 上传路径
*/
uploadPath: string;
/**
* 任务 id
*/
id: string;
/**
* 文件大小
*/
size: number;
/**
* 上传进度
*/
progress: number;
/**
* 上传速度
*/
speed?: number;
/**
* 任务状态
*/
status: 'uploading' | 'paused' | 'canceled' | 'done' | 'error' | 'waiting';
/**
* 开始时间
*/
startTime?: Date;
/**
* 结束时间
*/
endTime?: Date;
/**
* 错误信息
*/
errorMessage?: string;
}
/**
* 上传任务
*/
interface UploadTask extends UploadTaskInfo {
file: File;
pieces: Array<boolean>;
abort?: AbortController;
}
/**
* 任务/哈希值映射
*/
const hashs = new Map();
/**
* 上传任务列表
*/
const uploadTasks: Array<UploadTask> = [];
/**
* 状态接收器
*/
const statusReceivers = new Map<string, MessagePort>();
/**
* token 仓库
*/
const tokenStore = {
/**
* token
*/
BearerToken: '',
};
/**
* 返回上传状态
* @param task 上传任务
*/
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);
});
};
/**
* 运行上传任务
* @param task 上传任务
*/
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();
}
};
/**
* 运行下一个上传任务
*/
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);
}
};
/**
* 排队上传
* @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();
};
/**
* 注册状态接收器
* @param e 消息事件
* @param sender 发送者
*/
const registerStatusReceiver = (
e: MessageEvent<MessageArg<string>>,
sender?: MessagePort
) => {
if (sender) statusReceivers.set(e.data.arg, sender);
};
/**
* 注销状态接收器
* @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 任务 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],
]);
// 监听连接
_self.onconnect = (e) => {
const port = e.ports[0];
port.onmessage = async (e) => {
// 调用函数
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';
/**
* 上传任务信息
*/
interface UploadTaskInfo {
/**
* 文件名
*/
fileName: string;
/**
* 上传路径
*/
uploadPath: string;
/**
* 任务 id
*/
id: string;
/**
* 文件大小
*/
size: number;
/**
* 上传进度
*/
progress: number;
/**
* 上传速度
*/
speed?: number;
/**
* 任务状态
*/
status: 'uploading' | 'paused' | 'canceled' | 'done' | 'error' | 'waiting';
/**
* 开始时间
*/
startTime?: Date;
/**
* 结束时间
*/
endTime?: Date;
/**
* 错误信息
*/
errorMessage?: string;
}
/**
* 上传服务
*/
export const useUploadService = () => {
const store = storeToRefs(useAuthStore());
// 创建共享 worker
const worker = new UploadWorker();
/**
* 上传任务列表
*/
const uploadTasks = ref<Map<string, UploadTaskInfo>>(
new Map<string, UploadTaskInfo>()
);
// 是否已注册状态接收器
const isRegistered = ref(false);
// 服务 id
const serviceId = crypto.randomUUID();
// 监听上传任务列表变化(只有在注册状态接收器后才会收到消息)
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,
});
});
/**
* 排队上传
* @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 任务 id
*/
const pauseUpload = (id: string) => {
worker.port.postMessage({
func: 'pauseUpload',
arg: id,
});
};
/**
* 取消上传
* @param id 任务 id
*/
const cancelUpload = (id: string) => {
worker.port.postMessage({
func: 'cancelUpload',
arg: id,
});
};
/**
* 注册状态接收器
*/
const registerStatusReceiver = () => {
worker.port.postMessage({
func: 'registerStatusReceiver',
arg: serviceId,
});
isRegistered.value = true;
};
/**
* 注销状态接收器
*/
const unregisterStatusReceiver = () => {
worker.port.postMessage({
func: 'unregisterStatusReceiver',
arg: serviceId,
});
isRegistered.value = false;
};
onUnmounted(() => {
unregisterStatusReceiver();
worker.port.close();
});
return {
uploadTasks,
queueUpload,
pauseUpload,
cancelUpload,
registerStatusReceiver,
unregisterStatusReceiver,
};
};
04. 用法
// 引入组合式函数
const uploadService = useUploadService();
// 注册状态接收器
uploadService.registerStatusReceiver();
// 表单绑定上传方法
const upload = (file: File, filePath: string) => {
uploadService.queueUpload(file, filePath);
}
// 监听上传进度,当然也可以直接展示在界面,毕竟是 Ref
watch(uploadService.uploadTasks, console.log);
Vue 3 中用组合式函数和 Shared Worker 实现后台分片上传(带哈希计算)的更多相关文章
- vue用阿里云oss上传图片使用分片上传只能上传100kb以内的解决办法
首先,vue和阿里云oss上传图片结合参考了 这位朋友的 https://www.jianshu.com/p/645f63745abd 文章,成功的解决了我用阿里云oss上传图片前的一头雾水. 该大神 ...
- vue+大文件分片上传
最近公司在使用vue做工程项目,实现大文件分片上传. 网上找了一天,发现网上很多代码都存在很多问题,最后终于找到了一个符合要求的项目. 工程如下: 对项目的大文件上传功能做出分析,怎么实现大文件分片上 ...
- 关于vue+element对ie9的兼容el-upload不支持在IE9上传
关于vue+element对ie9的兼容el-upload不支持在IE9上传 https://lian-yue.github.io/vue-upload-component/#/zh-cn/ 解决方案 ...
- vue+element+oss实现前端分片上传和断点续传
纯前端实现: 切片上传 断点续传 .断点续传需要在切上上传的基础上实现 前端之前上传OSS,无需后端提供接口.先上完整代码,直接复制,将new OSS里的参数修改成自己公司OSS相关信息后可用,如遇问 ...
- 聚是一团火散作满天星,前端Vue.js+elementUI结合后端FastAPI实现大文件分片上传
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_175 分片上传并不是什么新概念,尤其是大文件传输的处理中经常会被使用,在之前的一篇文章里:python花式读取大文件(10g/50 ...
- 以寡治众各个击破,超大文件分片上传之构建基于Vue.js3.0+Ant-desgin+Tornado6纯异步IO高效写入服务
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_218 分治算法是一种很古老但很务实的方法.本意即使将一个较大的整体打碎分成小的局部,这样每个小的局部都不足以对抗大的整体.战国时期 ...
- 前端vue的JsPDF html2canvas 生成pdf并以文件流形式上传到后端(转载)
原文地址 1.首先在文件内引入htmlToPdf.js这里代码引入了html2canvas和jspdf//需要 npm i html2Canvas 和 npm i jspdf 在这里将getPdf 这 ...
- JAVA SpringMVC + FormDate + Vue + file表单 ( 实现 js 单文件和多文件上传 )
JS 部分 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <tit ...
- Vue 使用axios分片上传
Vue的界面 <input type="file"/> 上传方法 fileUpload: function () { var num = 1 var file = do ...
- vue富文本编辑,编辑自动预览,单个图片上传不能预览的问题解决:
//预览<div class="htmlViewBox"> <p v-html="activity_html_defaultMsg" v-sh ...
随机推荐
- 现代C++(Modern C++)基本用法实践:零、概述&测试项目
序言 习惯上,我们把C++11之前的C++语法特性称之为"传统C++(traditional c++)",而把c++11之后的语法特性称之为现代C++(modern c++).有一 ...
- Mybatis(Map)
Map 假设,我们的实体类,或者数据库中的表,字段或参数过多,我们应当考虑使用map 创建接口 //万能的mapper,我们不需要知道数据库里面有什么,是一个键值对的表现 //我们只需查询对应的字段 ...
- MySQL——GROUP BY详解与优化
在 MySQL 中,GROUP BY用于将具有指定列中相同值的行分组在一起.这是在处理大量数据时非常有用的功能,允许对数据进行分类和聚合. 基本使用 语法 以下是GROUP BY子句的基本语法: &q ...
- 2021-7-29 MySql进阶
Alter的使用: 列的增加和删减 alter table users add user_name VARCHAR(100);#添加一列在末尾 SELECT * from users; alter t ...
- CenOS 安装 mysql 临时密码 处理
数据库 版本 Server version: 8.0.26 MySQL Community Server - GPL:官网下载的包 wget https://cdn.mysql.com//Downlo ...
- Pandas: title函数的作用
代码演示 将首字母大写,其余小写 效果演示 参考链接 https://www.w3resource.com/pandas/series/series-str-title.php
- Echarts: 同时显示柱状图和前端
完整代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF- ...
- Android OAID 获取 基于MSA oaid_sdk_1.0.25.zip
目录 简介 SDK下载 支持版本 开始使用 可能遇到的问题 替代方案 APK下载 写在最后 简介 本文链接:https://www.cnblogs.com/dks-/p/14357041.html 根 ...
- mysql拓展
事务定义 就是将一组SQL语句放在同一批次内去执行 如果一个sql语句出错,则改批次内的所有sql都将被取消执行 (1)原子性 一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作, ...
- Programming abstractions in C阅读笔记: p118-p122
<Programming Abstractions In C>学习第49天,p118-p122,总结如下: 一.技术总结 1.随机数 (1)seed p119,"The init ...