前端性能优化之大文件上传

大文件上传是前端开发中常见的需求之一,特别是在需要处理较大的Excel表格数据、高清图片、视频或其他大型文件时。优化大文件上传不仅可以提升用户体验,还能有效减轻服务器负担。本文将深入探讨大文件上传的几种常见优化技术,包括 文件切片与并发上传、断点续传、云服务上传、后台处理优化、安全性考虑和用户体验优化。

一、前言

在现代Web应用中,用户上传大文件已成为常见需求。然而,直接上传大文件会面临诸多挑战,例如网络不稳定导致上传中断、长时间上传导致用户体验差、服务器压力大等。因此,优化大文件上传性能显得尤为重要。

二、优化方案 

1. 文件切片与并发上传

1.1 文件切片原理

文件切片(Chunking)将大文件切割成多个小片段,然后分别上传。可以利用HTML5中的File API和Blob对象,通过FileReader读取文件内容(读取文件内容到JavaScript中,并支持多种格式的输出,如文本、DataURL,然后使用XMLHttpRequest或fetch API发送 post 每个小片段,并在服务器端将它们合并成完整的文件。

---将一个大文件切割成多个小文件,分别上传,然后在服务端组合(需要唯一的标识确定顺序)。这种方式可以提高上传速度和可靠性,但需要额外的前后端开发和维护工作。

这种方法可以提高上传效率和稳定性,并且支持断点续传

1.2 实现步骤
  1. 前端切片:利用Blob对象的slice方法将文件切片(计算每个分片的哈希值+序号)。
  2. 并发上传:使用Promise.all实现多个切片并发上传。
  3. 合并请求:切片上传完成后,通知服务器合并这些切片。

每个切片通常需要包含以下信息:

  • 切片的序号:用于标识切片的顺序,如第1片、第2片等。
  • 切片的大小:切片的字节大小。
  • 总切片数:整个文件切割后的总切片数量。
  • 文件唯一标识符:用于区分不同文件的标识,如文件名、文件MD5哈希等。
  • 切片内容:切片的实际数据。
  • 切片的起始和结束字节:可选信息,表示切片在原始文件中的位置。

谁来合并切片?何时合并切片?如何合并切片?

  • 谁来合并切片:通常是服务器来处理切片的合并。当所有切片上传完成后,服务器会进行合并操作。
  • 何时合并切片:合并可以在接收到最后一个切片之后,或者通过特定的API调用(如merge请求)来触发合并操作。
  • 如何合并切片:服务器将所有切片按照顺序读取,逐个拼接成完整的文件,通常会将切片存储在一个临时位置,在合并后再移动到最终存储位置。

注意以下几点:

大文件切片上传时,切片数量取决于几个关键因素:文件总大小、每个切片的大小(即切片大小),以及任何特定于应用或服务的限制。计算切片数量的过程包括确定合理的切片大小,然后根据文件总大小来计算需要多少个这样大小的切片。

文件分片大小的选择:过小的分片会增加上传请求的数量,而过大的分片可能会导致上传过程中的内存和网络压力增加;一般分片不要超过10M

重试机制:对于上传失败的分片,可以设置重试次数,并在重试失败后提示用户 (根据业务情况而定)

上传进度的显示:可以使用XMLHttpRequest的upload事件或fetch API的ProgressEvent来获取上传进度,并将其显示给用户。

错误处理和恢复:如果上传过程中出现错误,需要及时捕获并给出错误提示,同时确保可以从错误处恢复并继续上传。

服务器端处理:在服务器端需要相应的接口来接收和处理分片上传的文件,并在上传完成后将其合并成完整的文件。同时,需要处理断点续传的逻辑,以保证上传进度的准确性。

1.3 计算文件哈希值
  • 使用 Web Workers 来计算每个分片的哈希值,以避免阻塞主线程。(可以根据业务方向进行选择)
  • 使用 spark-md5 库来计算 MD5 哈希值
适用于小文本的简单方法
function calculateHash(text) {
    const hash = SparkMD5.hash(text);
    console.log(hash); // 输出hash值
    return hash;
}
 
// 示例使用
const text = "这是一段测试文本";
calculateHash(text);
适用于大文本的异步方法(使用Web Worker)

对于非常大的文本,直接在主线程上计算hash可能会导致UI冻结。这时,可以使用Web Worker来异步计算。

  1. 创建Web Worker脚本 (worker.js):

    self.onmessage = function(e) {
        const hash = SparkMD5.ArrayBuffer.hash(e.data, true); // 使用ArrayBuffer处理大文件数据流
        postMessage(hash); // 将hash值发送回主线程
    };
  2. 在主线程中调用Web Worker:

    function calculateHashWithWorker(text) {
        const worker = new Worker('worker.js'); // 创建worker实例
        worker.onmessage = function(e) {
            console.log(e.data); // 输出hash值
            worker.terminate(); // 完成计算后终止worker
        };
        worker.postMessage(text); // 发送文本数据到worker
    }
     
    // 示例使用大文本数据(例如,从文件读取)
    const largeText = "重复很多次的文本..."; // 这里可以是很大的字符串或者文件内容流
    calculateHashWithWorker(largeText);
注意事项:
  • 确保在处理大文件时使用ArrayBuffer,这样可以更有效地利用内存和减少阻塞。SparkMD5.ArrayBuffer.hash()方法可以处理二进制数据流。

  • 使用Web Worker时,确保处理好跨域资源共享(CORS)问题,尤其是在加载外部脚本时。

  • 对于实际应用中的文件上传场景,通常还需要将文件分割成小块(chunk),分别计算每个块的hash,然后上传这些块的hash到服务器进行比对,以实现秒传功能。这通常涉及后端逻辑的支持。

2. 断点续传

断点续传(Resumable Uploads)将大文件分成多个小片段,每个小片段上传成功后记录其上传进度,若中断或失败(如网络故障、页面关闭等)后可从上次记录的进度继续上传,避免重新上传整个文件。可以使用XMLHttpRequest或fetch API发送每个小片段,同时在服务器端保留上传进度信息。这通常通过记录已上传的分片索引/hash来实现。

2.1 实现步骤
  1. 前端记录进度:使用localStorage记录已上传成功的切片信息。这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失。
  2. 断点续传:上传时检查哪些切片未上传,继续上传未完成的部分。在上传前,向服务器查询已上传的分片,只上传未完成的分片

续传时,客户端如何知晓已上传过哪些切片?

  • 状态查询断点续传 & 秒传原理  客户端在开始上传之前,向服务器发送请求,询问该文件的上传状态(请求中包含文件的唯一标识符),服务端响应该文件已上传分片,客户端再将未上传分片上传即可;
    如果没有需要上传的分片就是秒传;
    如果有需要上传的分片就是断点续传;
  • 服务器响应:服务器会返回一个包含已上传切片编号或切片信息的列表,客户端据此判断哪些切片已经上传,从而跳过已上传的部分,只上传未完成的切片。

分段上传 与 断点续传  比较:
分段上传和断点续传都是为了优化大文件上传的过程,提高上传的效率和可靠性。它们的实现方式不同,适用情况也略有差异。下面是两者的区别:

分段上传:将一个大文件切割成多个小部分进行上传,每个小部分的大小可以根据具体需求来确定。这种方法可以减少单个请求的数据量,提高上传效率,并且可以在上传失败后只重新上传失败的部分,而不需要重新上传整个文件。常见的例子包括百度网盘、腾讯微云等。

断点续传:当文件上传过程中出现网络断开或其他异常情况时,可以通过记录已上传的部分,下次从上次上传的位置继续上传。这种方法可以保证上传的连续性,避免上传失败后需要重新上传整个文件,同时也可以提高上传效率。常见的例子包括云存储、视频网站等。

尽管它们的实现方式不同,但是它们有一些相似之处:

都需要使用特殊的上传方式或协议来支持分段上传或断点续传。例如,HTTP协议默认并不支持断点续传,需要在服务器端做特殊处理。

都需要在前端和后端做相应的处理来支持。在前端,需要将大文件切割成多个小部分并发送到后端;在后端,需要接收和保存上传的文件片段,并在最后将它们合并成一个完整的文件。

分段上传和断点续传都是为了解决大文件上传的问题,提高上传效率和可靠性。不同的场景和需求可能需要选择不同的方法来实现。

3. 秒传功能

  • 通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了。

已经上传过的文件,并且在后端已经拼接完成,如果再次上传的话后端不做处理,直接返回拼接好的文件的信息即可,一般主要后端实现。

4. WebSocket 上传

使用WebSocket协议进行文件上传。WebSocket提供了全双工通信,可以实时传输数据,适合处理大文件上传。通过WebSocket,可以将文件拆分成多个分片,并逐个发送到服务器端。

通过WebSocket协议实现实时的双向数据传输,适用于大文件或大量数据的实时传输,但需要特殊的服务器支持。

5. 基于WebWorker的并行处理

  • 使用WebWorker来并行计算文件的分片hash值,可以显著提高大文件处理的速度。

WebWorker 实际上是运行在浏览器后台的一个单独的线程,因此可以执行一些耗时的操作而不会阻塞主线程。WebWorker 通过与主线程之间传递消息实现通信,这种通信是双向的。WebWorker不能访问 DOM 不能获取dom对象,也不能使用像 window 对象这样的浏览器接口对象,但可以使用一些WebWorker 标准接口和 Navigator 对象的部分属性和方法。

1、主线程

主线程创建 worker 实例,向子线程通过 postMessage 发送消息,通过 onmessage 监听子线程返回的数据。

/*
在主线程中 通过 new Worker(线上地址服务器资源) 传入文件url来实现
返回 worker实例对象,该对象是主线程和其他线程的通讯桥梁

如果js文件是ES6 module的规范的话,那么new Worker的时候就需要说明类型
const worker = new Worker('http://localhost:5000/worker.js', {type:'module'})

*/
  const worker = new Worker('http://localhost:5000/worker.js')
 
  // 监听子线程返回的数据
  worker.onmessage = function (e) {
    console.log('Fibonacci result:', e.data)
    doSomething();
  }
  function doSomething() {
    // 执行任务
    worker.postMessage('Work done!');
  }
 
  // 主线程通过 postMessage 向子线程发送消息
  worker.postMessage('Hello World!')

  // Worker 完成任务以后,主线程就可以把它关掉
  worker.terminate();

Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败 

2、Worker 子线程 self

子线程 worker.js 

// worker.js

self.onmessage = (e) => {
    console.log('主线程发送过来的数据',e.data)
    // 处理数据
    self.postMessage(// 向主线程发送消息,主线程通过onmessage 接收)
}
//还可以通过监听 message 事件来接收消息
self.addEventListener('message', function(e) {
  console.log('收到消息:' + e.data);
})

// self代表子线程自身,即子线程的全局对象。因此,等同于下面两种写法。

// 写法一
this.addEventListener('message', function (e) {
  this.postMessage('You said: ' + e.data);
}, false);
// 写法二
addEventListener('message', function (e) {
  postMessage('You said: ' + e.data);
}, false);

self.close() // 用于在 Worker 内部关闭自身。

除了使用self.addEventListener()指定监听函数,也可以使用self.onmessage指定。监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。self.postMessage()方法用来向主线程发送消息。

3、Worker 加载脚本

Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()。

importScripts('script1.js');

// 该方法可以同时加载多个脚本
importScripts('script1.js', 'script2.js');
4、错误处理

主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error事件。

worker.onerror(function (event) {

  console.log([

    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message

  ].join(''));

});

// 或者

worker.addEventListener('error', function (event) {

  // ...

});

5、关闭 Worker

使用完毕,为了节省系统资源,必须关闭 Worker。

// 主线程

worker.terminate();

// Worker 线程

self.close();

6、数据通信

主线程与 Worker 之间的通信内容,可以是文本,也可以是二进制数据。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。

主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。下面是一个例子。

// 主线程

var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i <>uInt8Array.length; ++i) {
  uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}

worker.postMessage(uInt8Array);


// Worker 线程
self.onmessage = function (e) {
  var uInt8Array = e.data;
  postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
  postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};
7、限制
  • 同源限制
    分配给 Worker 线程的文件必须与主线程的文件同源
  • DOM 限制
    Worker 线程不能直接操作 DOM,但Worker 线程可以使用navigator对象和location对象部分属性和方法。
  • 通信限制
    不再同一个上下文,不能直接通信,须通过 postMessage 、 onmessage完成
  • 条数限制
    浏览器能创建webworker线程 基本上都在20条以内,每条线程大概5M左右
  • 文件限制
    线程不能打开本地文件,只能打开网络文件(服务器)

    在Web Workers中直接访问本地文件系统是不可能的,因为出于安全和隐私的考虑,浏览器不允许Web Workers访问本地文件系统。Web Workers主要用于执行后台任务,如数据处理、大量计算等,它们运行在浏览器的主线程之外,但不能直接访问DOM或本地文件系统。

    然而,你可以通过以下几种方式来处理与本地文件相关的需求:
    1. 使用 File API 和 Blob

    如果你的应用需要处理用户上传的文件,你可以使用 HTML 的 <input type="file"> 元素让用户选择文件,然后通过 File API 来读取文件内容。例如:

    // 在主线程中
    document.getElementById('fileInput').addEventListener('change', function(event) {
        const file = event.target.files[0];
        const reader = new FileReader();
        reader.onload = function(e) {
            const fileContent = e.target.result;
            postMessage(fileContent);  // 发送到worker
        };
        reader.readAsText(file);  // 或者 readAsDataURL, readAsBinaryString 等,根据需要
    });
     
    // 在Worker中
    self.onmessage = function(e) {
        const fileContent = e.data;
        // 处理文件内容
    };

    2. 使用 XMLHttpRequest 或 Fetch API 加载文件

    如果你的应用需要加载服务器上的文件(而不是本地文件),你可以在主线程中使用 XMLHttpRequest 或 fetch API 加载文件,然后将数据发送到 Worker。例如:

    // 在主线程中
    fetch('path/to/your/file').then(response => response.text()).then(data => {
        postMessage(data);  // 发送到worker
    });
     
    // 在Worker中
    self.onmessage = function(e) {
        const fileContent = e.data;
        // 处理文件内容
    };
  • 脚本限制
    Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
  • 有些东西是无法通过主线程传给子线程的,如 方法、dom节点、一些对象里的特殊设置(freeze、getter、setter,所以vue的响应式对象是不能传递的)
  • 模块的引入问题
    如果js文件是ES6 module的规范的话,那么new Worker的时候就需要说明类型
    const worker = new Worker('http://localhost:5000/worker.js', {type:'module'})
8、应用场景
  1. 大量的后台计算
  2. 一般不会用到 worker,因为浏览器主线程很少会出现很复杂从而导致阻塞的操作。如果真的遇到了,可以用 worker 来创建一个子线程进行处理,提高用户体验
  3. 随着webgl、canvas等能力的加入,web前端有越来越多的可视化操作。如 在线滤镜、在线绘图、web游戏等。这些东西是非常耗时计算的。

6. 通过第三方服务上传

比如使用云存储服务(如七牛、阿里云OSS等)进行文件上传,即使是大文件也可以快速上传,同时也可以享受到云存储服务的稳定性和安全性。

针对不同的场景和业务需求,可以选择适合自己的上传方案

7. 使用第三方库

有许多开源的第三方库可以简化大文件上传的过程,例如PluploadFineUploaderUppy等。这些库提供了丰富的API和功能,可以处理分片上传、断点续传、上传进度显示等复杂的操作。

8. 压缩传输数据

  • 对传输的数据进行压缩处理,减少传输时间和带宽消耗

9. 分布式存储

  • 使用分布式存储系统,提高文件存储和访问的性能和可扩展性,减轻单个服务器的负载压力。

10. 后台处理优化

  1. 分片接收与合并:服务器需要支持接收分片请求,并在所有分片上传完成后合并文件。可以利用中间件或服务端程序语言实现这一逻辑。

11. 安全性考虑

  1. 文件类型校验:在前端和后端都应对文件类型进行校验,确保上传的文件类型符合预期。
  2. 文件大小限制:限制单个文件和总上传文件的大小,防止恶意用户上传过大的文件造成服务器压力。

12. 用户体验优化

  1. 进度显示:通过显示上传进度条,让用户了解上传进度,提升用户体验。
  2. 网络波动处理:考虑到用户可能在网络不稳定的环境中上传文件,可以增加失败重试机制。

文件切片完整实例

后端代码(Node.js + Express)

安装依赖

npm init -y
npm install express multer fs

 创建服务器文件(server.js)

const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const bodyParser = require('body-parser');

const app = express();
const upload = multer({ dest: 'uploads/' });
app.use(bodyParser.json());

// 路由:处理文件切片上传
app.post('/upload', upload.single('chunk'), (req, res) => {
  const { index, fileName } = req.body;
  const chunkPath = path.join(__dirname, 'uploads', `${fileName}-${index}`);
  fs.renameSync(req.file.path, chunkPath);
  res.status(200).send('Chunk uploaded');
});

// 路由:合并切片
app.post('/merge', (req, res) => {
  const { totalChunks, fileName } = req.body;
  const filePath = path.join(__dirname, 'uploads', fileName);
  const writeStream = fs.createWriteStream(filePath);

  for (let i = 0; i < totalChunks; i++) {
    const chunkPath = path.join(__dirname, 'uploads', `${fileName}-${i}`);
    const data = fs.readFileSync(chunkPath);
    writeStream.write(data);
    fs.unlinkSync(chunkPath);
  }

  writeStream.end();
  res.status(200).send('File merged');
});

app.listen(3000, () => {
  console.log('Server started on http://localhost:3000');
});
前端代码(index.html + script.js)

1.创建HTML文件(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>大文件上传</title>
</head>
<body>
  <input type="file" id="fileInput">
  <progress id="progressBar" value="0" max="100"></progress>
  <button onclick="uploadFile()">上传文件</button>
  <script src="script.js"></script>
</body>
</html>

2.创建JavaScript文件(script.js)

const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('progressBar'); // 进度条
const chunkSize = 5 * 1024 * 1024; // 5MB 每次最大切片的长度

// 接口1:每个切片要发送的ajax
const uploadChunk = async (chunk, index, fileName) => {
  const formData = new FormData();
  formData.append('chunk', chunk);
  formData.append('index', index);
  formData.append('fileName', fileName);

  // 发送切片上传请求
  await fetch('/upload', {
    method: 'POST',
    body: formData
  });

  updateProgressBar(index);
};

const updateProgressBar = (index) => {
  const uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
  if (!uploadedChunks.includes(index)) {
    uploadedChunks.push(index);
    progressBar.value = (uploadedChunks.length / totalChunks) * 100;
    localStorage.setItem('uploadedChunks', JSON.stringify(uploadedChunks));
  }
};

// 上传文件按钮事件
// 接口2:将文件分片成promise请求数组
const uploadFile = async () => {
  const file = fileInput.files[0];
  const totalChunks = Math.ceil(file.size / chunkSize);
  const uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
  const promises = [];

  for (let i = 0; i < totalChunks; i++) {
    if (!uploadedChunks.includes(i)) {
      const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
      promises.push(uploadChunk(chunk, i, file.name));
    }
  }

  // 多个切片并发上传
  await Promise.all(promises);
  // Promise.allSettled()当你需要所有Promise都结束(不管成功还是失败)时才继续执行后续操作。
  
  // 接口3:promise.all执行结束后,请求一个是否合并成功的接口
  await fetch('/merge', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ totalChunks, fileName: file.name })
  });

  localStorage.removeItem('uploadedChunks');
  alert('文件上传成功');
};

Promise.all()当你需要所有Promise都成功时才继续。

Promise.allSettled()当你需要所有Promise都结束(不管成功还是失败)时才继续执行后续操作。 

思路:我们首先获取文件对象,并设置每个分片的大小(这里可以设置为1MB-5MB)。然后使用slice方法将文件切割成多个分片,并通过FormData对象将分片数据和其他参数(如当前分片索引、总分片数)一同发送到服务器端。 

启动后端服务器

3.在浏览器中打开前端页面

index.html文件在浏览器中打开,选择文件并点击“上传文件”按钮即可看到文件上传进度。

node server.js

三、插件

目前成熟的大文件上传方案 目前社区已经存在一些成熟的大文件上传解决方案,也许并不需要我们手动去实现一个简陋的大文件上传库,但是了解其原理还是十分有必要的。

推荐的前端vue组件:vue-simple-uploader,支持vue2,vue3

vue-simple-uploader是基于simple-Uploader.js封装的大文件上传组件,具有以下优点:

  1. 支持单文件、多文件、文件夹上传;支持拖拽文件、文件夹上传
  2. 可暂停、继续上传
  3. 错误处理
  4. 支持“秒传”,通过文件判断服务端是否已存在从而实现“秒传”
  5. 分块上传
  6. 支持进度、预估剩余时间、出错自动重试、重传等操作

 安装与配置

npm install vue-simple-uploader --save

然后在你的main.js中引入并使用它:

import Vue from 'vue';
import uploader from 'vue-simple-uploader';
Vue.use(uploader);

接下来,配置上传选项,这些选项可以根据你的后端接口和业务需求进行调整:

options: {
  target: ' http://localhost:8080', // SpringBoot后台接收文件夹数据的接口
  simultaneousUploads: 10,          // 支持同时上传数量
  autoStart: false,                 // 自动上传
  panelShow: false,
  allowDuplicateUploads: false,    // 上传过得文件不可以再上传
  testChunks: false,               // 是否分片-不分片
  chunkSize: '102400000000',       // 块大小
  // query参数是带有数据的post的额外参数,policy、OSSAccessKeyId和signature是获取到的后端签名返回,
  query: (file) => {
    return {
      name: file.name,
      key: file.key,
      policy,
      OSSAccessKeyId: accessId,
      signature,
      success_action_status: 200,  // success_action_status需设置为 200
    };
  },
}

常用方法与事件

vue-simple-uploader提供了多种方法和事件,以便于开发者根据需要进行自定义处理:

  • assignBrowse:将非组件按钮绑定为上传按钮。
  • getSize:获取上传文件的总大小。
  • progress:获取上传进度。
  • addFile:手动添加文件到上传队列。

事件处理包括但不限于:

  • fileAdded:文件添加到上传队列时触发。
  • filesAdded:多文件添加时触发。
  • fileSuccess:文件上传成功时触发。
  • complete:所有文件上传完成时触发。
  • fileError:文件上传失败时触发。

代码实现

以下是vue-simple-uploader组件的一个基本使用示例,包括组件声明、事件绑定和样式配置:

<template>
  <!-- 定义Uploader组件 -->
  <uploader
    :key="uploader_key"            <!-- 使用key确保组件在数据更新时重新渲染 -->
    :options="options"             <!-- 绑定配置项 -->
    class="uploader-example"       <!-- 添加自定义类名 -->
    @file-added="onFileAdded"      <!-- 文件添加时触发的事件 -->
    @file-success="onFileSuccess"  <!-- 文件上传成功时触发的事件 -->
    @upload-start="uploadStr"      <!-- 开始上传时触发的事件 -->
    @complete="uploadEnd"          <!-- 所有文件上传完成时触发的事件 -->
    @file-error="fileError"        <!-- 文件上传失败时触发的事件 -->
  >
    <!-- 定义不支持上传的提示 -->
    <uploader-unsupport></uploader-unsupport>
    <!-- 定义拖拽区域 -->
    <uploader-drop>
      <!-- 定义上传按钮,使用Element UI的按钮组件 -->
      <el-button class="uploaders-btn">
        <uploader-btn class="btn" :directory="true">  <!-- 设置为目录上传 -->
          <el-icon><Notification /></el-icon>         <!-- 使用Element UI的图标组件 -->
          <span>上传文件夹</span>                      <!-- 按钮文本 -->
        </uploader-btn>
      </el-button>
    </uploader-drop>
  </uploader>
</template>
 
<script>
import md5 from "js-md5";
export default {
  data() {
    return {
      // 用于刷新组件的key,每次上传时更改其值以刷新组件状态
      uploader_key: new Date().getTime(),
      // 配置项,根据后端接口和业务需求进行配置
      options: {
        //目标上传 URL,默认POST
        target: "/api/file/uploadFile", // 后端接收数据的接口
        //上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
        ileParameterName: 'upfile',
        //失败后最多自动重试上传次数
        maxChunkRetries: 3,
        query: (file, res, status) => {
          // 返回上传所需的额外参数
          return {
            filePath: "",
            identifier: md5(file.uniqueIdentifier),
            parentUserFileId: this.firstId,
            sourceMenuId: this.findId,
            uuid: this.uuid,
          };
        },
        headers: {
          "Blade-Auth": "bearer " + getToken(), // 认证信息
        },
        testChunks: true, // 不分片上传
        //分块大小(单位:字节)
        chunkSize: '2048000',
      },

      fileStatusText: {
        success: '上传成功',
        error: '上传失败',
        uploading: '上传中',
        paused: '暂停',
        waiting: '等待上传'
      }
    };
  },
  created() {
    // 组件创建时初始化options
    this.options = {
      // ... 具体配置
    };
  },
  methods: {
    // 文件添加到上传队列时的处理函数
    onFileAdded(file) {
      console.log("文件添加到队列:", file);
      // 每次添加文件时生成新的uuid
      this.uuid = new Date().getTime();
    },
    // 文件上传成功时的处理函数
    onFileSuccess(rootFile, file, response, chunk) {
      console.log("文件上传成功:", file, response);
      // 根据服务器返回的response处理业务逻辑
    },
    // 文件上传失败时的处理函数
    fileError(rootFile, file, response, chunk) {
      console.error("文件上传失败:", file, response);
      // 显示错误信息
      this.$message.error("文件夹上传失败");
    },
    // 开始上传时的处理函数
    uploadStr() {
      this.loadingFile = true; // 设置加载状态
    },
    // 所有文件上传完成时的处理函数
    uploadEnd() {
      this.loadingFile = false; // 重置加载状态
    },
  },
};
</script>
 
<style lang="scss" scoped>
/* 自定义样式 */
.uploader-example {
  .uploaders-btn {
    /* 按钮样式 */
  }
  .btn {
    /* 上传按钮内的图标和文本样式 */
  }
}
</style>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值