web-dev-qa-db-fra.com

Comment puis-je utiliser du contenu dégonflé / compressé avec une fonction XHR onProgress?

J'ai déjà vu un tas de questions similaires à celle-ci, mais je n'en ai pas trouvé qui décrit exactement mon problème actuel, alors voici:

J'ai une page qui charge un gros document JSON (entre 0,5 et 10 Mo) via AJAX pour que le code côté client puisse le traiter. Une fois le fichier chargé, je n'ai plus tous les problèmes auxquels je ne m'attendais pas. Cependant, le téléchargement prend beaucoup de temps, j'ai donc essayé de tirer parti de XHR Progress API pour afficher une barre de progression pour indiquer à l'utilisateur que le document est en cours de chargement. Cela a bien fonctionné.

Ensuite, dans un effort pour accélérer les choses, j'ai essayé de compresser la sortie côté serveur via gzip et dégonfler. Cela a également fonctionné, avec des gains énormes, cependant, ma barre de progression a cessé de fonctionner.

J'ai étudié le problème pendant un certain temps et j'ai constaté que si un Content-Length l'en-tête n'est pas envoyé avec la ressource AJAX ressource, le gestionnaire d'événements onProgress ne peut pas fonctionner comme prévu car il ne sait pas à quelle distance il se trouve dans le téléchargement. Lorsque cela se produit, une propriété appelée lengthComputable est définie sur false sur l'objet événement.

Cela avait du sens, j'ai donc essayé de définir l'en-tête explicitement avec la longueur non compressée et compressée de la sortie. Je peux vérifier que l'en-tête est envoyé et je peux vérifier que mon navigateur sait comment décompresser le contenu. Mais le gestionnaire onProgress signale toujours lengthComputable = false.

Donc ma question est: y a-t-il un moyen de compresser/dégonfler du contenu avec l'API de progression AJAX?) Et si c'est le cas , qu'est-ce que je fais mal en ce moment?


Voici comment la ressource apparaît dans le panneau Chrome Network, montrant que la compression fonctionne:

network panel

Ce sont les en-têtes de requête pertinents , montrant que la requête est AJAX et que Accept-Encoding est correctement défini:

GET /dashboard/reports/ajax/load HTTP/1.1
Connection: keep-alive
Cache-Control: no-cache
Pragma: no-cache
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.99 Safari/537.22
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

Ce sont les en-têtes de réponse pertinents , montrant que le Content-Length et Content-Type sont définis correctement:

HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Content-Encoding: deflate
Content-Type: application/json
Date: Tue, 26 Feb 2013 18:59:07 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
P3P: CP="CAO PSA OUR"
Pragma: no-cache
Server: Apache/2.2.8 (Unix) mod_ssl/2.2.8 OpenSSL/0.9.8g PHP/5.4.7
X-Powered-By: PHP/5.4.7
Content-Length: 223879
Connection: keep-alive

Pour ce que ça vaut, j'ai essayé cela sur une connexion standard (http) et sécurisée (https), sans aucune différence: le contenu se charge bien dans le navigateur, mais n'est pas traité par l'API Progress.


Par suggestion d'Adam , j'ai essayé de passer du côté serveur à l'encodage gzip sans succès ni changement. Voici les en-têtes de réponse pertinents:

HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Content-Encoding: gzip
Content-Type: application/json
Date: Mon, 04 Mar 2013 22:33:19 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
P3P: CP="CAO PSA OUR"
Pragma: no-cache
Server: Apache/2.2.8 (Unix) mod_ssl/2.2.8 OpenSSL/0.9.8g PHP/5.4.7
X-Powered-By: PHP/5.4.7
Content-Length: 28250
Connection: keep-alive

Juste pour répéter: le contenu est téléchargé et décodé correctement, c'est juste l'API de progression avec laquelle j'ai du mal.


Par demande de Bertrand , voici la demande:

$.ajax({
    url: '<url snipped>',
    data: {},
    success: onDone,
    dataType: 'json',
    cache: true,
    progress: onProgress || function(){}
});

Et voici le gestionnaire d'événements onProgress que j'utilise (ce n'est pas trop fou):

function(jqXHR, evt)
{
    // yes, I know this generates Infinity sometimes
    var pct = 100 * evt.position / evt.total;

    // just a method that updates some styles and javascript
    updateProgress(pct);
});
42
Jimmy Sawczuk

Je n'ai pas pu résoudre le problème d'utilisation de onProgress sur le contenu compressé lui-même, mais j'ai trouvé cette solution de contournement semi-simple. En bref: envoyez une demande HEAD au serveur en même temps qu'une demande GET, et rendez la barre de progression une fois qu'il y a suffisamment d'informations pour le faire.


function loader(onDone, onProgress, url, data)
{
    // onDone = event handler to run on successful download
    // onProgress = event handler to run during a download
    // url = url to load
    // data = extra parameters to be sent with the AJAX request
    var content_length = null;

    self.meta_xhr = $.ajax({
        url: url,
        data: data,
        dataType: 'json',
        type: 'HEAD',
        success: function(data, status, jqXHR)
        {
            content_length = jqXHR.getResponseHeader("X-Content-Length");
        }
    });

    self.xhr = $.ajax({
        url: url,
        data: data,
        success: onDone,
        dataType: 'json',
        progress: function(jqXHR, evt)
        {
            var pct = 0;
            if (evt.lengthComputable)
            {
                pct = 100 * evt.position / evt.total;
            }
            else if (self.content_length != null)
            {
                pct = 100 * evt.position / self.content_length;
            }

            onProgress(pct);
        }
    });
}

Et puis pour l'utiliser:

loader(function(response)
{
    console.log("Content loaded! do stuff now.");
},
function(pct)
{
    console.log("The content is " + pct + "% loaded.");
},
'<url here>', {});

Côté serveur, définissez le X-Content-Length en-tête sur les requêtes GET et HEAD (qui doivent représenter la longueur du contenu non compressé ), et abandonner l'envoi du contenu sur la demande HEAD.

En PHP, définir l'en-tête ressemble à:

header("X-Content-Length: ".strlen($payload));

Et puis abandonnez l'envoi du contenu s'il s'agit d'une demande HEAD:

if ($_SERVER['REQUEST_METHOD'] == "HEAD")
{
    exit;
}

Voici à quoi cela ressemble en action:

screenshot

La raison pour laquelle HEAD prend autant de temps dans la capture d'écran ci-dessous est que le serveur doit encore analyser le fichier pour savoir combien de temps il est, mais c'est quelque chose que je peux certainement améliorer, et c'est certainement une amélioration par rapport à l'endroit où il était.

9
Jimmy Sawczuk

Une variante un peu plus élégante de votre solution serait de définir un en-tête comme `` x-decompressed-content-length '' ou autre dans votre réponse HTTP avec la valeur décompressée complète du contenu en octets et de le lire l'objet xhr dans votre onProgress gestionnaire.

Votre code pourrait ressembler à:

request.onProgress = function (e) {
  var contentLength;
  if (e.lengthComputable) {
    contentLength = e.total;
  } else {
    contentLength = parseInt(e.target.getResponseHeader('x-decompressed-content-length'), 10);
  }
  progressIndicator.update(e.loaded / contentLength);
};
13
Nat

Ne restez pas bloqué simplement parce qu'il n'y a pas de solution native; un hack d'une ligne peut résoudre votre problème sans jouer avec la configuration d'Apache (qui dans certains hébergements est interdit ou très restreint):

PHP à la rescousse:

var size = <?php echo filesize('file.json') ?>;

Ça y est, vous connaissez probablement déjà le reste, mais juste comme référence ici c'est:

<script>
var progressBar = document.getElementById("p"),
    client = new XMLHttpRequest(),
    size = <?php echo filesize('file.json') ?>;

progressBar.max = size;

client.open("GET", "file.json")

function loadHandler () {
  var loaded = client.responseText.length;
  progressBar.value = loaded;
}

client.onprogress = loadHandler;

client.onloadend = function(pe) {
  loadHandler();
  console.log("Success, loaded: " + client.responseText.length + " of " + size)
}
client.send()
</script>

Exemple en direct:

Un autre SO utilisateur pense que je mens sur la validité de cette solution alors la voici en direct: http://nyudvik.com/Zip/ , c'est gzip- ed et le vrai fichier pèse 8 Mo



Liens connexes:

4
Ivan Castellanos

Nous avons créé une bibliothèque qui estime la progression et définit toujours lengthComputable sur true.

Chrome 64 a toujours ce problème (voir Bug )

C'est un shim javascript que vous pouvez inclure dans votre page qui résout ce problème et vous pouvez utiliser normalement la new XMLHTTPRequest() standard.

La bibliothèque javascript se trouve ici:

https://github.com/AirConsole/xmlhttprequest-length-computable

2

Essayez de changer l'encodage de votre serveur en gzip.

L'en-tête de votre demande affiche trois encodages potentiels (gzip, deflate, sdch), afin que le serveur puisse choisir l'un de ces trois. Par l'en-tête de réponse, nous pouvons voir que votre serveur choisit de répondre avec dégonflage.

Gzip est un format de codage qui inclut une charge utile de dégonflage en plus d'en-têtes et de pied de page supplémentaires (qui inclut la longueur non compressée d'origine) et un algorithme de somme de contrôle différent:

Gzip sur Wikipedia

Dégonfler a quelques problèmes. En raison de problèmes hérités liés à des algorithmes de décodage inappropriés, les implémentations clientes de dégonflage doivent passer par des vérifications stupides juste pour déterminer quelle implémentation elles traitent, et malheureusement, elles se trompent souvent:

Pourquoi utiliser deflate au lieu de gzip pour les fichiers texte servis par Apache?

Dans le cas de votre question, le navigateur voit probablement un fichier de dégonflage descendre dans le tuyau et jette juste ses bras et dit: "Quand je ne sais même pas exactement comment je vais finir par décoder cette chose, comment pouvez-vous vous attendre moi de m'inquiéter de faire les bons progrès, humain? "

Si vous changez la configuration de votre serveur pour que la réponse soit compressée (c'est-à-dire que gzip s'affiche comme encodage de contenu), j'espère que votre script fonctionnera comme vous l'auriez espéré/prévu.

2
AdamJonR

La seule solution à laquelle je peux penser est de compresser manuellement les données (plutôt que de les laisser au serveur et au navigateur), car cela vous permet d'utiliser la barre de progression normale et devrait toujours vous apporter des gains considérables par rapport à la version non compressée. Si, par exemple, seul le système doit fonctionner dans les navigateurs Web de dernière génération, vous pouvez par exemple le compresser côté serveur (quelle que soit la langue que vous utilisez, je suis sûr qu'il existe une fonction ou une bibliothèque Zip) et côté client, vous pouvez utiliser Zip.js . Si un support supplémentaire du navigateur est requis, vous pouvez vérifier this SO answer pour un certain nombre de fonctions de compression et de décompression (choisissez simplement celle qui est prise en charge dans la langue côté serveur que vous ' Dans l'ensemble, cela devrait être raisonnablement simple à mettre en œuvre, bien qu'il fonctionnera moins bien (mais toujours probablement bien) que la compression/décompression native. (Btw, après y avoir réfléchi un peu plus, il pourrait en théorie fonctionner encore mieux que le natif version au cas où vous choisiriez un algorithme de compression qui correspond au type de données que vous utilisez et les données sont suffisamment volumineuses)

Une autre option serait d'utiliser une websocket et de charger les données dans des parties où vous analysez/manipulez chaque partie en même temps qu'elle est chargée (vous n'avez pas besoin de websockets pour cela, mais faire des dizaines de requêtes http après l'autre peut être assez compliqué) ). Que cela soit possible dépend du scénario spécifique, mais pour moi, il semble que les données de rapport soient le type de données qui peuvent être chargées en plusieurs parties et ne doivent pas être téléchargées au préalable en entier.

0
David Mulder