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% 接口问题》:
-
Token 自动携带:从 Storage 中获取 token,添加到 Authorization 请求头
-
Loading 智能控制 :支持通过
hideLoading
参数控制是否显示加载中 -
响应统一拦截:
-
HTTP 状态码判断(200-299 为成功)
-
业务状态码处理(401 跳转登录页,其他错误提示)
-
网络错误捕获(超时、断网等场景)
- 请求任务管理 :通过
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;
}