<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ?基于 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 {
                filenull,
                chunkSize2 * 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 {
                filenull,
                chunkSize2 * 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 {
                filenull,
                chunkSize2 * 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)贊在看” 支持一下

          瀏覽 215
          3點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          3點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线吴梦梦视频一区二区 | 做爱网站中文字幕 | 久热福利视频 | 色v在线 | 人人操人人摸人人射 |