¿Cómo prometo XHR nativo?
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);
});
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 onReadyStateChange
tonterí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 resolve
y reject
). Puede considerarlos como devoluciones de llamada, uno para el éxito y otro para el fracaso. Los ejemplos son increíbles, actualicemos makeRequest
con 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 .catch
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 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 ).
Esto podría ser tan simple como el siguiente código.
Tenga en cuenta que este código solo activará la reject
devolución de llamada cuando onerror
se 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 reject
devolución de llamada con una instancia de Error
y 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
});
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í fetch
que lo hace por mí de inmediato :)
Creo que podemos hacer que la respuesta principal sea mucho más flexible y reutilizable si no creamos el XMLHttpRequest
objeto. 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 XMLHttpRequest
objeto como entrada y pasándolo como resultado .
Esta función convierte un XMLHttpRequest
objeto 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 Promise
texto, sin sacrificar la flexibilidad de la XMLHttpRequest
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
se 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);
});