浙里办 文件实现分片上传
前言:
浙里办项目中涉及到上传图片或者视频、文件的地方,当我们保存时,如果文件稍大点,一般都会失败。
我们做了测试,现只知道3M以内的没问题,决定将文件切片分批次上传。
环境是vue2 + vant2,上传使用的 input type="file" 的方式。分片上传要看后端逻辑怎么写,这里只给出我这边配合后端实现的部分,可自行调整。此上传是单张分别上传,并不是提交时一起上传,所以删除最好调接口删除。
实现:
data.js
// 文件分片 // file:文件 pieceSize:分片大小 uploadId:后台返回的参数值,用于分片方法的传参 success:成功的回调 error:失败的回调 uploading:上传状态(进度)处理 export const uploadByPieces = ({ file, pieceSize = 2, uploadId, success, error, uploading }) => { if (!file) return false // 如果文件传入为空直接 return 返回 let fileMD5 = ''// 总文件列表 let base64 = '' // 每片转成的base64数据 let entity = null // 定义参数对象 const chunkSize = pieceSize * 1024 * 1024 // 每片的大小(M) const chunkCount = Math.ceil(file.size / chunkSize) // 总片数 // 获取md5 const readFileMD5 = () => { // 读取视频文件的md5 let fileRederInstance = new FileReader() fileRederInstance.readAsBinaryString(file) fileRederInstance.addEventListener('load', e => { let fileBolb = e.target.result fileMD5 = md5(fileBolb) readChunkMD5() }) } // 获取分片数据(分片) const getChunkInfo = async (file, currentChunk, chunkSize) => { let start = currentChunk * chunkSize let end = Math.min(file.size, start + chunkSize) let chunk = file.slice(start, end) // 文件转成base64格式字符串 base64 = await store.dispatch('getFileToBase64', chunk) return { start, end, chunk } } // 获取文件数据(不分片) const singleInfo = async (file) => { // 文件转成base64格式字符串 base64 = await store.dispatch('getFileToBase64', file) return base64 } // 针对每个文件进行chunk处理 const readChunkMD5 = async () => { if (chunkCount != 1) { // 大于1片的 console.log('分片'); for (var i = 0; i < chunkCount; i++) { const { chunk } = await getChunkInfo(file, i, chunkSize) await uploadChunk({ chunk, currentChunk: i, chunkCount }) } } else { // 只有1片 console.log('不分片'); const result = await singleInfo(file) const data = { Data: result } http.mgopPOST('/xxxx/xxxx/addfile', data, {}, true).then(res => { return new Promise((resolver, reject) => { if (res.ErrorType == 200) { uploading(1, 1) resolver(true) success({ skipUpload: true, url: res.Data }) } else { error(res.Message) } }).catch((e) => { console.log(e); error && error(e) }) }) } } // 上传分片 const uploadChunk = (chunkInfo) => { return new Promise((resolver, reject) => { entity = { Data: { base64: base64, chunkNumber: chunkInfo.currentChunk + 1, chunkSize: chunkSize, currentChunkSize: chunkInfo.chunk.size, filename: file.name, totalChunks: chunkInfo.chunkCount, identifier: fileMD5, uploadId } } http.mgopPOST('/xxx/xxx/uploadChunk', entity, {}, true).then(res => { if (res.ErrorType == 200) { uploading(chunkInfo.currentChunk + 1, chunkInfo.chunkCount) resolver(true) success(res.Data) } else { error(res.Message) } }).catch((e) => { error && error(e) }) }) } readFileMD5() // 开始执行代码 }
引入:md5加密(npm install js-md5)、store、http(封装的请求)
// 文件转成base64格式字符串 getFileToBase64({ commit }, file) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.readAsDataURL(file) reader.onload = () => resolve(reader.result) reader.onerror = (error) => reject(error) }) }
uploadFile.vue
<!-- 文件上传 图片视频文件等 --> <template> <div class="flex-ali-cen content"> <span class="file-item" style="width: 80px; height: 80px;" v-for="(item, index) in fileBox" :key="index" > <!-- 图片 --> <img v-if="item.data.type.indexOf('image/') !== -1" :src="item.flowUrl" alt="图片" @click="preview(item)" > <!-- 视频 img-视频封面图片 --> <div v-else-if="item.data.type.indexOf('video/') !== -1" class="other-file" style="width: 80px; height: 80px;" @click="preview(item)"> <img src="../assets/img/video.png" alt="" width="80" height="80"> </div> <!-- 其他文件 --> <div v-else class="other-file" @click="preview(item)" style="width: 80px; height: 80px;" > <van-icon name="description" size="20px" /> <span>{{item.data.name}}</span> </div> <!-- 删除按钮 --> <span class="close-icon" @click.stop="closeImg(item)" > <van-icon name="cross" color="#ffffff" size="24px" /> </span> </span> <!-- 上传 --> <div v-if="fileBox.length < maxCount" class="upload-box" style="width: 80px; height: 80px;" > <van-icon name="photograph" size="24" color="#dcdee0" /> <input id="fileUpload" type="file" @change="upload" > </div> <!-- 进度条展示 --> <van-overlay :show="overlayShow"> <p class="upload-text">文件上传中...</p> <van-progress v-if="processflag" :percentage="percentage" stroke-width="8" /> </van-overlay> </div> </template> <script> import { hasStr, uploadByPieces } from '@/utils/data' import { mapGetters } from 'vuex' import md5 from 'js-md5' export default { props: { // 是否可以上传文件(多项 包括图片和视频等) true可以 false不可以 hasFile: { type: Boolean, default: () => { return false } }, // 文件个数限制 maxCount: { type: Number, default: () => { return 9 } }, // 是否只上传图片 isImg: { type: Boolean, default: () => { return false } } }, data() { return { fileBox: [], xlclBase: [], percentage: 0, processflag: false, overlayShow: false, flowFileUrl: window.g.READ_FILE_URL } }, computed: { ...mapGetters(['getFileUrl']) }, mounted() {}, methods: { // 删除方法 closeImg(data) { this.fileBox.forEach((item, index, array) => { if (item.resultUrl === data.resultUrl) { array.splice(index, 1) this.xlclBase.splice(index, 1) } }) this.$http.mgopGET('/xxx/xxxx/deleteFile', { lllj: data.resultUrl }).then(res => { if (res.ErrorType == 200) { this.$notify.success('删除成功') } }) }, // 获取上传后的数据 getUrls() { return this.xlclBase }, // 上传方法 upload() { const file = document.getElementById('fileUpload'), fileObj = file.files[0], that = this const size = fileObj.size / (1024 * 1024) // 文件大小 // 上传条件判断 if (this.isImg && !hasStr(fileObj.type, 'image/')) { this.$_toast('请上传照片') return false } if (!this.hasFile && !this.isImg && !hasStr(fileObj.type, 'image/') && !hasStr(fileObj.type, 'video/')) { this.$_toast('请上传照片或视频') return false } if (size > 50) { // 文件大小大于 50兆 先提醒 this.$dialog.confirm({ title: '提醒', message: '上传的文件超过50M,要继续上传吗?', cancelButtonText: '取消', confirmButtonText: '确定' }) .then(() => { that.uploadChunk(fileObj, that) }) .catch(() => { }); } else { that.uploadChunk(fileObj, that) } }, // 处理上传逻辑 uploadChunk(fileObj, that) { // 重置数据 const reset = (that) => { setTimeout(() => { that.overlayShow = false that.processflag = false; that.percentage = 0 }, 2000); } // 存返回的文件信息 const setFileData = (result, file, that) => { that.$notify.success('文件上传成功') that.xlclBase.push(result.url) that.fileBox.push({ url: this.getFileUrl + result.url, // 地址前缀拼接展示 flowUrl: this.flowFileUrl + result.url, // 文件流拼接展示 resultUrl: result.url, data: file }) console.log('fileBox', that.fileBox); } let fileReder = new FileReader() fileReder.readAsBinaryString(fileObj) fileReder.addEventListener('load', e => { const result = e.target.result, fileMD5 = md5(result), req = { Data: { filename: fileObj.name, identifier: fileMD5 } } that.$http.mgopPOST('/xxx/xxxx/uploadChunk', req, {}, true).then(res => { if (res.ErrorType == 200) { if (res.Data.skipUpload) { setFileData(res.Data, fileObj, that) reset(that) } else { that.overlayShow = true uploadByPieces({ file: fileObj, pieceSize: 3, uploadId: res.Data.uploadId, success: (result) => { if (result.skipUpload) { setFileData(result, fileObj, that) reset(that) } }, error: (e) => { that.$_toast(e); // 打印报错信息 reset(that) }, uploading: (chunk, allChunk) => { that.processflag = true; // 上传时进度条展示 根据需要添加 let st = Math.floor((chunk / allChunk) * 100); // 这里是用上传的第几片除以总片数进行百分比计算 that.percentage = st; }, }) } } }) }) }, // 处理上传后的回显 preview(data) {} } } </script> <style lang="scss" scoped> .content { flex-wrap: wrap; .other-file { background-color: #f7f8fa; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center; span { display: inline-block; width: 100%; margin-top: 8px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } } .van-progress { top: 48%; } } .upload-text { position: absolute; top: 40%; left: 50%; transform: translate(-50%, 0); color: var(--global); font-size: var(--font16); background: #1989fa; padding: 3px 10px; border-radius: 5px; } </style>
部分方法、文件的预览等可自行实现,这里没给出。主要的上传部分都已展示。
当做回显时,如果后台返回的文件流形式,一般采用直接拼接方式(实际也是接口调取)后台需配置网关忽略来展示。浙里办不支持responseType: 'blob'类的直接请求(并未明文指出,经过我们大量实践总结)。
如:READ_FILE_URL: 'https://xxxxxxxx/xxxx/xxxx/xxx/ossShow?fileUrl=',
使用:
<template> <div class="container"> <van-field class="file-field" label-width="5rem" name="uploader" label="文件"> <template #input> <UploadFile ref="uploadFile" :has-file="true" /> </template> </van-field> <van-button class="submit" @click="submit">提交</van-button> </div> </template> <script> import UploadFile from '@/components/uploadFile' export default { components: { UploadFile }, methods: { async submit() { const arr = await this.$refs.uploadFile.getUrls() console.log('arr', arr); } } } </script> <style lang="scss"> .file-field { .van-field__control { flex-wrap: wrap; } .van-uploader { display: inline-block; } .file-item { display: inline-block; position: relative; margin: 0 5px 5px 0; box-sizing: border-box; img, video { width: 100%; height: 100%; } } .upload-box { display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 0 5px 5px 0; background-color: #f7f8fa; position: relative; box-sizing: border-box; #fileUpload { width: 100%; height: 100%; position: absolute; top: 0; left: 0; overflow: hidden; cursor: pointer; opacity: 0; } } } .submit { width: 90%; margin-left: 5%; margin-top: 20px; } </style>
获取到的最终数据: