【vue+el-upload】当action=“#“,代表不使用默认上传,使用自定义上传,http-request获取文件流

'# 【vue+el-upload】当action="#",代表不使用默认上传,使用自定义上传,http-request获取文件流

一、背景与问题

在Vue项目中使用Element UI的el-upload组件时,开发者常常需要处理文件上传场景。默认情况下,el-upload通过action属性指定上传地址,将文件直接提交到服务器。但某些业务场景下,这种默认行为无法满足需求:

  • 需要自定义上传逻辑(如文件预处理、分片上传、上传中处理)
  • 需要将文件流返回前端进行二次处理(如图片OCR、文件加密)
  • 需要与第三方服务集成(如云存储SDK、文件签名服务)
  • 需要处理上传过程中的动态参数(如临时Token、签名)

在这种情况下,将action设置为#,并使用http-request方法进行自定义上传,是更灵活的选择。

二、基本原理

当action="#"时,el-upload会拦截默认的上传行为,转而调用http-request方法。这个方法接收一个file参数(File对象),开发者需要实现以下核心逻辑:

  1. 文件流处理:通过FileReader读取文件内容,支持多种格式(ArrayBuffer/Text/Blob)
  2. 自定义请求:使用fetch/axios等工具发送HTTP请求,支持自定义headers、body、请求方法
  3. 响应处理:解析服务器返回的响应数据,可能包含文件URL、上传状态、业务数据
  4. 状态同步:通过this.$emit向组件传递上传状态(上传中、成功、失败)

三、环境准备

技术栈

  • 前端:Vue 3 + Element Plus
  • 后端:Node.js + Express(示例用)
  • 文件处理:FileReader、Blob、FormData

依赖安装

npm install axios element-plus

四、核心实现

1. 基础用法:自定义上传

<template>
  <el-upload
    action="#"
    :http-request="uploadFile"
    :on-success="handleSuccess"
    :on-error="handleError"
    :before-upload="beforeUpload"
  >
    <el-button type="primary">上传文件</el-button>
  </el-upload>
</template>

<script>
export default {
  methods: {
    uploadFile(file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const blob = new Blob([e.target.result], { type: file.type });
        // 自定义上传逻辑
        this.uploadToServer(blob);
      };
      reader.readAsArrayBuffer(file);
    },
    uploadToServer(blob) {
      // 模拟上传到服务器
      const formData = new FormData();
      formData.append('file', blob, file.name);
      
      fetch('/api/upload', {
        method: 'POST',
        body: formData
      })
      .then(res => res.json())
      .then(data => {
        this.$emit('success', data.url, file);
      })
      .catch(err => {
        this.$emit('error', err, file);
      });
    },
    handleSuccess(response, file) {
      console.log('上传成功:', response, file);
    },
    handleError(err, file) {
      console.error('上传失败:', err, file);
    },
    beforeUpload(file) {
      const isValid = file.size < 1024 * 1024 * 5; // 5MB
      if (!isValid) {
        this.$message.error('文件大小超过限制');
        return false;
      }
      return true;
    }
  }
}
</script>

关键点解释:

  • http-request接收File对象,通过FileReader读取文件内容
  • 使用Blob对象包装文件内容,支持不同格式
  • 通过fetch发送POST请求,模拟上传到服务器
  • 通过this.$emit向组件传递上传状态

2. 高级用法:文件预处理

uploadToServer(blob) {
  // 1. 压缩图片(仅限图片文件)
  if (file.type.startsWith('image/')) {
    const compressedBlob = this.compressImage(blob, 0.5); // 压缩到50%
    // 2. 添加水印
    const watermarkedBlob = this.addWatermark(compressedBlob);
    // 3. 上传到服务器
    fetch('/api/upload', {
      method: 'POST',
      body: new FormData([
        ['file', watermarkedBlob, 'watermarked.png']
      ])
    })
    .then(res => res.json())
    .then(data => {
      this.$emit('success', data.url, file);
    })
    .catch(err => {
      this.$emit('error', err, file);
    });
  } else {
    fetch('/api/upload', {
      method: 'POST',
      body: new FormData([
        ['file', blob, file.name]
      ])
    })
    .then(...);
  }
}

// 图片压缩函数
compressImage(blob, quality = 0.8) {
  return new Promise((resolve) => {
    const img = new Image();
    const reader = new FileReader();
    reader.onload = (e) => {
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = img.width * quality;
        canvas.height = img.height * quality;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        canvas.toBlob((blob) => resolve(blob), 'image/jpeg', 0.8);
      };
      img.src = e.target.result;
    };
    reader.readAsDataURL(blob);
  });
}

3. 分片上传方案

uploadToServer(blob) {
  const chunkSize = 1 * 1024 * 1024; // 1MB
  const totalChunks = Math.ceil(blob.size / chunkSize);
  
  const uploadChunks = () => {
    let index = 0;
    const uploadNextChunk = () => {
      if (index >= totalChunks) return;
      
      const start = index * chunkSize;
      const end = Math.min(start + chunkSize, blob.size);
      const chunk = blob.slice(start, end);
      
      const formData = new FormData();
      formData.append('file', chunk, `chunk-${index}.jpg`);
      formData.append('chunkIndex', index);
      formData.append('totalChunks', totalChunks);
      
      fetch('/api/upload', {
        method: 'POST',
        body: formData
      })
      .then(res => res.json())
      .then(data => {
        if (data.success) {
          index++;
          uploadNextChunk();
        } else {
          throw new Error('分片上传失败');
        }
      })
      .catch(err => {
        this.$emit('error', err, file);
      });
    };
    
    uploadNextChunk();
  };
  
  uploadChunks();
}

五、完整案例:图片上传到阿里云OSS

项目结构

src/
├── components/
│   └── UploadImage.vue
├── services/
│   └── oss.js
└── main.js

1. 上传组件代码

<template>
  <el-upload
    action="#"
    :http-request="uploadToOSS"
    :on-success="handleSuccess"
    :on-error="handleError"
    :before-upload="beforeUpload"
    :file-list="fileList"
  >
    <el-button type="primary">上传图片</el-button>
  </el-upload>
</template>

<script>
import { uploadToOSS } from '@/services/oss';

export default {
  data() {
    return {
      fileList: []
    };
  },
  methods: {
    uploadToOSS(file) {
      uploadToOSS(file).then(url => {
        this.$emit('success', url, file);
        this.fileList.push({ name: file.name, url });
      }).catch(err => {
        this.$emit('error', err, file);
      });
    },
    handleSuccess(url, file) {
      console.log('上传成功:', url, file);
    },
    handleError(err, file) {
      console.error('上传失败:', err, file);
    },
    beforeUpload(file) {
      const isValid = file.type.startsWith('image/') && file.size < 1024 * 1024 * 5;
      if (!isValid) {
        this.$message.error('仅支持小于5MB的图片文件');
        return false;
      }
      return true;
    }
  }
};
</script>

2. OSS服务代码

// src/services/oss.js
import axios from 'axios';

export async function uploadToOSS(file) {
  // 1. 获取OSS临时签名
  const { data: { accessUrl, uploadId } } = await axios.get('/api/oss/signature');
  
  // 2. 上传文件到OSS
  const reader = new FileReader();
  reader.onload = (e) => {
    const blob = new Blob([e.target.result], { type: file.type });
    
    const formData = new FormData();
    formData.append('key', uploadId); // OSS要求的参数名
    formData.append('file', blob, file.name);
    
    axios.post(accessUrl, formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      }
    })
    .then(res => {
      if (res.data.code === 200) {
        return res.data.url; // 返回OSS文件URL
      }
      throw new Error('OSS上传失败');
    })
    .catch(err => {
      throw new Error('OSS上传失败');
    });
  };
  
  reader.readAsArrayBuffer(file);
}

六、源码解析

1. http-request方法调用流程

// el-upload组件的http-request方法
function httpRequest(file) {
  this.$emit('http-request', file);
  // 其他逻辑
}

当action="#"时,el-upload会调用http-request方法,此时开发者需要实现该方法。

2. 文件流处理关键点

  • 使用FileReader读取文件时,建议使用readAsArrayBuffer处理二进制数据
  • 大文件上传时应使用分片处理,避免内存溢出
  • 对于图片文件,建议使用readAsDataURL进行预览

3. 上传状态同步机制

// 自定义上传方法
uploadToServer(blob) {
  // 上传逻辑...
  .then(() => {
    this.$emit('success', data.url, file);
  })
  .catch((err) => {
    this.$emit('error', err, file);
  });
}

通过this.$emit向组件传递状态,组件会自动更新上传状态。

七、进阶使用

1. 上传进度反馈

uploadToServer(blob) {
  const reader = new FileReader();
  reader.onload = (e) => {
    const blob = new Blob([e.target.result], { type: file.type });
    
    const reader2 = new FileReader();
    reader2.onload = (e) => {
      const fileSize = e.target.result.byteLength;
      const progress = (blob.size / fileSize) * 100;
      this.$emit('progress', progress, file);
    };
    reader2.readAsArrayBuffer(blob);
  };
  reader.readAsArrayBuffer(blob);
}

2. 上传过程中文件处理

uploadToServer(blob) {
  const reader = new FileReader();
  reader.onload = (e) => {
    const arrayBuffer = e.target.result;
    
    // 1. 解析文件头信息
    const header = arrayBuffer.slice(0, 16);
    const fileFormat = parseFileFormat(header);
    
    // 2. 处理文件内容
    const content = arrayBuffer.slice(16);
    
    // 3. 上传到服务器
    fetch('/api/upload', {
      method: 'POST',
      body: content
    })
    .then(res => res.json())
    .then(data => {
      this.$emit('success', data.url, file);
    })
    .catch(err => {
      this.$emit('error', err, file);
    });
  };
  reader.readAsArrayBuffer(blob);
}

八、性能与工程实践

1. 性能优化方案

优化策略说明
分片上传大文件分片处理,避免内存溢出
Web Worker将文件处理逻辑放到Worker中,避免阻塞主线程
压缩处理使用libvips或sharp进行图片压缩
缓存策略对重复文件进行缓存,避免重复上传

2. 异常处理机制

  • 网络中断:重试机制(最多3次)
  • 文件损坏:校验文件CRC32
  • 服务端错误:重试+错误日志记录

3. 安全考虑

  • 服务端校验文件类型和大小
  • 限制上传文件的MIME类型
  • 限制文件存储路径,防止路径遍历攻击
  • 使用HTTPS进行加密传输
  • 对敏感文件进行二次验证

九、常见问题与踩坑

1. 常见错误

错误类型原因解决方案
上传失败未正确处理文件流确保使用readAsArrayBuffer读取文件
无进度反馈未触发progress事件在http-request中手动触发
上传中断文件过大未分片采用分片上传策略
安全漏洞未校验文件类型增加服务器端文件类型校验
无法预览未使用readAsDataURL为图片文件添加预览功能

2. 典型错误示例

// 错误示例:未处理文件流
uploadToServer(file) {
  fetch('/api/upload', {
    method: 'POST',
    body: file
  })
  .then(...);
}

错误原因:直接传递File对象,未进行序列化处理。

改进方案:

uploadToServer(file) {
  const reader = new FileReader();
  reader.onload = (e) => {
    fetch('/api/upload', {
      method: 'POST',
      body: e.target.result
    })
    .then(...);
  };
  reader.readAsArrayBuffer(file);
}

十、最佳实践

1. 推荐使用场景

  • 需要文件预处理(压缩、加密、水印)
  • 需要与第三方服务集成(OSS、MinIO)
  • 需要处理大文件上传(超过5MB)
  • 需要动态生成上传参数(签名、Token)

2. 不推荐使用场景

  • 简单的文件上传需求
  • 需要快速实现的上传功能
  • 不需要额外处理的文件上传场景

3. 推荐实践方案

  1. 使用readAsArrayBuffer处理二进制文件
  2. 对图片文件进行预览和压缩
  3. 实现分片上传机制
  4. 增加上传进度反馈
  5. 做好异常处理和重试机制

十一、总结

通过action="#"和http-request机制,我们可以实现高度定制化的文件上传方案。这种方案在需要处理复杂业务逻辑时非常有用,但同时也需要开发者深入理解文件处理流程和网络请求机制。在实际开发中,要根据具体业务需求选择合适的实现方案,平衡开发成本与功能复杂度。同时,要重视安全性、性能优化和异常处理,确保上传功能的稳定性和可靠性。

评论已关闭

推荐阅读

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日