首先是文件上传以及分片,话不多说,直接贴代码
<template>
<div>
<input type="file" @change="handleFileChange" class="file" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const CHUNK_SIZE = 2 * 1024 * 1024 // 每片 2MB
const THREAD_COUNT = navigator.hardwareConcurrency || 4 // 线程数
import SparkMD5 from 'spark-md5'
import axios from 'axios';
const CONCURRENCY = 5; // 并发数
async function handleFileChange (e) {
console.log('文件变化了', e);
const file = e.target.files[0]
const _fileName = file.name
console.time('开始')
const chunks = await cutFile(file)
console.timeEnd('开始')
console.log('分片:', chunks)
}
async function cutFile (file) {
const fileId = await calculateFileMd5(file); // ⬅️ 先计算整个文件的 MD5
return new Promise((resolve, reject) => {
const chunkCount = Math.ceil(file.size / CHUNK_SIZE); // 计算分片数量
const threadChunkCount = Math.ceil(chunkCount / THREAD_COUNT); // 计算每个线程分片数量
const result = [];
let finisCount = 0; // 计算完成数量
for (let i = 0; i < THREAD_COUNT; i++) {
const worker = new Worker(new URL('./文件分片/线程/worker.js', import.meta.url), {
type: 'module'
});
let start = i * threadChunkCount; // 开始位置
let end = Math.min(start + threadChunkCount, chunkCount); // 结束位置
worker.postMessage({
file, // 切割文件
start, // 开始位置
end,// 结束位置
CHUNK_SIZE, // 每片大小
});
worker.onmessage = (e) => {
worker.terminate();
result[i] = e.data; // 获取worker返回的数据
finisCount++;
if (finisCount === THREAD_COUNT) {
resolve({ result: result.flat(), fileId });
}
}
}
})
}
function calculateFileMd5 (file, chunkSize = 2 * 1024 * 1024) {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer();
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const reader = new FileReader();
reader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
reader.onerror = reject;
function loadNext () {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
reader.readAsArrayBuffer(blob);
}
loadNext();
});
}
</script>
<style lang="scss" scoped>
</style>
worker.js中的代码
import { createChunk } from './c'; // 保留原来的 createChunk(每个 chunk 的 MD5 或数据)
onmessage = async function (e) {
const { file, start, end, CHUNK_SIZE, fileId } = e.data;
const result = [];
for (let i = start; i < end; i++) {
const chunk = await createChunk(file, i, CHUNK_SIZE);
result.push({ ...chunk }); // ⬅️ 添加 fileId
}
postMessage(result); // ⬅️ 添加 fileId
};
createChunk函数
import SparkMD5 from 'spark-md5'
export function createChunk (file, index, chunkSize) {
return new Promise((resolve, reject) => {
const start = index * chunkSize; // 开始位置
const end = start + chunkSize; // 结束位置
const spark = new SparkMD5.ArrayBuffer(); // 创建一个md5对象
const fileReader = new FileReader(); // 创建一个文件读取器
const blob = file.slice(start, end); // 切割文件
fileReader.onload = (e) => { // 文件读取完成
spark.append(e.target.result);
const checkObj = {
start,
end,
index,
hash: spark.end(), // 计算md5值
blob
}
resolve(checkObj)
}
fileReader.readAsArrayBuffer(blob);
})
}
解释一下,checkUploadedChunks这个方法主要是给整个文件生成一个MD5编号。cutFile这个方法是给每个分片生成一个MD5编号,主要是为了上传使用
接下来上node(文件名:server.js)代码,重要:启动命令:node server.js
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
const cors = require('cors');
const app = express();
const PORT = 3000;
app.use(cors());
app.use(express.json());
// 配置上传目录
const UPLOAD_DIR = path.resolve(__dirname, 'uploads');
const MERGE_DIR = path.resolve(__dirname, 'merged');
// 动态创建上传目录
function getChunkDir (fileId) {
return path.resolve(UPLOAD_DIR, fileId);
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const { fileId } = req.body;
const chunkDir = getChunkDir(fileId);
fs.mkdirSync(chunkDir, { recursive: true });
cb(null, chunkDir);
},
filename: function (req, file, cb) {
const { index } = req.body;
cb(null, `chunk-${index}`);
}
});
const upload = multer({ storage });
// 上传接口
app.post('/api/upload', upload.single('chunk'), (req, res) => {
res.send({ message: '分片上传成功' });
});
// 检查已上传分片
app.post('/api/check', async (req, res) => {
const { fileId } = req.body;
const chunkDir = getChunkDir(fileId);
if (!fs.existsSync(chunkDir)) {
return res.send({ uploaded: [] });
}
const files = await fsp.readdir(chunkDir);
const uploaded = files.map(name => name.split('-')[1]); // 返回 chunk id(例如 index)
res.send({ uploaded });
});
// 合并分片
app.post('/api/merge', async (req, res) => {
const { fileId, fileName } = req.body;
const chunkDir = getChunkDir(fileId);
const filePath = path.resolve(MERGE_DIR, fileName);
await fsp.mkdir(MERGE_DIR, { recursive: true });
const chunkFiles = await fsp.readdir(chunkDir);
const sortedChunks = chunkFiles
.sort((a, b) => Number(a.split('-')[1]) - Number(b.split('-')[1]));
const writeStream = fs.createWriteStream(filePath);
for (const chunkFile of sortedChunks) {
const chunkPath = path.resolve(chunkDir, chunkFile);
const data = fs.readFileSync(chunkPath);
writeStream.write(data);
}
writeStream.end();
writeStream.on('finish', () => {
res.send({ message: '文件合并成功' });
fs.rmSync(chunkDir, { recursive: true, force: true }); // 删除临时分片
});
});
app.listen(PORT, () => {
console.log(`🚀 Server running at http://localhost:${PORT}`);
});
完整代码示例
<template>
<div>
<input type="file" @change="handleFileChange" class="file" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const CHUNK_SIZE = 2 * 1024 * 1024 // 每片 2MB
const THREAD_COUNT = navigator.hardwareConcurrency || 4 // 线程数
import SparkMD5 from 'spark-md5'
import axios from 'axios';
const CONCURRENCY = 5; // 并发数
async function handleFileChange (e) {
console.log('文件变化了', e);
const file = e.target.files[0]
const _fileName = file.name
console.time('开始')
const chunks = await cutFile(file)
console.timeEnd('开始')
console.log('分片:', chunks)
const uploadedList = await checkUploadedChunks(chunks.fileId);
console.log('已上传分片:', uploadedList);
const toUploadChunks = chunks.result.filter(c => !uploadedList.includes(c.hash));
console.log('待上传分片:', toUploadChunks);
await uploadChunks(chunks.fileId, toUploadChunks); //
await mergeFile(chunks.fileId, _fileName);
}
async function checkUploadedChunks (fileId) {
const res = await axios.post('http://localhost:3000/api/check', { fileId });
return res.data.uploaded || [];
}
async function uploadChunks (fileId, chunks) {
let completed = 0;
if (!fileId || !chunks.length) {
alert('没有需要上传的分片!');
return;
}
async function uploadChunk (chunk) {
const form = new FormData();
form.append('fileId', fileId);
form.append('chunkId', chunk.hash);
form.append('index', chunk.index);
form.append('chunk', chunk.blob);
await axios.post('http://localhost:3000/api/upload', form);
completed++;
let check = ((completed / chunks.length) * 100).toFixed(2); // 可选
console.log(check, "%");
}
let index = 0;
const pool = new Array(CONCURRENCY).fill(null).map(() => {
return new Promise(async function next (resolve) {
if (index >= chunks.length) return resolve();
const chunk = chunks[index++];
await uploadChunk(chunk);
next(resolve);
});
});
await Promise.all(pool);
}
async function mergeFile (fileId, fileName) {
await axios.post('http://localhost:3000/api/merge', { fileId, fileName });
}
async function cutFile (file) {
const fileId = await calculateFileMd5(file); // ⬅️ 先计算整个文件的 MD5
return new Promise((resolve, reject) => {
const chunkCount = Math.ceil(file.size / CHUNK_SIZE); // 计算分片数量
const threadChunkCount = Math.ceil(chunkCount / THREAD_COUNT); // 计算每个线程分片数量
const result = [];
let finisCount = 0; // 计算完成数量
for (let i = 0; i < THREAD_COUNT; i++) {
const worker = new Worker(new URL('./文件分片/线程/worker.js', import.meta.url), {
type: 'module'
});
let start = i * threadChunkCount; // 开始位置
let end = Math.min(start + threadChunkCount, chunkCount); // 结束位置
worker.postMessage({
file, // 切割文件
start, // 开始位置
end,// 结束位置
CHUNK_SIZE, // 每片大小
});
worker.onmessage = (e) => {
worker.terminate();
result[i] = e.data; // 获取worker返回的数据
finisCount++;
if (finisCount === THREAD_COUNT) {
resolve({ result: result.flat(), fileId });
}
}
}
})
}
function calculateFileMd5 (file, chunkSize = 2 * 1024 * 1024) {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer();
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const reader = new FileReader();
reader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
reader.onerror = reject;
function loadNext () {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
reader.readAsArrayBuffer(blob);
}
loadNext();
});
}
// function calculateFileMd5 (file, chunkSize = CHUNK_SIZE) {
// return new Promise((resolve, reject) => {
// const worker = new Worker(new URL('./文件分片/线程/md5.worker.js', import.meta.url), {
// type: 'module',
// });
// worker.postMessage({ file, chunkSize });
// worker.onmessage = (e) => {
// const { fileId, error } = e.data;
// worker.terminate();
// if (error) {
// reject(error);
// } else {
// resolve(fileId);
// }
// };
// worker.onerror = (err) => {
// worker.terminate();
// reject(err);
// };
// });
// }
</script>
<style lang="scss" scoped>
</style>
重点:这个思路就是将整个的文件ID传给后端,如果没有上传,就返回给你空数组,上传了,则返回上传的id,然后根据id筛选出没有上传的数据,进行接口上传,其中也写了并发数以及线程数量,速度优化了50%
接下来把断点续传,以及样式和分片进度、上传进度、速度优化交给你们来写