uniapp基于uni-file-picker,实现单文件直传和大文件分片上传
分片上传文件需要后端接口配合的,万变不离其中,大概思路应该都是一样的
思路 (只能自己手搓,因为接口可能存在差异,无法C V 直接使用)
1.小文件直传就不多说了
2.大文件上传,首先需要一个算法对文件进行加密得到一个文件id(我使用的SparkMD5),文件id用于后续判断文件是否上传过了,以及断点续传。拿到文件id后调用后端文件登记接口,把文件的基础信息和id传过去,后续文件分片上传会用到。
// 生成文件id
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
fileReader.onload = (e) => {
spark.append(e.target?.result)
resolve(spark.end())
uni.hideLoading()
}
fileReader.onerror = () => {
reject('生成文件hash值失败')
uni.hideLoading()
}
uni.showLoading({
title: '文件处理中...'
})
fileReader.readAsArrayBuffer(file)
})
// initUploadFile 登记文件接口
let res = await initUploadFile({
fileMd5,
fileName,
fileSize
});
3.文件登记成功后,一般会返回文件在服务器的状态信息,如:当前已上传的文件分片信息,是否已完成上传,已完成上传会直接返回文件地址
// 接口返回数据
fileInfo = {
exitPartList: [], // 已上传文件列表
totalSlices: 50, // 总共多少片
sliceSize: 5, // 每片大小
uploadStatus: 1, // 1完成上传 0表示未上传或未完成上传
fileUrl: 'https://*********', // 上传完成会返回文件地址
....其他信息
}
if (uploadStatus == 1) {
// 直接把数据返回
return fileInfo
} else {
// uploadStatus == 0 则需要上传或断点续传
// 自己的上传逻辑 见 4
}
4.开始文件分片上传。文件分片使用file.slice(),这里要注意下自己的文件结构,不然会报错。
uni-file-picker 的文件结构是这样的,分割的时候取file
// 当前第几个切片
const startChunks = exitPartList.length ? exitPartList[exitPartList.length - 1].partNumber + 1 : 1;
// 总切片
const totalChunks = Number(totalSlices);
console.log(startChunks, '从几个切片开始上传')
console.log(totalChunks, '总切片数')
for (let currentChunk = startChunks; currentChunk <= totalChunks; currentChunk++) {
const start = (currentChunk - 1) * sliceSize; // 计算出已上传的文件的位置
const end = Math.min(start + sliceSize, file.size); // 计算出当前上传分片结束位置
const blobSlice = file.file.slice(start, end); // 分割文件,截取当前分片文件
try {
const params = {
fileBlob: blobSlice, // 分片文件
fileMd5: this.fileInfo.fileHash, // 文件hash值
partNumber: currentChunk // 分片编号
}
const sectionFile = await uploadSliceFile(params) // 文件分片上传接口
if (sectionFile.success) {
console.log(sectionFile, `分片${currentChunk}上传成功`)
} else {
console.log(sectionFile, `第${currentChunk}片上传失败`)
break
}
// 分片文件全部上传成功后,执行合并操作
if (currentChunk == totalChunks) {
const mergeData = await uploadFileMerge(fileMd5) // 合并分片接口,参数一般是SparkMD5生成的文件id
}
} catch (e) {
console.log('分片上传报错', e)
}
}
5.以上是大概逻辑,下面是完整代码
<!-- 文件上传组件 -->
<template>
<view>
<uni-file-picker v-model="fileData" fileMediatype="all" :limit="limit" mode="grid" @select="selectFile"
:auto-upload="false" @progress="progress" @success="success" @delete="deleteFile" @fail="fail">
<slot name="uploadDom"></slot>
</uni-file-picker>
<uni-popup ref="fileProgressPopup" type="center" :animation="false" :is-mask-click="false">
<view class="progress-box">
<view id="text">
<text style="--i:1">文</text>
<text style="--i:2">件</text>
<text style="--i:3">上</text>
<text style="--i:4">传</text>
<text style="--i:5">中</text>
<text style="--i:6">.</text>
<text style="--i:7">.</text>
<text style="--i:8">.</text>
</view>
<view>
<progress :percent="fileInfo.progress" show-info stroke-width="10" />
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import {
uploadFile, // 单文件上传方法
uploadFileSlice, // 文件分片上传方法
uploadFileMerge // 合并分片文件方法
} from '@/utils/request.js'
import SparkMD5 from 'spark-md5'
import {
queryApplicationUploadConfigByAppCodeApi, // 查询文件分片上传配置接口
queryCurrentUploadResultByFileMd5Api, // 查询文件状态接口
initUploadRegisterRecordApi, // 初始化文件接口
} from './api';
export default {
props: {
// 最大文件上传数
limit: {
type: Number,
default: 1
}
},
data() {
return {
fileData: [],
fileInfo: {
customPartStoragePath: "/dev", // 文件存储的环境
fileName: '',
fileSize: '',
file: null, // 上传的文件
progress: 0, // ui显示的进度值
// 文件hash
fileHash: '',
},
chunkSize: null, // 分片大小
maxSize: null, // 直传文件最大50m 超过则分片
sliceSwitch: 0, // 是否开启分片上传(分片功能启用开关,0开启,1关闭)
}
},
async mounted() {
await this.getFileConfig()
},
methods: {
// 文件切片
async section(file, fileInfo) {
const fileArr = await queryCurrentUploadResultByFileMd5Api(this.fileInfo.fileHash)
console.log(fileArr, 'fileArr')
if (!fileArr.success) {
return
}
// exitPartList 已上传分片数组 totalSlices总片数 sliceSize 每片大小
const {
exitPartList,
totalSlices,
sliceSize
} = fileArr.data
// 总片数和当前上传片数一致说明分片都上传完成了,直接合并
if (exitPartList.length == totalSlices) {
const mergeData = await uploadFileMerge(this.fileInfo.fileHash)
return mergeData
}
// 大文件才显示上传进度条
this.$refs.fileProgressPopup.open()
// 当前第几个切片
const startChunks = exitPartList.length ? exitPartList[exitPartList.length - 1].partNumber + 1 : 1;
// 总切片
const totalChunks = Number(totalSlices);
console.log(startChunks, '从几个切片开始上传')
console.log(totalChunks, '总切片数')
this.fileInfo.progress = Math.floor(startChunks / totalChunks * 100)
console.log('初始进度', this.fileInfo.progress)
for (let currentChunk = startChunks; currentChunk <= totalChunks; currentChunk++) {
const start = (currentChunk - 1) * sliceSize; // 计算出已上传的文件大小
const end = Math.min(start + sliceSize, file.size); // 计算出当前上传分片结束位置
// console.log(file, '要分片的文件')
const blobSlice = file.file.slice(start, end); // 分割文件,截取当前分片文件
try {
const params = {
sliceBlob: blobSlice, // 分片文件
fileMd5: this.fileInfo.fileHash, // 文件hash值
partNumber: currentChunk // 分片编号
}
const sectionFile = await uploadFileSlice(params)
this.fileInfo.progress = Math.floor(currentChunk / totalChunks * 100)
console.log('上传进度', this.fileInfo.progress)
if (sectionFile.success) {
console.log(sectionFile, `分片${currentChunk}上传成功`)
} else {
console.log(sectionFile, `第${currentChunk}片上传失败`)
break
}
if (currentChunk == totalChunks) {
// 查询分片文件是否全部上传成功
const mergeData = await uploadFileMerge(this.fileInfo.fileHash)
this.$refs.fileProgressPopup.close()
return mergeData
}
} catch (e) {
console.log('分片上传报错', e)
}
}
},
async fileSharding(file) {
const fileInfo = await this.uploadCheck(this.fileInfo)
console.log(fileInfo, 'fileInfo')
// uploadStatus:1完成上传 0表示未上传或未完成上传
if (fileInfo.uploadStatus == 1) {
// uploadStatus == 1 接口会返回文件的地址以及其他信息, 直接return即可
return {
success: true,
data: fileInfo
}
} else {
// uploadStatus == 0 则需要上传或断点续传
return this.section(file, fileInfo)
}
},
// 在静态资源服务器初始化文件信息,保存MD5加密信息,用于后续检查文件是否存在或是否上传完成
async uploadCheck(params) {
const {
fileHash,
fileName,
fileSize,
customPartStoragePath
} = params;
let res = await initUploadRegisterRecordApi({
fileMd5: fileHash,
fileName,
fileSize,
customPartStoragePath
});
// 返回值 uploadStatus:1完成上传 0表示未上传或未完成上传
return res.data;
},
// 整个文件使用spark-md5生成一个唯一id值,这个id改文件名是不会变的,只有改文件内容才会变
getFileMd5(file) {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
fileReader.onload = (e) => {
spark.append(e.target?.result)
resolve(spark.end())
uni.hideLoading()
}
fileReader.onerror = () => {
reject('生成文件hash值失败')
uni.hideLoading()
}
uni.showLoading({
title: '文件处理中...'
})
fileReader.readAsArrayBuffer(file)
})
},
// 此事件可用于处理文件上传前的效验
selectFile(e) {
this.$emit('selectFile', {
file: e,
type: 'select-file',
selectFileFc: (res) => {
// 处理文件上传自定义事件
if (res === 'cancel') {
// 取消上传
console.log('取消文件上传', e)
console.log('uni-file-picker文件列表', this.fileData)
const file = e.tempFiles[0]
// uni-file-picker选择了文件就会在页面显示,取消上传需要手动删除当前上传的数据
this.fileData = this.fileData.filter(item => item.uuid != file.uuid)
} else {
// 执行上传
console.log('执行文件上传')
this.select(e)
}
}
})
},
// 获取上传状态
async select(e) {
this.fileInfo.file = e.tempFiles[0]
const uuid = e.tempFiles[0].uuid
if (!this.fileInfo.file) return; // 文件不存在return
console.log('选择文件:', this.fileInfo.file)
this.fileInfo.fileName = this.fileInfo.file.name
this.fileInfo.fileSize = this.fileInfo.file.size
// 文件大于阈值,且开启了分片上传
if (this.fileInfo.file.size > this.maxSize && this.sliceSwitch == 0) {
console.log('分片')
// 文件hash值
this.fileInfo.fileHash = await this.getFileMd5(this.fileInfo.file.file)
console.log(this.fileInfo.fileHash, "文件生成的hash值")
if (this.fileInfo.fileHash) {
const res = await this.fileSharding(this.fileInfo.file)
console.log('分片上传结果', res)
if (res.success) {
console.log('分片上传结果', res)
res.data.uuid = uuid
this.$emit('uploadFileSuccess', res)
} else {
this.$emit('uploadFileError', res)
}
}
} else {
console.log('不分片')
uni.showLoading({
title: '文件上传中...'
})
const res = await uploadFile(this.fileInfo.file.path)
uni.hideLoading()
console.log(res, '不分片上传结果')
if (res.success) {
res.data.url = res.data.fileUrl
res.data.uuid = uuid
this.$emit('uploadFileSuccess', res)
} else {
this.$emit('uploadFileError', res)
}
}
},
// 获取上传进度
progress(e) {
console.log('上传进度:', e)
},
// 上传成功
success(e) {
console.log('上传成功')
},
// 上传失败
fail(e) {
console.log('上传失败:', e)
},
deleteFile(e) {
console.log('删除文件', e)
this.$emit('fileRemove', e)
},
async getFileConfig() {
const res = await queryApplicationUploadConfigByAppCodeApi()
if (res.success) {
// //服务器配置的单文件最大值(byte)
this.maxSize = res.data.limitFileSize || 50 * 1024 * 1024;
this.chunkSize = res.data.sliceSize || 5 * 1024 * 1024
this.sliceSwitch = res.data.sliceSwitch;
}
return res
}
}
}
</script>
<style>
.progress-box {
width: calc(100vw - 80rpx);
background-color: #fff;
padding: 40rpx 20rpx;
border-radius: 12rpx;
}
/* 文本盒子样式 */
#text {
display: flex;
align-items: center;
justify-content: center;
}
/* 设置动画 */
@keyframes donghua {
0 {
transform: translateY(0rpx);
}
20% {
transform: translateY(-20rpx);
}
40%,
100% {
transform: translateY(0rpx);
}
}
#text text {
display: inline-block;
animation: donghua 2s ease-in-out infinite;
animation-delay: calc(.1s*var(--i));
padding: 0 4rpx;
}
</style>