当前位置: 首页 > 知识库问答 >
问题:

前端 - 如何优雅地实现文件上传+文件夹上传+拖拽上传+进度追踪+...?

拓拔君博
2024-05-04

如何优雅地实现文件上传+文件夹上传+拖拽上传+进度追踪+...?

需求分析:

基础功能

  • 显示

    • 上传文件或文件夹的名字类型大小状态

      • 类型

        • 文件夹的类型可以"文件夹"或或者没有
        • 文件的类型范围MIME
      • 大小

        • 文件夹的大小为该文件夹及其子文件夹下所有文件的大小总和
      • 状态

        • 最少应该有的状态(假设上传一定成功,不会出现错误)

          • 未上传
          • 上传中(当处于此状态的时候要实时显示上传进度)
          • 上传完成
        • 其他扩展状态

          • 暂停
          • ...
    • 文件夹显示可以展开和折叠
    • 上传按钮在上传中是disabled,并且在没有文件上传的时候点击是不会触发上传的(可以在此时给出一个提醒)
  • 多个文件或文件夹上传
  • 文件选中方式

    • 通过input[type="file"]来选中
    • 拖拽
  • 进度追踪

    • 一个文件的上传进度为loaded/total
    • 一个文件夹的上传进度为该文件夹下(包括子文件夹)的所有文件的loaded之和除以所有文件的total之和。

image.png

扩展功能

  • 取消某些选中的将要上传文件
  • 暂停

    • 可以暂停上传的文件,之后再继续上传的时候不重新开始上传而是从上传完成的部分之后上传。
  • 中断重试

    • 和暂停功能类似,只是这里的中断是不是人主动要求的,而是外部的一些不可预料的事件(网络中断、服务器故障..)造成的。
  • 文件上传性能优化

    • 大文件分片上传
    • 并发上传
    • ....

我自己用react+tailwindcss尝试勉强实现了基础功能。
维护了一个树结构来记录文件上传的状态。
其中每个节点的字段含义

  • type

    • 0表示文件, 1表示文件夹
  • file

    • File: 对于文件来说是File对象,记录了文件的一些信息。
    • { name: <directory name>, size: <directory size>, type: "文件夹"}: 对于文件夹来说只是记录了文件夹的名字、大小和类型。
  • children:

    • 对于文件来说该字段是没有的。
    • 对于文件夹来说它是一个数组,包含一些节点,表示该文件夹下的所有文件和文件夹。
  • parent

    • [node, idx]: node表示该节点的父节点,idx表示该节点在其父节点的children中位于第几个。

      • 问题: 如果我们想要删除某一个节点(相当于取消某个文件的上传)

        • 方案1 splice

          • 其后面的兄弟节点的parent也要改变,所有依赖idx的数据也改变。
          • 当我们需要恢复该节点的时候,还需要回退我们的修改(需要记住之前的idx,否则渲染的顺序就发生变化了。)
          • 实现起来还是比较麻烦,但没有占据不必要的空间。
        • 方案2 假的删除

          • 在其父元素的children数组的对应位置(idx)设置为null,表示该节点已经被删除。
            • 恢复的时候也容易。
          • 实现起来简单,但是可能浪费了一些空间,特别是当一层中文件条目比较多的时候。
        • 你有什么更好的方案吗?也可以重新设计这个node,使得删除、恢复更容易,且占据的空间少。
    • null: 表明该节点为根节点。
  • progress:

    • num: 对于文件来说,它就是一个数值。由XMLHttpRequest实例的中的upload对象上progress事件中得到的event.targetevent.loaded计算而来得到。(loaded * 100 /tatoal)。
    • {loaded: [...], total: [....]}: 对于文件夹来说,我们需要统计其子节点的loadedtoatal之后再计算得到其最终的进度。

      • 当一个子节点的的进度发生改变的时候,它会顺着parent,去修改其祖先节点中progress.loadedprogress.total。(这就是我们需要parent字段的原因)

问题:这棵树在该react项目中为一个状态,应该保持它的不可变性。对于这种复杂的数据结构该怎么保持它的不可变性呢?借助immer?

下面为更新进度的过程,该怎么修改保持不可变性呢?

// src/utils.jsexport function uploadProcess(uploadFileList, setUploadFileList, setStatus) {  // TopNode用作哨兵,让边界条件处理更容易。  for (let i = 0; i < uploadFileList.length; ++i) {    uploadFileList[i].parent = [TopNode, i];    TopNode.children.push(uploadFileList[i]);  }  TopNode.progress = {    loaded: Array(uploadFileList.length).fill(0),    total: Array(uploadFileList.length).fill(0),  };  uploadFileOrDirectory(    TopNode,    () => {      setUploadFileList([...uploadFileList]);    },    setStatus  );}function uploadFileOrDirectory(entry, update, setStatus) {  if (entry === null) return;  if (entry.type === 0) {    const xhr = new XMLHttpRequest();    const data = new FormData();    data.append("file", entry.file);    xhr.open("POST", UPLOAD_URL);    xhr.upload.addEventListener("progress", (e) => {      const percent = Number(((e.loaded * 100) / e.total).toFixed(0));      entry.progress = percent;      propagateProcessUpward(entry, e.loaded, e.total, setStatus);      update();    });    xhr.send(data);  } else {    for (const e of entry.children) {      uploadFileOrDirectory(e, update, setStatus);    }  }}function propagateProcessUpward(entry, loaded, total, setStatus) {  if (entry.parent) {    const [parent, idx] = entry.parent;    parent.progress.loaded[idx] = loaded;    parent.progress.total[idx] = total;    propagateProcessUpward(      parent,      parent.progress.loaded.reduce((acc, cur) => acc + cur, 0),      parent.progress.total.reduce((acc, cur) => acc + cur, 0),      setStatus    );  } else {    // topNode    if (      entry.progress.loaded.every(        (item, idx) => entry.progress.total[idx] === item      )    )      setStatus(2);  }}
  • 我的实现一点也不优雅而且应该还有很多问题,欢迎指正。
  • 如果你有更好的的方案,欢迎在解答区分享。最好有完整的代码和较为详细的思路讲解。

附:

  • codesandbox
  • github(问题可以直接在issue里提)
  • 做的一些笔记

共有2个答案

左丘源
2024-05-04
import produce from "immer";const initialState = {  files: []};const uploadReducer = produce((draft, action) => {  switch (action.type) {    case 'ADD_FILE':      draft.files.push(action.payload);      break;    case 'UPDATE_PROGRESS':      const file = draft.files.find(file => file.id === action.payload.id);      if (file) {        file.progress = action.payload.progress;      }      break;    default:      break;  }}, initialState);function updateProgress(id, progress) {  return {    type: 'UPDATE_PROGRESS',    payload: { id, progress }  };}
蓝昊然
2024-05-04

你的实现已经包含了大部分需要的功能,并且你对数据结构的设计也很到位。然而,确实有一些可以改进和优化的地方。以下是我对你的问题的一些解答和建议:

问题1:如何优雅地删除和恢复节点?

一种更优雅的方法是使用一种称为"路径枚举"或"路径压缩"的技术。你可以将每个节点的路径(从根节点到该节点的所有父节点的索引)存储在每个节点中,而不是仅仅存储父节点的索引。这样,当你需要删除或恢复一个节点时,你只需要更新该节点的路径,而不需要更新其所有祖先节点的子节点。这种方法可以在O(1)的时间内完成删除和恢复操作,而不需要遍历整个树。

问题2:如何保持数据结构的不可变性?

对于保持数据结构的不可变性,你可以使用一种称为"函数式编程"的技术。在这种编程风格中,你永远不会直接修改数据,而是创建新的数据来替代旧的数据。在JavaScript中,你可以使用immer库来帮助你实现函数式编程。immer允许你以一种更直观、更易于理解的方式创建新的数据,而不需要手动创建所有的新数据。

问题3:如何更新进度并保持数据结构的不可变性?

对于更新进度,你可以将进度信息存储在每个节点中,并在上传过程中更新这些信息。当你需要更新整个数据结构的进度时,你可以遍历整个树并收集所有节点的进度信息。然后,你可以使用这些信息来创建一个新的数据结构,其中包含最新的进度信息。

下面是一个简化的示例代码,展示了如何使用immer和函数式编程来更新进度并保持数据结构的不可变性:

import produce from "immer";const initialState = {  // 你的初始状态};function updateProgress(draftState, entry, loaded, total) {  const traverse = (node) => {    if (node.type === 0) {      node.progress = (loaded * 100) / total;    } else {      node.progress = {        loaded: node.children.reduce((acc, cur) => acc + cur.progress.loaded, 0),        total: node.children.reduce((acc, cur) => acc + cur.progress.total, 0),      };      node.progress.loaded = (node.progress.loaded * 100) / node.progress.total;      node.children.forEach(traverse);    }  };  traverse(draftState.find((node) => node.id === entry.id));}function handleUpload(state, action) {  return produce(state, (draftState) => {    switch (action.type) {      case "UPDATE_PROGRESS":        updateProgress(draftState, action.payload.entry, action.payload.loaded, action.payload.total);        break;      // 处理其他动作      default:        break;    }  });}// 在你的组件中使用handleUpload来更新状态

在这个示例中,updateProgress函数使用递归来遍历树并更新每个节点的进度。handleUpload函数使用immerproduce函数来创建一个新的状态,其中包含最新的进度信息。

请注意,这只是一个简化的示例,你可能需要根据你的具体需求来修改和扩展它。另外,你可能还需要处理其他类型的动作,如添加、删除和恢复节点等。你可以使用类似的方法来处理这些动作,以保持数据结构的不可变性。

 类似资料:
  • 本文向大家介绍简单实现ajax拖拽上传文件,包括了简单实现ajax拖拽上传文件的使用技巧和注意事项,需要的朋友参考一下 AJAX拖拽上传功能实现,供大家参考,具体内容如下 //server.php 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持呐喊教程。

  • 本文向大家介绍如何实现文件拖动上传?相关面试题,主要包含被问及如何实现文件拖动上传?时的应答技巧和注意事项,需要的朋友参考一下 利用 HTML5 的 API 来实现。需要注意的是,必须要设置 事件,不然不会触发 事件。 示例:https://codepen.io/Konata9/pen/BaBzExe?editors=1011

  • 问题内容: 看来我还没有清楚地传达出我的问题。我需要发送一个文件(使用AJAX),并且需要使用Nginx HttpUploadProgressModule 获取文件的上传进度。我需要一个很好的解决方案。我已经尝试过使用jquery.uploadprogress插件,但是我发现自己不得不重写其中的大部分内容,以使其在所有浏览器中都能正常工作并使用AJAX发送文件。 我所需要的只是执行此操作的代码,它

  • 本文向大家介绍PHP实现文件上传和多文件上传,包括了PHP实现文件上传和多文件上传的使用技巧和注意事项,需要的朋友参考一下 在PHP程序开发中,文件上传是一个使用非常普遍的功能,也是PHP程序员的必备技能之一。值得高兴的是,在PHP中实现文件上传功能要比在Java、C#等语言中简单得多。下面我们结合具体的代码实例来详细介绍如何通过PHP实现文件上传和多文件上传功能。 要使用PHP实现文件上传功能,

  • 本文向大家介绍jQuery插件实现文件上传功能(支持拖拽),包括了jQuery插件实现文件上传功能(支持拖拽)的使用技巧和注意事项,需要的朋友参考一下 先贴上源代码地址,点击获取。然后直接进入主题啦,当然,如果你觉得我有哪里写的不对或者欠妥的地方,欢迎留言指出。在附上一些代码之前,我们还是先来了解下,上传文件的时候需要利用的一些必要的知识。  首先我们要说的就是FileReader对象,这是一个H

  • 问题内容: 当用户将文件上传到我的Web应用程序时,我想显示比gif动画更有意义的内容。我有什么可能性? 编辑:我正在使用.Net,但我不介意是否有人向我展示平台不可知的版本。 问题答案: 以下是一些常用JavaScript工具包的几种版本。 Mootools- http: //digitarald.de/project/fancyupload/ Extjs- http: //extjs.com/