Come promuovo XHR nativo?


183

Voglio usare le promesse (native) nella mia app frontend per eseguire la richiesta XHR, ma senza tutta la confusione di un framework enorme.

Voglio che il mio XHR per restituire una promessa, ma questo non funziona (dandomi: 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);
});

Risposte:


369

Suppongo che tu sappia come effettuare una richiesta XHR nativa (puoi ripassare qui e qui )

Dal momento che qualsiasi browser che supporta le promesse native supporterà anche xhr.onload, possiamo saltare tutti i onReadyStateChangebuffonati. Facciamo un passo indietro e iniziamo con una funzione di richiesta XHR di base utilizzando i callback:

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

Evviva! Ciò non comporta nulla di terribilmente complicato (come intestazioni personalizzate o dati POST) ma è sufficiente per farci avanzare.

Il costruttore della promessa

Possiamo costruire una promessa in questo modo:

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

Il costruttore promessa assume una funzione a cui verranno passati due argomenti (chiamiamoli resolvee reject). Puoi pensarli come callback, uno per il successo e uno per il fallimento. Gli esempi sono fantastici, aggiorniamo makeRequestcon questo costruttore:

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

Ora possiamo attingere al potere delle promesse, concatenando più chiamate XHR (e .catchsi innescherà un errore su entrambe le chiamate):

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

Possiamo migliorarlo ulteriormente, aggiungendo sia parametri POST / PUT sia intestazioni personalizzate. Usiamo un oggetto opzioni invece di più argomenti, con la firma:

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

makeRequest ora sembra qualcosa del genere:

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

Un approccio più completo è disponibile presso MDN .

In alternativa, è possibile utilizzare l' API di recupero ( polyfill ).


3
Potresti anche voler aggiungere opzioni per responseType, autenticazione, credenziali, timeout... E gli paramsoggetti dovrebbero supportare BLOB / buffervview ed FormDataistanze
Bergi,

4
Sarebbe meglio restituire un nuovo errore al rifiuto?
prasanthv,

1
Inoltre, non ha senso tornare xhr.statuse xhr.statusTextin errore, poiché in questo caso sono vuoti.
dqd

2
Questo codice sembra funzionare come pubblicizzato, tranne per una cosa. Mi aspettavo che il modo giusto di passare i parametri a una richiesta GET fosse via xhr.send (params). Tuttavia, le richieste GET ignorano qualsiasi valore inviato al metodo send (). Invece, devono solo essere parametri della stringa di query sull'URL stesso. Quindi, per il metodo sopra, se si desidera applicare l'argomento "params" a una richiesta GET, la routine deve essere modificata per riconoscere un GET vs. POST e quindi aggiungere condizionalmente quei valori all'URL che viene passato a xhr .Aperto().
Hairbo,

1
Uno dovrebbe usare resolve(xhr.response | xhr.responseText);Nella maggior parte dei browser nel frattempo la risposta è in responseText.
heinob

50

Questo potrebbe essere semplice come il seguente codice.

Tieni presente che questo codice genererà il rejectcallback solo quando onerrorviene chiamato ( solo errori di rete ) e non quando il codice di stato HTTP indica un errore. Ciò escluderà anche tutte le altre eccezioni. Gestirli dovrebbe essere compito tuo, IMO.

Inoltre, si consiglia di chiamare il rejectcallback con un'istanza Errore non l'evento stesso, ma per semplicità, ho lasciato così com'è.

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

E invocare potrebbe essere questo:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });

14
@MadaraUchiha Immagino sia la versione tl; dr. Fornisce all'OP una risposta alla loro domanda e solo a quella.
Peleg,

dove va il corpo di una richiesta POST?
caub

1
@crl proprio come in un normale XHR:xhr.send(requestBody)
Peleg

si ma perché non l'hai permesso nel tuo codice? (dal momento che hai parametrizzato il metodo)
caub

6
Mi piace questa risposta in quanto fornisce un codice molto semplice con cui lavorare immediatamente che risponde alla domanda.
Steve Chamaillard,

12

Per chiunque lo cerchi ora, è possibile utilizzare la funzione di recupero . Ha un buon supporto .

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

In primo luogo ho usato la risposta di @ SomeKittens, ma poi ho scoperto fetchche lo fa per me fuori dalla scatola :)


2
I browser meno recenti non supportano la fetchfunzione, ma GitHub ha pubblicato un polyfill .
bdesham,

1
Non lo consiglierei fetchperché non supporta ancora la cancellazione.
James Dunne,


La risposta su stackoverflow.com/questions/31061838/… ha un esempio di codice cancellable-fetch che finora funziona già in Firefox 57+ e Edge 16+
sideshowbarker

1
@ microo8 Sarebbe bello avere un semplice esempio usando fetch, e qui sembra un buon posto per dirlo.
jpaugh

8

Penso che possiamo rendere la risposta migliore molto più flessibile e riutilizzabile non facendola creare l' XMLHttpRequestoggetto. L'unico vantaggio di farlo è che non dobbiamo scrivere noi stessi 2 o 3 righe di codice per farlo, e ha l'enorme svantaggio di togliere il nostro accesso a molte funzionalità dell'API, come l'impostazione delle intestazioni. Nasconde anche le proprietà dell'oggetto originale dal codice che dovrebbe gestire la risposta (sia per successi che per errori). Quindi possiamo rendere una funzione più flessibile e maggiormente applicabile semplicemente accettando l' XMLHttpRequestoggetto come input e passandolo come risultato .

Questa funzione converte un XMLHttpRequestoggetto arbitrario in una promessa, trattando i codici di stato non 200 come un errore per impostazione predefinita:

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

Questa funzione si adatta in modo molto naturale a una catena di Promises, senza sacrificare la flessibilità XMLHttpRequestdell'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);
});

catchè stato omesso sopra per semplificare il codice di esempio. Dovresti sempre averne uno e ovviamente possiamo:

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

E disabilitare la gestione del codice di stato HTTP non richiede molte modifiche nel codice:

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

Il nostro codice chiamante è più lungo, ma concettualmente è ancora semplice capire cosa sta succedendo. E non dobbiamo ricostruire l'intera API di richiesta Web solo per supportare le sue funzionalità.

Possiamo aggiungere alcune funzioni utili per riordinare anche il nostro codice:

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

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

Quindi il nostro codice diventa:

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

5

La risposta di jpmc26 è abbastanza vicina alla perfezione secondo me. Ha alcuni inconvenienti, tuttavia:

  1. Espone la richiesta xhr solo fino all'ultimo momento. Ciò non consente a POST-quests di impostare il corpo della richiesta.
  2. È più difficile da leggere poiché la sendchiamata cruciale è nascosta all'interno di una funzione.
  3. Introduce un bel po 'di boilerplate quando si effettua effettivamente la richiesta.

La scimmia che rattoppa l'oggetto xhr affronta questi problemi:

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

Ora l'utilizzo è semplice come:

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

Naturalmente, questo introduce un diverso svantaggio: l'applicazione di patch sulle scimmie compromette le prestazioni. Tuttavia, ciò non dovrebbe costituire un problema presupponendo che l'utente stia aspettando principalmente il risultato di xhr, che la richiesta stessa richieda ordini di grandezza più lunghi rispetto all'impostazione della chiamata e che le richieste xhr non vengano inviate frequentemente.

PS: E ovviamente se scegli come target i browser moderni, usa fetch!

PPS: nei commenti è stato sottolineato che questo metodo modifica l'API standard che può creare confusione. Per maggiore chiarezza si potrebbe applicare un metodo diverso all'oggetto xhr sendAndGetPromise().


Evito di rattoppare le scimmie perché è sorprendente. La maggior parte degli sviluppatori si aspetta che i nomi delle funzioni API standard invochino la funzione API standard. Questo codice nasconde ancora la sendchiamata effettiva ma può anche confondere i lettori che sanno che sendnon ha valore di ritorno. L'uso di chiamate più esplicite chiarisce che è stata invocata la logica aggiuntiva. La mia risposta deve essere adattata per gestire gli argomenti send; tuttavia, è probabilmente meglio usarlo fetchora.
jpmc26,

Immagino che dipenda. Se restituisci / esponi la richiesta xhr (che sembra comunque dubbia) hai assolutamente ragione. Tuttavia non vedo perché non si dovrebbe fare questo all'interno di un modulo ed esporre solo le promesse risultanti.
t

Mi riferisco in particolare a chiunque debba mantenere il codice in cui lo fai.
jpmc26

Come ho detto: dipende. Se il tuo modulo è così grande che la funzione promisify si perde tra il resto del codice, probabilmente hai altri problemi. Se hai un modulo in cui vuoi solo chiamare alcuni endpoint e restituire promesse, non vedo alcun problema.
t

Non sono d'accordo sul fatto che dipende dalle dimensioni della tua base di codice. È confuso vedere una funzione API standard fare qualcosa di diverso dal suo comportamento standard.
jpmc26,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.