Vue实现封装一个切片上传组件
作者:前端小小梦
平时业务开发中用el-upload能满足大部分场景,但是对于一些大文件的上传时会比较慢,所以自己基于el-upload封装了一个切片上传组件,希望对大家有所帮助
组件效果
单文件切片上传
多文件切片上传
组件使用案例
<template> <div id="app"> <div class="upload-wrap"> <UploadSlice :action="uploadInfoSlice.actionChunk" :headers="uploadInfoSlice.headers" :limit="uploadInfoSlice.limit" :accept="uploadInfoSlice.accept" :show-file-list="false" cancelable :on-success="handleSuccess" :on-remove="handleRemove" :on-cancel="handleCancel" :on-upload-pre="handleUploadPre" :on-upload-merge="handleUploadMerge" :on-form-data="genFormData" /> </div> </div> </template> <script> import UploadSlice from './components/UploadSlice.vue' import { uploadPre, uploadMerge } from '@/api/upload-slice' export default { name: 'App', components: { UploadSlice }, data() { return { // 上传部分 uploadInfoSlice: { actionChunk: process.env.VUE_APP_BASE_API + '/storage/file/v3/chunk', // 切片请求上传路径 headers: { 'Authorization': 'Bearer XXX' } } } }, methods: { // 分片预请求 async handleUploadPre(file) { const form = new FormData() form.append('fileSource', 'APPLICATION') form.append('originFileName', file.name) let res = '' try { res = await uploadPre(form) } catch (error) { throw new Error(error) } }, // 构造分片参数 genFormData(chunks, uid) { const prepareId = this.getCurrentPrepareId(uid) return chunks.map(chunk => { const form = new FormData() form.append('chunk', chunk.file) form.append('uploadId', prepareId) form.append('partNumber', chunk.index) return form }) }, // 合并请求 async handleUploadMerge(file, uid) { const prepareId = this.getCurrentPrepareId(uid) const form = new FormData() form.append('fileSource', 'APPLICATION') form.append('hash', prepareId) form.append('filename', file.name) // return 建议使用, 用于handleSuccess获取数据 try { const res = await uploadMerge(form) return res } catch (error) { return error } }, // 判断当前处理prepareId getCurrentPrepareId(uid) { for (const item of this.progressFileList) { if (item.uid === uid) { return item.prepareId } } } } } </script>
使用文档
Attribute
标红色
部分为二次封装处理过的功能,其他为el-upload自带属性
参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 |
---|---|---|---|---|---|
action | 必选参数,分片上传的地址,预请求和合并请求在组件外操作 | String | - | - | |
headers | 设置上传的请求头部 | String | - | - | |
multiple | 是否支持多选文件 | boolean | - | ||
accept | 可上传文件类型,多种类型用","分隔 (格式不符合自动提示) | String | - | - | |
on-remove | 文件列表移除文件时的钩子 | function(file, fileList) | — | — | |
on-success | 文件上传成功时的钩子 | function(response, file, fileList) | — | — | |
on-error | 文件上传失败时的钩子 | function(err, file, fileList) | — | — | |
on-progress | 文件上传时的钩子 | function(event, file, fileList) | — | — | |
on-change | 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 | function(file, fileList) | — | — | |
on-exceed | 文件超出个数限制时的钩子 | function(files, fileList) | — | — | |
list-type | 文件列表的类型 | string | text/picture/picture-card | text | |
show-file-list | 是否显示已上传文件列表(文件分片上传时建议设置false,否则会有两个进度条) | boolean | — | true | |
file-list | 上传的文件列表, 例如: [{name: 'food.jpg', url: 'xxx.cdn.com/xxx.jpg'}] | array | — | [] | |
disabled | 是否禁用 | boolean | — | false | |
cancelable | 是否支持取消 | boolean | — | false | |
limit | 最多允许上传个数(超出个数自动提示) | number | — | — | |
size | 限制大小 | String | — | — | |
hideBtn | 是否在上传过程中隐藏上传按钮 | boolean | — | — | false |
Slot
插槽名 | 说明 |
---|---|
trigger | 触发文件选择框的内容 |
tip | 提示说明文字 |
more-tips | 在默认提示后补充说明 |
封装过程
切片上传组件是基于el-upload
进行的二次封装,文章开头组件效果演示可以看到上传一个文件会发送三个请求:prepare,chunk, merge
,也就是整个上传过程,主要分为三步:1.预请求 2.分片请求 3.合并请求,预请求和合并请求就是我们正常的http请求,主要处理的是分片请求,分片请求主要的步骤是:
- 将文件切片
- 构造切片请求参数
- 控制分片请求的并发
1. 文件切片
在el-upload
上传后, 在on-change
属性的回调里可以获取文件file
,通过file.raw.slice
对文件进行切片,目前的切片规则是:1.小于10M 固定一片 2.小于50M 文件10%为一片 3.大于50M 固定5M 一片(可以根据自己的需求进行修改)
genFileChunks(file) { const chunks = [] let cur = 0 // 小于10M 固定一片 if (file.size < (10 * 1024 * 1024)) { chunks.push({ index: cur, file: file.raw.slice(cur, file.size), originFilename: file.name }) return chunks } // 小于50M 文件10%为一片 if (file.size < (50 * 1024 * 1024)) { const chunkSize = parseInt(file.size * 0.1) while (cur < file.size) { chunks.push({ index: cur, file: file.raw.slice(cur, cur + chunkSize), originFilename: file.name }) cur += chunkSize } return chunks } // 大于50M 固定5M 一片 const chunkSize = parseInt(5 * 1024 * 1024) while (cur < file.size) { chunks.push({ index: cur, file: file.raw.slice(cur, cur + chunkSize), originFilename: file.name }) cur += chunkSize } return chunks },
一个32M的文件按照10%切一片,构造好的切片数据是这样的
2. 构造切片请求参数
切片请求不同业务的参数是变化的,所以参数部分可以抛出给父组件处理,增加组件的复用性
父组件
<template> <UploadSlice :action="uploadInfoSlice.actionChunk" :headers="uploadInfoSlice.headers" :on-form-data="genFormData" /> </template> <script> methods: { // 构造分片参数 genFormData(chunks, uid) { const prepareId = this.getCurrentPrepareId(uid) return chunks.map(chunk => { const form = new FormData() form.append('chunk', chunk.file) form.append('uploadId', prepareId) form.append('partNumber', chunk.index) return form }) }, }, </script>
子组件
<template> <el-upload action="" :accept="accept" > </template> <script> props: { onFormData: { type: Function, default: () => {} }, }, methods: { async uploadChunks(uid) { // 预请求 // --------------- // 上传切片 const requests = this._genRequest(this._genUploadData(uid), uid) // 控制并发 await this.sendRequest(requests) // 合并请求 // --------------- }, // 构造分片参数 _genUploadData(uid) { const chunks = this.getCurrentChunks(uid) return this.onFormData(chunks, uid) }, // 生成调用请求:[Promise, Promise] _genRequest(uploadData, uid) { console.log('uploadData', uploadData) const file = this.getCurrentFile(uid) const chunks = this.getCurrentChunks(uid) return uploadData.map((form, index) => { const options = { headers: this.$attrs.headers, file: file, data: form, action: this.action, onProgress: progress => { chunks[index].progress = Number( ((progress.loaded / progress.total) * 100).toFixed(2) ) this.handleProgress(progress, file, uid) } } return options }) }, }, </script>
3. 控制分片请求的并发
切片上传如果不控制并发,在分片很多时,就会同时发送很多个http请求,导致线程阻塞,影响页面其他请求的操作,所以控制并发是需要的。我设置的是最多允许3个并发请求。
sendRequest(requests, limit = 3) { return new Promise((resolve, reject) => { const len = requests.length let counter = 0 let isTips = false // 只提示一次失败 let isStop = false // 如果一个片段失败超过三次 认为当前网洛有问题 停止全部上传 const startRequest = async() => { if (isStop) return const task = requests.shift() if (task && task.file.status !== 'cancel') { // 利用try...catch捕获错误 try { // 具体的接口 抽离出去了 await ajax(task) if (counter === len - 1) { // 最后一个任务 resolve() } else { // 否则接着执行 counter++ startRequest() // 启动下一个任务 } } catch (error) { // 网络异常 if (error === 'NETWORK_ERROR' && !isTips) { Message.error('网络异常,文件上传失败') this.upLoading = false this.preLoading = false isTips = true this.handleRemove('', []) } // 接口报错重试,限制为3次 if (task.error < 3) { task.error++ requests.unshift(task) startRequest() } else { isStop = true reject(error) } } } } // 启动任务 while (limit > 0) { // 模拟不同大小启动 setTimeout(() => { startRequest() }, Math.random() * 2000) limit-- } }) } }
完整代码
文章只整理了核心代码,对于格式数量限制、进度条处理、取消上传等操作也进行了封装,详细请看完整代码
https://github.com/Xmengling/el-upload-slice
待完善
- 目前还没有做断点续传的处理
- 可能还有一些异常边界情况没有考虑完整
到此这篇关于Vue实现封装一个切片上传组件的文章就介绍到这了,更多相关Vue封装切片上传组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!