Comment puis-je promettre le XHR natif?

183

Je veux utiliser des promesses (natives) dans mon application frontend pour effectuer une requête XHR mais sans toute la folie d'un framework massif.

Je veux que mon XHR pour revenir une promesse , mais cela ne fonctionne pas (me donner: 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);
});
Certains chatons
la source
2
Voir aussi la référence générique Comment convertir une API de rappel existante en promesses?
Bergi

Réponses:

369

Je suppose que vous savez comment faire une requête XHR native (vous pouvez brosser ici et ici )

Étant donné que tout navigateur qui prend en charge les promesses natives le prendra également en charge xhr.onload, nous pouvons ignorer toute la onReadyStateChangefolie. Prenons du recul et commençons par 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 très compliqué (comme des en-têtes personnalisés ou des données POST) mais suffit pour nous faire avancer.

Le constructeur de promesses

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 qui recevra deux arguments (appelons-les resolveet 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 makeRequestavec 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);
});

Maintenant, nous pouvons puiser dans la puissance des promesses, enchaînant plusieurs appels XHR (et le .catchdéclenchera une erreur sur l'un ou l'autre appel):

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 à quelque chose comme 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 globale peut être trouvée chez MDN .

Vous pouvez également utiliser l' API fetch ( polyfill ).

Certains chatons
la source
3
Vous pouvez également ajouter des options pour responseType, l'authentification, les informations d'identification, timeout… Et les paramsobjets doivent prendre en charge les blobs / bufferviews et les FormDatainstances
Bergi
4
Serait-il préférable de renvoyer une nouvelle erreur en cas de rejet?
prasanthv
1
De plus, il n'a pas de sens de retourner xhr.statuset xhr.statusTexten cas d'erreur, car ils sont vides dans ce cas.
dqd
2
Ce code semble fonctionner comme annoncé, sauf pour une chose. Je m'attendais à ce que la bonne façon de transmettre des paramètres à une requête GET soit via xhr.send (params). Cependant, les requêtes GET ignorent toutes les valeurs envoyées à la méthode send (). Au lieu de cela, ils doivent simplement être des paramètres de chaîne de requête sur l'URL elle-même. Donc, pour la méthode ci-dessus, si vous voulez que l'argument "params" soit appliqué à une requête GET, la routine doit être modifiée pour reconnaître un GET vs POST, puis ajouter conditionnellement ces valeurs à l'URL transmise à xhr .ouvert().
hairbo
1
On devrait utiliser resolve(xhr.response | xhr.responseText);Dans la plupart des navigateurs, le repsonse est dans responseText entre-temps.
heinob
50

Cela peut être aussi simple que le code suivant.

Gardez à l'esprit que ce code déclenchera le rejectrappel uniquement lorsqu'il onerrorest appelé ( erreurs réseau uniquement) et non lorsque le code d'état HTTP signifie une erreur. Cela exclura également toutes les autres exceptions. La gestion de ceux-ci devrait être à vous, OMI.

De plus, il est recommandé d'appeler le rejectcallback avec une instance de Erroret non l'événement lui-même, mais par souci de simplicité, je l'ai laissé 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 l'invoquer pourrait être ceci:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });
Peleg
la source
14
@MadaraUchiha Je suppose que c'est la version tl; dr. Cela donne au PO une réponse à sa question et seulement cela.
Peleg
où va le corps d'une requête POST?
caub
1
@crl comme dans un XHR normal:xhr.send(requestBody)
Peleg
oui mais pourquoi ne l'avez-vous pas autorisé dans votre code? (puisque vous avez paramétré la méthode)
caub
6
J'aime cette réponse car elle fournit un code très simple avec lequel travailler immédiatement et qui répond à la question.
Steve Chamaillard
12

Pour tous ceux qui recherchent ceci maintenant, vous pouvez utiliser la fonction de récupération . Il a un assez bon support .

fetch('http://example.com/movies.json')
  .then(response => response.json())
  .then(data => console.log(data));

J'ai d'abord utilisé la réponse de @ SomeKittens, mais j'ai ensuite découvert fetchque cela le faisait pour moi hors de la boîte :)

microo8
la source
2
Les navigateurs plus anciens ne prennent pas en charge la fetchfonction, mais GitHub a publié un polyfill .
bdesham
1
Je ne le recommanderais pas fetchcar il ne prend pas encore en charge l'annulation.
James Dunne
2
La spécification de l'API Fetch prévoit désormais l'annulation. Le support a jusqu'à présent été fourni dans Firefox 57 bugzilla.mozilla.org/show_bug.cgi?id=1378342 et Edge 16. Démos: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort -api . Et il y a des bogues de fonctionnalité Chrome et Webkit ouverts bugs.chromium.org/p/chromium/issues/detail?id=750599 & bugs.webkit.org/show_bug.cgi?id=174980 . Mode d'
emploi
La réponse sur stackoverflow.com/questions/31061838/… a un exemple de code de récupération annulable qui jusqu'à présent fonctionne déjà dans Firefox 57+ et Edge 16+
Sideshowbarker
1
@ microo8 Ce serait bien d'avoir un exemple simple utilisant fetch, et ici semble liek un bon endroit pour le mettre.
jpaugh
8

Je pense que nous pouvons rendre la réponse supérieure beaucoup plus flexible et réutilisable en ne la faisant pas créer l' XMLHttpRequestobjet. Le seul avantage de le faire est que nous n'avons pas à écrire nous-mêmes 2 ou 3 lignes de code pour le faire, et cela a l'énorme inconvénient de nous priver de l'accès à de nombreuses fonctionnalités de l'API, comme la définition des en-têtes. Il masque également les propriétés de l'objet d'origine du code censé gérer la réponse (pour les réussites et les erreurs). Nous pouvons donc créer une fonction plus flexible et plus largement applicable en acceptant simplement l' XMLHttpRequestobjet comme entrée et en le transmettant comme résultat .

Cette fonction convertit un XMLHttpRequestobjet arbitraire en promesse, traitant les codes de statut 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' XMLHttpRequestAPI:

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

catcha é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 la désactivation de 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 toute 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);
});
jpmc26
la source
5

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

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

Monkey corrige l'objet xhr résout ces problèmes:

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 sûr, cela présente un inconvénient différent: la correction des singes nuit aux performances. Cependant, cela ne devrait pas poser de problème en supposant que l'utilisateur attend principalement le résultat du xhr, que la demande elle-même prend des ordres de grandeur plus longs que l'établissement 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é, on peut appliquer une méthode différente à l'objet xhr sendAndGetPromise().

t. animal
la source
J'évite le patching de singe car c'est surprenant. La plupart des développeurs s'attendent à ce que les noms de fonction API standard invoquent la fonction API standard. Ce code cache toujours l' sendappel réel, mais peut également dérouter les lecteurs qui savent que cela sendn'a pas de valeur de retour. L'utilisation d'appels plus explicites indique plus clairement qu'une logique supplémentaire a été invoquée. Ma réponse doit être ajustée pour gérer les arguments à send; cependant, il est probablement préférable de l'utiliser fetchmaintenant.
jpmc26
Je suppose que cela dépend. Si vous retournez / exposez la requête xhr (qui semble douteuse de toute façon), vous avez tout à fait raison. Cependant, je ne vois pas pourquoi on ne ferait pas cela dans un module et n'exposerait que les promesses qui en résultent.
t.animal
Je fais référence spécialement à quiconque doit maintenir le code dans
lequel
Comme je l'ai dit: cela dépend. Si votre module est si énorme que la fonction promisify se perd entre le reste du code, vous avez probablement d'autres problèmes. Si vous avez un module dans lequel vous souhaitez simplement appeler des points de terminaison et renvoyer des promesses, je ne vois pas de problème.
t.animal
Je ne suis pas d'accord que cela dépend de la taille de votre base de code. Il est déroutant de voir une fonction API standard faire autre chose que son comportement standard.
jpmc26