Jak mogę obiecać natywną XHR?

183

Chcę używać (natywnych) obietnic w mojej aplikacji frontendowej do wykonywania żądania XHR, ale bez całej głupoty ogromnego frameworka.

Chcę wrócić do mojego XHR obietnicę, ale to nie działa (co daje mi: 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
źródło
2
Zobacz także ogólne odniesienie Jak przekonwertować istniejące API zwrotne na obietnice?
Bergi

Odpowiedzi:

369

Zakładam, że wiesz, jak złożyć natywną prośbę XHR (możesz odświeżyć tutaj i tutaj )

Ponieważ każda przeglądarka, która obsługuje natywne obietnice, będzie również obsługiwać xhr.onload, możemy pominąć wszelkie onReadyStateChangewygłupy. Cofnijmy się i zacznijmy od podstawowej funkcji żądania XHR za pomocą wywołań zwrotnych:

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! Nie wiąże się to z niczym strasznie skomplikowanym (jak niestandardowe nagłówki lub dane POST), ale wystarczy, abyśmy posunęli się naprzód.

Konstruktor obietnicy

Możemy zbudować taką obietnicę:

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

Konstruktor obietnicy przyjmuje funkcję, która otrzyma dwa argumenty (nazwijmy je resolvei reject). Można je traktować jako oddzwanianie, jeden dla sukcesu, a drugi dla niepowodzenia. Przykłady są niesamowite, zaktualizujmy za makeRequestpomocą tego konstruktora:

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

Teraz możemy skorzystać z mocy obietnic, łącząc wiele połączeń XHR (i .catchspowoduje to błąd przy każdym połączeniu):

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

Możemy to jeszcze poprawić, dodając zarówno parametry POST / PUT, jak i niestandardowe nagłówki. Użyjmy obiektu opcji zamiast wielu argumentów z podpisem:

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

makeRequest teraz wygląda mniej więcej tak:

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

Bardziej kompleksowe podejście można znaleźć w MDN .

Alternatywnie możesz użyć interfejsu API pobierania ( polyfill ).

SomeKittens
źródło
3
Możesz także chcieć dodać opcje responseType, uwierzytelnianie, poświadczenia, timeout… A paramsobiekty powinny obsługiwać obiekty BLOB / bufory i FormDatainstancje
Bergi
4
Czy lepiej byłoby zwrócić nowy Błąd przy odrzuceniu?
prasanthv
1
Ponadto nie ma sensu zwracać xhr.statusi xhr.statusTextpo błędzie, ponieważ w tym przypadku są puste.
dqd
2
Ten kod wydaje się działać zgodnie z reklamą, z wyjątkiem jednej rzeczy. Spodziewałem się, że właściwym sposobem przekazywania params do żądania GET jest xhr.send (params). Jednak żądania GET ignorują wszelkie wartości wysyłane do metody send (). Zamiast tego muszą to być parametry ciągu zapytania w samym adresie URL. Tak więc, dla powyższej metody, jeśli chcesz zastosować argument „params” do żądania GET, należy zmodyfikować procedurę, aby rozpoznać GET vs. POST, a następnie warunkowo dołączyć te wartości do adresu URL przekazanego xhr .otwarty().
hairbo
1
Należy używać resolve(xhr.response | xhr.responseText);W większości przeglądarek repsonse jest w responseText w międzyczasie.
heinob
50

Może to być tak proste, jak poniższy kod.

Należy pamiętać, że ten kod będzie rejectwyzwalał wywołanie zwrotne tylko wtedy, gdy onerrorzostanie wywołany ( tylko błędy sieciowe ), a nie wtedy, gdy kod stanu HTTP oznacza błąd. Wyklucza to również wszystkie inne wyjątki. Zajmowanie się nimi powinno zależeć od ciebie, IMO.

Ponadto zaleca się wywoływanie rejectwywołania zwrotnego z wystąpieniem Errorzdarzenia, a nie z samym zdarzeniem, ale dla uproszczenia pozostawiłem to, co jest.

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

Przywoływanie może być następujące:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });
Peleg
źródło
14
@MadaraUchiha Chyba jest to wersja tl; dr. To daje PO odpowiedź na ich pytanie i tylko to.
Peleg
gdzie jest treść żądania POST?
kaub
1
@crl jak w zwykłym XHR:xhr.send(requestBody)
Peleg
tak, ale dlaczego nie pozwoliłeś na to w swoim kodzie? (ponieważ sparametryzowałeś metodę)
kauc
6
Podoba mi się ta odpowiedź, ponieważ zapewnia ona bardzo prosty kod do natychmiastowej pracy z odpowiedzią na pytanie.
Steve Chamaillard
12

Dla każdego, kto szuka tego teraz, możesz użyć funkcji pobierania . Ma całkiem dobre wsparcie .

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

Najpierw użyłem odpowiedzi @ SomeKittens, ale potem odkryłem, fetchże robi to dla mnie po wyjęciu z pudełka :)

microo8
źródło
2
Starsze przeglądarki nie obsługują tej fetchfunkcji, ale GitHub opublikował polifill .
bdesham
1
Nie polecam, fetchponieważ nie obsługuje jeszcze anulowania.
James Dunne,
2
Specyfikacja interfejsu API pobierania umożliwia teraz anulowanie. Wsparcie zostało do tej pory dostarczone w przeglądarce Firefox 57 bugzilla.mozilla.org/show_bug.cgi?id=1378342 i Edge 16. Dema: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort -api . I są otwarte błędy funkcji Chrome & Webkit bugs.chromium.org/p/chromium/issues/detail?id=750599 i bugs.webkit.org/show_bug.cgi?id=174980 . Instrukcje: developers.google.com/web/updates/2017/09/abortable-fetch & developer.mozilla.org/en-US/docs/Web/API/AbortSignal#Examples
sideshowbarker
Odpowiedź na stackoverflow.com/questions/31061838/... posiada anulować-fetch przykład kodu, który do tej pory już działa w Firefoksie 57+ i 16+ Krawędzi
sideshowbarker
1
@ microo8 Byłoby miło mieć prosty przykład z wykorzystaniem funkcji pobierania, a tutaj wydaje się, że jest to dobre miejsce do umieszczenia go.
jpaugh
8

Myślę, że możemy uczynić najwyższą odpowiedź bardziej elastyczną i wielokrotnego użytku, nie tworząc XMLHttpRequestobiektu. Jedyną korzyścią z tego jest to, że nie musimy sami pisać 2 lub 3 wierszy kodu, a ma to ogromną wadę polegającą na tym, że zabieramy dostęp do wielu funkcji API, takich jak ustawianie nagłówków. Ukrywa także właściwości oryginalnego obiektu przed kodem, który ma obsłużyć odpowiedź (zarówno w przypadku sukcesów, jak i błędów). Możemy więc uczynić bardziej elastyczną, szerzej stosowaną funkcję, akceptując XMLHttpRequestobiekt jako dane wejściowe i przekazując go jako wynik .

Ta funkcja przekształca dowolny XMLHttpRequestobiekt w obietnicę, domyślnie traktując kody stanu inne niż 200 jako błąd:

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

Ta funkcja bardzo naturalnie wpasowuje się w łańcuch Promises, bez poświęcania elastyczności XMLHttpRequestinterfejsu API:

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

catchzostał pominięty powyżej, aby uprościć przykładowy kod. Zawsze powinieneś go mieć i oczywiście możemy:

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

A wyłączenie obsługi kodu statusu HTTP nie wymaga dużych zmian w kodzie:

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

Nasz kod wywołujący jest dłuższy, ale koncepcyjnie nadal łatwo jest zrozumieć, co się dzieje. I nie musimy odbudowywać całego interfejsu API żądania internetowego, aby obsługiwać jego funkcje.

Możemy dodać kilka wygodnych funkcji, aby uporządkować nasz kod, a także:

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

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

Następnie nasz kod staje się:

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

Moim zdaniem odpowiedź jpmc26 jest prawie idealna. Ma jednak pewne wady:

  1. Odsłania żądanie xhr tylko do ostatniej chwili. To nie pozwala POST-requests na ustawienie treści żądania.
  2. Trudniej jest go odczytać, ponieważ kluczowa sendfunkcja jest ukryta w funkcji.
  3. Wprowadza sporo bojlera podczas faktycznego żądania.

Małpa łatająca obiekt xhr rozwiązuje następujące problemy:

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

Teraz użycie jest tak proste, jak:

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

Oczywiście wprowadza to inną wadę: łatanie małp szkodzi wydajności. Nie powinno to jednak stanowić problemu przy założeniu, że użytkownik czeka głównie na wynik xhr, że samo żądanie zajmuje rząd wielkości dłużej niż ustawienie połączenia i żądania xhr nie są często wysyłane.

PS: I oczywiście, jeśli celujesz w nowoczesne przeglądarki, użyj funkcji pobierania!

PPS: W komentarzach zaznaczono, że ta metoda zmienia standardowe API, co może być mylące. Dla większej przejrzystości można wstawić inną metodę na obiekt xhr sendAndGetPromise().

t. zwierzę
źródło
Unikam łatania małp, bo to zaskakujące. Większość programistów oczekuje, że standardowe nazwy funkcji API wywołują standardową funkcję API. Ten kod nadal ukrywa rzeczywiste sendwywołanie, ale może też mylić czytelników, którzy wiedzą, że sendnie ma wartości zwracanej. Użycie bardziej wyraźnych wywołań wyjaśnia, że ​​została wywołana dodatkowa logika. Moja odpowiedź musi zostać dostosowana do obsługi argumentów send; jednak prawdopodobnie lepiej go fetchteraz użyć .
jpmc26
Myślę, że to zależy. Jeśli zwrócisz / ujawnisz żądanie xhr (które i tak wydaje się wątpliwe) masz absolutną rację. Nie rozumiem jednak, dlaczego nie można tego zrobić w module i ujawnić tylko wynikające z tego obietnice.
Zwierzę
Mam na myśli szczególnie tych, którzy muszą utrzymywać kod, w którym to robisz.
jpmc26
Tak jak powiedziałem: to zależy. Jeśli moduł jest tak duży, że funkcja promisify gubi się między resztą kodu, prawdopodobnie masz inne problemy. Jeśli masz moduł, w którym chcesz po prostu wywołać niektóre punkty końcowe i zwrócić obietnice, nie widzę problemu.
Zwierzę
Nie zgadzam się, że zależy to od wielkości bazy kodu. To mylące, gdy standardowa funkcja API robi coś innego niż standardowe zachowanie.
jpmc26