Ant Design Vue Upload 自定义上传 customRequest,这一篇很详细
'# Ant Design Vue Upload 自定义上传 customRequest,这一篇很详细
一、背景与问题
在实际开发中,文件上传需求往往需要与业务逻辑深度结合。Ant Design Vue 的 Upload 组件虽然提供了丰富的配置项,但其默认的上传行为可能无法满足复杂业务场景的需求。例如:
- 需要将文件上传到非标准的服务器接口(如阿里云OSS、MinIO等)
- 需要实现上传过程的进度跟踪和实时反馈
- 需要对接第三方文件存储服务(如AWS S3、Google Cloud Storage)
- 需要实现文件分片上传、断点续传等高级功能
- 需要自定义上传前的预处理(如文件压缩、格式转换)
在这些场景下,customRequest 配置项提供了极大的灵活性。但开发人员在使用时容易陷入以下误区:
- 误以为
customRequest是简单的请求替换 - 忽略了上传过程中的错误处理机制
- 未正确处理上传状态变更事件
- 忽视了跨域和安全风险
- 未考虑大文件上传时的性能优化
二、基本原理
Ant Design Vue 的 Upload 组件通过 customRequest 配置项实现自定义上传逻辑,其核心原理是:
- 当用户点击上传按钮时,Upload 组件会将文件包装成
FileItem对象 - 调用
customRequest配置函数,该函数接收FileItem对象作为参数 在
customRequest函数中,开发人员可以完全控制上传过程:- 自定义请求参数
- 自定义请求头
- 自定义上传地址
- 自定义上传方式(GET/POST/PUT等)
- 自定义上传超时时间
- 自定义上传进度回调
- 通过
onProgress、onSuccess、onError等回调函数,可以实现上传状态的实时反馈 - 通过
onRemove等方法,可以实现文件的删除和状态更新
三、环境准备
# 安装依赖
npm install ant-design-vue
npm install axios项目结构建议:
src/
├── components/
│ └── UploadCustomRequest.vue
├── services/
│ └── uploadService.js
├── utils/
│ └── fileUtils.js
├── App.vue
└── main.js四、核心实现
1. 基础自定义上传(文件上传到本地服务器)
<template>
<a-upload
:custom-request="handleUpload"
:file-list="fileList"
@change="handleChange"
>
<a-button>上传文件</a-button>
</a-upload>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
fileList: []
};
},
methods: {
handleUpload(options) {
const { file, onProgress, onSuccess, onError } = options;
// 自定义上传逻辑
axios.post('/api/upload', file, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (event) => {
const percent = Math.round((event.loaded / event.total) * 100);
onProgress({ percent, event });
}
})
.then(() => {
onSuccess(file, 'success');
})
.catch((err) => {
onError(err, file);
});
},
handleChange(info) {
if (info.file.status === 'done') {
this.fileList.push(info.file);
}
}
}
};
</script>关键代码解释:
customRequest接收一个函数,该函数接收options参数,包含file、onProgress、onSuccess、onError等方法- 使用 axios 发送 POST 请求,注意设置
Content-Type为multipart/form-data - 通过
onUploadProgress监听上传进度,调用onProgress回调更新进度条 - 使用
onSuccess和onError回调处理上传结果 handleChange方法用于处理文件状态变化,更新文件列表
2. 上传到第三方存储服务(如AWS S3)
// services/uploadService.js
import axios from 'axios';
export const uploadToS3 = (file) => {
return axios.post('/api/upload-to-s3', file, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
};<template>
<a-upload
:custom-request="handleUploadToS3"
:file-list="fileList"
@change="handleChange"
>
<a-button>上传文件</a-button>
</a-upload>
</template>
<script>
import { uploadToS3 } from '@/services/uploadService';
export default {
data() {
return {
fileList: []
};
},
methods: {
handleUploadToS3(options) {
const { file, onProgress, onSuccess, onError } = options;
uploadToS3(file)
.then(() => {
onSuccess(file, 'success');
})
.catch((err) => {
onError(err, file);
});
},
handleChange(info) {
if (info.file.status === 'done') {
this.fileList.push(info.file);
}
}
}
};
</script>关键代码解释:
- 使用封装好的
uploadToS3函数与后端接口通信 - 后端需要处理 AWS S3 的签名和上传逻辑
- 前端需要处理跨域问题,可以通过配置 CORS 或使用代理服务器
3. 大文件分片上传(支持断点续传)
// utils/fileUtils.js
export function sliceFile(file, chunkSize = 1 * 1024 * 1024) {
const chunks = [];
let start = 0;
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
chunks.push(chunk);
start = end;
}
return chunks;
}// services/uploadService.js
import axios from 'axios';
import { sliceFile } from '@/utils/fileUtils';
export const uploadLargeFile = (file, uploadUrl) => {
const chunks = sliceFile(file);
const totalChunks = chunks.length;
return new Promise((resolve, reject) => {
let uploadedChunks = 0;
chunks.forEach((chunk, index) => {
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', index);
formData.append('totalChunks', totalChunks);
axios.post(uploadUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (event) => {
const percent = Math.round((event.loaded / event.total) * 100);
console.log(`Chunk ${index}上传进度: ${percent}%`);
}
})
.then(() => {
uploadedChunks++;
if (uploadedChunks === totalChunks) {
resolve();
}
})
.catch((err) => {
reject(err);
});
});
});
};<template>
<a-upload
:custom-request="handleLargeFileUpload"
:file-list="fileList"
@change="handleChange"
>
<a-button>上传大文件</a-button>
</a-upload>
</template>
<script>
import { uploadLargeFile } from '@/services/uploadService';
export default {
data() {
return {
fileList: []
};
},
methods: {
handleLargeFileUpload(options) {
const { file, onProgress, onSuccess, onError } = options;
uploadLargeFile(file, 'https://api.upload.largefile')
.then(() => {
onSuccess(file, 'success');
})
.catch((err) => {
onError(err, file);
});
},
handleChange(info) {
if (info.file.status === 'done') {
this.fileList.push(info.file);
}
}
}
};
</script>关键代码解释:
- 使用
sliceFile函数将大文件分割为多个小块 - 通过
uploadLargeFile函数处理分片上传 - 每个分片上传完成后更新进度
- 需要后端支持分片上传和合并逻辑
五、完整案例
创建一个完整的文件上传系统,包含前端页面、后端接口和文件存储服务。
前端页面(UploadCustomRequest.vue)
<template>
<div class="upload-container">
<a-upload
:custom-request="handleCustomUpload"
:file-list="fileList"
:before-upload="beforeUpload"
:show-upload-list="false"
@change="handleChange"
>
<a-button type="primary">选择文件</a-button>
</a-upload>
<div v-if="fileList.length > 0">
<h3>已上传文件:</h3>
<ul>
<li v-for="file in fileList" :key="file.uid">
{{ file.name }} - {{ file.status }}
</li>
</ul>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
fileList: [],
uploadProgress: 0
};
},
methods: {
beforeUpload(file) {
const isValid = file.type === 'application/pdf';
if (!isValid) {
this.$message.error('只能上传PDF文件');
return false;
}
return true;
},
handleCustomUpload(options) {
const { file, onProgress, onSuccess, onError } = options;
// 1. 自定义上传地址
const uploadUrl = 'https://api.example.com/upload';
// 2. 自定义请求头
const headers = {
'X-Upload-Type': 'custom',
'Authorization': 'Bearer ' + this.getToken()
};
// 3. 处理大文件时可添加分片逻辑
const isLargeFile = file.size > 10 * 1024 * 1024; // 10MB
if (isLargeFile) {
this.handleLargeFileUpload(file, uploadUrl, onProgress, onSuccess, onError);
return;
}
// 4. 发送普通文件上传
axios.post(uploadUrl, file, {
headers,
onUploadProgress: (event) => {
const percent = Math.round((event.loaded / event.total) * 100);
onProgress({ percent, event });
this.uploadProgress = percent;
}
})
.then(() => {
onSuccess(file, 'success');
})
.catch((err) => {
onError(err, file);
});
},
handleLargeFileUpload(file, uploadUrl, onProgress, onSuccess, onError) {
// 实现分片上传逻辑
// 这里简化处理,实际开发中需要实现分片上传和断点续传
const chunkSize = 1 * 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
const formData = new FormData();
formData.append('file', file);
formData.append('totalChunks', totalChunks);
axios.post(uploadUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (event) => {
const percent = Math.round((event.loaded / event.total) * 100);
onProgress({ percent, event });
this.uploadProgress = percent;
}
})
.then(() => {
onSuccess(file, 'success');
})
.catch((err) => {
onError(err, file);
});
},
handleChange(info) {
if (info.file.status === 'done') {
this.fileList.push(info.file);
} else if (info.file.status === 'error') {
this.$message.error('上传失败');
}
}
}
};
</script>
<style scoped>
.upload-container {
max-width: 600px;
margin: 20px auto;
}
</style>后端接口示例(Node.js + Express)
// server.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const port = 3000;
// 配置multer存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
}
});
const upload = multer({ storage });
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
status: 'success',
message: 'File uploaded successfully',
file: {
name: req.file.originalname,
size: req.file.size,
path: req.file.path
}
});
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});文件存储服务配置
# 创建存储目录
mkdir uploads六、源码解析
Ant Design Vue 的 Upload 组件源码中,customRequest 的实现关键在于 upload 方法:
// ant-design-vue/src/components/upload/index.vue
upload(fileItem, options) {
const {
onProgress,
onSuccess,
onError,
onDone
} = options;
const req = this.customRequest(fileItem, {
onProgress: (e) => {
onProgress(e);
},
onSuccess: (response) => {
onSuccess(response);
},
onError: (err) => {
onError(err);
},
onDone: () => {
onDone();
}
});
return req;
}关键点分析:
customRequest接收一个fileItem对象和配置选项- 通过
onProgress等回调函数传递上传状态 - 返回的
req对象用于管理上传请求的生命周期
七、进阶使用
1. 文件类型校验增强
beforeUpload(file) {
const isValidType = ['pdf', 'doc', 'docx'].includes(file.type.split('/')[1]);
const isValidSize = file.size < 10 * 1024 * 1024; // 10MB
if (!isValidType) {
this.$message.error('只能上传PDF、Word等文件');
return false;
}
if (!isValidSize) {
this.$message.error('文件大小不能超过10MB');
return false;
}
return true;
}2. 前端压缩优化
import { getBase64 } from '@/utils/fileUtils';
beforeUpload(file) {
return new Promise((resolve, reject) => {
getBase64(file).then(base64 => {
const size = base64.length / 1.048576; // 转换为MB
if (size > 10) {
this.$message.error('文件过大,最大支持10MB');
reject();
} else {
resolve();
}
});
});
}3. 结合 Vue 3 Composition API
<script setup>
import { ref } from 'vue';
import axios from 'axios';
const fileList = ref([]);
const uploadProgress = ref(0);
const handleCustomUpload = (options) => {
const { file, onProgress, onSuccess, onError } = options;
axios.post('/api/upload', file, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (event) => {
const percent = Math.round((event.loaded / event.total) * 100);
onProgress({ percent, event });
uploadProgress.value = percent;
}
})
.then(() => {
onSuccess(file, 'success');
})
.catch((err) => {
onError(err, file);
});
};
</script>八、性能与工程实践
1. 大文件上传优化
- 使用分片上传(如Web Worker处理分片逻辑)
- 实现断点续传功能(需要后端支持)
- 使用压缩库(如 compressorjs )压缩图片
- 使用 WebAssembly 加速大文件处理
2. 错误处理机制
handleCustomUpload(options) {
const { file, onError } = options;
try {
// 上传逻辑
} catch (err) {
onError(err, file);
}
}3. 安全加固措施
- 前端校验文件类型和大小
- 后端二次校验文件类型和大小
- 限制上传文件的存储路径
- 使用临时文件存储,上传完成后删除
- 实现上传权限控制(基于用户身份)
4. 跨域问题处理
// 前端配置
axios.defaults.baseURL = '/api'; // 配置代理
// 后端配置CORS
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});九、常见问题与踩坑
1. 上传文件未显示
原因:未正确处理 onSuccess 回调
解决:确保在 onSuccess 中调用 onSuccess(file, 'success')
2. 上传进度条不更新
原因:未正确使用 onProgress 回调
解决:确保 onProgress 接收的参数包含进度百分比
3. 文件类型校验失效
原因:前端校验未处理文件对象的 type 属性
解决:使用 file.type.split('/') 获取 MIME 类型
4. 大文件上传超时
原因:未设置合适的超时时间
解决:在 axios 请求中设置 timeout 参数
5. 跨域问题
原因:未配置 CORS 头信息
解决:在后端接口中设置 Access-Control-Allow-Origin 等头信息
十、最佳实践
1. 推荐使用场景
- 需要对接第三方存储服务(如 AWS S3、阿里云OSS)
- 需要实现文件分片上传、断点续传功能
- 需要自定义上传参数和请求头
- 需要实现文件类型、大小校验
- 需要实时上传进度反馈
2. 不推荐使用场景
- 上传逻辑简单(如直接上传到本地服务器)
- 需要快速开发的简单项目
- 不需要复杂上传功能的场景
- 需要支持多文件上传的场景(可使用
multiple属性)
3. 推荐实现方式
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 普通文件上传 | 自定义 request | 灵活控制上传流程 |
| 大文件上传 | 分片上传 | 支持断点续传 |
| 多文件上传 | multiple 属性 | 简化代码 |
| 安全性要求高 | 前后端双重校验 | 防止文件类型欺骗 |
| 需要进度反馈 | onProgress 回调 | 实时显示上传进度 |
十一、总结
Ant Design Vue 的 customRequest 配置项提供了强大的自定义上传能力,但其使用需要深入理解上传流程和错误处理机制。在实际开发中,需要根据具体业务需求选择合适的实现方式:
- 对于简单场景,可直接使用默认配置
- 对于复杂业务,应充分利用
customRequest的灵活性 - 对于大文件上传,应考虑分片上传和断点续传
- 对于安全性要求高的场景,应实现前后端双重校验
- 对于性能敏感的场景,应考虑压缩优化和分片上传
开发过程中需要注意以下关键点:
- 正确处理上传状态变更事件
- 实现完善的错误处理机制
- 处理跨域和安全问题
- 优化大文件上传性能
- 遵循最佳实践,避免重复造轮子
通过合理使用 customRequest,可以构建灵活、安全、高效的文件上传系统,满足各种复杂的业务需求。
评论已关闭