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

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

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

文章目录

  • [uniapp 中基于 request 封装实现多文件上传完整指南 📁](#uniapp 中基于 request 封装实现多文件上传完整指南 📁)
    • [一、前置基础:已封装的 request 核心逻辑回顾 🔍](#一、前置基础:已封装的 request 核心逻辑回顾 🔍)
    • [二、文件上传实现:单文件 + 多文件统一封装 📤](#二、文件上传实现:单文件 + 多文件统一封装 📤)
      • [1. 适配多文件的 uploadFile 封装(支持单 / 多文件)](#1. 适配多文件的 uploadFile 封装(支持单 / 多文件))
      • [2. 单文件上传示例(沿用原场景,适配新封装)](#2. 单文件上传示例(沿用原场景,适配新封装))
      • [3. 多文件上传示例(新增核心场景)](#3. 多文件上传示例(新增核心场景))

一、前置基础:已封装的 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<string>} 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. 多文件上传示例(新增核心场景)

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

复制代码
<!-- 页面结构:文件选择按钮 + 已选文件预览 + 上传按钮 -->

<view class="upload-container">

  <!-- 选择多文件按钮 -->

  <button @click="chooseMultiFiles" type="primary">选择多张图片</button>

  

  <!-- 已选文件预览(显示图片缩略图 + 删除按钮) -->

  <view class="file-list" v-if="selectedFilePaths.length > 0">

    <view class="file-item" v-for="(path, index) in selectedFilePaths" :key="index">

      <image :src="path" mode="widthFix" class="file-preview"></image>

      <button @click="deleteFile(index)" size="mini" type="warn">删除</button>

    </view>

  </view>

  

  <!-- 批量上传按钮 -->

  <button 

    @click="uploadMultiFiles" 

    type="primary" 

    :disabled="selectedFilePaths.length === 0"

    v-if="selectedFilePaths.length > 0"

  \>

    批量上传({{selectedFilePaths.length}}张)

  </button>

</view>

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;

}
相关推荐
fakaifa4 小时前
【全开源】企业微信SCRM社群营销高级版系统+uniapp前端
uni-app·开源·企业微信·scrm·源码下载·企业微信scrm
棋子一名7 小时前
跑马灯组件 Vue2/Vue3/uni-app/微信小程序
微信小程序·小程序·uni-app·vue·js
游戏开发爱好者88 小时前
BShare HTTPS 集成与排查实战,从 SDK 接入到 iOS 真机调试(bshare https、签名、回调、抓包)
android·ios·小程序·https·uni-app·iphone·webview
2501_916008898 小时前
iOS 26 系统流畅度实战指南|流畅体验检测|滑动顺畅对比
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_9151063210 小时前
苹果软件加固与 iOS App 混淆完整指南,IPA 文件加密、无源码混淆与代码保护实战
android·ios·小程序·https·uni-app·iphone·webview
2501_9159214310 小时前
iOS 26 崩溃日志解析,新版系统下崩溃获取与诊断策略
android·ios·小程序·uni-app·cocoa·iphone·策略模式
2501_9160137413 小时前
iOS 推送开发完整指南,APNs 配置、证书申请、远程推送实现与上架调试经验分享
android·ios·小程序·https·uni-app·iphone·webview
2501_9159090618 小时前
HTML5 与 HTTPS,页面能力、必要性、常见问题与实战排查
前端·ios·小程序·https·uni-app·iphone·html5
草字1 天前
uniapp 防止长表单数据丢失方案,缓存表单填写内容,放置卡退或误操作返回。
前端·javascript·uni-app