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:
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);
});
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:
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.
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);
};
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:
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
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:
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.
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.