¿Cómo prometo XHR nativo?

183

Quiero usar promesas (nativas) en mi aplicación de interfaz para realizar solicitudes XHR pero sin toda la tontería de un marco masivo.

Quiero que mi xhr para volver una promesa, pero esto no funciona (me da: 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);
});
Algunos gatitos
fuente
2
Consulte también la referencia genérica ¿Cómo convierto una API de devolución de llamada existente en promesas?
Bergi

Respuestas:

369

Supongo que sabes cómo hacer una solicitud XHR nativa (puedes repasar aquí y aquí )

Dado que cualquier navegador que admita promesas nativas también admitirá xhr.onload, podemos omitir toda la onReadyStateChangetontería. Retrocedamos un paso y comencemos con una función de solicitud XHR básica usando devoluciones de llamada:

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! Esto no implica nada terriblemente complicado (como encabezados personalizados o datos POST), pero es suficiente para que avancemos.

El constructor de promesas

Podemos construir una promesa así:

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

El constructor de la promesa toma una función a la que se le pasarán dos argumentos (llamémoslos resolvey reject). Puede considerarlos como devoluciones de llamada, una para el éxito y otra para el fracaso. Los ejemplos son asombrosos, vamos a actualizar makeRequestcon este constructor:

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

Ahora podemos aprovechar el poder de las promesas, encadenando múltiples llamadas XHR (y .catchse activará un error en cualquiera de las llamadas):

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

Podemos mejorar esto aún más, agregando parámetros POST / PUT y encabezados personalizados. Usemos un objeto de opciones en lugar de múltiples argumentos, con la firma:

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

makeRequest ahora se parece a esto:

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

Se puede encontrar un enfoque más integral en MDN .

Alternativamente, podría usar la API de recuperación ( polyfill ).

Algunos gatitos
fuente
3
También es posible que desee agregar opciones para responseType, autenticación, credenciales, timeout... Y los paramsobjetos deben admitir blobs / bufferviews e FormDatainstancias
Bergi
44
¿Sería mejor devolver un nuevo error al rechazar?
prasanthv
1
Además, no tiene sentido regresar xhr.statusy xhr.statusTexten caso de error, ya que están vacíos en ese caso.
dqd
2
Este código parece funcionar como se anuncia, excepto por una cosa. Esperaba que la forma correcta de pasar los parámetros a una solicitud GET fuera a través de xhr.send (parámetros). Sin embargo, las solicitudes GET ignoran cualquier valor enviado al método send (). En cambio, solo necesitan ser parámetros de cadena de consulta en la URL misma. Entonces, para el método anterior, si desea que el argumento "params" se aplique a una solicitud GET, la rutina debe modificarse para reconocer un GET vs. POST, y luego agregar condicionalmente esos valores a la URL que se entrega a xhr .abierto().
hairbo
1
Uno debe usar resolve(xhr.response | xhr.responseText);En la mayoría de los navegadores, la respuesta es en respuestaTexto mientras tanto.
heinob
50

Esto podría ser tan simple como el siguiente código.

Tenga en cuenta que este código solo activará la rejectdevolución de llamada cuando onerrorse llame ( solo errores de red ) y no cuando el código de estado HTTP indique un error. Esto también excluirá todas las demás excepciones. Manejar esos debe ser tu decisión, OMI.

Además, se recomienda llamar a la rejectdevolución de llamada con una instancia de Errory no el evento en sí, pero por simplicidad, lo dejé como está.

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 invocarlo podría ser esto:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });
Peleg
fuente
14
@MadaraUchiha supongo que es la versión tl; dr de la misma. Le da al OP una respuesta a su pregunta y solo eso.
Peleg
¿A dónde va el cuerpo de una solicitud POST?
Caub
1
@crl como en un XHR normal:xhr.send(requestBody)
Peleg
sí, pero ¿por qué no permitiste eso en tu código? (desde que parametrizaste el método)
caub
66
Me gusta esta respuesta, ya que proporciona un código muy simple para trabajar de inmediato que responde a la pregunta.
Steve Chamaillard
12

Para cualquiera que busque esto ahora, puede usar la función de búsqueda . Tiene bastante buen soporte .

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

Primero utilicé la respuesta de @ SomeKittens, pero luego descubrí fetchque lo hace por mí de forma inmediata :)

microo8
fuente
2
Los navegadores más antiguos no admiten la fetchfunción, pero GitHub ha publicado un polyfill .
bdesham
1
No lo recomendaría fetchya que aún no admite la cancelación.
James Dunne
2
La especificación para la API Fetch ahora proporciona la cancelación. El soporte se ha enviado hasta ahora en Firefox 57 bugzilla.mozilla.org/show_bug.cgi?id=1378342 y Edge 16. Demostraciones: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort -api . Y hay errores de funciones de Chrome y Webkit abiertos bugs.chromium.org/p/chromium/issues/detail?id=750599 & bugs.webkit.org/show_bug.cgi?id=174980 . Cómo hacerlo: developers.google.com/web/updates/2017/09/abortable-fetch & developer.mozilla.org/en-US/docs/Web/API/AbortSignal#Examples
sideshowbarker
La respuesta en stackoverflow.com/questions/31061838/… tiene un ejemplo de código de recuperación cancelable que hasta ahora ya funciona en Firefox 57+ y Edge 16+
sideshowbarker
1
@ microo8 Sería bueno tener un ejemplo simple usando fetch, y aquí parece ser un buen lugar para ponerlo.
jpaugh
8

Creo que podemos hacer que la respuesta superior sea mucho más flexible y reutilizable al no hacer que cree el XMLHttpRequestobjeto. El único beneficio de hacerlo es que no tenemos que escribir 2 o 3 líneas de código para hacerlo, y tiene el enorme inconveniente de quitar nuestro acceso a muchas de las funciones de la API, como configurar encabezados. También oculta las propiedades del objeto original del código que se supone que maneja la respuesta (tanto para éxitos como para errores). Por lo tanto, podemos hacer una función más flexible y más aplicable simplemente aceptando el XMLHttpRequestobjeto como entrada y pasándolo como resultado .

Esta función convierte un XMLHttpRequestobjeto arbitrario en una promesa, tratando los códigos de estado que no son 200 como un error por defecto:

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

Esta función encaja muy naturalmente en una cadena de Promises, sin sacrificar la flexibilidad de la 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);
});

catchse omitió anteriormente para mantener el código de muestra más simple. Siempre debe tener uno y, por supuesto, podemos:

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

Y deshabilitar el manejo del código de estado HTTP no requiere muchos cambios en el código:

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

Nuestro código de llamada es más largo, pero conceptualmente, todavía es simple entender lo que está sucediendo. Y no tenemos que reconstruir toda la API de solicitud web solo para admitir sus características.

También podemos agregar algunas funciones convenientes para ordenar nuestro código:

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

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

Entonces nuestro código se convierte en:

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

La respuesta de jpmc26 es bastante perfecta en mi opinión. Sin embargo, tiene algunos inconvenientes:

  1. Expone la solicitud xhr solo hasta el último momento. Esto no permite que POST-requests establezca el cuerpo de la solicitud.
  2. Es más difícil de leer ya que la sendllamada crucial está oculta dentro de una función.
  3. Introduce un poco de repetitivo al hacer la solicitud.

Monkey parcheando el objeto xhr aborda estos problemas:

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

Ahora el uso es tan simple como:

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

Por supuesto, esto presenta un inconveniente diferente: el parcheado de monos perjudica el rendimiento. Sin embargo, esto no debería ser un problema suponiendo que el usuario está esperando principalmente el resultado del xhr, que la solicitud en sí misma toma órdenes de magnitud más largas que la configuración de la llamada y que las solicitudes xhr no se envían con frecuencia.

PD: Y, por supuesto, si se dirige a los navegadores modernos, ¡use fetch!

PPS: Se ha señalado en los comentarios que este método cambia la API estándar, lo que puede ser confuso. Para mayor claridad, se podría aplicar un método diferente al objeto xhr sendAndGetPromise().

t.animal
fuente
Evito los parches de mono porque es sorprendente. La mayoría de los desarrolladores esperan que los nombres de función API estándar invoquen la función API estándar. Este código aún oculta la sendllamada real, pero también puede confundir a los lectores que saben que sendno tiene valor de retorno. El uso de llamadas más explícitas deja en claro que se ha invocado una lógica adicional. Mi respuesta necesita ser ajustada para manejar argumentos send; sin embargo, probablemente sea mejor usarlo fetchahora.
jpmc26
Supongo que depende. Si devuelve / expone la solicitud xhr (que de todos modos parece dudosa) tiene toda la razón. Sin embargo, no veo por qué uno no haría esto dentro de un módulo y expondría solo las promesas resultantes.
t.animal
Me refiero especialmente a cualquiera que tenga que mantener el código en el que lo hace.
jpmc26
Como dije: depende. Si su módulo es tan grande que la función de promisificación se pierde entre el resto del código, probablemente tenga otros problemas. Si tiene un módulo en el que solo desea llamar a algunos puntos finales y devolver promesas, no veo ningún problema.
t.animal
No estoy de acuerdo con que dependa del tamaño de su base de código. Es confuso ver que una función API estándar hace algo más que su comportamiento estándar.
jpmc26