6
votes

Les éléments Javascript DataTransfer ne persistent pas lors d'appels asynchrones

J'utilise Vuejs avec DataTransfer pour télécharger des fichiers de manière asynchrone, et je souhaite autoriser le glisser-déposer de plusieurs fichiers pour le téléchargement en même temps.

Je peux obtenir le premier téléchargement, mais au moment où ce téléchargement est terminé, Javascript a soit garbage collecté, soit modifié l'objet des éléments DataTransfer.

Comment puis-je retravailler ceci (ou cloner l'objet événement / DataTransfer) afin que les données soient toujours disponibles pour moi tout au long des appels ajax?

J'ai suivi la documentation MDN sur comment utiliser DataTransfer mais j'ai du mal à l'appliquer à mon cas spécifique. J'ai aussi essayé de copier les objets événement, comme vous pouvez le voir dans mon code, mais cela ne fait évidemment pas de copie profonde, passe simplement la référence, ce qui n'aide pas.

    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();
            }
        },


3 commentaires

Le problème n'est même pas spécifique à Vue.js ... c'est également un problème avec les applications vanilla JS. J'ai fait un cas de test plus simple pour reproduire le problème: jsfiddle.net/rjq6b83t/1 Si vous utilisez les outils de développement du navigateur, vous verrez que la "prochaine boucle" ne se produit même pas, car l'instance DataTransfer semble être morte à ce moment-là.


@Brad qu'en est-il de pousser des promesses dans le tableau et de les gérer plus tard avec Promise.All ? jsfiddle.net/g5h4ajm8/2


@TemoJr. Ouais, cela fonctionne, je pense que la clé obtient l ' entrée avant de sortir de la pile d'appels.


3 Réponses :


2
votes

Il semble que le contexte de DataTransfer manque avec le temps. Ma solution est de copier les données requises avant de les manquer et de les réutiliser si nécessaire:

<div class="dropZone">
  Drop Zone
</div>

Code modifié de jsfiddle de @Brad avec ma solution:

p >

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;
}
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);
});
const files = [...e.dataTransfer.items].map(item => item.getAsFile());


0 commentaires

5
votes

Une fois que vous appelez wait , vous n'êtes plus dans la pile d'appels d'origine de la fonction. C'est quelque chose qui importerait particulièrement à l'auditeur de l'événement.

Nous pouvons reproduire le même effet avec setTimeout:

<div class="dropZone">
  Drop Zone
</div>

Par exemple, faire glisser quatre fichiers produira:

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

Après l'événement, l'état a changé et des éléments ont été perdus .

Il existe deux façons de gérer ce problème:

  • Copiez les éléments et parcourez-les
  • Poussez les tâches asynchrones (Promises) dans le tableau et gérez-les plus tard avec Promise.all

La deuxième solution est plus intuitive que d'utiliser await dans la boucle. Pensez également à que les connexions parallèles sont limitées . Avec un tableau, vous pouvez créer des blocs pour limiter les importations simultanées.

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);
});
DataTransferItemList {0: DataTransferItem, 1: DataTransferItem, 2: DataTransferItem, 3: DataTransferItem, length: 4}  
DataTransferItemList {length: 0}
dropZone.addEventListener('drop', async (e) => {
  e.preventDefault();
  console.log(e.dataTransfer.items);
  setTimeout(()=> {
    console.log(e.dataTransfer.items);
  })
});


6 commentaires

L'utilisation de Promise.all dans les cas d'envoi de requêtes peut potentiellement produire un problème de limite de connexions. Par exemple, si l'utilisateur essaie de télécharger 20 fichiers à la fois, le navigateur plante quelques demandes de frais généraux . Mais bien sûr, de l'autre côté est plus rapide que async ... loop


Bon point @AlexandrTovmach. Dans ce cas, la solution consiste à diviser un tableau en morceaux et à créer une file d'attente.


... ou utilisez simplement async ... loop =)


@AlexandrTovmach si vous utilisez la boucle async , les requêtes ne seront pas exécutées en parallèle. En divisant le tableau des promesses en morceaux et en utilisant Promise.all , vous obtenez des résultats plus rapides et vous êtes sûr de ne pas atteindre les limites.


Oui, je sais et j'ai remarqué que dans le commentaire précédent, mais diviser la file d'attente en morceaux est un peu plus complexe du point de vue du code. Souvenez-vous simplement: «Vous écrivez du code pour les humains, pas pour les machines» et vous n'avez pas besoin de penser à des «résultats plus rapides» avant d'avoir des problèmes avec cela.


et un générateur? Quelque chose dans ce sens, si je comprends bien le domaine du problème et cette réponse ... medium.com/javascript-scene/...



0
votes

J'ai rencontré ce problème et je cherchais à conserver l'intégralité de l'objet DataTransfer , pas seulement les éléments ou types , car mon L'API de code utilise le type DataTransfer lui-même. Ce que j'ai fini par faire, c'est créer un nouveau DataTransfer () , et copier efficacement les propriétés de l'original (à l'exception de l'image de glissement).

Voici l'essentiel (en TypeScript): https://gist.github.com/mitchellirvin/261d82bbf09d5fdee41715fa2622d4a6

Vous pouvez consommer ceci comme tel, puis utiliser clone de la même manière que vous aviez initialement prévu d'utiliser evt.dataTransfer:

const clone = cloneDataTransfer (evt.dataTransfer);


0 commentaires