2024年最后一篇:uniapp单文件分片上传和直传

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>
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的乌龟哟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值