¿Cómo prometo XHR nativo?

Resuelto SomeKittens asked hace 9 años • 6 respuestas

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

Quiero que mi xhr devuelva una promesa pero esto no funciona (dándome 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 avatar May 03 '15 05:05 SomeKittens
Aceptado

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

Dado que cualquier navegador que admita promesas nativas también las admitirá xhr.onload, podemos saltarnos todas las onReadyStateChangetonterías. Demos un paso atrás 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, uno para el éxito y otro para el fracaso. Los ejemplos son increíbles, actualicemos 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 (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: xhr.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: xhr.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 se .catchactivará 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
}

makeRequestahora se ve algo como esto:

function makeRequest (opts) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open(opts.method, opts.url);
    xhr.onload = function () {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: xhr.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: xhr.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);
});

Puede encontrar un enfoque más completo en MDN .

Alternativamente, puede utilizar la API de recuperación ( polyfill ).

SomeKittens avatar May 02 '2015 22:05 SomeKittens

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. Manejarlos debería depender de usted, en mi opinión.

Además, se recomienda llamar a la rejectdevolución de llamada con una instancia de Errory no con 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();
    });
}

Y invocarlo podría ser este:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });
Peleg avatar May 15 '2015 17:05 Peleg

Cualquiera que busque esto ahora puede utilizar la función de búsqueda . Tiene un soporte bastante bueno .

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 inmediato :)

microo8 avatar Jul 07 '2017 06:07 microo8

Creo que podemos hacer que la respuesta principal sea mucho más flexible y reutilizable si no creamos el XMLHttpRequestobjeto. El único beneficio de hacerlo es que no tenemos que escribir 2 o 3 líneas de código nosotros mismos para hacerlo, y tiene el enorme inconveniente de quitarnos el 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 debe manejar la respuesta (tanto para éxitos como para errores). Por lo tanto, podemos crear una función más flexible y de mayor aplicación simplemente aceptando el XMLHttpRequestobjeto como entrada y pasándolo como resultado .

Esta función convierte un XMLHttpRequestobjeto arbitrario en una promesa y trata los códigos de estado distintos de 200 como un error de forma predeterminada:

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 de forma muy natural en una cadena de mensajes de Promisetexto, 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ó arriba para mantener el código de muestra más simple. Siempre debes 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, sigue siendo sencillo entender lo que está pasando. Y no tenemos que reconstruir toda la API de solicitud web solo para admitir sus funciones.

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 avatar Nov 22 '2017 23:11 jpmc26