鸿蒙OSUniApp 开发的文件上传与下载功能#三方框架 #Uniapp
使用 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, // 默认9success: (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);});}});},// 生成文件IDgenerateFileId(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为例// 实际开发中需要根据平台使用不同的APIuni.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 中实现文件上传与下载功能,从基本用法到高级特性,再到实战案例,希望能帮助开发者在实际项目中更好地处理文件操作需求。文件上传下载虽然是常见功能,但要做好做全面并不简单,需要考虑用户体验、性能优化、安全性等多方面因素。
在实际开发中,建议根据具体的业务需求和目标平台特点,灵活运用本文提供的方法,打造出更加完善的文件处理功能。
你可能还需要了解服务端如何处理文件上传和分片合并,这部分内容因为涉及后端开发,本文未做详细展开。如有需要,可查阅相关服务端技术文档。