【问题标题】:Javascript DataTransfer items not persisting through async callsJavascript DataTransfer 项目不会通过异步调用持久化
【发布时间】:2019-04-12 20:15:28
【问题描述】:

我正在使用 Vuejs 和 DataTransfer 异步上传文件,我希望允许一次拖放多个文件进行上传。

我可以进行第一次上传,但在上传完成时,Javascript 已经收集垃圾或更改了 DataTransfer 项目对象。

我怎样才能重新处理这个(或克隆事件/DataTransfer 对象),以便在整个 ajax 调用过程中我仍然可以使用数据?

我已经按照 MDN 文档了解如何使用 DataTransfer,但我很难将它应用到我的具体案例中。我也尝试过复制事件对象,正如您在我的代码中看到的那样,但它显然不会进行深度复制,只是传递引用,这没有帮助。

    methods: {
        dropHandler: function (event) {
            if (event.dataTransfer.items) {
                let i = 0;
                let self = this;
                let ev = event;

                function uploadHandler() {
                    let items = ev.dataTransfer.items;
                    let len = items.length;

                    // len NOW EQUALS 4

                    console.log("LEN: ", len);
                    if (items[i].kind === 'file') {
                        var file = items[i].getAsFile();
                        $('#id_file_name').val(file.name);
                        var file_form = $('#fileform2').get(0);
                        var form_data = new FormData(file_form); 

                        if (form_data) {
                            form_data.append('file', file);
                            form_data.append('type', self.type);
                        }

                        $('#file_progress_' + self.type).show();
                        var post_url = '/blah/blah/add/' + self.object_id + '/'; 
                        $.ajax({
                            url: post_url,
                            type: 'POST',
                            data: form_data,
                            contentType: false,
                            processData: false,
                            xhr: function () {
                                var xhr = $.ajaxSettings.xhr();
                                if (xhr.upload) {
                                    xhr.upload.addEventListener('progress', function (event) {
                                        var percent = 0;
                                        var position = event.loaded || event.position;
                                        var total = event.total;
                                        if (event.lengthComputable) {
                                            percent = Math.ceil(position / total * 100);
                                            $('#file_progress_' + self.type).val(percent);
                                        }
                                    }, true);
                                }
                                return xhr;
                            }
                        }).done((response) => {
                                i++;
                                if (i < len) {

                                    // BY NOW, LEN = 0.  ????

                                    uploadHandler();
                                } else {
                                    self.populate_file_lists();
                                }
                            }
                        );
                    }
                }

                uploadHandler();
            }
        },

【问题讨论】:

  • 这个问题甚至不是 Vue.js 特有的......它也是 vanilla JS 应用程序的问题。我做了一个更简单的测试用例来重现这个问题:jsfiddle.net/rjq6b83t/1 如果您使用浏览器的开发人员工具,您会看到“下一个循环”甚至不会发生,因为到那时 DataTransfer 实例似乎已经死了。
  • @Brad 将 Promise 推入数组并稍后使用 Promise.All 处理它们怎么样? jsfiddle.net/g5h4ajm8/2
  • @TemoJr。是的,这行得通,我认为关键是在退出调用堆栈之前获取entry。

标签: javascript html vuejs2 drag-and-drop data-transfer-objects


【解决方案1】:

一旦您调用await,您就不再位于函数的原始调用堆栈中。这在事件侦听器中尤其重要。

我们可以用setTimeout重现同样的效果:

dropZone.addEventListener('drop', async (e) => {
  e.preventDefault();
  console.log(e.dataTransfer.items);
  setTimeout(()=> {
    console.log(e.dataTransfer.items);
  })
});

例如拖动四个文件会输出:

DataTransferItemList {0: DataTransferItem, 1: DataTransferItem, 2: DataTransferItem, 3: DataTransferItem, length: 4}  
DataTransferItemList {length: 0}

事件发生后,状态发生了变化,物品丢失了。

有两种方法可以处理这个问题:

  • 复制项目并对其进行迭代
  • 将异步作业(Promises)推送到数组中,稍后使用Promise.all 处理它们

第二种解决方案比在循环中使用await 更直观。另外,请考虑parallel connections are limited。使用数组,您可以创建块来限制同时上传。

function pointlessDelay() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000);
  });
}

const dropZone = document.querySelector('.dropZone');

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault();
});

dropZone.addEventListener('drop', async (e) => {
  e.preventDefault();
  console.log(e.dataTransfer.items);
  const queue = [];
  
  for (const item of e.dataTransfer.items) {
    console.log('next loop');
    const entry = item.webkitGetAsEntry();
    console.log({item, entry});
    queue.push(pointlessDelay().then(x=> console.log(`${entry.name} uploaded`)));
  }
  
  await Promise.all(queue);
});
body {
  font-family: sans-serif;
}

.dropZone {
  display: inline-flex;
  background: #3498db;
  color: #ecf0f1;
  border: 0.3em dashed #ecf0f1;
  border-radius: 0.3em;
  padding: 5em;
  font-size: 1.2em;
}
<div class="dropZone">
  Drop Zone
</div>

【讨论】:

  • 在发送请求的情况下使用Promise.all 可能会产生连接限制问题。例如,如果用户尝试一次上传 20 个文件,浏览器会崩溃几次overhead requests。但当然从另一边比async ... loop
  • 好点@AlexandrTovmach。在这种情况下,解决方案是将数组拆分为块并创建一个队列。
  • ...或者只使用async...loop =)
  • @AlexandrTovmach 如果您使用async 循环,那么您将不会让请求并行执行。将 promises 数组拆分为块并使用Promise.all,您可以获得更快的结果,并且不会达到任何限制。
  • 是的,我知道并且我注意到在之前的评论中,但是将队列拆分为块从代码角度来看有点开销。请记住:“您是在为人类编写代码,而不是为机器编写代码”,并且在您真正遇到问题之前,您无需考虑“更快的结果”。
【解决方案2】:

似乎DataTransfer 的上下文随着时间的推移而丢失。我的解决方案是在丢失之前复制所需的数据并在需要时重新使用它:

const files = [...e.dataTransfer.items].map(item => item.getAsFile());

使用我的解决方案从jsfiddle 的@Brad 修改代码:

const dropZone = document.querySelector(".dropZone");
const sendFile = file => {
  const formData = new FormData();
  for (const name in file) {
    formData.append(name, file[name]);
  }
  /**
   * https://docs.postman-echo.com/ - postman mock server
   * https://cors-anywhere.herokuapp.com/ - CORS proxy server
   **/
  return fetch(
    "https://cors-anywhere.herokuapp.com/https://postman-echo.com/post",
    {
      method: "POST",
      body: formData
    }
  );
};

dropZone.addEventListener("dragover", e => {
  e.preventDefault();
});

dropZone.addEventListener("drop", async e => {
  e.preventDefault();
  const files = [...e.dataTransfer.items].map(item => item.getAsFile());
  const responses = [];

  for (const file of files) {
    const res = await sendFile(file);
    responses.push(res);
  }
  console.log(responses);
});
body {
  font-family: sans-serif;
}

.dropZone {
  display: inline-flex;
  background: #3498db;
  color: #ecf0f1;
  border: 0.3em dashed #ecf0f1;
  border-radius: 0.3em;
  padding: 5em;
  font-size: 1.2em;
}
<div class="dropZone">
  Drop Zone
</div>

【讨论】:

    【解决方案3】:

    我遇到了这个问题,并希望保留整个 DataTransfer 对象,而不仅仅是 items 或 types,因为我的异步代码的 API 使用 DataTransfer 类型本身。我最终做的是创建一个new DataTransfer(),并有效地复制原件的属性(拖动图像除外)。

    这是要点(在 TypeScript 中):https://gist.github.com/mitchellirvin/261d82bbf09d5fdee41715fa2622d4a6

    // https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/kind
    enum DataTransferItemKind {
      FILE = "file",
      STRING = "string",
    }
    
    /**
     * Returns a properly deep-cloned object of type DataTransfer. This is necessary because dataTransfer items are lost
     * in asynchronous calls. See https://stackoverflow.com/questions/55658851/javascript-datatransfer-items-not-persisting-through-async-calls
     * for more details.
     * 
     * @param original the DataTransfer to deep clone
     */
    export function cloneDataTransfer(original: DataTransfer): DataTransfer {
      const cloned = new DataTransfer();
      cloned.dropEffect = original.dropEffect;
      cloned.effectAllowed = original.effectAllowed;
    
      const originalItems = original.items;
      let i = 0;
      let originalItem = originalItems[i];
      while (originalItem != null) {
        switch (originalItem.kind) {
          case DataTransferItemKind.FILE:
            const file = originalItem.getAsFile();
            if (file != null) {
              cloned.items.add(file);
            }
            break;
          case DataTransferItemKind.STRING:
            cloned.setData(originalItem.type, original.getData(originalItem.type));
            break;
          default:
            console.error("Unrecognized DataTransferItem.kind: ", originalItem.kind);
            break;
        }
    
        i++;
        originalItem = originalItems[i];
      }
      return cloned;
    }
    

    您可以像这样使用它,然后以您最初计划使用 evt.dataTransfer 的相同方式使用 clone:

    const clone = cloneDataTransfer(evt.dataTransfer);

    【讨论】:

      猜你喜欢
      • 2016-03-16
      • 1970-01-01
      • 2022-01-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-16
      • 2015-11-02
      相关资源
      最近更新 更多