<

.jpeg>)

1.如何编写项目经历 #

1.1 描述方式 #

  1. 明确项目名称和你的角色:首先要清晰地标注项目的名称以及你在项目中扮演的角色

  2. 使用简洁、明了的语言:用简洁明了的语言描述你的项目经验。避免过多的技术术语,使得非技术人员也能理解。

  3. 突出使用的技术和工具:列出你在项目中使用的主要技术和工具,例如 React, Vue, JavaScript, CSS, Git 等。

  4. 详细描述你的贡献:具体描述你在项目中的工作,包括你负责的部分、你如何实现它、遇到的挑战以及你是如何克服这些挑战的。

  5. 展示成果和影响:如果可能,提供一些具体的成果,比如提高了网站的性能、增加了用户参与度、或者优化了代码结构等。使用数据和百分比来支持你的成果是一个很好的方法。

  6. 项目成就和学习:简述你从这个项目中学到了什么,以及这些经验如何帮助你成长为一个更好的前端开发者。

  7. 保持简洁和相关性:如果你有多个项目经验,选择最相关和最有影响力的项目来展示。保持每个项目描述的简洁,避免冗长和不必要的细节。

  8. 视觉效果:可以适当地使用列表、粗体或斜体来强调关键信息,使简历更具可读性。

1.2 项目示例 #

项目名称:企业级数据分析平台重构

角色:前端团队负责人

项目时间:2021 年 1 月 - 2021 年 12 月

使用技术:React, Redux, D3.js, Webpack, Node.js, Jest

项目概述: 作为前端团队的负责人,我带领一个由八名工程师组成的团队,完成了对我们公司核心产品——一款企业级数据分析平台的全面重构。这个项目的目标是提升平台的性能、增强用户体验,并引入新的数据可视化功能。

主要贡献

项目难点

项目亮点

成果

学习与成长: 这个项目不仅让我深化了对前端架构和性能优化的理解,还锻炼了我的团队管理和项目协调能力。通过这次经历,我学会了如何在保持技术创新的同时,有效地管理一个多元化的团队,确保项目目标的达成。

2.大文件上传面试题 #

  1. 基础概念

    • 描述在前端实现大文件上传的基本流程。
    • 解释为什么大文件上传比普通文件上传更具挑战性。
    • 什么是分片上传,它如何帮助在前端处理大文件上传?
  2. 技术实现

    • 如何在前端使用 JavaScript 实现大文件的切片上传?
    • 描述使用 HTML5 的 File API 来处理大文件上传的过程。
    • 在处理大文件上传时,如何在前端进行文件类型和大小的验证?
  3. 性能优化

    • 在大文件上传时,如何优化前端性能以提高用户体验?
    • 如果用户在上传过程中暂停或断网,你将如何处理已上传的数据?
    • 如何在前端实现上传进度的可视化反馈?
  4. 错误处理和安全性

    • 在大文件上传过程中,如果遇到网络错误或服务器错误,前端应该如何优雅地处理?
    • 描述在大文件上传中常见的安全问题及其解决方案。
    • 如何在前端实现大文件上传的加密处理?
  5. 后端交互

    • 大文件上传中的前后端协作通常是怎样的?请描述整个流程。
    • 如果后端 API 在上传过程中发生更改,前端应该如何灵活适应?
    • 在大文件上传中,前端和后端如何协调处理上传进度和错误恢复?
  6. 用户体验

    • 如何设计一个用户友好的大文件上传界面?
    • 在上传大文件时,如何减少对用户其他网站操作的影响?
    • 如果用户需要上传多个大文件,你将如何设计这个上传流程?
  7. 现代技术和框架

    • 使用 React/Angular/Vue 等现代前端框架,如何实现大文件上传的功能?
    • 在大文件上传中,Web Workers 能发挥什么作用?
    • 描述使用云服务(如 AWS S3)进行大文件上传的优势和挑战。

2.1 描述在前端实现大文件上传的基本流程 #

在前端实现大文件上传的基本流程通常包括以下几个关键步骤:

  1. 文件选择

    • 用户通过文件选择界面(通常是一个表单输入或拖放区域)选择要上传的文件。
    • 使用 HTML5 的 File API 来获取用户选择的文件信息。
  2. 文件切片

    • 对于大文件,直接上传整个文件可能导致问题(如网络不稳定导致上传失败、浏览器崩溃等)。因此,将大文件切分成多个小块(通常称为“切片”)是一个常见做法。
    • 使用 JavaScript 将文件切分成多个小块。每个块可以是固定大小(如 1MB)。
  3. 上传前准备

    • 可以向服务器发送一个预请求,告知服务器将要上传的文件的信息(如文件名、文件大小、切片数量等)。
    • 服务器可以在此时进行一些准备工作,如检查文件是否已部分上传、为即将上传的文件分配资源等。
  4. 并行或顺序上传切片

    • 切片可以并行上传,以提高上传效率,也可以顺序上传,以降低服务器压力。
    • 每个切片作为一个单独的请求发送到服务器。这些请求通常包含切片数据和一些元数据(如切片索引、总切片数等)。
  5. 错误处理和重试机制

    • 在上传过程中,一些切片可能因网络问题或服务器问题上传失败。前端应该有机制来检测这些失败,并且能够重新尝试上传这些切片。
    • 可以设置重试次数限制,以避免无限重试。
  6. 上传进度反馈

    • 在上传过程中,前端应提供实时的上传进度反馈。这可以通过监听每个切片上传请求的进度事件来实现。
  7. 完成上传

    • 所有切片上传完成后,前端向服务器发送一个完成上传的信号。
    • 服务器收到完成信号后,开始将所有切片组合成原始文件。
  8. 文件验证和清理

    • 服务器组合文件后,可以对文件进行验证,确保文件的完整性和正确性。
    • 验证完成后,前端和后端可以进行必要的清理工作,如删除已上传的切片文件。
  9. 错误处理和用户反馈

    • 如果在任何上传阶段发生错误,应及时将错误信息反馈给用户。
    • 同时,前端应提供用户友好的错误信息和可能的解决方案。

2.2 解释为什么大文件上传比普通文件上传更具挑战性 #

上传大文件比上传普通大小的文件更具挑战性,主要原因有几个:

  1. 网络稳定性问题:大文件上传需要更长的时间,这增加了网络连接中断的风险。如果在上传过程中出现网络问题,可能需要重新开始上传整个文件,这不仅浪费时间,还可能导致数据丢失。

  2. 资源消耗:上传大文件会占用更多的网络带宽和服务器资源。这可能导致服务器性能下降,影响其他用户的使用体验。

  3. 浏览器和服务器限制:某些浏览器和服务器对上传文件的大小有限制。大文件可能超出这些限制,需要特殊的配置或技术来处理。

  4. 数据完整性和安全性:在长时间的传输过程中,确保数据的完整性和安全性是一大挑战。需要采用额外的措施来保证文件在传输过程中不被损坏或篡改。

  5. 用户体验问题:大文件上传可能需要很长时间,这可能导致用户体验不佳。用户可能不清楚上传进度,或者在等待过程中感到沮丧。

为了解决这些挑战,通常会采用一些策略,如文件分割上传(将大文件分割成小块,分别上传),断点续传(在连接中断后能从中断点重新开始上传),以及优化网络和服务器配置等。

2.3 什么是分片上传,它如何帮助在前端处理大文件上传? #

分片上传是一种处理大文件上传的技术,它将大文件分割成多个小片段(分片),然后逐个上传这些分片。这种方法在前端处理大文件上传时具有多个优势:

  1. 提高可靠性:通过分片上传,即使在上传过程中发生网络问题,也只需要重新上传受影响的分片,而不是整个文件。这显著减少了因网络不稳定导致的重传需求。

  2. 支持断点续传:如果上传过程中断,可以在网络恢复后继续上传未完成的分片。这对于移动设备用户特别重要,因为他们可能会在不稳定的网络环境下上传文件。

  3. 优化上传速度:分片允许并行上传,可以同时上传多个分片,这样可以更有效地利用网络带宽,从而加快上传速度。

  4. 减轻服务器负担:服务器处理一系列小文件比处理一个大文件要容易。这有助于降低服务器的资源消耗,特别是在高负载情况下。

  5. 灵活的错误处理:在分片上传中,可以针对单个分片进行错误检测和恢复,而不是对整个文件进行处理。这提高了上传过程的稳定性和效率。

  6. 进度控制和管理:分片上传使得可以更精确地控制和展示上传进度,提高了用户体验。

  7. 绕过文件大小限制:某些浏览器或服务器对单次上传的文件大小有限制。分片上传可以绕过这些限制,因为每个分片都小于这个限制。

为了实现分片上传,前端需要具备将文件分割成分片的能力,并且后端服务器需要能够接收这些分片,并在所有分片上传完毕后将它们重新组装成原始文件。

2.4 如何在前端使用 JavaScript 实现大文件的切片上传? #

在前端使用 JavaScript 实现大文件的切片上传主要涉及以下几个步骤:

1. 选择文件

首先,用户需要能够选择要上传的文件。这通常通过 HTML 的 <input type="file"> 元素实现。

<input type="file" id="fileInput" />

2. 切割文件

在文件被选择后,使用 JavaScript 的 Blob.slice() 方法来切割文件。这个方法可以从原始文件中提取出一部分,创建一个新的 Blob 对象。

function sliceFile(file, chunkSize) {
  let chunks = [];
  let size = file.size;

  for (let start = 0; start < size; start += chunkSize) {
    let end = Math.min(start + chunkSize, size);
    chunks.push(file.slice(start, end));
  }

  return chunks;
}

这个函数接受一个文件对象和一个分片大小(例如 1MB),然后将文件分割成多个分片。

3. 上传分片

接下来,需要一个函数来处理每个分片的上传。这通常涉及到创建一个 FormData 对象,将分片添加到其中,然后使用 fetchXMLHttpRequest 发送到服务器。

async function uploadChunk(chunk, index) {
  let formData = new FormData();
  formData.append("file", chunk);
  formData.append("index", index);

  let response = await fetch("/upload", {
    // 服务器上传接口
    method: "POST",
    body: formData,
  });

  return response.ok;
}

4. 处理文件上传

当用户选择文件后,你可以将文件切割成分片,并逐个上传它们。

document
  .getElementById("fileInput")
  .addEventListener("change", async (event) => {
    let file = event.target.files[0];
    let chunkSize = 1024 * 1024; // 1MB
    let chunks = sliceFile(file, chunkSize);

    for (let i = 0; i < chunks.length; i++) {
      let success = await uploadChunk(chunks[i], i);
      if (!success) {
        console.error("Upload failed for chunk " + i);
        return;
      }
    }

    console.log("File uploaded successfully");
  });

5. 服务器端处理

服务器端需要有相应的逻辑来接收这些分片,并在所有分片上传后,重新组装这些分片成原始文件。

注意事项

2.5 描述使用 HTML5 的 File API 来处理大文件上传的过程 #

使用 HTML5 的 File API 处理大文件上传主要包括以下几个步骤:

1. 文件选择 首先,你需要一个 HTML 元素让用户选择文件。通常使用<input type="file">元素:

<input type="file" id="fileInput" />

2. 读取文件 当用户选择文件后,你可以使用 File API 来读取文件。这通常在一个事件监听器中完成,该监听器响应文件选择输入的更改:

document
  .getElementById("fileInput")
  .addEventListener("change", function (event) {
    var file = event.target.files[0]; // 获取选择的文件
    // 接下来可以处理这个文件
  });

3. 分片文件 由于直接上传大文件可能会遇到各种问题,因此通常会将文件分割成更小的部分(分片)来上传。你可以使用Blob.slice()方法来分割文件:

function sliceFile(file, chunkSize) {
  let chunks = [];
  let size = file.size;

  for (let start = 0; start < size; start += chunkSize) {
    let end = Math.min(start + chunkSize, size);
    chunks.push(file.slice(start, end));
  }

  return chunks;
}

在这个函数中,chunkSize是每个分片的大小(例如 1MB),chunks数组包含了所有分片。

4. 上传分片 接下来,需要为每个分片创建一个FormData对象,并使用XMLHttpRequestfetch API 将它上传到服务器:

async function uploadChunk(chunk, index) {
  let formData = new FormData();
  formData.append("file", chunk);
  formData.append("index", index);

  let response = await fetch("/upload", {
    // 服务器上传接口
    method: "POST",
    body: formData,
  });

  return response.ok;
}

5. 合并分片 一旦所有分片都成功上传到服务器,服务器需要一个机制来重新组合这些分片。这通常在服务器端的上传脚本中处理。

6. 完整的上传过程 将上述步骤结合起来,创建一个完整的上传过程:

document
  .getElementById("fileInput")
  .addEventListener("change", async (event) => {
    let file = event.target.files[0];
    let chunkSize = 1024 * 1024; // 例如,每个分片1MB
    let chunks = sliceFile(file, chunkSize);

    for (let i = 0; i < chunks.length; i++) {
      let success = await uploadChunk(chunks[i], i);
      if (!success) {
        console.error("Upload failed for chunk " + i);
        return;
      }
    }

    console.log("File uploaded successfully");
  });

注意事项

2.6 在处理大文件上传时,如何在前端进行文件类型和大小的验证? #

在前端进行文件类型和大小的验证是一个重要步骤,以确保用户上传的文件符合特定的要求。这可以通过 JavaScript 实现,通常在文件选择后立即进行。以下是如何在前端进行文件类型和大小的验证的步骤:

1. 获取文件引用 首先,你需要获取到用户选择的文件。这通常是通过 HTML 的文件输入字段实现的:

<input type="file" id="fileInput" />

在 JavaScript 中,你可以通过监听文件输入字段的变化来获取文件:

document
  .getElementById("fileInput")
  .addEventListener("change", function (event) {
    var file = event.target.files[0]; // 获取选择的文件
    // 接下来进行验证
  });

2. 验证文件大小 要验证文件的大小,可以检查文件对象的size属性,该属性以字节为单位给出文件的大小。例如,如果你想限制文件大小不超过 10MB,可以这样做:

const MAX_SIZE = 10 * 1024 * 1024; // 10MB

if (file.size > MAX_SIZE) {
  alert("File is too large. Maximum size is 10MB.");
  return;
}

3. 验证文件类型 验证文件类型通常是通过检查文件的 MIME 类型(通过文件对象的type属性)或文件扩展名(通过文件名的字符串操作)。例如,如果你想允许用户上传 JPEG 和 PNG 图片,可以这样验证:

const ALLOWED_TYPES = ["image/jpeg", "image/png"];

if (!ALLOWED_TYPES.includes(file.type)) {
  alert("Invalid file type. Only JPEG and PNG are allowed.");
  return;
}

或者,通过文件扩展名验证:

const ALLOWED_EXTENSIONS = [".jpeg", ".jpg", ".png"];

if (!ALLOWED_EXTENSIONS.some((extension) => file.name.endsWith(extension))) {
  alert("Invalid file type. Only JPEG and PNG are allowed.");
  return;
}

4. 处理验证结果 如果文件不符合要求,你可以向用户显示一个错误消息,并停止进一步的上传处理。如果文件通过验证,你可以继续上传处理。

5. 综合示例 将上述步骤结合,可以创建一个函数来处理文件选择和验证:

document
  .getElementById("fileInput")
  .addEventListener("change", function (event) {
    var file = event.target.files[0];

    // 验证文件大小
    const MAX_SIZE = 10 * 1024 * 1024; // 10MB
    if (file.size > MAX_SIZE) {
      alert("File is too large. Maximum size is 10MB.");
      return;
    }

    // 验证文件类型
    const ALLOWED_TYPES = ["image/jpeg", "image/png"];
    if (!ALLOWED_TYPES.includes(file.type)) {
      alert("Invalid file type. Only JPEG and PNG are allowed.");
      return;
    }

    // 文件通过验证,继续处理上传
    // ...
  });

注意事项

2.7 在大文件上传时,如何优化前端性能以提高用户体验? #

优化前端性能以提高大文件上传的用户体验需要综合考虑多个方面。以下是一些关键的策略:

1. 使用分片上传

2. 断点续传

3. 前端文件验证

4. 显示上传进度

5. 优化内存使用

6. 用户界面响应性

7. 适应网络条件

8. 服务器端优化

9. 安全性和隐私

10. 反馈和支持

通过这些策略,可以显著提高大文件上传的效率和用户体验。记住,用户体验不仅仅是关于速度,还包括整个过程的易用性、可靠性和反馈。

2.8 如果用户在上传过程中暂停或断网,你将如何处理已上传的数据? #

在处理用户在上传过程中暂停或断网的情况时,主要目标是保留已上传的数据,以便用户可以在稍后继续上传而不是从头开始。以下是处理这种情况的关键步骤:

1. 使用分片上传

2. 跟踪上传进度

3. 实现断点续传功能

4. 在服务器端处理分片

5. 提供恢复上传的选项

6. 优化用户体验

7. 处理长时间断开的情况

8. 安全性考虑

通过实现这些策略,可以确保即使在用户暂停或网络中断的情况下,也能有效管理上传进度,提供更好的用户体验,并减少重复上传的需要。

2.9 如何在前端实现上传进度的可视化反馈? #

在前端实现上传进度的可视化反馈主要涉及两个方面:追踪上传进度和在用户界面中展示这些进度。以下是实现这一功能的步骤:

1. 设计进度显示界面

2. 使用 JavaScript 追踪上传进度

3. 实时更新进度条

4. 处理上传完成和错误

5. 考虑用户体验

通过实现这些步骤,你可以有效地在前端为用户提供直观的上传进度反馈,从而增强用户体验。

2.10 在大文件上传过程中,如果遇到网络错误或服务器错误,前端应该如何优雅地处理? #

在大文件上传过程中遇到网络错误或服务器错误时,前端的处理方式应当旨在保持用户体验的平稳和友好,同时提供必要的错误恢复机制。以下是一些处理这种情况的策略:

1. 捕获和识别错误

2. 显示友好的错误信息

3. 自动重试和断点续传

4. 提供手动重试选项

5. 日志记录

6. 保持上传界面的状态

7. 考虑上传取消和暂停功能

8. 用户指导

9. 优雅的失败

通过实施这些策略,可以确保即使在遇到错误的情况下,用户体验也能保持流畅和积极,同时提供恢复上传的途径。

2.11 描述在大文件上传中常见的安全问题及其解决方案。 #

在大文件上传中,安全问题是一个重要的考虑因素,尤其是在处理敏感数据或在公共平台上操作时。以下是一些常见的安全问题及其解决方案:

1. 未授权的文件上传

2. 服务拒绝攻击(DoS)

3. 敏感数据泄露

4. 跨站脚本攻击(XSS)

5. 文件完整性问题

6. 身份验证和授权

7. 服务器端执行代码

通过这些策略的综合应用,可以显著提高大文件上传过程的安全性,保护服务器和用户数据免受多种网络攻击和风险的威胁。

2.12 如何在前端实现大文件上传的加密处理? #

在前端实现大文件上传的加密处理涉及到在文件上传到服务器之前对其内容进行加密,从而确保数据的安全性和隐私性。以下是实现这一过程的步骤:

1. 选择加密算法

2. 生成密钥

3. 文件分片和加密

4. 处理加密数据

5. 上传加密的文件分片

6. 服务器端处理

7. 客户端和服务器端的安全考虑

示例代码(使用 Web Crypto API) 这是一个简化的示例,展示如何使用 Web Crypto API 加密文件分片:

async function encryptData(data, key) {
  // 假设key是一个通过Web Crypto API生成的密钥
  let encrypted = await window.crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv: window.crypto.getRandomValues(new Uint8Array(12)), // 初始化向量
    },
    key,
    data
  );
  return encrypted;
}

// 示例:加密文件分片
async function encryptFileChunk(chunk) {
  // 这里应该有密钥生成和管理的代码
  let key = await window.crypto.subtle.generateKey(
    {
      name: "AES-GCM",
      length: 256,
    },
    true,
    ["encrypt", "decrypt"]
  );

  return await encryptData(chunk, key);
}

注意

2.13 大文件上传中的前后端协作通常是怎样的?请描述整个流程。 #

大文件上传中的前后端协作通常涉及复杂的数据处理、网络通信和错误处理机制。整个流程可以分为以下几个主要步骤:

1. 用户选择文件

2. 前端准备文件上传

3. 初始化上传

4. 分片上传

5. 后端处理

6. 错误处理和重试机制

7. 上传完成

8. 安全和验证

9. 清理和维护

整个流程需要前后端紧密协作,共同处理文件切片、数据传输、错误处理和安全问题,以实现高效、可靠且用户友好的大文件上传功能。

2.14 如果后端 API 在上传过程中发生更改,前端应该如何灵活适应? #

如果后端 API 在上传过程中发生更改,前端需要灵活适应以保证上传功能的连续性和用户体验。以下是应对此类情况的策略:

1. 版本控制

2. 及时的沟通

3. 灵活的配置

4. 特征检测和兼容性处理

5. 逐步部署和测试

6. 错误处理和回退机制

7. 用户通知

8. 持续集成和持续部署(CI/CD)

通过采取这些措施,前端可以灵活应对后端 API 的更改,减少对用户体验的负面影响,并保持服务的连续性和稳定性。

2.15 在大文件上传中,前端和后端如何协调处理上传进度和错误恢复? #

在大文件上传过程中,前端和后端需要紧密协作以协调处理上传进度和错误恢复。这通常涉及以下几个关键方面:

上传进度协调

  1. 前端进度追踪:

    • 使用 HTML5 的XMLHttpRequestfetch API 的上传事件来追踪每个文件分片的上传进度。
    • 显示一个进度条或其他指示器来反映当前上传状态。
  2. 后端进度反馈:

    • 后端在接收每个分片后,可以发送一个确认响应,表明该分片已成功上传。
    • 对于分片上传,后端可以提供一个接口,让前端查询已上传分片的状态或进度。
  3. 同步进度信息:

    • 前端定期轮询后端,获取整体上传进度。
    • 在前端显示实时进度,包括从后端获取的信息。

错误恢复协调

  1. 错误检测:

    • 前端需要捕获可能发生的错误,如网络中断、服务器错误响应等。
    • 后端在处理上传请求时,应检测并处理可能出现的错误,如文件损坏、不合法的分片等。
  2. 错误通信:

    • 当后端检测到错误时,应向前端发送明确的错误响应,包括错误代码和描述。
    • 前端解析错误响应,并根据错误类型作出相应处理。
  3. 断点续传机制:

    • 实现断点续传功能,允许在上传中断后从最后一个成功上传的分片处继续上传。
    • 前端保存上传状态(如已上传的分片索引),以便在重新连接后继续上传。
  4. 重试策略:

    • 前端实现自动或手动重试机制,尤其是针对临时性的网络错误。
    • 后端确保能够处理重复的分片上传请求,避免数据重复。
  5. 用户界面和反馈:

    • 前端提供用户友好的错误信息和恢复选项,如“重试”按钮。
    • 实时更新用户界面以反映当前状态,包括错误状态和恢复进度。

综合考虑

通过这种协调合作,前端和后端可以共同提供一个稳定、高效且用户友好的大文件上传体验。

2.16 如何设计一个用户友好的大文件上传界面? #

设计一个用户友好的大文件上传界面需要考虑易用性、信息清晰度、交互响应性和视觉吸引力。以下是一些关键要素和设计建议:

1. 清晰的上传指引

2. 进度反馈

3. 错误处理和信息反馈

4. 交互设计

5. 响应式设计

6. 美观的界面设计

7. 高级选项

8. 辅助信息和帮助

9. 访问性

通过结合这些设计原则,可以创建一个既美观又功能强大的用户友好上传界面,提升用户的整体体验。

2.17 在上传大文件时,如何减少对用户其他网站操作的影响? #

在上传大文件时减少对用户其他网站操作的影响,主要涉及优化网络资源的使用和确保前端性能。以下是一些关键策略:

1. 控制上传带宽

2. 使用 Web Workers

3. 优化文件处理

4. 用户控制

5. 异步上传

6. 可视化反馈

7. 服务器端优化

8. 错误处理

9. 网络状况检测

通过采取这些措施,可以在上传大文件时最大限度地减少对用户在网站上进行其他操作的影响,提供平滑且友好的用户体验。

2.18 如果用户需要上传多个大文件,你将如何设计这个上传流程? #

设计一个针对多个大文件上传的流程时,需要考虑用户体验、网络效率和服务器负载。以下是一个有效的上传流程设计:

1. 用户界面设计

2. 文件校验

3. 上传队列管理

4. 上传进度和控制

5. 上传策略

6. 错误处理和恢复

7. 后端处理

8. 完成和反馈

9. 性能和安全

10. 用户指南和帮助

通过这样的设计,你可以提供一个既强大又用户友好的多文件上传解决方案,使得上传大文件的过程既高效又稳定。

2.19 用 React/Angular/Vue 等现代前端框架,如何实现大文件上传的功能? #

在 React、Angular、Vue 等现代前端框架中实现大文件上传功能,核心步骤大致相同,尽管具体实现细节会根据所用框架的不同而有所差异。下面以这三个框架为例,提供实现大文件上传功能的基本指导:

1. React

在 React 中,你可以使用fetchaxios等库来处理文件上传,并通过组件的状态来管理上传进度和显示。

步骤概述:

  1. 创建上传组件:创建一个 React 组件来处理文件选择和显示上传进度。
  2. 文件选择:使用<input type="file">来让用户选择文件。
  3. 分片上传:将文件分割成小块,并逐块上传。
  4. 显示进度:使用 React 的状态来追踪和显示每个分片的上传进度。
  5. 错误处理:在组件内部处理可能出现的上传错误,并提供用户反馈。

示例代码片段:

class FileUpload extends React.Component {
  // ... 状态管理和方法定义 ...

  handleFileChange = (event) => {
    const file = event.target.files[0];
    // ... 处理文件上传逻辑 ...
  };

  render() {
    return (
      <div>
        <input type="file" onChange={this.handleFileChange} />
        {/* 进度条和状态信息 */}
      </div>
    );
  }
}

2. Angular

在 Angular 中,你可以使用内置的HttpClient模块来处理文件上传,并利用 Angular 的数据绑定来更新 UI。

步骤概述:

  1. 创建上传服务:使用 Angular 服务来封装文件上传逻辑。
  2. 文件选择:使用 Angular 表单控件来处理文件选择。
  3. 上传进度:使用HttpClient的上传事件来追踪进度。
  4. 显示进度:使用 Angular 的数据绑定来更新进度条和状态信息。

示例代码片段:

// 文件上传服务
@Injectable()
export class FileUploadService {
  // ... 上传方法 ...
}

// 组件
@Component({
  selector: "app-file-upload",
  template: `
    <input type="file" (change)="handleFileChange($event)" />
    <!-- 进度条和状态信息 -->
  `,
})
export class FileUploadComponent {
  // ... 组件逻辑 ...
}

3. Vue

在 Vue 中,你可以利用axios或原生的XMLHttpRequest来处理文件上传,并使用 Vue 的响应式系统来更新 UI。

步骤概述:

  1. 创建上传组件:创建一个 Vue 组件用于文件上传。
  2. 文件选择:使用 v-model 或事件监听来处理文件选择。
  3. 分片上传和进度追踪:将文件分割为分片,使用axios等进行上传,并追踪进度。
  4. 显示进度:使用 Vue 的数据绑定来动态显示上传进度和状态。

示例代码片段:

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <!-- 进度条和状态信息 -->
  </div>
</template>

<script>
export default {
  // ... Vue实例数据和方法 ...
  methods: {
    handleFileChange(event) {
      const file = event.target.files[0];
      // ... 处理文件上传逻辑 ...
    },
  },
};
</script>

跨框架共通点

在实际开发过程中,你可能还需要根据具体需求调整和优化上传功能,比如添加暂停、继续上传的功能,或者实现更复杂的错误恢复机制。

2.20 在大文件上传中,Web Workers 能发挥什么作用? #

在大文件上传中,Web Workers 能发挥重要作用,特别是在提高前端性能和用户体验方面。以下是 Web Workers 的几个关键用途:

1. 处理计算密集型任务

2. 提升性能和响应性

3. 实现复杂的上传逻辑

4. 背景数据同步

5. 用户体验优化

使用 Web Workers 的注意事项

通过利用 Web Workers,可以显著提升处理大文件上传时的前端性能,同时保持应用界面的流畅和响应,提供更好的用户体验。

2.21 描述使用云服务(如阿里云)进行大文件上传的优势和挑战。 #

使用云服务(如阿里云)进行大文件上传带来了许多优势,同时也伴随着一些挑战。

优势

  1. 高可用性和可靠性

    • 云服务提供高可用性,确保数据上传过程的稳定性和可靠性。
    • 数据中心通常会有数据备份和恢复机制,减少数据丢失风险。
  2. 弹性和可扩展性

    • 根据需求自动扩展资源,无需担心因用户数量增加而导致的服务器负载问题。
    • 可以根据流量和数据量动态调整带宽和存储。
  3. 全球数据中心

    • 云服务提供商通常拥有遍布全球的数据中心,可根据用户位置选择最近的服务器,提高上传速度。
    • 有助于降低延迟,提升跨地域用户的体验。
  4. 成本效率

    • 云服务通常采用按需付费模式,节省了企业的前期硬件投资和维护成本。
    • 可以根据实际使用情况调整资源,优化成本效率。
  5. 安全性和合规性

    • 云服务提供商通常提供高级别的安全保障,包括数据加密、身份验证、访问控制等。
    • 符合国际安全标准和地区法规要求。

挑战

  1. 数据隐私和安全性

    • 上传敏感数据到云可能涉及隐私问题,需要确保数据在传输和存储过程中的加密和安全。
    • 需要严格遵守数据保护法规,如 GDPR。
  2. 网络依赖性

    • 云服务对网络的依赖性较高,网络不稳定可能影响上传效果。
    • 在一些网络条件较差的地区,上传大文件可能会遇到困难。
  3. 成本控制

    • 虽然云服务具有成本效率,但未经优化的资源使用可能导致费用增加。
    • 需要仔细监控和管理云资源的使用,以避免意外的高费用。
  4. 技术复杂性

    • 集成云服务需要一定的技术能力,可能需要专业知识和额外的开发工作。
    • 云服务的配置和管理比传统服务器更加复杂。
  5. 供应商锁定

    • 使用特定云服务商的产品和 API 可能导致供应商锁定问题,迁移到其他平台可能存在挑战。

总之,使用云服务进行大文件上传能够提供高效、可靠和灵活的解决方案,但也需要仔细考虑和应对数据安全、成本控制和技术挑战。

3.大文件上传的业务背景和挑战 #

大文件上传是许多在线应用和服务中的一个常见需求,尤其是在那些需要处理视频、音频、大型文档集或高分辨率图片的场景中。这项功能的业务背景和挑战可以从多个角度来看:

3.1 业务背景 #

  1. 媒体处理:视频编辑平台、音频处理软件、图像库等需要上传大量媒体文件。
  2. 数据备份与迁移:企业需要备份或迁移大量数据,包括数据库文件、系统镜像等。
  3. 内容分发网络:在 CDN 中上传大文件以便更快地在全球范围内分发。
  4. 科学与研究:上传大型数据集,例如基因组序列、气象模型数据等。
  5. 教育和在线学习:上传高质量的教学视频和教材。
  6. 法律和财务:共享大量的法律文档或财务报表。

3.2 挑战 #

  1. 性能问题:大文件上传可能导致客户端(浏览器)性能下降,特别是在资源有限的设备上。
  2. 网络不稳定:大文件更有可能在上传过程中遇到网络问题,如断线、超时等。
  3. 服务器负载:大文件上传会给服务器带来更大的负载,特别是在处理大量此类请求时。
  4. 用户体验:长时间的上传过程可能导致用户感到不耐烦,影响用户体验。
  5. 文件完整性和安全性:确保文件在传输过程中不被破坏或篡改,同时保证数据的隐私和安全。
  6. 断点续传:支持在网络中断后能够继续上传,而不是重新开始。
  7. 数据处理:大文件需要更复杂的处理流程,例如切片、压缩和解压缩。
  8. 兼容性和标准化:确保各种浏览器和设备都能顺利完成上传过程。

4.大文件上传原理与实现 #

4.1 创建项目 #

npx create-react-app uploadfile-client
npm install @ant-design/icons antd  axios
npm start

4.2 绘制页面 #

4.2.1 src\index.js #

src\index.js

import React from "react";
import ReactDOM from "react-dom/client";
import FileUploader from "./FileUploader";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<FileUploader />);

4.2.2 FileUploader.js #

src\FileUploader.js

import { InboxOutlined } from "@ant-design/icons";
import "./FileUploader.css";
const FileUploader = () => {
  return (
    <div className="upload-container">
      <InboxOutlined />
    </div>
  );
};
export default FileUploader;

4.2.3 FileUploader.css #

src\FileUploader.css

.upload-container{
    width: 100%;
    height: 200px;
    display: flex;
    justify-content: center;
    align-items: center;
    border: 1px dashed #d9d9d9;
    background: #fafafa;
}
.upload-container:hover{
    border-color: #40a9ff;
}
.upload-container span{
    font-size:60px;
}

4.3 拖入文件 #

4.3.1 FileUploader.js #

src\FileUploader.js

+import { useRef} from 'react';
import { InboxOutlined } from "@ant-design/icons";
+import useDrag from "./useDrag";
import "./FileUploader.css";
const FileUploader = () => {
+ const uploadContainerRef = useRef(null);
+ useDrag(uploadContainerRef);
  return (
+   <div className="upload-container" ref={uploadContainerRef}>
      <InboxOutlined />
    </div>
  );
};
export default FileUploader;

4.3.2 useDrag.js #

src\useDrag.js

import { useState, useEffect } from "react";
function useDrag(uploadContainerRef) {
  const [selectedFile, setSelectedFile] = useState(null);
  const handleDrag = (event) => {
    event.preventDefault();
    event.stopPropagation();
  };
  const handleDrop = (event) => {
    event.preventDefault();
    event.stopPropagation();
    const { files } = event.dataTransfer;
    console.log("files", files);
  };
  useEffect(() => {
    const uploadContainer = uploadContainerRef.current;
    uploadContainer.addEventListener("dragenter", handleDrag);
    uploadContainer.addEventListener("dragover", handleDrag);
    uploadContainer.addEventListener("drop", handleDrop);
    uploadContainer.addEventListener("dragleave", handleDrag);
    return () => {
      uploadContainer.removeEventListener("dragenter", handleDrag);
      uploadContainer.removeEventListener("dragover", handleDrag);
      uploadContainer.removeEventListener("drop", handleDrop);
      uploadContainer.removeEventListener("dragleave", handleDrag);
    };
  }, []);
  return { selectedFile };
}
export default useDrag;

4.4 检查并预览文件 #

4.4.1 FileUploader.js #

src\FileUploader.js

import { useRef } from "react";
+import { InboxOutlined,FileOutlined } from "@ant-design/icons";
import useDrag from "./useDrag";
import "./FileUploader.css";
const FileUploader = () => {
  const uploadContainerRef = useRef(null);
+ const { filePreview} = useDrag(uploadContainerRef);
  return (
    <div className="upload-container" ref={uploadContainerRef}>
+     {renderFilePreview(filePreview)}
    </div>
  );
};
+function renderFilePreview(filePreview){
+  if (filePreview.url) {
+    if (filePreview.type.startsWith("video/")) {
+      return <video src={filePreview.url} alt="Preview" controls />;
+    } else if (filePreview.type.startsWith("image/")) {
+      return <img src={filePreview.url} alt="Preview" />;
+    } else {
+      return filePreview.url;
+    }
+  } else {
+    return <InboxOutlined />;
+  }
+}
export default FileUploader;

4.4.2 FileUploader.css #

src\FileUploader.css

.upload-container{
    width: 100%;
    height: 200px;
    display: flex;
    justify-content: center;
    align-items: center;
    border: 1px dashed #d9d9d9;
    background: #fafafa;
}
.upload-container:hover{
    border-color: #40a9ff;
}
.upload-container span{
    font-size:60px;
}
+.upload-container video,img{
+    height:100%;
+}

4.4.3 constant.js #

src\constant.js

export const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;

4.4.4 useDrag.js #

src\useDrag.js

import { useState,useEffect} from 'react';
+import {MAX_FILE_SIZE} from './constant';
function useDrag(uploadContainerRef) {
  const [selectedFile, setSelectedFile] = useState(null);
+ const [filePreview, setFilePreview] = useState({
+   url: null,
+   type: null
+ });
+ const checkFile = files => {
+   const file = files[0];
+   if (!file) {
+     message.error("没有选择任何文件");
+     return;
+   }
+   if (file.size > MAX_FILE_SIZE) {
+     message.error("文件大小不能超过2GB");
+     return;
+   }
+   if (!file.type.startsWith("image/") && !file.type.startsWith("video/")) {
+     message.error("文件类型必须是图片或视频");
+     return;
+   }
+   setSelectedFile(file);
+ };
+ useEffect(()=>{
+   if(!selectedFile) return;
+   const url = URL.createObjectURL(selectedFile);
+   setFilePreview({url,type: selectedFile.type});
+   return () => {
+     URL.revokeObjectURL(url);
+   };
+ },[selectedFile]);
  const handleDrag = (event) => {
    event.preventDefault();
    event.stopPropagation();
  };
  const handleDrop = (event) => {
    event.preventDefault();
    event.stopPropagation();
+   checkFile(event.dataTransfer.files);
  };
  useEffect(() => {
    const uploadContainer = uploadContainerRef.current;
    uploadContainer.addEventListener("dragenter", handleDrag);
    uploadContainer.addEventListener("dragover", handleDrag);
    uploadContainer.addEventListener("drop", handleDrop);
    uploadContainer.addEventListener("dragleave", handleDrag);
    return () => {
      uploadContainer.removeEventListener("dragenter", handleDrag);
      uploadContainer.removeEventListener("dragover", handleDrag);
      uploadContainer.removeEventListener("drop", handleDrop);
      uploadContainer.removeEventListener("dragleave", handleDrag);
    };
  }, []);
  return {filePreview};
}
export default useDrag;

4.5 分片上传 #

4.5.1 FileUploader.js #

src\FileUploader.js

import { useRef } from "react";
import { InboxOutlined } from "@ant-design/icons";
+import { Button, message } from "antd";
import useDrag from "./useDrag";
+import { CHUNK_SIZE } from './constant';
+import axiosInstance from "./axiosInstance";
import "./FileUploader.css";
const FileUploader = () => {
  const uploadContainerRef = useRef(null);
+ const { selectedFile, filePreview, resetFileStatus } = useDrag(uploadContainerRef);
+ const handleUpload = async () => {
+   if (!selectedFile) {
+     message.error("请先选择一个文件");
+     return;
+   }
+   const filename = await getFileName(selectedFile);
+   await uploadFile(selectedFile,filename);
+ }
+ const renderButton = () => {
+   return <Button onClick={handleUpload}>上传</Button>;
+ }
  return (
+    <>
      <div className="upload-container" ref={uploadContainerRef}>
        {renderFilePreview(filePreview)}
      </div>
+     {renderButton()}
+   </>
  );
};
+async function createRequest(filename, chunkFileName, chunk) {
+  return axiosInstance.post(`/upload/${filename}`, chunk, {
+    headers: {
+      "Content-Type": "application/octet-stream"
+    },
+    params: {
+      chunkFileName
+    }
+  });
+}
+async function getFileName(file) {
+  const fileHash = await calculateHash(file);
+  const fileExtension = file.name.split(".").pop();
+  return `${fileHash}.${fileExtension}`;
+}
+async function uploadFile(file,filename) {
+  const chunks = createFileChunks(file, filename);
+  const requests = chunks.map(({ chunk, chunkFileName }) => createRequest(filename, chunkFileName, chunk));
+  try {
+    await Promise.all(requests);
+    await axiosInstance.get(`/merge/${filename}`);
+    message.success("上传完成");
+  } catch (error) {
+    console.error("上传出错:", error);
+    message.error("上传出错");
+  }
+}
+function createFileChunks(file, filename) {
+  let chunks = [];
+  let count = Math.ceil(file.size / CHUNK_SIZE);
+  for (let i = 0; i < count; i++) {
+    let chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
+    chunks.push({
+      chunk,
+      chunkFileName: `${filename}-${i}`
+    });
+  }
+  return chunks;
+}
+async function calculateHash(file) {
+  const arrayBuffer = await file.arrayBuffer();
+  const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
+  return bufferToHex(hashBuffer);
+}
+function bufferToHex(buffer) {
+  return Array.from(new Uint8Array(buffer))
+    .map(b => b.toString(16).padStart(2, '0'))
+    .join('');
+}
function renderFilePreview(filePreview) {
  if (filePreview.url) {
    if (filePreview.type.startsWith("video/")) {
      return <video src={filePreview.url} alt="Preview" controls />;
    } else if (filePreview.type.startsWith("image/")) {
      return <img src={filePreview.url} alt="Preview" />;
    } else {
      return filePreview.url;
    }
  } else {
    return <InboxOutlined />;
  }
}
export default FileUploader;

4.5.2 constant.js #

src\constant.js

export const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;
+export const CHUNK_SIZE = 100 * 1024 * 1024;

4.5.3 src\axiosInstance.js #

src\axiosInstance.js

import axios from "axios";
const axiosInstance = axios.create({
  baseURL: "http://localhost:8080",
});
axiosInstance.interceptors.response.use(
  (response) => {
    if (response.data && response.data.success) {
      return response.data;
    } else {
      throw new Error(response.data.message || "服务器响应错误");
    }
  },
  (error) => {
    console.error("请求出错:", error);
    throw error;
  }
);
export default axiosInstance;

4.5.4 src\useDrag.js #

src\useDrag.js

import { useState,useEffect} from 'react';
import {MAX_FILE_SIZE} from './constant';
function useDrag(uploadContainerRef) {
  const [selectedFile, setSelectedFile] = useState(null);
  const [filePreview, setFilePreview] = useState({
    url: null,
    type: null
  });
  const checkFile = files => {
    const file = files[0];
    if (!file) {
      message.error("没有选择任何文件");
      return;
    }
    if (file.size > MAX_FILE_SIZE) {
      message.error("文件大小不能超过2GB");
      return;
    }
    if (!file.type.startsWith("image/") && !file.type.startsWith("video/")) {
      message.error("文件类型必须是图片或视频");
      return;
    }
    setSelectedFile(file);
  };
  useEffect(()=>{
    if(!selectedFile) return;
    const url = URL.createObjectURL(selectedFile);
    setFilePreview({url,type: selectedFile.type});
    return () => {
      URL.revokeObjectURL(url);
    };
  },[selectedFile]);
  const handleDrag = (event) => {
    event.preventDefault();
    event.stopPropagation();
  };
  const handleDrop = (event) => {
    event.preventDefault();
    event.stopPropagation();
    checkFile(event.dataTransfer.files);
  };
  useEffect(() => {
    const uploadContainer = uploadContainerRef.current;
    uploadContainer.addEventListener("dragenter", handleDrag);
    uploadContainer.addEventListener("dragover", handleDrag);
    uploadContainer.addEventListener("drop", handleDrop);
    uploadContainer.addEventListener("dragleave", handleDrag);
    return () => {
      uploadContainer.removeEventListener("dragenter", handleDrag);
      uploadContainer.removeEventListener("dragover", handleDrag);
      uploadContainer.removeEventListener("drop", handleDrop);
      uploadContainer.removeEventListener("dragleave", handleDrag);
    };
  }, []);
+ const resetFileStatus = () => {
+   setSelectedFile(null);
+   setFilePreview({
+     url: null,
+     type: null
+   });
+ }
+ return {selectedFile,filePreview,resetFileStatus};
}
export default useDrag;

4.6 上传进度 #

4.6.1 FileUploader.js #

src\FileUploader.js

+import { useRef ,useState} from "react";
import { InboxOutlined } from "@ant-design/icons";
+import { Button, message,Progress } from "antd";
import useDrag from "./useDrag";
import { CHUNK_SIZE } from './constant';
import axiosInstance from "./axiosInstance";
import "./FileUploader.css";
const FileUploader = () => {
  const uploadContainerRef = useRef(null);
+ const { selectedFile, filePreview, resetFileStatus } = useDrag(uploadContainerRef);
+ const [uploadProgress, setUploadProgress] = useState({});
+ const resetAllStatus=()=>{
+   resetFileStatus();
+   setUploadProgress({});
+ }
  const handleUpload = async () => {
    if (!selectedFile) {
      message.error("请先选择一个文件");
      return;
    }
    const filename = await getFileName(file);
+   await uploadFile(selectedFile,filename,setUploadProgress,resetAllStatus);
  }
  const renderButton = () => {
    return <Button onClick={handleUpload}>上传</Button>;
  }
+ const renderProgress = () => {
+   return Object.keys(uploadProgress).map((chunkName,index) => <div key={chunkName}>
+     <span>切片{index}:</span>
+     <Progress percent={uploadProgress[chunkName]} />
+   </div>);
+ };
  return (
    <>
      <div className="upload-container" ref={uploadContainerRef}>
        {renderFilePreview(filePreview)}
      </div>
      {renderButton()}
+     {renderProgress()}
    </>
  );
};
+async function createRequest(filename, chunkFileName, chunk,setUploadProgress) {
  return axiosInstance.post(`/upload/${filename}`, chunk, {
    headers: {
      "Content-Type": "application/octet-stream"
    },
+   onUploadProgress: progressEvent => {
+     const percentCompleted = Math.round(progressEvent.loaded * 100 / progressEvent.total);
+     setUploadProgress(prevProgress => ({
+       ...prevProgress,
+       [chunkFileName]: percentCompleted
+     }));
+   },
    params: {
      chunkFileName
    }
  });
}
async function getFileName(file) {
  const fileHash = await calculateHash(file);
  const fileExtension = file.name.split(".").pop();
  return `${fileHash}.${fileExtension}`;
}
+async function uploadFile(file,setUploadProgress,resetAllStatus) {
  const chunks = createFileChunks(file, filename);
+ const requests = chunks.map(({ chunk, chunkFileName }) => createRequest(filename, chunkFileName, chunk,setUploadProgress));
  try {
    await Promise.all(requests);
    await axiosInstance.get(`/merge/${filename}`);
    message.success("上传完成");
    resetAllStatus();
  } catch (error) {
    console.error("上传出错:", error);
    message.error("上传出错");
  }
}
function createFileChunks(file, filename) {
  let chunks = [];
  let count = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < count; i++) {
    let chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    chunks.push({
      chunk,
      chunkFileName: `${filename}-${i}`
    });
  }
  return chunks;
}
async function calculateHash(file) {
  const arrayBuffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
  return bufferToHex(hashBuffer);
}
function bufferToHex(buffer) {
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}
function renderFilePreview(filePreview) {
  if (filePreview.url) {
    if (filePreview.type.startsWith("video/")) {
      return <video src={filePreview.url} alt="Preview" controls />;
    } else if (filePreview.type.startsWith("image/")) {
      return <img src={filePreview.url} alt="Preview" />;
    } else {
      return filePreview.url;
    }
  } else {
    return <InboxOutlined />;
  }
}
export default FileUploader;

4.7 秒传 #

4.7.1 FileUploader.js #

src\FileUploader.js

import { useRef ,useState} from "react";
import { InboxOutlined } from "@ant-design/icons";
import { Button, message,Progress } from "antd";
import useDrag from "./useDrag";
import { CHUNK_SIZE } from './constant';
import axiosInstance from "./axiosInstance";
import "./FileUploader.css";
const FileUploader = () => {
  const uploadContainerRef = useRef(null);
  const { selectedFile, filePreview, resetFileStatus } = useDrag(uploadContainerRef);
  const [uploadProgress, setUploadProgress] = useState({});
  const resetAllStatus=()=>{
    resetFileStatus();
    setUploadProgress({});
  }
  const handleUpload = async () => {
    if (!selectedFile) {
      message.error("请先选择一个文件");
      return;
    }
    const filename = await getFileName(file);
    await uploadFile(selectedFile,filename,setUploadProgress,resetAllStatus);
  }
  const renderButton = () => {
    return <Button onClick={handleUpload}>上传</Button>;
  }
  const renderProgress = () => {
    return Object.keys(uploadProgress).map((chunkName,index) => <div key={chunkName}>
      <span>切片{index}:</span>
      <Progress percent={uploadProgress[chunkName]} />
    </div>);
  };
  return (
    <>
      <div className="upload-container" ref={uploadContainerRef}>
        {renderFilePreview(filePreview)}
      </div>
      {renderButton()}
      {renderProgress()}
    </>
  );
};
async function createRequest(filename, chunkFileName, chunk,setUploadProgress) {
  return axiosInstance.post(`/upload/${filename}`, chunk, {
    headers: {
      "Content-Type": "application/octet-stream"
    },
    onUploadProgress: progressEvent => {
      const percentCompleted = Math.round(progressEvent.loaded * 100 / progressEvent.total);
      setUploadProgress(prevProgress => ({
        ...prevProgress,
        [chunkFileName]: percentCompleted
      }));
    },
    params: {
      chunkFileName
    }
  });
}
async function getFileName(file) {
  const fileHash = await calculateHash(file);
  const fileExtension = file.name.split(".").pop();
  return `${fileHash}.${fileExtension}`;
}
+async function uploadFile(file,filename,setUploadProgress,resetAllStatus) {
+ const {needUpload} = await axiosInstance.get(`/verify/${filename}`);
+ if (!needUpload) {
+   message.success("文件已存在,秒传成功");
+   return resetAllStatus();
+ }
  const chunks = createFileChunks(file, filename);
  const requests = chunks.map(({ chunk, chunkFileName }) => createRequest(filename, chunkFileName, chunk,setUploadProgress));
  try {
    await Promise.all(requests);
    await axiosInstance.get(`/merge/${filename}`);
    message.success("上传完成");
    resetAllStatus();
  } catch (error) {
    console.error("上传出错:", error);
    message.error("上传出错");
  }
}
function createFileChunks(file, filename) {
  let chunks = [];
  let count = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < count; i++) {
    let chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    chunks.push({
      chunk,
      chunkFileName: `${filename}-${i}`
    });
  }
  return chunks;
}
async function calculateHash(file) {
  const arrayBuffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
  return bufferToHex(hashBuffer);
}
function bufferToHex(buffer) {
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}
function renderFilePreview(filePreview) {
  if (filePreview.url) {
    if (filePreview.type.startsWith("video/")) {
      return <video src={filePreview.url} alt="Preview" controls />;
    } else if (filePreview.type.startsWith("image/")) {
      return <img src={filePreview.url} alt="Preview" />;
    } else {
      return filePreview.url;
    }
  } else {
    return <InboxOutlined />;
  }
}
export default FileUploader;

4.8 暂停上传 #

4.8.1 FileUploader.js #

src\FileUploader.js

import { useRef, useState } from "react";
+import { InboxOutlined ,PauseCircleOutlined,PlayCircleOutlined} from "@ant-design/icons";
import { Button, message, Progress } from "antd";
+import axios from "axios";
import useDrag from "./useDrag";
import { CHUNK_SIZE } from './constant';
import axiosInstance from "./axiosInstance";
import "./FileUploader.css";
+const UploadStatus = {
+  NOT_STARTED: "NOT_STARTED",
+  UPLOADING: "UPLOADING",
+  PAUSED: "PAUSED"
+};
const FileUploader = () => {
  const uploadContainerRef = useRef(null);
  const { selectedFile, filePreview, resetFileStatus } = useDrag(uploadContainerRef);
  const [uploadProgress, setUploadProgress] = useState({});
+ const [uploadStatus, setUploadStatus] = useState(UploadStatus.NOT_STARTED);
+ const [cancelTokens, setCancelTokens] = useState([]);
  const resetAllStatus = () => {
    resetFileStatus();
    setUploadProgress({});
+   setUploadStatus(UploadStatus.NOT_STARTED);
  }
  const handleUpload = async () => {
    if (!selectedFile) {
      message.error("请先选择一个文件");
      return;
    }
+   setUploadStatus(UploadStatus.UPLOADING);
+   const filename = await getFileName(selectedFile);
+   await uploadFile(selectedFile,filename, setUploadProgress, resetAllStatus,setCancelTokens);
  }
+ const pauseUpload = () => {
+   setUploadStatus(UploadStatus.PAUSED);
+   cancelTokens.forEach(cancelToken => cancelToken.cancel("用户取消上传"));
+ };
+ const resumeUpload = async () => {
+   setUploadStatus(UploadStatus.UPLOADING);
+   handleUpload();
+ };
  const renderButton = () => {
+   switch (uploadStatus) {
+     case UploadStatus.NOT_STARTED:
+       return <Button onClick={handleUpload}>上传</Button>;
+     case UploadStatus.UPLOADING:
+       return <Button icon={<PauseCircleOutlined />} onClick={pauseUpload}>
+         暂停
+       </Button>;
+     case UploadStatus.PAUSED:
+       return <Button icon={<PlayCircleOutlined />} onClick={resumeUpload}>
+         恢复上传
+       </Button>;
+     default:
+       return null;
+   }
  }
  const renderProgress = () => {
+   if (uploadStatus !== UploadStatus.NOT_STARTED) {
      return Object.keys(uploadProgress).map((chunkName, index) => <div key={chunkName}>
      <span>切片{index}:</span>
      <Progress percent={uploadProgress[chunkName]} />
    </div>);
+   }
  };
  return (
    <>
      <div className="upload-container" ref={uploadContainerRef}>
        {renderFilePreview(filePreview)}
      </div>
      {renderButton()}
      {renderProgress()}
    </>
  );
};
+async function createRequest(filename, chunkFileName, chunk, setUploadProgress,cancelToken) {
  return axiosInstance.post(`/upload/${filename}`, chunk, {
    headers: {
      "Content-Type": "application/octet-stream"
    },
    onUploadProgress: progressEvent => {
      const percentCompleted = Math.round(progressEvent.loaded * 100 / progressEvent.total);
      setUploadProgress(prevProgress => ({
        ...prevProgress,
        [chunkFileName]: percentCompleted
      }));
    },
    params: {
      chunkFileName
    },
+   cancelToken: cancelToken.token
  });
}
async function getFileName(file) {
  const fileHash = await calculateHash(file);
  const fileExtension = file.name.split(".").pop();
  return `${fileHash}.${fileExtension}`;
}
+async function uploadFile(file,filename, setUploadProgress, resetAllStatus,setCancelTokens) {
  const { needUpload } = await axiosInstance.get(`/verify/${filename}`);
  if (!needUpload) {
    message.success("文件已存在,秒传成功");
    return resetAllStatus();
  }
  const chunks = createFileChunks(file, filename);
+ const newCancelTokens = [];
+ const requests = chunks.map(({ chunk, chunkFileName }) => {
+   const cancelToken = axios.CancelToken.source();
+   newCancelTokens.push(cancelToken);
+   return createRequest(filename, chunkFileName, chunk, setUploadProgress,cancelToken);
+ });
  try {
+   setCancelTokens(newCancelTokens);
    await Promise.all(requests);
    await axiosInstance.get(`/merge/${filename}`);
    resetAllStatus();
    message.success("上传完成");
  } catch (error) {
+   if (axios.isCancel(error)) {
+     console.log("上传暂停");
+     message.error("上传暂停");
+   } else {
+     console.error("上传出错:", error);
+     message.error("上传出错");
+   }
  }
}
function createFileChunks(file, filename) {
  let chunks = [];
  let count = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < count; i++) {
    let chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    chunks.push({
      chunk,
      chunkFileName: `${filename}-${i}`
    });
  }
  return chunks;
}
async function calculateHash(file) {
  const arrayBuffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
  return bufferToHex(hashBuffer);
}
function bufferToHex(buffer) {
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}
function renderFilePreview(filePreview) {
  if (filePreview.url) {
    if (filePreview.type.startsWith("video/")) {
      return <video src={filePreview.url} alt="Preview" controls />;
    } else if (filePreview.type.startsWith("image/")) {
      return <img src={filePreview.url} alt="Preview" />;
    } else {
      return filePreview.url;
    }
  } else {
    return <InboxOutlined />;
  }
}
export default FileUploader;

4.9 断点续传 #

4.9.1 FileUploader.js #

src\FileUploader.js

import { useRef, useState } from "react";
import { InboxOutlined ,PauseCircleOutlined,PlayCircleOutlined} from "@ant-design/icons";
import { Button, message, Progress } from "antd";
import axios from "axios";
import useDrag from "./useDrag";
import { CHUNK_SIZE } from './constant';
import axiosInstance from "./axiosInstance";
import "./FileUploader.css";
const UploadStatus = {
  NOT_STARTED: "NOT_STARTED",
  UPLOADING: "UPLOADING",
  PAUSED: "PAUSED"
};
const FileUploader = () => {
  const uploadContainerRef = useRef(null);
  const { selectedFile, filePreview, resetFileStatus } = useDrag(uploadContainerRef);
  const [uploadProgress, setUploadProgress] = useState({});
  const [uploadStatus, setUploadStatus] = useState(UploadStatus.NOT_STARTED);
  const [cancelTokens, setCancelTokens] = useState([]);
  const resetAllStatus = () => {
    resetFileStatus();
    setUploadProgress({});
    setUploadStatus(UploadStatus.NOT_STARTED);
  }
  const handleUpload = async () => {
    if (!selectedFile) {
      message.error("请先选择一个文件");
      return;
    }
    setUploadStatus(UploadStatus.UPLOADING);
    const filename = await getFileName(selectedFile);
    await uploadFile(selectedFile, filename,setUploadProgress, resetAllStatus,setCancelTokens);
  }
  const pauseUpload = () => {
    setUploadStatus(UploadStatus.PAUSED);
    cancelTokens.forEach(cancelToken => cancelToken.cancel("用户取消上传"));
  };
  const resumeUpload = async () => {
    setUploadStatus(UploadStatus.UPLOADING);
    handleUpload();
  };
  const renderButton = () => {
    switch (uploadStatus) {
      case UploadStatus.NOT_STARTED:
        return <Button onClick={handleUpload}>上传</Button>;
      case UploadStatus.UPLOADING:
        return <Button icon={<PauseCircleOutlined />} onClick={pauseUpload}>
          暂停
        </Button>;
      case UploadStatus.PAUSED:
        return <Button icon={<PlayCircleOutlined />} onClick={resumeUpload}>
          恢复上传
        </Button>;
      default:
        return null;
    }
  }
  const renderProgress = () => {
    if (uploadStatus !== UploadStatus.NOT_STARTED) {
      return Object.keys(uploadProgress).map((chunkName, index) => <div key={chunkName}>
      <span>切片{index}:</span>
      <Progress percent={uploadProgress[chunkName]} />
    </div>);
    }
  };
  return (
    <>
      <div className="upload-container" ref={uploadContainerRef}>
        {renderFilePreview(filePreview)}
      </div>
      {renderButton()}
      {renderProgress()}
    </>
  );
};
+async function createRequest(filename, chunkFileName, chunk, setUploadProgress,cancelToken,start) {
  return axiosInstance.post(`/upload/${filename}`, chunk, {
    headers: {
      "Content-Type": "application/octet-stream"
    },
    onUploadProgress: progressEvent => {
      const percentCompleted = Math.round(progressEvent.loaded * 100 / progressEvent.total);
      setUploadProgress(prevProgress => ({
        ...prevProgress,
        [chunkFileName]: percentCompleted
      }));
    },
    params: {
      chunkFileName,
+     start
    },
    cancelToken: cancelToken.token
  });
}
async function getFileName(file) {
  const fileHash = await calculateHash(file);
  const fileExtension = file.name.split(".").pop();
  return `${fileHash}.${fileExtension}`;
}
async function uploadFile(file, filename,setUploadProgress, resetAllStatus,setCancelTokens) {
+ const { needUpload,uploadList } = await axiosInstance.get(`/verify/${filename}`);
  if (!needUpload) {
    message.success("文件已存在,秒传成功");
    return resetAllStatus();
  }
  const chunks = createFileChunks(file, filename);
  const newCancelTokens = [];
+ const requests = chunks.map(({ chunk, chunkFileName },index) => {
    const cancelToken = axios.CancelToken.source();
    newCancelTokens.push(cancelToken);
    const existingChunk = uploadList.find(item => item.chunkFileName === chunkFileName);
+   if (existingChunk) {
+     const uploadedSize = existingChunk.size;
+     const remainingChunk = chunk.slice(uploadedSize);
+     if (remainingChunk.size === 0) {
+       return Promise.resolve();
+     }
+     return createRequest(filename, chunkFileName, remainingChunk, setUploadProgress,cancelToken,uploadedSize);
+   } else {
+     return createRequest(filename, chunkFileName, chunk, setUploadProgress,cancelToken, index * CHUNK_SIZE);
+   }
  });
  try {
    setCancelTokens(newCancelTokens);
    await Promise.all(requests);
    await axiosInstance.get(`/merge/${filename}`);
    resetAllStatus();
    message.success("上传完成");
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log("上传暂停");
      message.error("上传暂停");
    } else {
      console.error("上传出错:", error);
      message.error("上传出错");
    }
  }
}
function createFileChunks(file, filename) {
  let chunks = [];
  let count = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < count; i++) {
    let chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    chunks.push({
      chunk,
      chunkFileName: `${filename}-${i}`
    });
  }
  return chunks;
}
async function calculateHash(file) {
  const arrayBuffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
  return bufferToHex(hashBuffer);
}
function bufferToHex(buffer) {
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}
function renderFilePreview(filePreview) {
  if (filePreview.url) {
    if (filePreview.type.startsWith("video/")) {
      return <video src={filePreview.url} alt="Preview" controls />;
    } else if (filePreview.type.startsWith("image/")) {
      return <img src={filePreview.url} alt="Preview" />;
    } else {
      return filePreview.url;
    }
  } else {
    return <InboxOutlined />;
  }
}

export default FileUploader;

5. 扩展 #

5.1 Web Workers #

要优化大文件上传并利用 Web Workers 的优势,你可以将耗时操作的逻辑移到 Web Worker 中。这样可以防止耗时的文件操作阻塞 UI 线程,从而提升用户界面的响应性。

5.1.1 FileUploader.js #

src\FileUploader.js

+import { useRef, useState ,useEffect} from "react";
import { InboxOutlined ,PauseCircleOutlined,PlayCircleOutlined} from "@ant-design/icons";
+import { Button, message, Progress,Spin } from "antd";
import axios from "axios";
import useDrag from "./useDrag";
import { CHUNK_SIZE } from './constant';
import axiosInstance from "./axiosInstance";
import "./FileUploader.css";
const UploadStatus = {
  NOT_STARTED: "NOT_STARTED",
  UPLOADING: "UPLOADING",
  PAUSED: "PAUSED"
};
const FileUploader = () => {
  const uploadContainerRef = useRef(null);
  const { selectedFile, filePreview, resetFileStatus } = useDrag(uploadContainerRef);
  const [uploadProgress, setUploadProgress] = useState({});
  const [uploadStatus, setUploadStatus] = useState(UploadStatus.NOT_STARTED);
  const [cancelTokens, setCancelTokens] = useState([]);
+ const [filenameWorker, setFilenameWorker] = useState(null);
+ const [calculatingFilename, setCalculatingFilename] = useState(false);
+ useEffect(() => {
+   const filenameWorker = new Worker('filenameWorker.js');
+   setFilenameWorker(filenameWorker);
+   return () => filenameWorker.terminate();
+ }, []);
  const resetAllStatus = () => {
    resetFileStatus();
    setUploadProgress({});
    setUploadStatus(UploadStatus.NOT_STARTED);
  }
  const handleUpload = async () => {
    if (!selectedFile) {
      message.error("请先选择一个文件");
      return;
    }
    setUploadStatus(UploadStatus.UPLOADING);
-   await uploadFile(selectedFile, setUploadProgress, resetAllStatus,setCancelTokens);
+   filenameWorker.postMessage(selectedFile);
+   setCalculatingFilename(true);
+   filenameWorker.onmessage = async (event) => {
+     setCalculatingFilename(false);
+     await uploadFile(selectedFile, event.data, setUploadProgress, resetAllStatus, setCancelTokens);
+   };
+ }
  const pauseUpload = () => {
    setUploadStatus(UploadStatus.PAUSED);
    cancelTokens.forEach(cancelToken => cancelToken.cancel("用户取消上传"));
  };
  const resumeUpload = async () => {
    setUploadStatus(UploadStatus.UPLOADING);
    handleUpload();
  };
  const renderButton = () => {
    switch (uploadStatus) {
      case UploadStatus.NOT_STARTED:
        return <Button onClick={handleUpload}>上传</Button>;
      case UploadStatus.UPLOADING:
        return <Button icon={<PauseCircleOutlined />} onClick={pauseUpload}>
          暂停
        </Button>;
      case UploadStatus.PAUSED:
        return <Button icon={<PlayCircleOutlined />} onClick={resumeUpload}>
          恢复上传
        </Button>;
      default:
        return null;
    }
  }
  const renderProgress = () => {
    if (uploadStatus !== UploadStatus.NOT_STARTED) {
      return Object.keys(uploadProgress).map((chunkName, index) => <div key={chunkName}>
      <span>切片{index}:</span>
      <Progress percent={uploadProgress[chunkName]} />
    </div>);
    }
  };
  return (
    <>
      <div className="upload-container" ref={uploadContainerRef}>
        {renderFilePreview(filePreview)}
      </div>

      {renderButton()}
+     {calculatingFilename && <Spin tip={<span>计算文件名中...</span>}> </Spin>}
      {renderProgress()}
    </>
  );
};
async function createRequest(filename, chunkFileName, chunk, setUploadProgress,cancelToken,start) {
  return axiosInstance.post(`/upload/${filename}`, chunk, {
    headers: {
      "Content-Type": "application/octet-stream"
    },
    onUploadProgress: progressEvent => {
      const percentCompleted = Math.round(progressEvent.loaded * 100 / progressEvent.total);
      setUploadProgress(prevProgress => ({
        ...prevProgress,
        [chunkFileName]: percentCompleted
      }));
    },
    params: {
      chunkFileName,
      start
    },
    cancelToken: cancelToken.token
  });
}

+async function uploadFile(file,filename, setUploadProgress, resetAllStatus,setCancelTokens) {
  const { needUpload,uploadList } = await axiosInstance.get(`/verify/${filename}`);
  if (!needUpload) {
    message.success("文件已存在,秒传成功");
    return resetAllStatus();
  }
  const chunks = createFileChunks(file, filename);
  const newCancelTokens = [];
  const requests = chunks.map(({ chunk, chunkFileName },index) => {
    const cancelToken = axios.CancelToken.source();
    newCancelTokens.push(cancelToken);
    const existingChunk = uploadList.find(item => item.chunkFileName === chunkFileName);
    if (existingChunk) {
      const uploadedSize = existingChunk.size;
      const remainingChunk = chunk.slice(uploadedSize);
      if (remainingChunk.size === 0) {
        return Promise.resolve();
      }
      return createRequest(filename, chunkFileName, remainingChunk, setUploadProgress,cancelToken,uploadedSize);
    } else {
      return createRequest(filename, chunkFileName, chunk, setUploadProgress,cancelToken, index * CHUNK_SIZE);
    }
  });
  try {
    setCancelTokens(newCancelTokens);
    await Promise.all(requests);
    await axiosInstance.get(`/merge/${filename}`);
    resetAllStatus();
    message.success("上传完成");
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log("上传暂停");
      message.error("上传暂停");
    } else {
      console.error("上传出错:", error);
      message.error("上传出错");
    }
  }
}
function createFileChunks(file, filename) {
  let chunks = [];
  let count = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < count; i++) {
    let chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    chunks.push({
      chunk,
      chunkFileName: `${filename}-${i}`
    });
  }
  return chunks;
}
async function calculateHash(file) {
  const arrayBuffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
  return bufferToHex(hashBuffer);
}
function bufferToHex(buffer) {
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}
function renderFilePreview(filePreview) {
  if (filePreview.url) {
    if (filePreview.type.startsWith("video/")) {
      return <video src={filePreview.url} alt="Preview" controls />;
    } else if (filePreview.type.startsWith("image/")) {
      return <img src={filePreview.url} alt="Preview" />;
    } else {
      return filePreview.url;
    }
  } else {
    return <InboxOutlined />;
  }
}

export default FileUploader;

5.1.2 filenameWorker.js #

public\filenameWorker.js

self.addEventListener("message", async (event) => {
  const file = event.data;
  const filename = await getFileName(file);
  self.postMessage(filename);
});
async function getFileName(file) {
  const fileHash = await calculateHash(file);
  const fileExtension = file.name.split(".").pop();
  return `${fileHash}.${fileExtension}`;
}
async function calculateHash(file) {
  const arrayBuffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);
  return bufferToHex(hashBuffer);
}

function bufferToHex(buffer) {
  return Array.from(new Uint8Array(buffer))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

5.2 重试机制 #

5.2.1 FileUploader.js #

src\FileUploader.js

import { useRef, useState ,useEffect} from "react";
import { InboxOutlined ,PauseCircleOutlined,PlayCircleOutlined} from "@ant-design/icons";
import { Button, message, Progress,Spin } from "antd";
import axios from "axios";
import useDrag from "./useDrag";
+import { CHUNK_SIZE,MAX_RETRIES } from './constant';
import axiosInstance from "./axiosInstance";
import "./FileUploader.css";
const UploadStatus = {
  NOT_STARTED: "NOT_STARTED",
  UPLOADING: "UPLOADING",
  PAUSED: "PAUSED"
};
const FileUploader = () => {
  const uploadContainerRef = useRef(null);
  const { selectedFile, filePreview, resetFileStatus } = useDrag(uploadContainerRef);
  const [uploadProgress, setUploadProgress] = useState({});
  const [uploadStatus, setUploadStatus] = useState(UploadStatus.NOT_STARTED);
  const [cancelTokens, setCancelTokens] = useState([]);
  const [filenameWorker, setFilenameWorker] = useState(null);
  const [calculatingFilename, setCalculatingFilename] = useState(false);
  useEffect(() => {
    const filenameWorker = new Worker('filenameWorker.js');
    setFilenameWorker(filenameWorker);
    return () => filenameWorker.terminate();
  }, []);
  const resetAllStatus = () => {
    resetFileStatus();
    setUploadProgress({});
    setUploadStatus(UploadStatus.NOT_STARTED);
  }
  const handleUpload = async () => {
    if (!selectedFile) {
      message.error("请先选择一个文件");
      return;
    }
    setUploadStatus(UploadStatus.UPLOADING);
    //await uploadFile(selectedFile, setUploadProgress, resetAllStatus,setCancelTokens);
    filenameWorker.postMessage(selectedFile);
    setCalculatingFilename(true);
    filenameWorker.onmessage = async (event) => {
      setCalculatingFilename(false);
      await uploadFile(selectedFile, event.data, setUploadProgress, resetAllStatus, setCancelTokens);
    };
  }
  const pauseUpload = () => {
    setUploadStatus(UploadStatus.PAUSED);
    cancelTokens.forEach(cancelToken => cancelToken.cancel("用户取消上传"));
  };
  const resumeUpload = async () => {
    setUploadStatus(UploadStatus.UPLOADING);
    handleUpload();
  };
  const renderButton = () => {
    switch (uploadStatus) {
      case UploadStatus.NOT_STARTED:
        return <Button onClick={handleUpload}>上传</Button>;
      case UploadStatus.UPLOADING:
        return <Button icon={<PauseCircleOutlined />} onClick={pauseUpload}>
          暂停
        </Button>;
      case UploadStatus.PAUSED:
        return <Button icon={<PlayCircleOutlined />} onClick={resumeUpload}>
          恢复上传
        </Button>;
      default:
        return null;
    }
  }
  const renderProgress = () => {
    if (uploadStatus !== UploadStatus.NOT_STARTED) {
      return Object.keys(uploadProgress).map((chunkName, index) => <div key={chunkName}>
      <span>切片{index}:</span>
      <Progress percent={uploadProgress[chunkName]} />
    </div>);
    }
  };
  return (
    <>
      <div className="upload-container" ref={uploadContainerRef}>
        {renderFilePreview(filePreview)}
      </div>

      {renderButton()}
      {calculatingFilename && <Spin tip={<span>计算文件名中...</span>}> </Spin>}
      {renderProgress()}
    </>
  );
};
async function createRequest(filename, chunkFileName, chunk, setUploadProgress,cancelToken,start) {
  return axiosInstance.post(`/upload/${filename}`, chunk, {
    headers: {
      "Content-Type": "application/octet-stream"
    },
    onUploadProgress: progressEvent => {
      const percentCompleted = Math.round(progressEvent.loaded * 100 / progressEvent.total);
      setUploadProgress(prevProgress => ({
        ...prevProgress,
        [chunkFileName]: percentCompleted
      }));
    },
    params: {
      chunkFileName,
      start
    },
    cancelToken: cancelToken.token
  });
}

+async function uploadFile(file,filename, setUploadProgress, resetAllStatus,setCancelTokens,retryCount = 0) {
  try {
    const { needUpload,uploadList } = await axiosInstance.get(`/verify/${filename}`);
    if (!needUpload) {
      message.success("文件已存在,秒传成功");
      return resetAllStatus();
    }
    const chunks = createFileChunks(file, filename);
    const newCancelTokens = [];
    const requests = chunks.map(({ chunk, chunkFileName },index) => {
      const cancelToken = axios.CancelToken.source();
      newCancelTokens.push(cancelToken);
      const existingChunk = uploadList.find(item => item.chunkFileName === chunkFileName);
      if (existingChunk) {
        const uploadedSize = existingChunk.size;
        const remainingChunk = chunk.slice(uploadedSize);
        if (remainingChunk.size === 0) {
          return Promise.resolve();
        }
        return createRequest(filename, chunkFileName, remainingChunk, setUploadProgress,cancelToken,uploadedSize);
      } else {
        return createRequest(filename, chunkFileName, chunk, setUploadProgress,cancelToken, index * CHUNK_SIZE);
      }
    });
    setCancelTokens(newCancelTokens);
    await Promise.all(requests);
    await axiosInstance.get(`/merge/${filename}`);
    resetAllStatus();
    message.success("上传完成");
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log("上传暂停");
      message.error("上传暂停");
    } else {
+     if (retryCount < MAX_RETRIES) {
+       console.log("上传出错,重试中:", error);
+       return await uploadFile(file,filename, setUploadProgress, resetAllStatus,setCancelTokens,retryCount + 1);
+     }
      console.error("上传出错:", error);
      message.error("上传出错");
    }
  }
}
function createFileChunks(file, filename) {
  let chunks = [];
  let count = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < count; i++) {
    let chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    chunks.push({
      chunk,
      chunkFileName: `${filename}-${i}`
    });
  }
  return chunks;
}
function renderFilePreview(filePreview) {
  if (filePreview.url) {
    if (filePreview.type.startsWith("video/")) {
      return <video src={filePreview.url} alt="Preview" controls />;
    } else if (filePreview.type.startsWith("image/")) {
      return <img src={filePreview.url} alt="Preview" />;
    } else {
      return filePreview.url;
    }
  } else {
    return <InboxOutlined />;
  }
}

export default FileUploader;

5.2.2 constant.js #

src\constant.js

export const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;
export const CHUNK_SIZE = 100 * 1024 * 1024;
+export const MAX_RETRIES = 3;

5.3 点击选择文件上传 #

在网页中实现点击按钮选择文件上传的功能,通常涉及到使用 JavaScript 和 HTML。这里是一个基本的步骤指南来实现这个功能,其中包括动态创建 input 元素:

  1. 创建上传按钮:首先,在 HTML 中创建一个按钮,用户将点击这个按钮来上传文件。

  2. 编写 JavaScript 函数:编写一个 JavaScript 函数来处理按钮点击事件。这个函数将动态创建一个 input 元素,该元素的类型为 file

  3. 触发文件选择:在 JavaScript 函数中,一旦 input 元素被创建,使用 click() 方法来模拟用户点击,这将打开文件浏览器窗口。

  4. 处理文件选择:为 input 元素添加 change 事件监听器,以便在用户选择文件后进行处理。

以下是一个示例代码:

HTML

<button id="uploadButton">上传文件</button>

JavaScript

document.getElementById("uploadButton").addEventListener("click", function () {
  // 创建一个input元素
  var fileInput = document.createElement("input");
  fileInput.type = "file";
  fileInput.style.display = "none"; // 隐藏input元素

  // 当文件被选择时,处理文件
  fileInput.addEventListener("change", function (event) {
    var file = event.target.files[0];
    // 处理文件上传逻辑
    console.log("选择的文件:", file.name);
  });

  // 将input元素添加到DOM中(必须在document中才能触发click)
  document.body.appendChild(fileInput);

  // 触发文件选择
  fileInput.click();

  // 移除input元素(如果不再需要)
  document.body.removeChild(fileInput);
});

这个示例中,当用户点击“上传文件”按钮时,会动态创建一个隐藏的 input 类型为 file 的元素。然后,模拟点击这个 input 元素,打开文件选择器。用户选择文件后,通过 change 事件监听器来获取文件信息。

5.4 确定合理的切片的大小 #

确定上传大文件时合理的切片大小是一个需要平衡上传效率和网络稳定性的问题。切片过大可能会导致上传失败,特别是在网络条件不稳定的情况下;而切片过小则可能会导致过多的 HTTP 请求,降低上传效率。以下是一些关于如何确定切片大小的建议:

  1. 网络状况考虑:如果你的用户通常有较好的网络条件(例如,高速宽带连接),你可以选择较大的切片大小(例如 5MB 到 10MB)。但如果用户的网络条件不稳定(例如,移动网络),较小的切片(例如 1MB 到 2MB)可能更为合适。

  2. 服务器限制:检查服务器端对上传文件大小的限制。一些服务器或中间件可能对 HTTP 请求的体积有限制。

  3. 错误恢复:较小的切片可以减少因单个上传失败而需要重新上传的数据量。

  4. 并发上传:如果你的应用程序支持并发上传多个切片,可以使用稍大一些的切片,以减少总的 HTTP 请求次数。

  5. 试验和调整:不同的应用和用户群可能需要不同的设置。最好的办法是进行实际测试,根据用户的反馈和使用情况调整切片大小。

  6. 进度反馈:对于非常大的文件,确保用户能够看到进度反馈,这样即使单个切片较大,用户也会感觉到进程在持续进行。

  7. 浏览器和设备能力:考虑用户设备的内存和处理能力。在一些低性能设备上,处理大切片可能会导致浏览器崩溃或响应缓慢。

  8. 动态调整:考虑实现一种机制,根据当前的上传成功率动态调整切片大小。例如,如果连续几个切片成功上传,可以适当增大切片大小;如果出现失败,可以减小切片大小。

综合考虑以上因素,你可以决定一个初始的切片大小,然后根据实际的应用表现进行调整。通常,一个介于 1MB 到 10MB 之间的切片大小是一个合理的起点。

5.5 合适的并发上传数量 #

在处理多个切片的同时上传时,确定合适的并发上传数量主要取决于两个因素:服务器的处理能力和用户的网络带宽。理想情况下,你想要最大化利用这两个资源,同时避免过载任何一端。

合适的并发上传数量

  1. 网络带宽:如果用户有较高的上传带宽,你可以增加并发数量。但是,如果带宽较低,过多的并发请求可能会导致拥堵和延迟。

  2. 服务器容量:服务器能同时处理的请求数量也限制了你应该并发上传的切片数量。太多的并发请求可能会导致服务器响应变慢或者崩溃。

  3. 通常做法:一个常见的做法是同时上传 3 到 5 个切片。这通常是一个平衡点,可以在不过度负担网络和服务器的情况下,提供较好的上传速度。

  4. 用户设备能力:同时处理多个上传请求也会占用用户设备的计算资源。在性能较低的设备上,可能需要降低并发数。

控制并发上传数量

  1. 使用异步 JavaScript:你可以使用 Promiseasync/await 来控制并发的上传。例如,使用 Promise.all() 来同时处理多个上传任务。

  2. 队列管理:创建一个上传队列,同时只处理队列中的一定数量的上传任务。完成一个任务后,从队列中取出下一个任务开始上传。

  3. 动态调整并发数:根据上传过程中的成功率和速度动态调整并发数。如果发现上传错误率增加或速度下降,可以减少并发数。

  4. 第三方库:考虑使用像 axios 这样的第三方库,这些库通常提供了并发控制的功能。

为了提高性能,确保在任何给定时间都有固定数量的切片上传,而不是等待所有并发上传完成后再开始下一批次,您可以实现一个更加动态的队列控制机制。这种方法可以确保一旦有一个上传任务完成,立即开始上传下一个切片。这样的方法通常被称为“滑动窗口”方法。

以下是如何实现这个方法的概述:

  1. 维护一个活跃上传任务列表:这个列表的长度最大为您希望的并发数(例如 5)。

  2. 启动初始批量上传任务:开始时,启动列表最大长度数量的上传任务。

  3. 监听每个上传任务的完成:每个上传任务应该有一个回调或者使用Promise处理其完成。

  4. 任务完成后,启动下一个任务:一旦有任务完成,立即从剩余的切片中取出下一个,开始上传。

  5. 重复此过程,直到所有切片上传完毕

示例代码

以下是一个简化的示例代码,展示了如何使用这种方法:

async function uploadFileInChunks(file, chunkSize, maxConcurrentUploads) {
  let chunks = createChunks(file, chunkSize);
  let activeUploads = [];
  let completedUploads = 0;
  async function uploadNextChunk() {
    if (chunks.length === 0 && activeUploads.length === 0) {
      if (completedUploads === 10) {
        console.log("上传完成");
      }
      return;
    }
    while (activeUploads.length < maxConcurrentUploads && chunks.length > 0) {
      let chunk = chunks.shift();
      let uploadPromise = uploadChunk(chunk)
        .then(() => {
          completedUploads++;
        })
        .catch((error) => {
          console.error("上传失败:", error);
        })
        .finally(() => {
          activeUploads.splice(activeUploads.indexOf(uploadPromise), 1);
          uploadNextChunk();
        });
      activeUploads.push(uploadPromise);
    }
  }
  await uploadNextChunk();
}
function createChunks(file, size) {
  return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}
function uploadChunk(chunk) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`上传第${chunk}块`);
      resolve();
    }, 1000);
  });
}
let file;
uploadFileInChunks(file, 1024 * 1024, 5);

5.6 文件校验 #

为了确保上传的文件内容未被篡改,可以在客户端和服务器端实施一系列校验机制。以下是一种可能的实现方法:

客户端

  1. 计算文件哈希:在上传前,使用 filenameWorker.js(可以是 Web Worker)来计算文件的哈希值。这个哈希值将作为文件的唯一标识符。

  2. 发送哈希值到服务器:在开始上传文件之前,将计算出的哈希值发送到服务器进行校验。服务器将检查此哈希值是否已经存在,从而避免重复上传。

  3. 在分片上传后发送分片哈希:为每个文件分片计算哈希值,并在上传每个分片时将其一起发送到服务器。

服务器端

  1. 验证文件哈希:在接收到文件哈希时,服务器应该验证它是否已知,是否有对应的文件已经存在或部分上传。

  2. 存储分片哈希:在每个分片上传时,存储其哈希值。

  3. 在所有分片上传完成后进行校验:在所有分片上传并合并后,再次计算整个文件的哈希值,确保它与客户端发送的初始哈希值匹配。

  4. 如果校验失败,删除文件:如果校验失败,即哈希值不匹配,服务器应删除已上传的文件,并向客户端报告错误。

const fs = require("fs");
const crypto = require("crypto");
async function calculateHashForFile(filePath) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash("sha256");
    const stream = fs.createReadStream(filePath);
    stream.on("data", (chunk) => {
      hash.update(chunk);
    });
    stream.on("end", () => {
      const hashValue = hash.digest("hex");
      resolve(hashValue);
    });
    stream.on("error", (err) => {
      reject(err);
    });
  });
}
async function mergeChunks(filename, originalFileHash) {
  const filePath = path.resolve(PUBLIC_DIR, filename);
  const calculatedFileHash = await calculateHashForFile(filePath);
  if (originalFileHash !== calculatedFileHash) {
    await fs.unlink(filePath);
  }
}

5.7 数据加密 #

在上传文件时实现切片数据的加密主要包括两个部分:在客户端加密文件切片,然后在服务器端解密。这个过程可以通过使用加密算法(如 AES)和密钥来实现。以下是实现步骤和代码示例:

1. 客户端加密:

在客户端,您可以使用 Web Cryptography API 进行文件切片的加密。首先,需要生成或提供一个密钥,然后使用这个密钥对每个文件切片进行加密。

示例代码(客户端):

async function encryptChunk(chunk, key) {
  //iv是加密过程中的一个随机值,用于确保即使相同的数据块使用相同的密钥加密多次,每次生成的加密文本也都是不同的。
  //这里使用window.crypto.getRandomValues生成了一个12字节的随机数组作为初始化向量。
  const algorithm = {
    name: "AES-GCM",
    iv: window.crypto.getRandomValues(new Uint8Array(12)),
  };
  const encryptedChunk = await window.crypto.subtle.encrypt(
    algorithm,
    key,
    chunk
  );
  return { encryptedChunk, iv: algorithm.iv };
}
const { encryptedChunk, iv } = await encryptChunk(chunk, key);

将密钥(key)安全地导出并提供给服务器端开发人员使用是一个敏感操作,需要谨慎处理以保证安全性。在 Web Cryptography API 中,您可以将密钥导出为某种格式(通常是rawjwk),然后通过安全的方式(比如 TLS 加密的通信)传输给服务器端。

rawjwk是指密钥的不同格式,用于在 Web Cryptography API 中导出或导入密钥。它们的主要区别在于表示方式和信息的丰富程度:

Raw(原始)格式

JWK(JSON Web Key)格式

总结

在选择哪种格式时,需要考虑应用场景、密钥类型以及交互的需求。例如,如果需要在不同系统之间共享密钥,并且这些系统需要了解密钥的具体信息(如算法、用途等),则jwk可能是更合适的选择。对于简单的或内部场景,raw可能就足够了。

步骤 1: 导出密钥

首先,您需要使用 Web Cryptography API 中的exportKey方法来导出密钥。以下是将密钥导出为raw格式的代码示例:

async function generateKey() {
  try {
    const key = await window.crypto.subtle.generateKey(
      {
        name: "AES-GCM",
        length: 256, // 可以是 128, 192, 或 256
      },
      true, // 是否可导出
      ["encrypt", "decrypt"] // 使用密钥的用途
    );
    return key;
  } catch (e) {
    console.error("密钥生成错误: ", e);
  }
}
const key = generateKey();
async function downloadKeyAsFile(key, fileName) {
  try {
    const exportedKey = await window.crypto.subtle.exportKey("raw", key);
    const blob = new Blob([exportedKey], { type: "application/octet-stream" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);
    document.body.removeChild(a);
  } catch (e) {
    console.error("密钥导出错误: ", e);
  }
}
downloadKeyAsFile(key, "key");

或者,您也可以将密钥导出为jwk(JSON Web Key)格式:

async function downloadKeyAsFile(key, fileName) {
  try {
    const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
    const jsonStr = JSON.stringify(exportedKey, null, 4);
    const blob = new Blob([jsonStr], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);
    document.body.removeChild(a);
  } catch (e) {
    console.error("密钥导出错误: ", e);
  }
}

步骤 2: 安全地传输密钥

将密钥转换为可传输的格式后,下一步是安全地将其发送给服务器端开发人员。重要的是要确保在传输过程中密钥不会被未授权的第三方截获。

步骤 3: 服务器端使用密钥

服务器端开发人员需要按照相应的格式(rawjwk)和算法来使用或导入这个密钥。

例如,如果密钥以jwk格式提供,服务器端(如使用 Node.js)可以使用相应的库来导入和使用这个密钥进行加密或解密操作。

2. 服务器端解密:

在服务器端,您可以使用 Node.js 的crypto模块来解密接收到的加密数据。确保服务器有用于解密的密钥和初始化向量(IV)。

示例代码(服务器端):

const crypto = require("crypto");
function decryptChunk(encryptedChunk, key, iv) {
  const algorithm = "aes-256-gcm"; // 确保与客户端使用相同的算法
  const decipher = crypto.createDecipheriv(algorithm, key, iv);
  let decrypted = decipher.update(encryptedChunk);
  decrypted = Buffer.concat([decrypted, decipher.final()]);
  return decrypted;
}

// 使用与客户端相同的密钥 key 和 IV
const decryptedChunk = decryptChunk(encryptedChunk, key, iv);

密钥共享:

为了确保安全性,密钥的共享通常是加密通信中最困难的部分。您可以考虑以下几种方法:

注意事项:

5.8 上传阿里云的 OSS 云服务 #

直接从前端上传文件到阿里云的对象存储服务(OSS)是一个常见的需求,特别是为了减轻服务器的负担和提高上传速度。要实现这一点,通常需要执行以下步骤:

1. 获取 OSS 访问权限:

为了安全地从前端上传文件到 OSS,您需要在服务器端生成一个带有限制权限的 OSS 访问签名(URL 签名)。这通常涉及到以下步骤:

2. 前端实现文件上传:

一旦前端收到签名的 URL 或 STS 令牌,就可以使用这些凭证直接将文件上传到 OSS。上传可以通过 HTML 表单或使用 JavaScript(例如使用 XMLHttpRequestfetch API)完成。

示例代码(JavaScript):

async function uploadFileToOSS(file, signedUrl) {
  const response = await fetch(signedUrl, {
    method: "PUT",
    body: file,
    headers: {
      "Content-Type": file.type,
    },
  });

  if (response.ok) {
    console.log("文件上传成功");
  } else {
    console.error("文件上传失败");
  }
}

// 使用示例
uploadFileToOSS(file, yourSignedUrl);

在这个例子中,yourSignedUrl 是从服务器获取的签名 URL,file 是需要上传的文件对象。

3. 安全性考虑:

6.后端实现 #

6.1.创建项目 #

npm init -y
npm install express morgan http-status-codes http-errors cors fs-extra

6.2.index.js #

// 引入 Express 模块
const express = require("express");
// 引入 Morgan 日志记录模块
const logger = require("morgan");
// 引入 HTTP 状态码
const { StatusCodes } = require("http-status-codes");
// 引入 CORS 跨域资源共享模块
const cors = require("cors");
// 引入 path 模块处理文件路径
const path = require("path");
// 引入 fs-extra 模块处理文件系统
const fs = require("fs-extra");
// 引入创建 HTTP 错误的模块
const createError = require("http-errors");
// 定义每个文件块的大小
const CHUNK_SIZE = 100 * 1024 * 1024;
// 定义公共文件夹的路径
const PUBLIC_DIR = path.resolve(__dirname, "public");
// 定义临时文件夹的路径
const TEMP_DIR = path.resolve(__dirname, "temp");
// 确保公共文件夹存在
fs.ensureDirSync(PUBLIC_DIR);
// 确保临时文件夹存在
fs.ensureDirSync(TEMP_DIR);
// 创建 Express 应用
const app = express();
// 使用 Morgan 中间件进行日志记录
app.use(logger("dev"));
// 解析 JSON 格式的请求体
app.use(express.json());
// 解析 URL 编码的请求体
app.use(express.urlencoded({ extended: true }));
// 使用 CORS 中间件允许跨域请求
app.use(cors());
// 设置静态文件目录
app.use(express.static(path.resolve(__dirname, "public")));
// 处理上传文件的请求
app.post("/upload/:filename", async (req, res, next) => {
  try {
    // 从请求参数中获取文件名
    const { filename } = req.params;
    // 从查询参数中获取块文件名
    const chunkFileName = req.query.chunkFileName;
    // 从查询参数中获取起始位置,如果不是数字则默认为 0
    const start = isNaN(req.query.start) ? 0 : parseInt(req.query.start, 10);
    // 定义块文件夹和块文件的路径
    const chunkDir = path.resolve(TEMP_DIR, filename);
    const chunkFilePath = path.resolve(chunkDir, chunkFileName);
    // 确保块文件夹存在
    await fs.ensureDir(chunkDir);
    // 创建写入流
    const ws = fs.createWriteStream(chunkFilePath, { start, flags: "a" });
    // 如果请求被中断,关闭写入流
    req.on("aborted", () => {
      ws.close();
    });
    // 管道流入写入流
    await pipeStream(req, ws);
    // 返回成功响应
    res.json({ success: true });
  } catch (error) {
    // 错误处理
    next(error);
  }
});
// 处理合并文件的请求
app.get("/merge/:filename", async (req, res, next) => {
  // 从请求参数中获取文件名
  const { filename } = req.params;
  try {
    // 合并文件块
    await mergeChunks(filename);
    // 返回成功响应
    res.json({ success: true });
  } catch (error) {
    // 错误处理
    next(error);
  }
});
// 处理文件验证请求
app.get("/verify/:filename", async (req, res, next) => {
  // 从请求参数中获取文件名
  const { filename } = req.params;
  // 检查文件是否已在公共目录中存在
  const filePath = path.resolve(PUBLIC_DIR, filename);
  const existFile = await fs.pathExists(filePath);
  // 如果文件已存在,不需要上传
  if (existFile) {
    return res.json({ success: true, needUpload: false });
  }
  // 检查临时目录是否存在
  const tempDir = path.resolve(TEMP_DIR, filename);
  const exist = await fs.pathExists(tempDir);
  // 初始化上传列表
  let uploadList = [];
  if (exist) {
    // 获取临时目录中的所有文件
    const files = await fs.readdir(tempDir);
    // 生成上传列表
    uploadList = await Promise.all(
      files.map(async (file) => {
        const stat = await fs.stat(path.resolve(tempDir, file));
        return { chunkFileName: file, size: stat.size };
      })
    );
  }
  // 返回需要上传的文件列表
  res.json({ success: true, needUpload: true, uploadList });
});
// 处理 404 错误
app.use((req, res, next) => {
  next(createError(StatusCodes.NOT_FOUND));
});
// 统一处理错误
app.use((error, req, res, next) => {
  console.error(error);
  res.status(error.status || StatusCodes.INTERNAL_SERVER_ERROR);
  res.json({ success: false, error: error.message });
});
// 定义管道流函数
function pipeStream(rs, ws) {
  return new Promise((resolve, reject) => {
    rs.pipe(ws).on("finish", resolve).on("error", reject);
  });
}
// 定义合并块文件的函数
async function mergeChunks(filename) {
  const filePath = path.resolve(PUBLIC_DIR, filename);
  const chunksDir = path.resolve(TEMP_DIR, filename);
  const chunkFiles = await fs.readdir(chunksDir);
  // 对块文件进行排序
  chunkFiles.sort((a, b) => Number(a.split("-")[1]) - Number(b.split("-")[1]));
  // 合并所有块文件
  await Promise.all(
    chunkFiles.map((chunkFile, index) =>
      pipeStream(
        fs.createReadStream(path.resolve(chunksDir, chunkFile), {
          autoClose: true,
        }),
        fs.createWriteStream(filePath, { start: index * CHUNK_SIZE })
      )
    )
  );
  // 删除临时文件夹
  await fs.rmdir(chunksDir, { recursive: true });
}
// 启动服务器监听 8080 端口
app.listen(8080, () => console.log(`Server started on port 8080`));

6.3 package.json #

package.json

{
  "scripts": {
    "start": "nodemon index.js"
  },
}

7.参考 #

7.1.拖拽 API #

事件

  1. dragenter:当拖动的元素或选中的文本进入有效拖放目标时触发。在这里,它用于初始化拖拽进入目标区域的行为。
  2. dragover:当元素或选中的文本在有效拖放目标上方移动时触发。通常用于阻止默认的处理方式,从而允许放置。

  3. drop:当拖动的元素或选中的文本在有效拖放目标上被放置时触发。这是处理文件放置逻辑的关键点。

  4. dragleave:当拖动的元素或选中的文本离开有效拖放目标时触发。可以用于处理元素拖离目标区域的行为。

API

7.2.URL.createObjectURL #

URL.createObjectURL 是一个非常实用的 Web API,它允许你创建一个指向特定文件对象或 Blob(Binary Large Object)的 URL。这个 URL 可以用于访问存储在用户本地的文件数据,而无需实际上传文件到服务器。

基本用法

const objectURL = URL.createObjectURL(blob);

应用场景

  1. 预览文件:在用户上传文件之前,你可以使用 URL.createObjectURL 来创建一个指向该文件的 URL,并用它来预览文件。例如,如果用户选择了一张图片,你可以立即在网页上显示这张图片,而不需要等待图片上传到服务器。

  2. 优化性能:对于大型二进制对象,使用这种方法可以避免将数据存储在 JavaScript 中,这样可以节省内存并提升性能。

  3. 处理数据流:它也可以用于处理实时的数据流,例如从摄像头捕获的视频流。

示例

假设你有一个文件输入元素和一个图片元素,你希望在用户选择图片后立即显示这张图片:

document.getElementById("fileInput").addEventListener("change", (event) => {
  const file = event.target.files[0];
  const url = URL.createObjectURL(file);
  document.getElementById("previewImage").src = url;
});

注意事项

总的来说,URL.createObjectURL 是处理本地文件和二进制数据的一个强大工具,特别是在需要实时处理或预览这些数据时。

7.3. flags #

在 Node.js 的文件系统(fs)模块中,flags 用于指定如何打开文件。这些标志定义了对文件的操作类型,例如读取、写入、追加等。下面是一些常见的 flags 选项及其含义:

  1. r:以只读方式打开文件。如果文件不存在,则抛出异常。

  2. r+:以读写方式打开文件。如果文件不存在,抛出异常。

  3. rs:以同步的只读方式打开文件。指示操作系统绕过本地文件系统缓存。

  4. rs+:以同步的读写方式打开文件。指示操作系统绕过本地文件系统缓存。

  5. w:以写入模式打开文件。如果文件不存在则创建文件,如果文件存在则截断(清空)文件。

  6. wx:类似于 w,但如果文件已存在则失败。

  7. w+:以读写模式打开文件。如果文件不存在则创建文件,如果文件存在则截断文件。

  8. wx+:类似于 w+,但如果文件已存在则失败。

  9. a:以追加模式打开文件。如果文件不存在则创建文件。

  10. ax:类似于 a,但如果文件已存在则失败。

  11. a+:以读写追加模式打开文件。如果文件不存在则创建文件。

  12. ax+:类似于 a+,但如果文件已存在则失败。

这些选项中,“x”(如在 wxax 中)用于确保文件在打开时不存在,防止意外覆盖已存在的文件。同步模式(包含 rsrs+)用于绕过操作系统的文件系统缓存,这对于某些要求数据一致性的应用非常重要,但可能会降低性能。

7.4.onUploadProgress #

axios 是一个流行的 HTTP 客户端库,它提供了在发送 HTTP 请求时处理请求进度的功能。onUploadProgressaxios 中用于处理上传进度的一个配置项。

如何工作 当你使用 axios 发送一个包含文件上传的请求时,onUploadProgress 允许你获得上传过程的实时信息。这对于提供用户反馈,如进度条显示,是非常有用的。

使用 onUploadProgress onUploadProgress 函数会接收一个进度事件(ProgressEvent)作为其参数。这个事件对象包含有关请求进度的信息,例如已经上传的字节量和总字节量。

这里是一个使用 onUploadProgress 的例子:

axios.post("/upload", file, {
  onUploadProgress: (progressEvent) => {
    const percentCompleted = Math.round(
      (progressEvent.loaded * 100) / progressEvent.total
    );
    console.log(`Upload Progress: ${percentCompleted}%`);
  },
});

在这个例子中,我们向 /upload 路径发送一个 POST 请求,上传一个文件。onUploadProgress 函数被用来计算和打印上传的百分比。它通过 progressEvent.loaded 获得已经上传的字节量,通过 progressEvent.total 获得总字节量。

注意事项

onUploadProgress 是处理文件上传并提供实时反馈的一个非常有用的工具,可以极大地改善用户体验。

7.5.CancelToken #

axios.CancelTokenaxios 库提供的一个功能,允许你在发起请求后,如果需要,取消这个请求。这对于处理那些不再需要的请求(例如用户导航离开当前页面,或者应用程序决定不再需要结果)是非常有用的。

如何工作 axios.CancelToken 通过创建一个 cancel token 对象来实现请求的取消。当创建一个请求时,你可以将这个 cancel token 对象传递给 axios 配置,然后在需要取消请求时,使用与该 token 相关联的 cancel 函数。

使用 axios.CancelToken 以下是如何使用 axios.CancelToken 的一个示例:

// 创建 CancelToken 源
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 发起请求并传递 cancel token
axios
  .get("/some/path", {
    cancelToken: source.token,
  })
  .catch(function (thrown) {
    if (axios.isCancel(thrown)) {
      console.log("Request canceled", thrown.message);
    } else {
      // 处理错误
    }
  });

// 在需要的时候取消请求
source.cancel("Operation canceled by the user.");

在这个例子中,我们首先创建了一个 CancelToken 源(source),它包含一个 token(用于请求)和一个 cancel 函数(用于取消请求)。然后,我们在发起 axios 请求时,将这个 token 传递到请求的配置中。如果需要取消请求,我们调用 source.cancel() 方法,并传递一个可选的取消原因。

注意事项

通过 axios.CancelToken,你可以有效地控制网络请求,避免不必要的网络活动和资源浪费。

7.6.Web Workers #

Web Workers 提供了一种在 Web 应用程序中执行脚本操作的方式,这些操作运行在与主执行线程(通常是 UI 线程)分离的后台线程中。这意味着 Web Workers 允许进行并行计算,而不会阻塞浏览器的用户界面。

基本概念

  1. 多线程执行:Web Workers 允许你在浏览器中创建一个独立的线程来执行 JavaScript 代码,这有助于处理高计算量或耗时的任务,而不会影响页面的性能和响应性。

  2. 与主线程隔离:Worker 线程与主线程是完全隔离的,它们有自己的全局上下文,不能直接访问 DOM、window 对象等。所有的数据交换都通过消息传递进行。

创建和使用 Web Worker 以下是如何使用 Web Workers 的基本步骤:

1. 创建 Worker 文件 首先,你需要创建一个 JavaScript 文件,其中包含将在 Worker 线程中运行的代码。例如,worker.js

// worker.js
self.addEventListener("message", function (e) {
  // 接收主线程消息
  const result = e.data;

  // 执行操作
  // ...

  // 向主线程发送消息
  self.postMessage(result);
});

2. 在主线程中使用 Worker 然后,在主线程(比如你的主 JavaScript 文件或网页脚本中)创建 Worker 的实例,并与之通信:

// 创建 Worker 实例
const myWorker = new Worker("worker.js");

// 向 Worker 发送消息
myWorker.postMessage(data);

// 监听 Worker 发送回来的消息
myWorker.addEventListener("message", function (e) {
  const result = e.data;
  console.log("Message received from worker:", result);
});

注意事项

应用场景 Web Workers 非常适合用于那些需要大量计算且可能会阻塞 UI 的任务,如图像处理、大数据计算、复杂的排序或搜索操作等。通过将这些操作移到后台线程,可以保持前端界面的流畅和响应。

7.7.二进制对象 #

这张图是关于前端 JavaScript 处理二进制数据流和图像的流程图,它详细地展示了不同的 Web API 之间的关系及其用途。下面是对图中各部分的解释:

左上角:Canvas 操作

右上角:图像与 URL

中间:Object URL

左下角:ImageData 操作

右下角:二进制数据操作

左下方:ArrayBuffer 与视图

底部中央:DataView

整体上,这张图表展示了在前端 web 应用中如何处理和转换二进制数据和图像,以及如何在不同的上下文(如 canvas、HTTP 请求、文件操作)之间传递和转换这些数据。

ArrayBuffer, TypedArray, DataView, Blob, 和 Object URL是 JavaScript 中用于处理二进制数据和文件的关键组件。它们在处理如文件上传、下载、图像处理等涉及大量数据的场景中非常有用。

ArrayBuffer ArrayBuffer 是一种表示通用的、固定长度的原始二进制数据缓冲区的类。它是一个字节数组,但并不能直接操作。通常,你会通过一个 TypedArrayDataView 来操作 ArrayBuffer 中的数据。

TypedArray TypedArray 对象描述了一个底层的二进制数据缓冲区(ArrayBuffer)的数组视图。不同类型的 TypedArray(例如 Uint8Array, Int16Array, Float32Array 等)提供了对 ArrayBuffer 中数据的不同解释方式。每种 TypedArray 都是为了处理特定的数值类型而设计的。

DataView DataView 提供了一个更复杂和灵活的方式来读取和写入 ArrayBuffer。与 TypedArray 不同,DataView 不是固定于一种数据表示,因此它允许你从同一个缓冲区中读写不同类型的数据。

Blob Blob(Binary Large OBject)代表了一个不可变的、原始数据的类文件对象。Blob 通常用于处理二进制文件数据,如从 File 接口中读取的文件数据。例如,你可以用 Blob 来处理上传的文件或生成下载链接。

Object URL Object URL 是一个特殊的 URL,它允许你创建一个指向内存中的数据(如 BlobFile 对象)的短暂的引用。通过 URL.createObjectURL() 方法可以创建这样的 URL,它可以用在任何期望 URL 的地方,比如在 <img>src 属性中显示一个本地选择的图像。创建的 Object URL 应该在不需要时通过 URL.revokeObjectURL() 来释放内存。

总结

这些组件共同为 JavaScript 提供了强大的处理和操作二进制数据的能力,对于现代网络应用中的文件操作和数据传输至关重要。

7.8.crypto.subtle.digest #

crypto.subtle.digest('SHA-256', arrayBuffer) 是 Web Cryptography API 的一部分,它用于计算传入数据的 SHA-256 哈希。这个方法是浏览器提供的一种原生方式来执行密码学操作,其中包括创建数据的散列(哈希)。

参数解析

  1. 'SHA-256':这是指定的哈希算法。SHA-256 是 SHA-2 算法家族中的一员,产生一个 256 位(32 字节)的哈希值。它是目前广泛使用的安全哈希算法之一。

  2. arrayBuffer:这是要进行哈希处理的数据,以 ArrayBuffer 的形式提供。ArrayBuffer 是一种表示固定长度原始二进制数据缓冲区的通用容器。

使用方法 要使用 crypto.subtle.digest,你需要按照以下步骤进行:

1. 准备数据 准备一个 ArrayBuffer,这可以是任何类型的数据,例如文件内容、文本字符串等。

2. 计算哈希 调用 crypto.subtle.digest 方法计算数据的 SHA-256 哈希值。由于这个方法返回一个 Promise,你需要使用 async/await.then() 来处理异步结果。

示例代码 以下是一个使用 crypto.subtle.digest 计算字符串哈希的示例:

async function calculateSHA256(string) {
  const encoder = new TextEncoder();
  const data = encoder.encode(string);
  const hash = await crypto.subtle.digest("SHA-256", data);
  return hash;
}

calculateSHA256("Hello, World!").then((hashBuffer) => {
  // 将 ArrayBuffer 转换为十六进制字符串以便显示
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  console.log(hashHex);
});

在这个例子中,我们首先将一个字符串转换为 ArrayBuffer,然后计算它的 SHA-256 哈希,并将结果转换为十六进制字符串以便显示。

安全性和用途

总之,crypto.subtle.digest 是一个强大的工具,用于在 Web 应用中进行安全和高效的哈希运算。