鸿蒙OS&UniApp 开发的文件上传与下载功能#三方框架 #Uniapp

发布于:2025-05-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

使用 UniApp 开发的文件上传与下载功能

前言

在移动应用开发中,文件上传与下载是非常常见且重要的功能需求。无论是上传用户头像、提交表单附件,还是下载资源文件、缓存图片,这些需求几乎存在于每一个成熟的应用中。UniApp 作为一个跨平台开发框架,提供了丰富的 API 来支持文件的上传与下载操作,使开发者能够便捷地实现相关功能。

本文将详细介绍如何在 UniApp 中实现文件上传与下载功能,包括基本使用方法、进度监控、断点续传等高级特性,并提供实际案例代码,帮助开发者快速掌握这些功能的开发技巧。

UniApp 文件操作基础

在深入讲解上传下载功能前,我们先来了解 UniApp 中与文件操作相关的几个基础 API:

  1. uni.uploadFile: 上传文件到服务器
  2. uni.downloadFile: 下载文件
  3. uni.saveFile: 保存文件到本地
  4. uni.chooseImage: 从相册选择图片或使用相机拍照
  5. uni.chooseVideo: 选择视频
  6. 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 本身并没有直接提供断点续传功能,但我们可以结合服务端实现:

  1. 前端分片上传:将大文件分成多个小块
  2. 记录已上传的分片
  3. 上传中断后,只上传未完成的分片

下面是一个简化的分片上传实现:

// 文件分片上传示例
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. 上传大文件失败

问题:上传大文件时经常会失败。
解决方案

  1. 实现上文中提到的分片上传和断点续传
  2. 检查服务器的上传大小限制
  3. 检查网络连接稳定性

2. 不同平台的兼容性问题

问题:在不同平台(iOS、Android、H5)上文件操作API的行为可能有差异。
解决方案

  1. 使用条件编译处理平台差异
  2. 使用 uni.getSystemInfo() 检测平台
  3. 测试各个平台的表现
// 条件编译示例
// #ifdef H5
// H5 平台特有代码
uni.chooseFile({
  count: 1,
  success: (res) => {
    // ...
  }
});
// #endif

// #ifdef APP-PLUS
// App 平台特有代码
uni.chooseImage({
  count: 1,
  success: (res) => {
    // ...
  }
});
// #endif

3. 文件类型限制

问题:无法指定允许用户选择的具体文件类型。
解决方案

  • 在 H5 平台,可以使用 uni.chooseFileextension 参数
  • 在 App 平台,需要通过 plus.io API 来实现更细粒度的控制
  • 也可以在选择文件后进行类型检查,不符合要求则给出提示

4. 下载文件后无法正确打开

问题:某些文件下载后无法通过 uni.openDocument 正确打开。
解决方案

  1. 检查文件类型是否受支持
  2. 对于特定文件,可能需要安装第三方应用来打开
  3. 在 App 平台,可以使用原生 API 提供更多打开方式

性能优化与最佳实践

  1. 使用缓存策略:对于频繁下载的文件,可以实现缓存机制,避免重复下载。

  2. 优化上传文件大小:在上传前压缩图片或其他可压缩的文件,减少传输时间和带宽消耗。

// 压缩图片示例
uni.compressImage({
  src: tempFilePath,
  quality: 80,
  success: (res) => {
    // 使用压缩后的图片路径上传
    this.uploadFile(res.tempFilePath);
  }
});
  1. 批量处理优化:对于批量上传或下载,可以控制并发数量,避免同时进行太多请求。

  2. 错误重试机制:添加自动重试逻辑,提高操作的成功率。

// 带重试的下载示例
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();
}
  1. 合理的进度反馈:避免在界面上频繁更新进度,可以设置节流,如每秒更新几次。

  2. 安全性考虑:校验文件类型和大小,防止用户上传恶意文件。

总结

本文详细介绍了如何在 UniApp 中实现文件上传与下载功能,从基本用法到高级特性,再到实战案例,希望能帮助开发者在实际项目中更好地处理文件操作需求。文件上传下载虽然是常见功能,但要做好做全面并不简单,需要考虑用户体验、性能优化、安全性等多方面因素。

在实际开发中,建议根据具体的业务需求和目标平台特点,灵活运用本文提供的方法,打造出更加完善的文件处理功能。

你可能还需要了解服务端如何处理文件上传和分片合并,这部分内容因为涉及后端开发,本文未做详细展开。如有需要,可查阅相关服务端技术文档。


网站公告

今日签到

点亮在社区的每一天
去签到