Ant Design Vue Upload 自定义上传 customRequest,这一篇很详细

'# Ant Design Vue Upload 自定义上传 customRequest,这一篇很详细

一、背景与问题

在实际开发中,文件上传需求往往需要与业务逻辑深度结合。Ant Design Vue 的 Upload 组件虽然提供了丰富的配置项,但其默认的上传行为可能无法满足复杂业务场景的需求。例如:

  • 需要将文件上传到非标准的服务器接口(如阿里云OSS、MinIO等)
  • 需要实现上传过程的进度跟踪和实时反馈
  • 需要对接第三方文件存储服务(如AWS S3、Google Cloud Storage)
  • 需要实现文件分片上传、断点续传等高级功能
  • 需要自定义上传前的预处理(如文件压缩、格式转换)

在这些场景下,customRequest 配置项提供了极大的灵活性。但开发人员在使用时容易陷入以下误区:

  1. 误以为 customRequest 是简单的请求替换
  2. 忽略了上传过程中的错误处理机制
  3. 未正确处理上传状态变更事件
  4. 忽视了跨域和安全风险
  5. 未考虑大文件上传时的性能优化

二、基本原理

Ant Design Vue 的 Upload 组件通过 customRequest 配置项实现自定义上传逻辑,其核心原理是:

  1. 当用户点击上传按钮时,Upload 组件会将文件包装成 FileItem 对象
  2. 调用 customRequest 配置函数,该函数接收 FileItem 对象作为参数
  3. customRequest 函数中,开发人员可以完全控制上传过程:

    • 自定义请求参数
    • 自定义请求头
    • 自定义上传地址
    • 自定义上传方式(GET/POST/PUT等)
    • 自定义上传超时时间
    • 自定义上传进度回调
  4. 通过 onProgressonSuccessonError 等回调函数,可以实现上传状态的实时反馈
  5. 通过 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 参数,包含 fileonProgressonSuccessonError 等方法
  • 使用 axios 发送 POST 请求,注意设置 Content-Typemultipart/form-data
  • 通过 onUploadProgress 监听上传进度,调用 onProgress 回调更新进度条
  • 使用 onSuccessonError 回调处理上传结果
  • 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;
}

关键点分析:

  1. customRequest 接收一个 fileItem 对象和配置选项
  2. 通过 onProgress 等回调函数传递上传状态
  3. 返回的 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 的灵活性
  • 对于大文件上传,应考虑分片上传和断点续传
  • 对于安全性要求高的场景,应实现前后端双重校验
  • 对于性能敏感的场景,应考虑压缩优化和分片上传

开发过程中需要注意以下关键点:

  1. 正确处理上传状态变更事件
  2. 实现完善的错误处理机制
  3. 处理跨域和安全问题
  4. 优化大文件上传性能
  5. 遵循最佳实践,避免重复造轮子

通过合理使用 customRequest,可以构建灵活、安全、高效的文件上传系统,满足各种复杂的业务需求。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日