web-dev-qa-db-fra.com

Comment puis-je promisify XHR natif?

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);
});
165
SomeKittens

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.

Le constructeur de promesse

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 ).

323
SomeKittens

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
    });
47
Peleg

Pour tous ceux qui recherchent ceci maintenant, vous pouvez utiliser la fonction fetch . Il en a de très bons support .

J'ai d'abord utilisé la réponse de @ SomeKittens, mais j'ai ensuite découvert fetch qui le fait pour moi tout de suite :)

9
microo8

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

la réponse de jpmc26 est assez proche de la perfection à mon avis. Il a cependant quelques inconvénients:

  1. Il expose la requête xhr seulement jusqu'au dernier moment. Cela n'autorise pas les demandes POST- à définir le corps de la demande.
  2. Il est plus difficile à lire car l'appel crucial send- est caché à l'intérieur d'une fonction.
  3. Il introduit un peu de passe-partout lors de la demande.

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().

5
t.animal