【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对象),开发者需要实现以下核心逻辑:
- 文件流处理:通过
FileReader读取文件内容,支持多种格式(ArrayBuffer/Text/Blob) - 自定义请求:使用
fetch/axios等工具发送HTTP请求,支持自定义headers、body、请求方法 - 响应处理:解析服务器返回的响应数据,可能包含文件URL、上传状态、业务数据
- 状态同步:通过
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.js1. 上传组件代码
<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. 推荐实践方案
- 使用
readAsArrayBuffer处理二进制文件 - 对图片文件进行预览和压缩
- 实现分片上传机制
- 增加上传进度反馈
- 做好异常处理和重试机制
十一、总结
通过action="#"和http-request机制,我们可以实现高度定制化的文件上传方案。这种方案在需要处理复杂业务逻辑时非常有用,但同时也需要开发者深入理解文件处理流程和网络请求机制。在实际开发中,要根据具体业务需求选择合适的实现方案,平衡开发成本与功能复杂度。同时,要重视安全性、性能优化和异常处理,确保上传功能的稳定性和可靠性。
评论已关闭