使用 UniApp 开发的文件上传与下载功能
前言
在移动应用开发中,文件上传与下载是非常常见且重要的功能需求。无论是上传用户头像、提交表单附件,还是下载资源文件、缓存图片,这些需求几乎存在于每一个成熟的应用中。UniApp 作为一个跨平台开发框架,提供了丰富的 API 来支持文件的上传与下载操作,使开发者能够便捷地实现相关功能。
本文将详细介绍如何在 UniApp 中实现文件上传与下载功能,包括基本使用方法、进度监控、断点续传等高级特性,并提供实际案例代码,帮助开发者快速掌握这些功能的开发技巧。
UniApp 文件操作基础
在深入讲解上传下载功能前,我们先来了解 UniApp 中与文件操作相关的几个基础 API:
uni.uploadFile
: 上传文件到服务器uni.downloadFile
: 下载文件uni.saveFile
: 保存文件到本地uni.chooseImage
: 从相册选择图片或使用相机拍照uni.chooseVideo
: 选择视频uni.chooseFile
: 选择文件(仅支持特定平台)
这些 API 为我们实现文件上传下载功能提供了基础支持。接下来,我们将详细探讨如何利用这些 API 实现具体功能。
文件上传功能实现
基本文件上传
最简单的文件上传功能可以通过 uni.uploadFile
方法来实现。以上传图片为例:
// 选择图片
uni.chooseImage({
count: 1, // 默认9
success: (chooseImageRes) => {
const tempFilePaths = chooseImageRes.tempFilePaths;
// 上传图片
uni.uploadFile({
url: 'https://your-server-url/upload', // 仅为示例,非真实接口地址
filePath: tempFilePaths[0],
name: 'file',
formData: {
'user': 'test'
},
success: (uploadFileRes) => {
console.log('上传成功', uploadFileRes.data);
// 可以在这里处理服务器返回的数据
},
fail: (error) => {
console.error('上传失败', error);
}
});
}
});
上传进度监控
在实际应用中,特别是上传大文件时,我们通常需要向用户展示上传进度。UniApp 提供了 onProgressUpdate
回调函数来监控上传进度:
const uploadTask = uni.uploadFile({
url: 'https://your-server-url/upload',
filePath: tempFilePaths[0],
name: 'file',
success: (res) => {
console.log('上传成功', res.data);
}
});
uploadTask.onProgressUpdate((res) => {
console.log('上传进度', res.progress);
console.log('已经上传的数据长度', res.totalBytesSent);
console.log('预期需要上传的数据总长度', res.totalBytesExpectedToSend);
// 更新界面上的进度条
this.uploadProgress = res.progress;
});
多文件上传
在很多场景下,我们需要同时上传多个文件。这可以通过循环调用 uni.uploadFile
或使用 Promise.all
来实现:
// 选择多张图片
uni.chooseImage({
count: 9,
success: async (chooseImageRes) => {
const tempFilePaths = chooseImageRes.tempFilePaths;
const uploadPromises = tempFilePaths.map(filePath => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'https://your-server-url/upload',
filePath: filePath,
name: 'file',
success: (res) => {
resolve(res);
},
fail: (error) => {
reject(error);
}
});
});
});
try {
const results = await Promise.all(uploadPromises);
console.log('所有文件上传成功', results);
} catch (error) {
console.error('文件上传失败', error);
}
}
});
断点续传实现
对于大文件上传,断点续传是一个非常有用的功能,可以在网络中断后继续上传,而不必重新开始。UniApp 本身并没有直接提供断点续传功能,但我们可以结合服务端实现:
- 前端分片上传:将大文件分成多个小块
- 记录已上传的分片
- 上传中断后,只上传未完成的分片
下面是一个简化的分片上传实现:
// 文件分片上传示例
export default {
data() {
return {
chunkSize: 1024 * 1024, // 1MB一个分片
uploadedChunks: [], // 已上传的分片索引
totalChunks: 0, // 总分片数
fileId: '', // 文件唯一标识
}
},
methods: {
// 选择文件并开始上传
async chooseAndUploadFile() {
// 选择文件(H5平台示例)
uni.chooseFile({
count: 1,
extension: ['.zip', '.doc', '.pdf'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
const file = res.tempFiles[0];
// 生成文件唯一标识
this.fileId = this.generateFileId(file.name, file.size);
// 计算分片数量
this.totalChunks = Math.ceil(file.size / this.chunkSize);
// 检查已上传的分片
this.checkUploadedChunks(this.fileId).then(() => {
// 开始上传未完成的分片
this.uploadChunks(tempFilePath, file);
});
}
});
},
// 生成文件ID
generateFileId(fileName, fileSize) {
return `${fileName}-${fileSize}-${Date.now()}`;
},
// 检查已上传的分片
async checkUploadedChunks(fileId) {
try {
const res = await uni.request({
url: 'https://your-server-url/check-chunks',
data: { fileId }
});
if (res.data.code === 0) {
this.uploadedChunks = res.data.data.uploadedChunks || [];
}
} catch(error) {
console.error('检查已上传分片失败', error);
this.uploadedChunks = [];
}
},
// 上传分片
async uploadChunks(filePath, file) {
// 此处仅为示例逻辑,实际实现可能需要根据平台特性调整
// 在H5平台可以使用File API进行分片,其他平台可能需要其他方式
for (let i = 0; i < this.totalChunks; i++) {
// 如果分片已上传,则跳过
if (this.uploadedChunks.includes(i)) {
continue;
}
try {
// 这里简化处理,实际中可能需要读取文件分片内容
await this.uploadChunk(filePath, i, this.fileId);
// 记录已上传的分片
this.uploadedChunks.push(i);
// 保存上传进度,用于断点续传
uni.setStorageSync(`upload-progress-${this.fileId}`, {
uploadedChunks: this.uploadedChunks,
totalChunks: this.totalChunks
});
} catch (error) {
console.error(`分片${i}上传失败`, error);
break;
}
}
// 检查是否所有分片都已上传
if (this.uploadedChunks.length === this.totalChunks) {
// 通知服务器合并分片
this.mergeChunks(this.fileId, file.name);
}
},
// 上传单个分片
uploadChunk(filePath, chunkIndex, fileId) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'https://your-server-url/upload-chunk',
filePath: filePath, // 实际应该是分片后的文件路径
name: 'chunk',
formData: {
chunkIndex,
fileId,
totalChunks: this.totalChunks
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(res);
}
},
fail: reject
});
});
},
// 通知服务器合并分片
async mergeChunks(fileId, fileName) {
try {
const res = await uni.request({
url: 'https://your-server-url/merge-chunks',
method: 'POST',
data: { fileId, fileName }
});
if (res.data.code === 0) {
console.log('文件上传完成', res.data);
// 清除上传进度缓存
uni.removeStorageSync(`upload-progress-${fileId}`);
}
} catch (error) {
console.error('合并分片失败', error);
}
}
}
}
需要注意的是,上述代码仅为示例,实际实现时需要结合具体的服务端接口和业务需求进行调整。
文件下载功能实现
基本文件下载
使用 uni.downloadFile
可以实现文件下载功能:
uni.downloadFile({
url: 'https://your-server-url/example.pdf',
success: (res) => {
if (res.statusCode === 200) {
console.log('下载成功', res.tempFilePath);
// 保存文件到本地
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
console.log('文件保存成功', saveRes.savedFilePath);
// 可以使用 savedFilePath 在应用内查看文件
}
});
}
}
});
下载进度监控
与上传类似,下载也可以监控进度:
const downloadTask = uni.downloadFile({
url: 'https://your-server-url/example.pdf',
success: (res) => {
console.log('下载完成', res);
}
});
downloadTask.onProgressUpdate((res) => {
console.log('下载进度', res.progress);
console.log('已经下载的数据长度', res.totalBytesWritten);
console.log('预期需要下载的数据总长度', res.totalBytesExpectedToWrite);
// 更新界面上的进度条
this.downloadProgress = res.progress;
});
文件下载与打开
下载完成后,我们常常需要打开文件供用户查看。UniApp 提供了 uni.openDocument
方法来打开文件:
uni.downloadFile({
url: 'https://your-server-url/example.pdf',
success: (res) => {
if (res.statusCode === 200) {
// 打开文档
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true, // 是否显示菜单
success: () => {
console.log('打开文档成功');
}
});
}
}
});
实战案例:多媒体文件管理器
下面是一个集成了上传、下载和文件管理功能的实例,实现一个简易的多媒体文件管理器:
<template>
<view class="container">
<view class="header">
<view class="title">文件管理器</view>
<view class="actions">
<button type="primary" size="mini" @click="chooseAndUploadFile">上传文件</button>
</view>
</view>
<!-- 上传进度展示 -->
<view class="progress-section" v-if="showUploadProgress">
<text>上传进度: {{ uploadProgress }}%</text>
<progress :percent="uploadProgress" stroke-width="4" />
<button size="mini" @click="cancelUpload">取消</button>
</view>
<!-- 文件列表 -->
<view class="file-list">
<view class="file-item" v-for="(item, index) in fileList" :key="index">
<view class="file-info">
<image class="file-icon" :src="getFileIcon(item.fileType)"></image>
<view class="file-detail">
<text class="file-name">{{ item.fileName }}</text>
<text class="file-size">{{ formatFileSize(item.fileSize) }}</text>
</view>
</view>
<view class="file-actions">
<button size="mini" @click="downloadFile(item)">下载</button>
<button size="mini" type="warn" @click="deleteFile(item.id)">删除</button>
</view>
</view>
</view>
<!-- 下载进度弹窗 -->
<view class="download-modal" v-if="showDownloadProgress">
<view class="modal-content">
<text>正在下载: {{ currentDownloadFile.fileName }}</text>
<progress :percent="downloadProgress" stroke-width="4" />
<text>{{ downloadProgress }}%</text>
<button type="primary" size="mini" @click="cancelDownload">取消</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
fileList: [],
uploadProgress: 0,
downloadProgress: 0,
showUploadProgress: false,
showDownloadProgress: false,
currentUploadTask: null,
currentDownloadTask: null,
currentDownloadFile: {}
}
},
onLoad() {
// 加载文件列表
this.loadFileList();
},
methods: {
// 加载文件列表
async loadFileList() {
try {
const res = await uni.request({
url: 'https://your-server-url/files',
method: 'GET'
});
if (res.data.code === 0) {
this.fileList = res.data.data.files || [];
}
} catch (error) {
console.error('获取文件列表失败', error);
uni.showToast({
title: '获取文件列表失败',
icon: 'none'
});
}
},
// 选择并上传文件
chooseAndUploadFile() {
// 由于平台差异,这里以H5为例
// 实际开发中需要根据平台使用不同的API
uni.chooseFile({
count: 1,
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
const file = res.tempFiles[0];
this.uploadFile(tempFilePath, file);
}
});
},
// 上传文件
uploadFile(filePath, file) {
this.showUploadProgress = true;
this.uploadProgress = 0;
this.currentUploadTask = uni.uploadFile({
url: 'https://your-server-url/upload',
filePath: filePath,
name: 'file',
formData: {
fileName: file.name,
fileSize: file.size,
fileType: file.type || this.getFileTypeByName(file.name)
},
success: (res) => {
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data);
if (data.code === 0) {
uni.showToast({
title: '上传成功',
icon: 'success'
});
// 刷新文件列表
this.loadFileList();
} else {
throw new Error(data.message || '上传失败');
}
} catch (error) {
uni.showToast({
title: error.message || '上传失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '服务器响应错误',
icon: 'none'
});
}
},
fail: (error) => {
console.error('上传失败', error);
uni.showToast({
title: '上传失败',
icon: 'none'
});
},
complete: () => {
this.showUploadProgress = false;
this.currentUploadTask = null;
}
});
this.currentUploadTask.onProgressUpdate((res) => {
this.uploadProgress = res.progress;
});
},
// 取消上传
cancelUpload() {
if (this.currentUploadTask) {
this.currentUploadTask.abort();
this.showUploadProgress = false;
uni.showToast({
title: '已取消上传',
icon: 'none'
});
}
},
// 下载文件
downloadFile(fileItem) {
this.showDownloadProgress = true;
this.downloadProgress = 0;
this.currentDownloadFile = fileItem;
this.currentDownloadTask = uni.downloadFile({
url: fileItem.downloadUrl,
success: (res) => {
if (res.statusCode === 200) {
// 保存文件
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.showToast({
title: '文件已保存',
icon: 'success'
});
// 打开文件
this.openFile(saveRes.savedFilePath, fileItem.fileType);
},
fail: (error) => {
console.error('保存文件失败', error);
uni.showToast({
title: '保存文件失败',
icon: 'none'
});
}
});
}
},
fail: (error) => {
console.error('下载失败', error);
uni.showToast({
title: '下载失败',
icon: 'none'
});
},
complete: () => {
this.showDownloadProgress = false;
this.currentDownloadTask = null;
}
});
this.currentDownloadTask.onProgressUpdate((res) => {
this.downloadProgress = res.progress;
});
},
// 取消下载
cancelDownload() {
if (this.currentDownloadTask) {
this.currentDownloadTask.abort();
this.showDownloadProgress = false;
uni.showToast({
title: '已取消下载',
icon: 'none'
});
}
},
// 打开文件
openFile(filePath, fileType) {
uni.openDocument({
filePath: filePath,
fileType: this.getDocumentFileType(fileType),
success: () => {
console.log('打开文档成功');
},
fail: (error) => {
console.error('打开文档失败', error);
uni.showToast({
title: '无法打开此类型文件',
icon: 'none'
});
}
});
},
// 删除文件
async deleteFile(fileId) {
try {
uni.showModal({
title: '提示',
content: '是否确认删除此文件?',
success: async (res) => {
if (res.confirm) {
const deleteRes = await uni.request({
url: 'https://your-server-url/delete-file',
method: 'POST',
data: { fileId }
});
if (deleteRes.data.code === 0) {
uni.showToast({
title: '删除成功',
icon: 'success'
});
// 刷新文件列表
this.loadFileList();
} else {
throw new Error(deleteRes.data.message || '删除失败');
}
}
}
});
} catch (error) {
console.error('删除文件失败', error);
uni.showToast({
title: error.message || '删除失败',
icon: 'none'
});
}
},
// 根据文件名获取文件类型
getFileTypeByName(fileName) {
const ext = fileName.split('.').pop().toLowerCase();
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp'];
const videoTypes = ['mp4', 'avi', 'mov', 'wmv', 'flv'];
const docTypes = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt'];
if (imageTypes.includes(ext)) return 'image';
if (videoTypes.includes(ext)) return 'video';
if (docTypes.includes(ext)) return 'document';
return 'other';
},
// 获取文件图标
getFileIcon(fileType) {
const iconMap = {
image: '/static/icons/image.png',
video: '/static/icons/video.png',
document: '/static/icons/document.png',
other: '/static/icons/file.png'
};
return iconMap[fileType] || iconMap.other;
},
// 格式化文件大小
formatFileSize(size) {
if (size < 1024) {
return size + 'B';
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + 'KB';
} else if (size < 1024 * 1024 * 1024) {
return (size / (1024 * 1024)).toFixed(2) + 'MB';
} else {
return (size / (1024 * 1024 * 1024)).toFixed(2) + 'GB';
}
},
// 获取文档类型(用于openDocument)
getDocumentFileType(fileType) {
if (fileType === 'document') {
return 'pdf'; // 默认作为PDF处理,实际应根据具体后缀判断
}
return '';
}
}
}
</script>
<style>
.container {
padding: 20rpx;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
}
.progress-section {
margin: 20rpx 0;
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
}
.file-list {
margin-top: 20rpx;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #eee;
}
.file-info {
display: flex;
align-items: center;
}
.file-icon {
width: 60rpx;
height: 60rpx;
margin-right: 20rpx;
}
.file-detail {
display: flex;
flex-direction: column;
}
.file-name {
font-size: 30rpx;
margin-bottom: 6rpx;
}
.file-size {
font-size: 24rpx;
color: #999;
}
.file-actions button {
margin-left: 10rpx;
}
.download-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.modal-content {
width: 80%;
padding: 30rpx;
background-color: #fff;
border-radius: 10rpx;
text-align: center;
}
.modal-content progress {
margin: 20rpx 0;
}
</style>
常见问题与解决方案
1. 上传大文件失败
问题:上传大文件时经常会失败。
解决方案:
- 实现上文中提到的分片上传和断点续传
- 检查服务器的上传大小限制
- 检查网络连接稳定性
2. 不同平台的兼容性问题
问题:在不同平台(iOS、Android、H5)上文件操作API的行为可能有差异。
解决方案:
- 使用条件编译处理平台差异
- 使用
uni.getSystemInfo()
检测平台 - 测试各个平台的表现
// 条件编译示例
// #ifdef H5
// H5 平台特有代码
uni.chooseFile({
count: 1,
success: (res) => {
// ...
}
});
// #endif
// #ifdef APP-PLUS
// App 平台特有代码
uni.chooseImage({
count: 1,
success: (res) => {
// ...
}
});
// #endif
3. 文件类型限制
问题:无法指定允许用户选择的具体文件类型。
解决方案:
- 在 H5 平台,可以使用
uni.chooseFile
的extension
参数 - 在 App 平台,需要通过
plus.io
API 来实现更细粒度的控制 - 也可以在选择文件后进行类型检查,不符合要求则给出提示
4. 下载文件后无法正确打开
问题:某些文件下载后无法通过 uni.openDocument
正确打开。
解决方案:
- 检查文件类型是否受支持
- 对于特定文件,可能需要安装第三方应用来打开
- 在 App 平台,可以使用原生 API 提供更多打开方式
性能优化与最佳实践
使用缓存策略:对于频繁下载的文件,可以实现缓存机制,避免重复下载。
优化上传文件大小:在上传前压缩图片或其他可压缩的文件,减少传输时间和带宽消耗。
// 压缩图片示例
uni.compressImage({
src: tempFilePath,
quality: 80,
success: (res) => {
// 使用压缩后的图片路径上传
this.uploadFile(res.tempFilePath);
}
});
批量处理优化:对于批量上传或下载,可以控制并发数量,避免同时进行太多请求。
错误重试机制:添加自动重试逻辑,提高操作的成功率。
// 带重试的下载示例
function downloadWithRetry(url, maxRetries = 3) {
let retryCount = 0;
function attempt() {
return new Promise((resolve, reject) => {
uni.downloadFile({
url,
success: resolve,
fail: (error) => {
if (retryCount < maxRetries) {
retryCount++;
console.log(`下载失败,第${retryCount}次重试`);
resolve(attempt());
} else {
reject(error);
}
}
});
});
}
return attempt();
}
合理的进度反馈:避免在界面上频繁更新进度,可以设置节流,如每秒更新几次。
安全性考虑:校验文件类型和大小,防止用户上传恶意文件。
总结
本文详细介绍了如何在 UniApp 中实现文件上传与下载功能,从基本用法到高级特性,再到实战案例,希望能帮助开发者在实际项目中更好地处理文件操作需求。文件上传下载虽然是常见功能,但要做好做全面并不简单,需要考虑用户体验、性能优化、安全性等多方面因素。
在实际开发中,建议根据具体的业务需求和目标平台特点,灵活运用本文提供的方法,打造出更加完善的文件处理功能。
你可能还需要了解服务端如何处理文件上传和分片合并,这部分内容因为涉及后端开发,本文未做详细展开。如有需要,可查阅相关服务端技术文档。