Je veux utiliser les promesses (natives) dans mon application frontale pour exécuter la requête XHR mais sans toute la tromperie d'un framework massif.
Je veux que mon xhr retourne une promesse mais cela ne marche pas (me donnant: Uncaught TypeError: Promise resolver undefined is not a function
)
function makeXHRRequest (method, url, done) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function() { return new Promise().resolve(); };
xhr.onerror = function() { return new Promise().reject(); };
xhr.send();
}
makeXHRRequest('GET', 'http://example.com')
.then(function (datums) {
console.log(datums);
});
Je suppose que vous savez comment faire une demande native XHR (vous pouvez rafraîchir ici et ici )
Étant donné que tout navigateur prenant en charge les promesses natives prend également en charge xhr.onload
, nous pouvons ignorer tous les onReadyStateChange
tomfoolery. Faisons un pas en arrière et commençons avec une fonction de requête XHR de base utilisant des rappels:
function makeRequest (method, url, done) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
done(null, xhr.response);
};
xhr.onerror = function () {
done(xhr.response);
};
xhr.send();
}
// And we'd call it as such:
makeRequest('GET', 'http://example.com', function (err, datums) {
if (err) { throw err; }
console.log(datums);
});
Hourra! Cela n'implique rien de terriblement compliqué (comme des en-têtes personnalisés ou des données POST), mais cela suffit pour nous faire avancer.
Nous pouvons construire une promesse comme ceci:
new Promise(function (resolve, reject) {
// Do some Async stuff
// call resolve if it succeeded
// reject if it failed
});
Le constructeur de promesse prend une fonction à laquelle seront transmis deux arguments (appelons-les resolve
et reject
). Vous pouvez les considérer comme des rappels, un pour le succès et un pour l’échec. Les exemples sont géniaux, mettons à jour makeRequest
avec ce constructeur:
function makeRequest (method, url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
if (this.status >= 200 && this.status < 300) {
resolve(xhr.response);
} else {
reject({
status: this.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: this.status,
statusText: xhr.statusText
});
};
xhr.send();
});
}
// Example:
makeRequest('GET', 'http://example.com')
.then(function (datums) {
console.log(datums);
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});
Nous pouvons maintenant exploiter le pouvoir des promesses en enchaînant plusieurs appels XHR (et le .catch
se déclenchera en cas d'erreur):
makeRequest('GET', 'http://example.com')
.then(function (datums) {
return makeRequest('GET', datums.url);
})
.then(function (moreDatums) {
console.log(moreDatums);
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});
Nous pouvons encore améliorer cela en ajoutant à la fois des paramètres POST/PUT et des en-têtes personnalisés. Utilisons un objet options au lieu de plusieurs arguments, avec la signature:
{
method: String,
url: String,
params: String | Object,
headers: Object
}
makeRequest
ressemble maintenant à ceci:
function makeRequest (opts) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(opts.method, opts.url);
xhr.onload = function () {
if (this.status >= 200 && this.status < 300) {
resolve(xhr.response);
} else {
reject({
status: this.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: this.status,
statusText: xhr.statusText
});
};
if (opts.headers) {
Object.keys(opts.headers).forEach(function (key) {
xhr.setRequestHeader(key, opts.headers[key]);
});
}
var params = opts.params;
// We'll need to stringify if we've been given an object
// If we have a string, this is skipped.
if (params && typeof params === 'object') {
params = Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
}
xhr.send(params);
});
}
// Headers and params are optional
makeRequest({
method: 'GET',
url: 'http://example.com'
})
.then(function (datums) {
return makeRequest({
method: 'POST',
url: datums.url,
params: {
score: 9001
},
headers: {
'X-Subliminal-Message': 'Upvote-this-answer'
}
});
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});
Une approche plus complète peut être trouvée à MDN .
Alternativement, vous pouvez utiliser le fetch API ( polyfill ).
Cela pourrait être aussi simple que le code suivant.
Gardez à l'esprit que ce code ne déclenchera le rappel reject
que lorsque onerror
sera appelé (résea erreurs uniquement) et non lorsque le code d'état HTTP signifiera une erreur. Cela exclura également toutes les autres exceptions. Le traitement de ces problèmes devrait être à vous, IMO.
De plus, il est recommandé d'appeler le callback reject
avec une instance de Error
et pas l'événement lui-même, mais par souci de simplicité, je suis parti tel quel.
function request(method, url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = resolve;
xhr.onerror = reject;
xhr.send();
});
}
Et invoquer cela pourrait être ceci:
request('GET', 'http://google.com')
.then(function (e) {
console.log(e.target.response);
}, function (e) {
// handle errors
});
Je pense que nous pouvons faire la réponse principale beaucoup plus souple et réutilisable en ne le faisant pas créer l'objet XMLHttpRequest
. Le seul avantage est que nous n’avons pas à écrire nous-mêmes deux ou trois lignes de code, ce qui présente l’énorme inconvénient de nous priver de l’accès à de nombreuses fonctionnalités de l’API, telles que la définition d’en-têtes. Il masque également les propriétés de l'objet d'origine au code censé gérer la réponse (pour les réussites et les erreurs). Ainsi, nous pouvons créer une fonction plus souple et plus largement applicable en acceptant simplement l’objet XMLHttpRequest
comme entrée et en le passant comme résultat .
Cette fonction convertit un objet XMLHttpRequest
arbitraire en une promesse, en traitant les codes d'état non 200 comme une erreur par défaut:
function promiseResponse(xhr, failNon2xx = true) {
return new Promise(function (resolve, reject) {
// Note that when we call reject, we pass an object
// with the request as a property. This makes it easy for
// catch blocks to distinguish errors arising here
// from errors arising elsewhere. Suggestions on a
// cleaner way to allow that are welcome.
xhr.onload = function () {
if (failNon2xx && (xhr.status < 200 || xhr.status >= 300)) {
reject({request: xhr});
} else {
resolve(xhr);
}
};
xhr.onerror = function () {
reject({request: xhr});
};
xhr.send();
});
}
Cette fonction s’intègre très naturellement dans une chaîne de Promise
s, sans sacrifier la flexibilité de l’API XMLHttpRequest
:
Promise.resolve()
.then(function() {
// We make this a separate function to avoid
// polluting the calling scope.
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://stackoverflow.com/');
return xhr;
})
.then(promiseResponse)
.then(function(request) {
console.log('Success');
console.log(request.status + ' ' + request.statusText);
});
catch
a été omis ci-dessus pour simplifier l’exemple de code. Vous devriez toujours en avoir un, et bien sûr nous pouvons:
Promise.resolve()
.then(function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://stackoverflow.com/doesnotexist');
return xhr;
})
.then(promiseResponse)
.catch(function(err) {
console.log('Error');
if (err.hasOwnProperty('request')) {
console.error(err.request.status + ' ' + err.request.statusText);
}
else {
console.error(err);
}
});
Et désactiver la gestion du code d'état HTTP ne nécessite pas beaucoup de changement dans le code:
Promise.resolve()
.then(function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://stackoverflow.com/doesnotexist');
return xhr;
})
.then(function(xhr) { return promiseResponse(xhr, false); })
.then(function(request) {
console.log('Done');
console.log(request.status + ' ' + request.statusText);
});
Notre code d'appel est plus long, mais conceptuellement, il est toujours simple de comprendre ce qui se passe. Et nous n'avons pas besoin de reconstruire l'intégralité de l'API de requête Web uniquement pour prendre en charge ses fonctionnalités.
Nous pouvons également ajouter quelques fonctions pratiques pour ranger notre code:
function makeSimpleGet(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
return xhr;
}
function promiseResponseAnyCode(xhr) {
return promiseResponse(xhr, false);
}
Alors notre code devient:
Promise.resolve(makeSimpleGet('https://stackoverflow.com/doesnotexist'))
.then(promiseResponseAnyCode)
.then(function(request) {
console.log('Done');
console.log(request.status + ' ' + request.statusText);
});
la réponse de jpmc26 est assez proche de la perfection à mon avis. Il a cependant quelques inconvénients:
POST
- à définir le corps de la demande.send
- est caché à l'intérieur d'une fonction.Monkey en corrigeant l'objet xhr s'attaque aux problèmes suivants:
function promisify(xhr, failNon2xx=true) {
const oldSend = xhr.send;
xhr.send = function() {
const xhrArguments = arguments;
return new Promise(function (resolve, reject) {
// Note that when we call reject, we pass an object
// with the request as a property. This makes it easy for
// catch blocks to distinguish errors arising here
// from errors arising elsewhere. Suggestions on a
// cleaner way to allow that are welcome.
xhr.onload = function () {
if (failNon2xx && (xhr.status < 200 || xhr.status >= 300)) {
reject({request: xhr});
} else {
resolve(xhr);
}
};
xhr.onerror = function () {
reject({request: xhr});
};
oldSend.apply(xhr, xhrArguments);
});
}
}
Maintenant, l'utilisation est aussi simple que:
let xhr = new XMLHttpRequest()
promisify(xhr);
xhr.open('POST', 'url')
xhr.setRequestHeader('Some-Header', 'Some-Value')
xhr.send(resource).
then(() => alert('All done.'),
() => alert('An error occured.'));
Bien entendu, cela présente un autre inconvénient: appliquer des correctifs aux singes nuit aux performances. Toutefois, cela ne devrait pas poser de problème si l’utilisateur attend principalement le résultat de la commande xhr, que la demande elle-même prend des ordres de grandeur plus longs que la configuration de l’appel et que les demandes xhr ne sont pas envoyées fréquemment.
PS: Et bien sûr, si vous ciblez les navigateurs modernes, utilisez fetch!
PPS: Il a été souligné dans les commentaires que cette méthode modifie l'API standard, ce qui peut prêter à confusion. Pour plus de clarté, vous pouvez appliquer une méthode différente à l'objet xhr sendAndGetPromise()
.