详细介绍:【2025最新】uniapp 中基于 request 封装实现多文件上传完整指南

uniapp 中基于 request 封装实现多文件上传完整指南

在 uniapp 开发中,文件上传和下载是常见需求,比如头像上传、报表下载等场景。本文将基于已有的 request 请求封装(包含 Token 携带、Loading 显示、错误拦截等核心能力),扩展实现单文件上传、多文件上传功能,让代码更具复用性和可维护性。

一、前置基础:已封装的 request 核心逻辑回顾

先回顾我们已有的 request 封装核心能力,该能力是后续扩展文件操作的基础;若有其他疑问,可直接查阅《【2025最新】UniApp request 请求全方位封装指南:从基础到进阶,解决 90% 接口问题》

  1. Token 自动携带:从 Storage 中获取 token,添加到 Authorization 请求头

  2. Loading 智能控制:支持通过hideLoading参数控制是否显示加载中

  3. 响应统一拦截

  • HTTP 状态码判断(200-299 为成功)

  • 业务状态码处理(401 跳转登录页,其他错误提示)

  • 网络错误捕获(超时、断网等场景)

  1. 请求任务管理:通过requestTasks存储任务,支持取消请求

接下来,我们基于这个封装,分别实现单文件上传、多文件上传和下载功能。

二、文件上传实现:单文件 + 多文件统一封装

uniapp 的uni.uploadFile默认支持单文件上传,要实现多文件上传,需通过循环调用 + Promise.all 统一管理请求,同时保持与现有 request 封装的能力对齐(Token、Loading、错误处理等)。

1. 适配多文件的 uploadFile 封装(支持单 / 多文件)

在原有请求工具类中,修改uploadFile方法,使其同时支持单文件路径(String)和多文件路径数组(Array):

// 存储请求任务(与request共用,用于取消请求)
const requestTasks = new Map();
let requestId = 0;
/**
 * 单/多文件上传统一封装
 * @param {Object} options - 上传配置
 * @param {string} options.url - 上传接口地址
 * @param {string|Array} options.filePaths - 单文件路径/多文件路径数组
 * @param {string} [options.name='file'] - 后端接收文件的字段名(多文件时后端需支持数组接收,如file[])
 * @param {Object} [options.formData={}] - 额外的表单数据(所有文件共用)
 * @param {boolean} [options.hideLoading=false] - 是否隐藏loading
 * @param {Object} [options.header={}] - 自定义请求头
 * @returns {Promise} - 上传结果Promise(多文件时返回结果数组,顺序与filePaths一致)
 */
export function uploadFile(options = {}) {
  // 生成唯一请求ID(多文件时共用一个父ID,子任务用ID+索引区分)
  requestId += 1;
  const parentRequestId = requestId;
  // 1. 合并默认配置与用户配置
  const finalOptions = {
    name: 'file', // 后端默认接收字段名,多文件时建议后端用file[]接收
    formData: {},
    hideLoading: false,
    header: {},
    ...options
  };
  // 2. 校验文件路径:统一转为数组格式(方便后续统一处理)
  if (!finalOptions.filePaths) {
    return Promise.reject(new Error('请传入filePaths(单文件路径或多文件路径数组)'));
  }
  const filePathsArr = Array.isArray(finalOptions.filePaths)
    ? finalOptions.filePaths
    : [finalOptions.filePaths];
  if (filePathsArr.length === 0) {
    return Promise.reject(new Error('filePaths不能为空'));
  }
  // 3. 自动携带Token(与request逻辑一致)
  const token = uni.getStorageSync('token');
  if (token) {
    finalOptions.header.Authorization = `Bearer ${token}`;
  }
  // 4. 显示Loading(多文件时只显示一个全局Loading,避免重复弹窗)
  if (!finalOptions.hideLoading) {
    uni.showLoading({
      title: filePathsArr.length > 1 ? '多文件上传中...' : '上传中...',
      mask: true
    });
  }
  // 5. 定义单个文件的上传逻辑(多文件时循环调用)
  const uploadSingleFile = (filePath, index) => {
    const childRequestId = `${parentRequestId}_${index}`; // 子任务ID(父ID+索引)
    return new Promise((resolve, reject) => {
      const task = uni.uploadFile({
        url: finalOptions.url,
        filePath,
        name: finalOptions.name,
        formData: finalOptions.formData,
        header: finalOptions.header,
        // 单个文件上传成功
        success: (res) => {
          const responseData = JSON.parse(res.data || '{}');
          // 统一响应拦截(与request逻辑对齐)
          if (res.statusCode >= 200 && res.statusCode < 300) {
            const { code, message, data } = responseData;
            if (code === 200) {
              resolve({
                index, // 标记当前文件在原数组中的索引
                data,  // 业务数据
                originalFilePath: filePath // 原始文件路径
              });
            } else if (code === 401) {
              // 401未授权:只处理一次(多文件时避免重复跳转)
              if (index === 0) {
                uni.removeStorageSync('token');
                uni.redirectTo({ url: '/pages/login/login' });
                uni.showToast({ title: message || '登录已过期,请重新登录', icon: 'none' });
              }
              reject(new Error(`文件${index+1}:${message || '未授权'}`));
            } else {
              reject(new Error(`文件${index+1}:${message || '上传失败'}`));
            }
          } else {
            reject(new Error(`文件${index+1}:HTTP错误${res.statusCode}`));
          }
        },
        // 单个文件上传失败
        fail: (err) => {
          let errMsg = `文件${index+1}:上传网络错误`;
          if (err.errMsg.includes('timeout')) {
            errMsg = `文件${index+1}:上传超时`;
          }
          reject(new Error(errMsg));
        },
        // 单个文件上传完成(清除当前子任务)
        complete: () => {
          requestTasks.delete(childRequestId);
        }
      });
      // 存储子任务(支持单独取消某个文件的上传,或统一取消所有子任务)
      requestTasks.set(childRequestId, task);
    });
  };
  // 6. 多文件时用Promise.all批量处理,单文件时直接调用
  return new Promise((resolve, reject) => {
    Promise.all(filePathsArr.map((filePath, index) => uploadSingleFile(filePath, index)))
      .then((results) => {
        // 所有文件上传成功:按原filePaths顺序返回结果
        resolve(results);
        if (!finalOptions.hideLoading) {
          uni.showToast({
            title: filePathsArr.length > 1 ? '全部文件上传成功' : '文件上传成功'
          });
        }
      })
      .catch((error) => {
        // 任一文件失败则整体 reject(也可改为允许部分成功,根据业务调整)
        reject(error);
        if (!finalOptions.hideLoading) {
          uni.showToast({ title: error.message, icon: 'none' });
        }
      })
      .finally(() => {
        // 无论成功失败,都关闭全局Loading
        if (!finalOptions.hideLoading) {
          uni.hideLoading();
        }
        // 清除父任务标记(子任务已在各自complete中清除)
        requestTasks.delete(parentRequestId);
      });
  });
}
// 取消上传请求(支持取消单个子任务或所有子任务)
export function cancelRequest(requestId) {
  // 若为父ID(如1),则取消所有子任务(1_0、1_1...)
  const isParentId = !requestId.includes('_');
  if (isParentId) {
    Array.from(requestTasks.keys()).forEach(key => {
      if (key.startsWith(`${requestId}_`)) {
        requestTasks.get(key).abort();
        requestTasks.delete(key);
      }
    });
    uni.showToast({ title: '已取消所有文件上传', icon: 'none' });
  } else if (requestTasks.has(requestId)) {
    // 若为子ID(如1_0),则取消单个文件上传
    requestTasks.get(requestId).abort();
    requestTasks.delete(requestId);
    uni.showToast({ title: '已取消当前文件上传', icon: 'none' });
  }
}

2. 单文件上传示例(沿用原场景,适配新封装)

以 “头像上传” 为例,选择单个图片后调用上传:

import { uploadFile } from '@/utils/request.js';
// 选择单个图片并上传
async function chooseAndUploadAvatar() {
  try {
    // 1. 选择单个图片(count设为1)
    const [chooseRes] = await uni.chooseImage({
      count: 1, // 限制只能选1张
      sizeType: ['compressed'], // 优先压缩图
      sourceType: ['album', 'camera']
    });
    // 2. 调用封装的上传方法(filePaths传单个路径字符串)
    const [uploadResult] = await uploadFile({
      url: '/api/user/upload-avatar', // 后端头像上传接口
      filePaths: chooseRes.tempFilePaths[0], // 单文件路径(字符串)
      name: 'avatar', // 后端接收字段名(如avatar)
      formData: { userId: uni.getStorageSync('userId') }, // 额外携带用户ID
      hideLoading: false
    });
    // 3. 上传成功:更新头像显示
    console.log('头像上传成功', uploadResult.data);
    this.avatarUrl = uploadResult.data.avatarUrl; // 假设后端返回头像地址
  } catch (error) {
    console.error('头像上传失败', error);
  }
}

3. 多文件上传示例(新增核心场景)

以 “多图片批量上传(如商品图库)” 为例,支持选择多张图片并统一上传:



  
  
  
  
    
      
      
    
  
  
  
import { uploadFile, cancelRequest } from '@/utils/request.js';
export default {
  data() {
    return {
      selectedFilePaths: [], // 存储已选择的多文件路径数组
      parentRequestId: null // 存储多文件上传的父请求ID(用于取消上传)
    };
  },
  methods: {
    // 1. 选择多文件(count设为5,支持最多选5张)
    async chooseMultiFiles() {
      try {
        const [chooseRes] = await uni.chooseImage({
          count: 5, // 限制最多选5张
          sizeType: ['compressed'],
          sourceType: ['album']
        });
        // 将新选择的文件添加到已选列表(避免覆盖)
        this.selectedFilePaths = [...this.selectedFilePaths, ...chooseRes.tempFilePaths];
      } catch (error) {
        console.error('选择文件失败', error);
      }
    },
    // 2. 删除已选文件
    deleteFile(index) {
      this.selectedFilePaths.splice(index, 1);
    },
    // 3. 批量上传多文件
    async uploadMultiFiles() {
      try {
        // 记录父请求ID(用于后续取消上传)
        this.parentRequestId = requestId; // 注意:需从request工具类导出requestId,或通过返回值获取
        // 调用封装的上传方法(filePaths传数组)
        const uploadResults = await uploadFile({
          url: '/api/goods/upload-images', // 后端多图片上传接口
          filePaths: this.selectedFilePaths, // 多文件路径数组
          name: 'file[]', // 后端需用数组接收(如SpringBoot用@RequestParam("file[]") MultipartFile[] files)
          formData: {
            goodsId: '1001', // 额外携带商品ID
            imageType: 'detail' // 图片类型(如详情图)
          },
          hideLoading: false
        });
        // 4. 上传成功:处理结果(uploadResults顺序与selectedFilePaths一致)
        console.log('所有文件上传成功', uploadResults);
        // 提取后端返回的图片URL列表(示例)
        const imageUrls = uploadResults.map(res => res.data.imageUrl);
        // 提交图片URL到商品接口(后续业务逻辑)
        // await this.submitGoodsImages(imageUrls);
        // 清空已选文件列表
        this.selectedFilePaths = [];
      } catch (error) {
        console.error('多文件上传失败', error);
        // 失败后可保留已选文件,方便用户重新上传
      }
    },
    // 4. 取消多文件上传(可选功能)
    cancelMultiUpload() {
      if (this.parentRequestId) {
        cancelRequest(this.parentRequestId); // 传入父ID,取消所有子任务
        this.selectedFilePaths = [];
        this.parentRequestId = null;
      }
    }
  }
};
/* 简单样式:文件预览列表 */
.upload-container {
  padding: 20rpx;
}
.file-list {
  margin: 20rpx 0;
  display: flex;
  flex-wrap: wrap;
  gap: 20rpx;
}
.file-item {
  width: 160rpx;
  position: relative;
}
.file-preview {
  width: 100%;
  height: 160rpx;
  border-radius: 10rpx;
  border: 1px solid #eee;
}
posted on 2025-11-08 20:49  blfbuaa  阅读(31)  评论(0)    收藏  举报