​基于 vue + element plus + node 实现大文件分片上传,断点续传和秒传的功能!牛哇~

简介: ​基于 vue + element plus + node 实现大文件分片上传,断点续传和秒传的功能!牛哇~

最近,我遇到一个有趣的需求:实现大文件的分片上传、断点续传和秒传功能

老板说这是为了让用户上传文件时体验更好,上传大文件时不再需要担心网络中断或重复上传的问题

作为一个技术宅,我立马想去实现这个功能。接下来,我将使用Vue 和 Element Plus 和 node 带大家一起探索如何实现这个复杂但有趣的功能。

项目初始化

首先,我们需要初始化一个 Vue 项目。如果你还没有安装 Vue CLI,可以通过以下命令安装:

npm install -g @vue/cli

然后,创建一个新的 Vue 项目:

vue create file-upload-demo
cd file-upload-demo

选择默认配置或者根据自己的需求进行配置。创建完成后,进入项目目录并启动开发服务器:

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');

实现分片上传

前端代码

首先,我们需要在前端实现文件分片上传的逻辑。在 src/components 目录下创建一个 FileUpload.vue 文件,并添加以下内容:

<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>

后端代码

在后端,我们需要处理分片上传的逻辑。以下是一个使用 Node.js 和 Express 实现的示例:

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');
});

实现断点续传

前端代码

为了实现断点续传,我们需要记录已经上传的分片,并在网络中断后继续上传未完成的分片。

<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>

后端代码

后端需要记录已上传的分片,并在客户端请求时返回这些信息。

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');
});

实现秒传功能

前端代码

秒传功能依赖于文件的哈希值。在上传前,我们先计算文件的哈希值,并检查服务器是否已经存在相同的文件。

<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');
});

总结

希望通过本文的介绍,大家能够更深入地了解大文件上传的实现方法,并在实际项目中灵活应用这些技巧,提升用户体验。

相关文章
|
1月前
|
JavaScript 前端开发 开发者
VUE 开发——Node.js学习(一)
VUE 开发——Node.js学习(一)
64 3
|
2月前
|
移动开发 前端开发 HTML5
Twaver-HTML5基础学习(8)拓扑元素(Element)_网元(Element)、节点(Node)
本文介绍了Twaver HTML5中的拓扑元素(Element),包括网元(Element)、节点(Node)和连线(Link)的基本概念和使用方法。文章详细解释了Element的属性和方法,并通过示例代码展示了如何在React组件中创建节点、设置节点属性和样式。
44 1
Twaver-HTML5基础学习(8)拓扑元素(Element)_网元(Element)、节点(Node)
|
2月前
|
JavaScript 前端开发
Vue、ElementUI配合Node、multiparty模块实现图片上传并反显_小demo
如何使用Vue和Element UI配合Node.js及multiparty模块实现图片上传并反显的功能,包括前端的Element UI组件配置和后端的Node.js服务端代码实现。
35 1
Vue、ElementUI配合Node、multiparty模块实现图片上传并反显_小demo
|
2月前
|
存储 JSON 前端开发
node使用token来实现前端验证码和登录功能详细流程[供参考]=‘很值得‘
本文介绍了在Node.js中使用token实现前端验证码和登录功能的详细流程,包括生成验证码、账号密码验证以及token验证和过期处理。
49 0
node使用token来实现前端验证码和登录功能详细流程[供参考]=‘很值得‘
|
2月前
|
JavaScript 应用服务中间件 Linux
宝塔面板部署Vue项目、服务端Node___配置域名
本文介绍了如何使用宝塔面板在阿里云服务器上部署Vue项目和Node服务端项目,并配置域名。文章详细解释了安装宝塔面板、上传项目文件、使用pm2启动Node项目、Vue项目打包上传、以及通过Nginx配置域名和反向代理的步骤。
511 0
宝塔面板部署Vue项目、服务端Node___配置域名
|
2月前
|
JavaScript 前端开发
vue配合axios连接express搭建的node服务器接口_简单案例
文章介绍了如何使用Express框架搭建一个简单的Node服务器,并使用Vue结合Axios进行前端开发和接口调用,同时讨论了开发过程中遇到的跨域问题及其解决方案。
57 0
vue配合axios连接express搭建的node服务器接口_简单案例
|
3月前
|
JavaScript 关系型数据库 MySQL
node连接mysql,并实现增删改查功能
【8月更文挑战第26天】node连接mysql,并实现增删改查功能
60 3
|
3月前
|
JavaScript
成功解决node、node-sass和sass-loader版本冲突问题、不需要降低node版本。如何在vue项目中安装node-sass,以及安装node-sass可能遇到的版本冲突问题
这篇文章介绍了在Vue项目中安装node-sass和sass-loader时遇到的版本冲突问题,并提供了解决这些问题的方法,包括在不降低node版本的情况下成功安装node-sass。
成功解决node、node-sass和sass-loader版本冲突问题、不需要降低node版本。如何在vue项目中安装node-sass,以及安装node-sass可能遇到的版本冲突问题
|
3月前
|
JavaScript API
Vue——node-ops.ts【十三】
Vue——node-ops.ts【十三】
22 2
|
2月前
|
JavaScript
NodeJs的安装
文章介绍了Node.js的安装步骤和如何创建第一个Node.js应用。包括从官网下载安装包、安装过程、验证安装是否成功,以及使用Node.js监听端口构建简单服务器的示例代码。
NodeJs的安装