Wie verspreche ich native XHR?

183

Ich möchte (native) Versprechen in meiner Frontend-App verwenden, um XHR-Anfragen auszuführen, aber ohne die Dummheit eines massiven Frameworks.

Ich mag , dass mein xhr ein Versprechen zurückzukehren , aber das funktioniert nicht (was mir: 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);
});
SomeKittens
quelle
2
Siehe auch die allgemeine Referenz. Wie konvertiere ich eine vorhandene Rückruf-API in Versprechen?
Bergi

Antworten:

368

Ich gehe davon aus, dass Sie wissen, wie man eine native XHR-Anfrage stellt (Sie können hier und hier auffrischen ).

Da jeder Browser, der native Versprechen unterstützt xhr.onload, dies auch unterstützt , können wir alle onReadyStateChangeDummheiten überspringen . Machen wir einen Schritt zurück und beginnen mit einer grundlegenden XHR-Anforderungsfunktion unter Verwendung von Rückrufen:

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

Hurra! Dies beinhaltet nichts schrecklich Kompliziertes (wie benutzerdefinierte Header oder POST-Daten), reicht aber aus, um uns vorwärts zu bringen.

Der Versprechenskonstrukteur

Wir können ein Versprechen wie folgt konstruieren:

new Promise(function (resolve, reject) {
  // Do some Async stuff
  // call resolve if it succeeded
  // reject if it failed
});

Der Versprechen-Konstruktor übernimmt eine Funktion, der zwei Argumente übergeben werden (nennen wir sie resolveund reject). Sie können sich diese als Rückrufe vorstellen, einen für den Erfolg und einen für den Misserfolg. Beispiele sind fantastisch, lassen Sie uns makeRequestmit diesem Konstruktor aktualisieren :

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

Jetzt können wir die Kraft von Versprechungen nutzen und mehrere XHR-Aufrufe verketten (und .catchbei beiden Anrufen wird ein Fehler ausgelöst):

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

Wir können dies noch weiter verbessern, indem wir sowohl POST / PUT-Parameter als auch benutzerdefinierte Header hinzufügen. Verwenden wir ein Optionsobjekt anstelle mehrerer Argumente mit der Signatur:

{
  method: String,
  url: String,
  params: String | Object,
  headers: Object
}

makeRequest sieht jetzt ungefähr so ​​aus:

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

Einen umfassenderen Ansatz finden Sie bei MDN .

Alternativ können Sie die Abruf-API ( Polyfill ) verwenden.

SomeKittens
quelle
3
Möglicherweise möchten Sie auch Optionen für responseType, Authentifizierung, Anmeldeinformationen usw. hinzufügen. timeoutUnd paramsObjekte sollten Blobs / Pufferansichten und FormDataInstanzen unterstützen
Bergi
4
Wäre es besser, bei Ablehnung einen neuen Fehler zurückzugeben?
Prasanthv
1
Darüber hinaus macht es keinen Sinn machen Rückkehr xhr.statusund xhr.statusTextauf Fehler, da sie in diesem Fall leer sind.
dqd
2
Dieser Code scheint wie angekündigt zu funktionieren, abgesehen von einer Sache. Ich habe erwartet, dass der richtige Weg, um Parameter an eine GET-Anfrage weiterzugeben, über xhr.send (params) ist. GET-Anforderungen ignorieren jedoch alle an die send () -Methode gesendeten Werte. Stattdessen müssen sie nur Abfragezeichenfolgenparameter für die URL selbst sein. Wenn für die obige Methode das Argument "params" auf eine GET-Anforderung angewendet werden soll, muss die Routine geändert werden, um ein GET vs. POST zu erkennen, und diese Werte dann bedingt an die URL anhängen, die an xhr übergeben wird .öffnen().
Hairbo
1
Man sollte verwenden resolve(xhr.response | xhr.responseText);In den meisten Browsern ist die Antwort in der Zwischenzeit in responseText.
Heinob
50

Dies kann so einfach sein wie der folgende Code.

Beachten Sie, dass dieser Code den rejectRückruf nur auslöst, wenn er onerroraufgerufen wird (nur Netzwerkfehler ) und nicht, wenn der HTTP-Statuscode einen Fehler anzeigt. Dies schließt auch alle anderen Ausnahmen aus. Der Umgang mit diesen sollte bei Ihnen liegen, IMO.

Außerdem wird empfohlen, den rejectRückruf mit einer Instanz von Errorund nicht mit dem Ereignis selbst aufzurufen , aber der Einfachheit halber habe ich es so belassen, wie es ist.

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

Und es aufzurufen könnte sein:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });
Peleg
quelle
14
@ MadaraUchiha Ich denke, es ist die tl; dr-Version davon. Es gibt dem OP eine Antwort auf ihre Frage und nur das.
Peleg
Wohin geht der Text einer POST-Anfrage?
Caub
1
@crl genau wie in einem regulären XHR:xhr.send(requestBody)
Peleg
Ja, aber warum haben Sie das in Ihrem Code nicht zugelassen? (da Sie die Methode parametrisiert haben)
Caub
6
Ich mag diese Antwort, da sie einen sehr einfachen Code bietet, mit dem man sofort arbeiten kann und der die Frage beantwortet.
Steve Chamaillard
11

Für alle, die jetzt danach suchen, können Sie die Abruffunktion verwenden. Es hat einige ziemlich gute Unterstützung .

Ich habe zuerst die Antwort von @ SomeKittens verwendet, dann aber festgestellt, fetchdass dies für mich sofort erledigt ist :)

microo8
quelle
2
Ältere Browser unterstützen die fetchFunktion nicht, aber GitHub hat eine Polyfüllung veröffentlicht .
Bdesham
1
Ich würde es nicht empfehlen, fetchda es die Stornierung noch nicht unterstützt.
James Dunne
Die Antwort unter stackoverflow.com/questions/31061838/… enthält ein Codebeispiel zum Abbrechen von Abrufen, das bisher bereits in Firefox 57+ und Edge 16+ funktioniert
Sideshowbarker
Das Abbrechen des Abrufs bereitet mir in Chrome 69 immer noch Probleme. Die Verwendung von XMLHttpRequest scheint einfach viel einfacher zu sein.
Matt Montag
8

Ich denke, wir können die Top-Antwort viel flexibler und wiederverwendbarer machen, indem wir das XMLHttpRequestObjekt nicht erstellen lassen . Der einzige Vorteil dabei ist, dass wir nicht zwei oder drei Codezeilen selbst schreiben müssen, um dies zu tun, und es hat den enormen Nachteil, dass wir nicht mehr auf viele Funktionen der API zugreifen können, z. B. das Setzen von Headern. Außerdem werden die Eigenschaften des Originalobjekts vor dem Code ausgeblendet, der die Antwort verarbeiten soll (sowohl für Erfolge als auch für Fehler). So können wir eine flexiblere und umfassendere Funktion erstellen, indem wir das XMLHttpRequestObjekt einfach als Eingabe akzeptieren und als Ergebnis übergeben .

Diese Funktion konvertiert ein beliebiges XMLHttpRequestObjekt in ein Versprechen und behandelt Nicht-200-Statuscodes standardmäßig als Fehler:

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

Diese Funktion passt ganz natürlich in eine Kette von Promise s, ohne die Flexibilität der XMLHttpRequestAPI zu beeinträchtigen:

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

catchwurde oben weggelassen, um den Beispielcode einfacher zu halten. Sie sollten immer eine haben, und natürlich können wir:

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

Das Deaktivieren der Verarbeitung des HTTP-Statuscodes erfordert keine großen Änderungen am 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);
});

Unser Anrufcode ist länger, aber konzeptionell ist es immer noch einfach zu verstehen, was los ist. Und wir müssen nicht die gesamte Webanforderungs-API neu erstellen, nur um ihre Funktionen zu unterstützen.

Wir können auch einige praktische Funktionen hinzufügen, um unseren Code aufzuräumen:

function makeSimpleGet(url) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    return xhr;
}

function promiseResponseAnyCode(xhr) {
    return promiseResponse(xhr, false);
}

Dann wird unser Code:

Promise.resolve(makeSimpleGet('https://stackoverflow.com/doesnotexist'))
.then(promiseResponseAnyCode)
.then(function(request) {
    console.log('Done');
    console.log(request.status + ' ' + request.statusText);
});
jpmc26
quelle
5

Die Antwort von jpmc26 ist meiner Meinung nach nahezu perfekt. Es hat jedoch einige Nachteile:

  1. Die xhr-Anforderung wird nur bis zum letzten Moment angezeigt. Dies erlaubt POST-requests nicht, den Anforderungshauptteil festzulegen.
  2. Es ist schwieriger zu lesen, da der entscheidende sendAufruf in einer Funktion verborgen ist.
  3. Bei der eigentlichen Anfrage wird einiges an Boilerplate eingeführt.

Das Patchen des xhr-Objekts durch Affen behebt folgende Probleme:

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

Jetzt ist die Verwendung so einfach wie:

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

Dies bringt natürlich einen anderen Nachteil mit sich: Das Patchen von Affen beeinträchtigt die Leistung. Dies sollte jedoch kein Problem sein, vorausgesetzt, der Benutzer wartet hauptsächlich auf das Ergebnis von xhr, die Anforderung selbst dauert um Größenordnungen länger als das Einrichten des Anrufs, und xhr-Anforderungen werden nicht häufig gesendet.

PS: Und wenn Sie auf moderne Browser abzielen, verwenden Sie natürlich fetch!

PPS: In den Kommentaren wurde darauf hingewiesen, dass diese Methode die Standard-API ändert, was verwirrend sein kann. Zur besseren Übersicht könnte man eine andere Methode auf das xhr-Objekt patchen sendAndGetPromise().

t.animal
quelle
Ich vermeide das Patchen von Affen, weil es überraschend ist. Die meisten Entwickler erwarten, dass Standard-API-Funktionsnamen die Standard-API-Funktion aufrufen. Dieser Code verbirgt immer noch den eigentlichen sendAufruf, kann aber auch Leser verwirren, die wissen, dass er sendkeinen Rückgabewert hat. Durch die Verwendung expliziterer Aufrufe wird klarer, dass zusätzliche Logik aufgerufen wurde. Meine Antwort muss angepasst werden, um Argumente zu verarbeiten send; Es ist jedoch wahrscheinlich besser, es fetchjetzt zu verwenden .
jpmc26
Ich denke es kommt darauf an. Wenn Sie die xhr-Anfrage zurückgeben / offenlegen (was ohnehin zweifelhaft erscheint), haben Sie absolut Recht. Ich verstehe jedoch nicht, warum man dies nicht innerhalb eines Moduls tun und nur die daraus resultierenden Versprechen offenlegen würde.
t.animal
Ich beziehe mich speziell auf jemanden, der den Code pflegen muss, in dem Sie es tun.
jpmc26
Wie ich schon sagte: Es kommt darauf an. Wenn Ihr Modul so groß ist, dass die Promisify-Funktion zwischen dem Rest des Codes verloren geht, haben Sie wahrscheinlich andere Probleme. Wenn Sie ein Modul haben, in dem Sie nur einige Endpunkte aufrufen und Versprechen zurückgeben möchten, sehe ich kein Problem.
t.animal
Ich bin nicht der Meinung, dass dies von der Größe Ihrer Codebasis abhängt. Es ist verwirrend zu sehen, dass eine Standard-API-Funktion etwas anderes als ihr Standardverhalten ausführt.
jpmc26