效果展示
技术关键词
前端:vue3+element-plus+axios
后端:node+express+multer/php
完整代码
拖拽选择文件
拖拽上传是利用HTML5特性dragover实现拖拽上传,有很多现成的例子
<label class="split-upload-file" @dragover="fileDragOver" @drop="fileDrop">
<input
ref="inputEl"
style="display: none"
type="file"
multiple="multiple"
@change="onFileChange"
/>
<i class="icon-upload"></i>
<span>将文件拖放到此处以上传或<b>选择文件</b></span>
</label>
const tempFileList=ref([])
const fileDragOver = e => {
e.preventDefault()
}
const fileDrop = e => {
e.preventDefault()
//const file=e.dataTransfer.files
onFileChange(e.dataTransfer.files,'drag')
}
const onFileChange= async (evt,type)=>{
let file = evt
if (type !== 'drag') {
file = evt.target && evt.target.files
}
for (let i = 0; i < file.length; i++) {
const data = file[i]
let src = ''
// 先将获取到的文件暂存起来,可实现选择文件后自动上传或手动点击击再上传
tempFileList.value.push({
size: data.size, // 大小
progress: 0, // 上传进度
speed: '', // 上传速度
remainingTime: '', //剩余时间预估
status: 0, //上传状态
name: data.name,
src: src, // 预览用的src
type: data.type,
file: data,
source: null // 用于取消上传
})
}
await confirmUpload() // 进入按指定的并发数上传文件
}
任务并发控制
限定每次请求只能进行固定次数的异步任务,每请求成功一个则新增一个,确保并发的数量固定。
const maxParallelUploads = 3 //同时上传并发数
const confirmUpload = async () => {
const queue = tempFileList.value.filter(
(item) => item.status === 0
) // 创建一个文件队列
const promises = []
for (const file of queue) {
// 如果当前运行的任务数达到最大并行上传数,则等待一个任务完成
while (promises.length >= maxParallelUploads) {
await Promise.race(promises) // 等待最先完成的一个任务
promises.shift() // 移除已完成的任务
}
// 添加新的上传任务到promises数组
promises.push(axiosUpload(file))
}
// 等待剩余的任务完成
await Promise.all(promises)
}
分块上传
思路
分块上传就是前端将大文件拆成一个个小的文件,发送给后端。后端接收完所有分片小文件后将后片小文件合并成新的文件。
在实现分片上传前,需思考和实现下面问题:
- 如何获取用户选择的文件
- 获取到的大文件如何进行分片(重点)
- 怎么区分多个不同的大文件
- 如何将每个小分片的数据发送给后端
- 如何通知后端合并文件
const chunkSize = 20 * 1024 * 1024 // 每片大小
let cancelToken
const axiosUpload=async(currentFile)=>{
const file=currentFile.file
const chunkCount = Math.ceil(file.size / chunkSize) // 总片数
const fileMd5 = await getFileMD5(file, chunkSize, chunkCount)//计算md5
const source = axios.CancelToken.source()
cancelToken = source.token
currentFile.source = source
for (let i = 0; i < chunkCount; i++) {
const { chunk } = getChunkInfo(file, i, chunkSize)//获取当前片
const params = {
chunkNumber: i + 1,
chunkSize: chunkSize, // 每片限制
currentChunkSize: chunk.size, // 当前分片大小
totalSize: file.size, // 文件总大小
identifier: fileMd5, // md5值
filename: file.name, // 文件名
totalChunks: chunkCount, // 总片数
file: chunk // 每片文件
}
await uploadChunk(params, currentFile)
}
}
const uploadChunk = (params, currentFile) => {
return new Promise((resolve, reject) => {
const formData = new FormData()
for (const i in params) {
formData.append(i, params[i])
}
const config = {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 0, // `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
onUploadProgress: (progressEvent: any) => {
//上传进度相关信息
},
cancelToken
}
axios.post('/api/upload',formData, config)
.then(()=>{})
.catch(()=>{})
})
}
//根据每片大小获取每片的内容
const getChunkInfo = (file, currentChunk, chunkSize) => {
const start = currentChunk * chunkSize
const end = Math.min(file.size, start + chunkSize)
const chunk = file.slice(start, end)
return { chunk }
}
秒传
实现秒传就是在上传之前,先调取后端接口,根据md5值判断文件是否已存在。使用SparkMD5进行md5计算:
import SparkMD5 from 'spark-md5'
const getFileMD5 = (file, chunkSize, chunkCount) => {
return new Promise((resolve, reject) => {
let currentChunk = 0
const startTime = new Date().getTime()
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
loadNextChunk()
fileReader.onload = function (e) {
spark.append(e.target?.result)
if (currentChunk < chunkCount) {
currentChunk++
loadNextChunk()
} else {
const md5 = spark.end()
console.log(
`MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms`
)
resolve(md5)
}
}
fileReader.onerror = function () {
reject('文件读取错误')
}
function loadNextChunk() {
const { chunk } = getChunkInfo(file, currentChunk, chunkSize)
fileReader.readAsArrayBuffer(chunk)
}
})
}
后端
后端示例仅供演示
<?php
//PHP服务端接收示例代码
header("Access-Control-Allow-Origin: *");
$file = $_FILES['file'];
$chunkNumber = $_POST['chunkNumber'];
$totalChunks = $_POST['totalChunks'];
$filename = $_POST['filename'];
$public_dir = 'upload';
$dir = '/';
$all_dir = $public_dir . $dir;
if (!is_dir($all_dir)) mkdir($all_dir, 0777, true);
$saveFileName = $all_dir . '/' . $filename . '__' . $chunkNumber;
move_uploaded_file($file['tmp_name'], $saveFileName);
$res['code'] = 0;
$res['msg'] = 'error';
$res['file_path'] = '';
if ($chunkNumber === $totalChunks) {
$blob = '';
for ($i = 1; $i <= $totalChunks; $i++) {
$blob .= file_get_contents($all_dir . '/' . $filename . '__' . $i);
}
file_put_contents($all_dir . '/' . $filename, $blob);
for ($i = 1; $i <= $totalChunks; $i++) {
@unlink($all_dir . '/' . $filename . '__' . $i);
}
if (file_exists($all_dir . '/' . $filename)) {
$res['code'] = 2;
$res['msg'] = 'success';
$res['file_path'] = $dir . '/' . $filename;
}
} else {
if (file_exists($all_dir . '/' . $filename . '__' . $chunkNumber)) {
$res['code'] = 1;
$res['msg'] = 'waiting';
$res['file_path'] = '';
}
}
echo json_encode($res);
?>