javascript - img - title label html
¿Cómo prometo XHR nativo? (5)
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: Tipo de error no capturado: Resolver promesas no
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);
});
Creo que podemos hacer que
la respuesta superior sea
mucho más flexible y reutilizable al no hacer que cree el objeto
XMLHttpRequest
.
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 objeto
XMLHttpRequest
como
entrada
y pasándolo como
resultado
.
Esta función convierte un objeto
XMLHttpRequest
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
Promise
s, sin sacrificar la flexibilidad de la API
XMLHttpRequest
:
Promise.resolve()
.then(function() {
// We make this a separate function to avoid
// polluting the calling scope.
var xhr = new XMLHttpRequest();
xhr.open(''GET'', ''https://.com/'');
return xhr;
})
.then(promiseResponse)
.then(function(request) {
console.log(''Success'');
console.log(request.status + '' '' + request.statusText);
});
catch
se 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://.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://.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 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://.com/doesnotexist''))
.then(promiseResponseAnyCode)
.then(function(request) {
console.log(''Done'');
console.log(request.status + '' '' + request.statusText);
});
Esto podría ser tan simple como el siguiente código.
Tenga en cuenta que este código solo activará la devolución de llamada de
reject
cuando se llama a un error (solo errores de
red
) y no cuando el código de estado HTTP significa 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 devolución de llamada de
reject
con una instancia de
Error
y 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
});
La respuesta de jpmc26 es bastante perfecta en mi opinión. Sin embargo, tiene algunos inconvenientes:
-
Expone la solicitud xhr solo hasta el último momento.
Esto no permite que las
POST
establezcan el cuerpo de la solicitud. -
Es más difícil de leer ya que la llamada crucial de
send
está oculta dentro de una función. - 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 parche a un método diferente en el objeto
sendAndGetPromise()
.
Supongo que sabes cómo hacer una solicitud XHR nativa (puedes repasar here y here )
Dado que
cualquier navegador que admita promesas nativas
también admitirá
xhr.onload
, podemos omitir toda la tontería
onReadyStateChange
.
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émosles
resolve
y
reject
).
Puede considerarlos como devoluciones de llamada, una para el éxito y otra para el fracaso.
Los ejemplos son impresionantes, vamos a actualizar
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 (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
.catch
provocará 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);
});
Un enfoque más integral se puede encontrar en MDN .
Alternativamente, podría usar la API de recuperación ( polyfill ).