?基于 Vue + Element plus + Node 實(shí)現(xiàn)大文件分片上傳,斷點(diǎn)續(xù)傳和秒傳的功能!牛哇~
共 20241字,需瀏覽 41分鐘
·
2024-06-20 13:36
大廠(chǎng)技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
點(diǎn)擊上方 藍(lán)字 關(guān)注我們
大家好,我是考拉??!
最近,我遇到一個(gè)有趣的需求:實(shí)現(xiàn)大文件的分片上傳、斷點(diǎn)續(xù)傳和秒傳功能。
老板說(shuō)這是為了讓用戶(hù)上傳文件時(shí)體驗(yàn)更好,上傳大文件時(shí)不再需要擔(dān)心網(wǎng)絡(luò)中斷或重復(fù)上傳的問(wèn)題。
作為一個(gè)技術(shù)宅,我立馬想去實(shí)現(xiàn)這個(gè)功能。接下來(lái),我將使用Vue 和 Element Plus 和 node 帶大家一起探索如何實(shí)現(xiàn)這個(gè)復(fù)雜但有趣的功能。
項(xiàng)目初始化
首先,我們需要初始化一個(gè) Vue 項(xiàng)目。如果你還沒(méi)有安裝 Vue CLI,可以通過(guò)以下命令安裝:
npm install -g @vue/cli
然后,創(chuàng)建一個(gè)新的 Vue 項(xiàng)目:
vue create file-upload-demo
cd file-upload-demo
選擇默認(rèn)配置或者根據(jù)自己的需求進(jìn)行配置。創(chuàng)建完成后,進(jìn)入項(xiàng)目目錄并啟動(dòng)開(kāi)發(fā)服務(wù)器:
npm run serve
安裝和配置 Element Plus
為了使用 Element Plus,我們需要先安裝它:
npm install element-plus --save
在 src/main.js 中引入 Element Plus:
import { createApp } from 'vue';
import App from './App.vue';
import ElementPlus from 'element-plus';
import 'element-plus/lib/theme-chalk/index.css';
const app = createApp(App);
app.use(ElementPlus);
app.mount('#app');
實(shí)現(xiàn)分片上傳
前端代碼
首先,我們需要在前端實(shí)現(xiàn)文件分片上傳的邏輯。在 src/components 目錄下創(chuàng)建一個(gè) FileUpload.vue 文件,并添加以下內(nèi)容:
<template>
<el-upload
class="upload-demo"
ref="upload"
:http-request="uploadFile"
:on-change="handleChange"
:auto-upload="false"
:before-upload="beforeUpload"
:multiple="false">
<el-button slot="trigger" type="primary">選取文件</el-button>
<el-button @click="submitUpload">上傳</el-button>
</el-upload>
</template>
<script>
export default {
data() {
return {
file: null,
chunkSize: 2 * 1024 * 1024 // 2MB
};
},
methods: {
handleChange(file) {
this.file = file.raw;
},
beforeUpload(file) {
this.file = file;
return false;
},
async uploadFile() {
const chunkCount = Math.ceil(this.file.size / this.chunkSize);
for (let i = 0; i < chunkCount; i++) {
const chunk = this.file.slice(i * this.chunkSize, (i + 1) * this.chunkSize);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', i);
formData.append('fileName', this.file.name);
await this.uploadChunk(formData);
}
},
async uploadChunk(formData) {
try {
const response = await fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log(result);
} catch (error) {
console.error('上傳失敗:', error);
}
},
submitUpload() {
this.uploadFile();
}
}
};
</script>
<style scoped>
.upload-demo {
display: flex;
flex-direction: column;
}
</style>
后端代碼
在后端,我們需要處理分片上傳的邏輯。以下是一個(gè)使用 Node.js 和 Express 實(shí)現(xiàn)的示例:
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('chunk'), (req, res) => {
const { index, fileName } = req.body;
const chunkFilePath = path.join(__dirname, 'uploads', `${fileName}-${index}`);
fs.renameSync(req.file.path, chunkFilePath);
res.json({ message: '上傳成功', index });
});
app.listen(3000, () => {
console.log('Server started on http://localhost:3000');
});
實(shí)現(xiàn)斷點(diǎn)續(xù)傳
前端代碼
為了實(shí)現(xiàn)斷點(diǎn)續(xù)傳,我們需要記錄已經(jīng)上傳的分片,并在網(wǎng)絡(luò)中斷后繼續(xù)上傳未完成的分片。
<template>
<el-upload
class="upload-demo"
ref="upload"
:http-request="uploadFile"
:on-change="handleChange"
:auto-upload="false"
:before-upload="beforeUpload"
:multiple="false">
<el-button slot="trigger" type="primary">選取文件</el-button>
<el-button @click="submitUpload">上傳</el-button>
</el-upload>
</template>
<script>
export default {
data() {
return {
file: null,
chunkSize: 2 * 1024 * 1024, // 2MB
uploadedChunks: []
};
},
methods: {
handleChange(file) {
this.file = file.raw;
},
beforeUpload(file) {
this.file = file;
return false;
},
async uploadFile() {
const chunkCount = Math.ceil(this.file.size / this.chunkSize);
const response = await fetch(`http://localhost:3000/uploaded-chunks?fileName=${this.file.name}`);
this.uploadedChunks = await response.json();
for (let i = 0; i < chunkCount; i++) {
if (this.uploadedChunks.includes(i)) continue;
const chunk = this.file.slice(i * this.chunkSize, (i + 1) * this.chunkSize);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', i);
formData.append('fileName', this.file.name);
await this.uploadChunk(formData);
}
},
async uploadChunk(formData) {
try {
const response = await fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
this.uploadedChunks.push(result.index);
console.log(result);
} catch (error) {
console.error('上傳失敗:', error);
}
},
submitUpload() {
this.uploadFile();
}
}
};
</script>
<style scoped>
.upload-demo {
display: flex;
flex-direction: column;
}
</style>
后端代碼
后端需要記錄已上傳的分片,并在客戶(hù)端請(qǐng)求時(shí)返回這些信息。
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('chunk'), (req, res) => {
const { index, fileName } = req.body;
const chunkFilePath = path.join(__dirname, 'uploads', `${fileName}-${index}`);
fs.renameSync(req.file.path, chunkFilePath);
res.json({ message: '上傳成功', index });
});
app.get('/uploaded-chunks', (req, res) => {
const { fileName } = req.query;
const uploadedChunks = [];
fs.readdirSync(path.join(__dirname, 'uploads')).forEach(file => {
const match = file.match(new RegExp(`${fileName}-(\\d+)`));
if (match) {
uploadedChunks.push(Number(match[1]));
}
});
res.json(uploadedChunks);
});
app.listen(3000, () => {
console.log('Server started on http://localhost:3000');
});
實(shí)現(xiàn)秒傳功能
前端代碼
秒傳功能依賴(lài)于文件的哈希值。在上傳前,我們先計(jì)算文件的哈希值,并檢查服務(wù)器是否已經(jīng)存在相同的文件。
<template>
<el-upload
class="upload-demo"
ref="upload"
:http-request="uploadFile"
:on-change="handleChange"
:auto-upload="false"
:before-upload="beforeUpload"
:multiple="false">
<el-button slot="trigger" type="primary">選取文件</el-button>
<el-button @click="submitUpload">上傳</el-button>
</el
-upload>
</template>
<script>
import SparkMD5 from 'spark-md5';
export default {
data() {
return {
file: null,
chunkSize: 2 * 1024 * 1024, // 2MB
uploadedChunks: [],
fileHash: ''
};
},
methods: {
handleChange(file) {
this.file = file.raw;
},
beforeUpload(file) {
this.file = file;
this.calculateHash(file);
return false;
},
calculateHash(file) {
const chunkSize = this.chunkSize;
const chunks = Math.ceil(file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
let currentChunk = 0;
const fileReader = new FileReader();
fileReader.onload = e => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
this.fileHash = spark.end();
console.log('文件哈希值:', this.fileHash);
}
};
const loadNext = () => {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
fileReader.readAsArrayBuffer(file.slice(start, end));
};
loadNext();
},
async uploadFile() {
const response = await fetch(`http://localhost:3000/check-file?hash=${this.fileHash}`);
const { exists } = await response.json();
if (exists) {
console.log('文件已存在,秒傳成功');
return;
}
const chunkCount = Math.ceil(this.file.size / this.chunkSize);
const uploadedChunksResponse = await fetch(`http://localhost:3000/uploaded-chunks?fileName=${this.file.name}`);
this.uploadedChunks = await uploadedChunksResponse.json();
for (let i = 0; i < chunkCount; i++) {
if (this.uploadedChunks.includes(i)) continue;
const chunk = this.file.slice(i * this.chunkSize, (i + 1) * this.chunkSize);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', i);
formData.append('fileName', this.file.name);
formData.append('hash', this.fileHash);
await this.uploadChunk(formData);
}
},
async uploadChunk(formData) {
try {
const response = await fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
this.uploadedChunks.push(result.index);
console.log(result);
} catch (error) {
console.error('上傳失敗:', error);
}
},
submitUpload() {
this.uploadFile();
}
}
};
</script>
<style scoped>
.upload-demo {
display: flex;
flex-direction: column;
}
</style>
后端代碼
后端需要支持文件哈希檢查和已存在文件的處理。
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('chunk'), (req, res) => {
const { index, fileName, hash } = req.body;
const chunkFilePath = path.join(__dirname, 'uploads', `${fileName}-${index}`);
fs.renameSync(req.file.path, chunkFilePath);
// 合并文件
const chunkCount = Math.ceil(req.file.size / (2 * 1024 * 1024));
const chunks = [];
for (let i = 0; i < chunkCount; i++) {
chunks.push(fs.readFileSync(path.join(__dirname, 'uploads', `${fileName}-${i}`)));
}
fs.writeFileSync(path.join(__dirname, 'uploads', fileName), Buffer.concat(chunks));
res.json({ message: '上傳成功', index });
});
app.get('/uploaded-chunks', (req, res) => {
const { fileName } = req.query;
const uploadedChunks = [];
fs.readdirSync(path.join(__dirname, 'uploads')).forEach(file => {
const match = file.match(new RegExp(`${fileName}-(\\d+)`));
if (match) {
uploadedChunks.push(Number(match[1]));
}
});
res.json(uploadedChunks);
});
app.get('/check-file', (req, res) => {
const { hash } = req.query;
const filePath = path.join(__dirname, 'uploads', hash);
const exists = fs.existsSync(filePath);
res.json({ exists });
});
app.listen(3000, () => {
console.log('Server started on http://localhost:3000');
});
總結(jié)
希望通過(guò)本文的介紹,大家能夠更深入地了解大文件上傳的實(shí)現(xiàn)方法,并在實(shí)際項(xiàng)目中靈活應(yīng)用這些技巧,提升用戶(hù)體驗(yàn)。
Node 社群
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(huà)(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點(diǎn)贊、在看” 支持一下
