各位靓仔靓女,晚上好!我是你们的老朋友,今天咱们不开车,来聊聊 Vue 项目里的文件上传那些事儿。目标只有一个:打造一个牛逼哄哄的通用文件上传组件,支持分片、断点续传、预览,让你在面试官面前都能抬头挺胸!
开场白:文件上传,痛点在哪里?
文件上传,听起来简单,不就一个 <input type="file">
吗?Too young, too simple! 当文件稍微大一点,问题就来了:
- 上传慢如蜗牛: 大文件一口气传,网络一波动,GG!
- 流量嗖嗖跑: 用户传个电影,你扣人家半个月流量?会被投诉的!
- 服务器压力山大: 一堆大文件同时上传,服务器直接躺平。
- 用户体验差: 传到一半断了?重来?用户想砸电脑的心都有了!
所以,我们需要一套更优雅、更高效的文件上传方案,也就是今天的主角:分片上传 + 断点续传。
第一幕:组件设计蓝图
首先,我们来规划一下这个通用文件上传组件的结构。考虑到通用性,我们需要一些可配置的选项,例如:
配置项 | 类型 | 描述 | 默认值 |
---|---|---|---|
uploadUrl |
String | 上传接口地址 | – |
chunkSize |
Number | 分片大小,单位字节 (B) | 1024 * 1024 (1MB) |
maxSize |
Number | 文件最大大小,单位字节 (B) | 1024 1024 1024 (1GB) |
accept |
String | 允许上传的文件类型,例如:'.jpg,.png,.gif' |
” |
headers |
Object | 上传请求头 | {} |
params |
Object | 上传请求参数 | {} |
onProgress |
Function | 上传进度回调函数,参数为进度百分比 (0-100) | – |
onSuccess |
Function | 上传成功回调函数,参数为服务器返回的数据 | – |
onError |
Function | 上传失败回调函数,参数为错误信息 | – |
preview |
Boolean | 是否开启预览功能 | true |
第二幕:Vue 组件骨架搭建
有了蓝图,我们开始撸代码,先搭个 Vue 组件的架子:
<template>
<div class="upload-container">
<input type="file" @change="handleFileChange" :accept="accept" ref="fileInput">
<button @click="handleUpload">上传</button>
<div v-if="preview && file" class="preview-container">
<img v-if="isFileImage" :src="previewUrl" alt="预览">
<video v-else-if="isFileVideo" :src="previewUrl" controls></video>
<div v-else>{{ file.name }}</div>
</div>
<div v-if="progress > 0" class="progress-bar">
<div class="progress" :style="{ width: progress + '%' }"></div>
<span>{{ progress }}%</span>
</div>
</div>
</template>
<script>
import axios from 'axios'; // 引入 axios,用于发送请求
export default {
props: {
uploadUrl: {
type: String,
required: true,
},
chunkSize: {
type: Number,
default: 1024 * 1024, // 1MB
},
maxSize: {
type: Number,
default: 1024 * 1024 * 1024, // 1GB
},
accept: {
type: String,
default: '',
},
headers: {
type: Object,
default: () => ({}),
},
params: {
type: Object,
default: () => ({}),
},
onProgress: {
type: Function,
default: () => {},
},
onSuccess: {
type: Function,
default: () => {},
},
onError: {
type: Function,
default: () => {},
},
preview: {
type: Boolean,
default: true,
},
},
data() {
return {
file: null,
chunks: [],
uploadedList: [], // 记录已上传的分片
progress: 0,
previewUrl: null,
isFileImage: false,
isFileVideo: false,
};
},
methods: {
handleFileChange(event) {
this.file = event.target.files[0];
if (!this.file) return;
// 文件大小校验
if (this.file.size > this.maxSize) {
alert('文件大小超出限制');
this.$refs.fileInput.value = null; // 清空 input,防止再次选择相同文件不触发 change 事件
this.file = null;
return;
}
this.createChunks();
this.previewFile();
},
handleUpload() {
if (!this.file) {
alert('请选择文件');
return;
}
this.uploadChunks();
},
createChunks() {
const fileSize = this.file.size;
const chunkSize = this.chunkSize;
let start = 0;
while (start < fileSize) {
const end = Math.min(start + chunkSize, fileSize);
this.chunks.push(this.file.slice(start, end));
start = end;
}
},
async uploadChunks() {
// 上传分片逻辑将在下一幕展开
},
previewFile() {
if (!this.preview) return;
const fileReader = new FileReader();
fileReader.onload = (event) => {
this.previewUrl = event.target.result;
this.isFileImage = this.isImage(this.file.type);
this.isFileVideo = this.isVideo(this.file.type);
};
fileReader.readAsDataURL(this.file);
},
isImage(fileType) {
return fileType.startsWith('image/');
},
isVideo(fileType) {
return fileType.startsWith('video/');
},
},
};
</script>
<style scoped>
.upload-container {
/* 样式自己发挥 */
}
.preview-container {
/* 样式自己发挥 */
}
.progress-bar {
/* 样式自己发挥 */
}
</style>
第三幕:分片上传,化整为零
createChunks
方法已经将文件分割成了若干个小块,接下来就是上传这些小块了。uploadChunks
方法将扮演主角:
async uploadChunks() {
this.progress = 0;
this.uploadedList = []; // 重置已上传列表
const totalChunks = this.chunks.length;
for (let i = 0; i < totalChunks; i++) {
if (this.uploadedList.includes(i)) {
// 如果已经上传过,跳过
continue;
}
const chunk = this.chunks[i];
const formData = new FormData();
formData.append('file', chunk);
formData.append('filename', this.file.name);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
try {
const response = await axios.post(this.uploadUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data',
...this.headers,
},
params: {
...this.params,
},
onUploadProgress: (progressEvent) => {
// 计算总进度
const loaded = progressEvent.loaded + (this.chunkSize * this.uploadedList.length); //已上传的分片大小加上当前分片已上传的大小
const total = this.file.size; // 总大小
this.progress = Math.round((loaded / total) * 100); // 计算进度
this.onProgress(this.progress);
},
});
if (response.status === 200) {
// 标记为已上传
this.uploadedList.push(i);
// 检查是否所有分片都已上传
if (this.uploadedList.length === totalChunks) {
this.onSuccess(response.data);
this.progress = 100;
alert('上传成功');
}
} else {
throw new Error(`上传失败,状态码:${response.status}`);
}
} catch (error) {
this.onError(error);
alert(`上传失败:${error.message}`);
break; // 停止上传
}
}
},
这段代码的核心逻辑是:
- 循环上传分片: 遍历
chunks
数组,逐个上传。 - FormData 封装: 将分片数据、文件名、分片索引、总分片数等信息封装到
FormData
中。 - axios 发送请求: 使用
axios
发送 POST 请求到服务器。 - 进度监听: 监听
onUploadProgress
事件,计算并更新上传进度。 - 成功/失败处理: 根据服务器返回的状态码,判断上传是否成功,并调用相应的回调函数。
- 已上传列表: 使用
uploadedList
记录已上传的分片索引。 - 断点续传雏形: 在循环开始前,判断当前分片是否已上传,如果是,则跳过。
第四幕:断点续传,重拾希望
上面的代码已经有了断点续传的雏形,但还不够完善。我们需要在服务器端配合,实现真正的断点续传。
服务器端要做什么?
- 记录已上传分片: 服务器需要记录每个文件已经上传的分片索引。可以使用数据库、Redis 等方式存储。
- 合并分片: 当所有分片都上传完成后,服务器需要将这些分片合并成完整的文件。
组件端如何配合?
- 初始化时查询已上传分片: 在
handleFileChange
方法中,向服务器查询当前文件已上传的分片索引,更新uploadedList
。
async handleFileChange(event) {
this.file = event.target.files[0];
if (!this.file) return;
// 文件大小校验...
try {
// 向服务器查询已上传分片
const response = await axios.get('/check-uploaded', {
params: {
filename: this.file.name,
totalChunks: this.chunks.length,
},
});
if (response.status === 200 && response.data.uploadedList) {
this.uploadedList = response.data.uploadedList;
}
} catch (error) {
console.error('查询已上传分片失败', error);
}
this.createChunks();
this.previewFile();
},
- 服务器返回的数据格式: 服务器应该返回一个包含已上传分片索引的数组,例如:
{
"uploadedList": [0, 1, 3, 5] // 已经上传了第 0, 1, 3, 5 个分片
}
第五幕:预览,眼见为实
代码中已经包含了预览功能的简单实现,通过 FileReader
读取文件内容,根据文件类型显示图片、视频或文件名。
补充说明:
- 安全问题: 上传文件时,务必进行安全校验,防止恶意文件上传。
- 错误处理: 需要更完善的错误处理机制,例如:重试机制、错误提示等。
- 用户体验: 可以添加更友好的用户界面,例如:上传队列、进度条动画等。
- 后端接口: 代码中
/check-uploaded
和this.uploadUrl
只是占位符,需要替换成实际的后端接口地址。 - 取消上传: 可以添加取消上传的功能,通过
axios.CancelToken
实现。 - 暂停/恢复: 可以添加暂停和恢复上传的功能,在
uploadChunks
方法中使用axios.CancelToken
控制请求的取消和恢复。需要注意的是,暂停后恢复,需要重新查询已上传分片,避免重复上传。
第六幕:代码优化,精益求精
- 使用 async/await 简化异步代码: 上面的代码大量使用了
async/await
,可以使异步代码更易读、易维护。 - 提取公共方法: 可以将一些公共方法提取出来,例如:文件类型判断、进度计算等。
- 使用 Vuex 管理状态: 如果需要在多个组件中使用上传状态,可以使用 Vuex 进行集中管理。
- 使用 TypeScript 增强类型安全: 可以使用 TypeScript 编写组件,提高代码的可维护性和可读性。
总结:
至此,我们已经完成了一个基本的文件上传组件,支持分片上传、断点续传和预览功能。 当然,这只是一个起点,还有很多可以优化和完善的地方。 记住,代码不是一次写成的,需要不断迭代、优化,才能达到最佳效果。 希望今天的分享对你有所帮助! 祝大家早日成为编程高手! 下课!