request(options)
Descripción
Realiza peticiones XHR (también conocidas como AJAX) y devuelve una promesa.
m.request({
method: 'PUT',
url: '/api/v1/users/:id',
params: { id: 1 },
body: { name: 'test' },
}).then(function (result) {
console.log(result);
});
Firma
promise = m.request(options)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
options | Object | Sí | Opciones de configuración de la petición. |
options.method | String | No | Método HTTP a utilizar. Debe ser uno de los siguientes: GET , POST , PUT , PATCH , DELETE , HEAD u OPTIONS . El valor por defecto es GET . |
options.url | String | Sí | Ruta a la que se enviará la petición. Puede ser interpolada con valores de options.params . |
options.params | Object | No | Datos que se interpolarán en la URL y/o se serializarán en la cadena de consulta. |
options.body | Object | No | Datos que se serializarán en el cuerpo de la petición (para métodos que lo admiten). |
options.async | Boolean | No | Indica si la petición debe ser asíncrona. El valor por defecto es true . |
options.user | String | No | Nombre de usuario para la autorización HTTP. El valor por defecto es undefined . |
options.password | String | No | Contraseña para la autorización HTTP. El valor por defecto es undefined . Esta opción se incluye por compatibilidad con XMLHttpRequest , pero se recomienda no usarla ya que envía la contraseña en texto plano a través de la red. |
options.withCredentials | Boolean | No | Indica si se deben enviar cookies a dominios de terceros. El valor por defecto es false . |
options.timeout | Number | No | Tiempo máximo en milisegundos que una petición puede tardar antes de ser terminada automáticamente. |
options.responseType | String | No | Tipo esperado de la respuesta. El valor por defecto es "" si se define extract , "json" si falta. Si responseType: "json" , internamente realiza JSON.parse(responseText) . |
options.config | xhr = Function(xhr) | No | Expone el objeto XMLHttpRequest subyacente para configuración avanzada y, opcionalmente, para su reemplazo (devolviendo un nuevo XHR). |
options.headers | Object | No | Cabeceras que se añadirán a la petición antes de enviarla (se aplican justo antes de options.config ). |
options.type | any = Function(any) | No | Constructor que se aplicará a cada objeto en la respuesta. El valor por defecto es la función identidad. |
options.serialize | string = Function(any) | No | Método de serialización que se aplicará a body . El valor por defecto es JSON.stringify , o si options.body es una instancia de FormData o URLSearchParams , el valor por defecto es la función identidad (es decir, function(value) {return value} ). |
options.deserialize | any = Function(any) | No | Método de deserialización que se aplicará a xhr.response o xhr.responseText normalizado. El valor por defecto es la función identidad. Si se define extract , se omitirá deserialize . |
options.extract | any = Function(xhr, options) | No | Punto de extensión para especificar cómo se debe leer la respuesta XMLHttpRequest. Útil para procesar los datos de la respuesta, leer las cabeceras y las cookies. Por defecto, esta es una función que devuelve options.deserialize(parsedResponse) , lanzando una excepción cuando el código de estado de la respuesta del servidor indica un error o cuando la respuesta no es sintácticamente válida. Si se proporciona una función extract personalizada, el parámetro xhr es la instancia XMLHttpRequest utilizada para la petición, y options es el objeto que se pasó a la llamada m.request . Además, se omitirá deserialize y el valor devuelto por la función extract se dejará tal cual cuando se resuelva la promesa. |
options.background | Boolean | No | Si es false , redibuja los componentes montados al finalizar la petición. Si es true , no lo hace. El valor por defecto es false . |
returns | Promise | Promesa que se resuelve con los datos de la respuesta, tras ser procesados por los métodos extract , deserialize y type . Si el código de estado de la respuesta indica un error, la promesa se rechaza, pero esto se puede evitar configurando la opción extract . |
promise = m.request(url, options)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
url | String | Sí | Ruta a la que se enviará la petición. options.url anula este valor cuando está presente. |
options | Object | No | Opciones de configuración de la petición. |
returns | Promise | Promesa que se resuelve con los datos de la respuesta, tras ser procesados por los métodos extract , deserialize y type . |
Esta segunda forma es casi equivalente a m.request(Object.assign({url: url}, options))
, solo que no depende internamente del global ES6 Object.assign
.
Cómo funciona
La utilidad m.request
es una capa delgada sobre XMLHttpRequest
, y permite realizar peticiones HTTP a servidores remotos para guardar y/o recuperar datos de una base de datos.
m.request({
method: 'GET',
url: '/api/v1/users',
}).then(function (users) {
console.log(users);
});
Una llamada a m.request
devuelve una promesa y dispara un redibujo al finalizar su cadena de promesas.
Por defecto, m.request
asume que la respuesta está en formato JSON y la analiza en un objeto JavaScript (o arreglo).
Proporcionar una función extract
evitará el rechazo de la promesa.
Uso típico
Aquí se muestra un ejemplo de un componente que usa m.request
para obtener datos de un servidor.
var Data = {
todos: {
list: [],
fetch: function () {
m.request({
method: 'GET',
url: '/api/v1/todos',
}).then(function (items) {
Data.todos.list = items;
});
},
},
};
var Todos = {
oninit: Data.todos.fetch,
view: function (vnode) {
return Data.todos.list.map(function (item) {
return m('div', item.title);
});
},
};
m.route(document.body, '/', {
'/': Todos,
});
Supongamos que hacer una petición a la URL del servidor /api/items
devuelve un arreglo de objetos en formato JSON.
Cuando se llama a m.route
en la parte inferior, se inicializa el componente Todos
. Se llama a oninit
, que llama a m.request
. Esto recupera un arreglo de objetos del servidor de forma asíncrona. "Asíncrona" significa que JavaScript continúa ejecutando otro código mientras espera la respuesta del servidor. En este caso, significa que fetch
regresa, y el componente se renderiza usando el arreglo vacío original como Data.todos.list
. Una vez que se completa la petición al servidor, el arreglo de objetos items
se asigna a Data.todos.list
y el componente se renderiza nuevamente, produciendo una lista de <div>
s que contienen los títulos de cada todo
.
Gestión de errores
Cuando una petición que no es file:
recibe una respuesta con un código de estado diferente a 2xx o 304, se rechaza con un error. Este error es una instancia normal de Error, pero con algunas propiedades especiales.
error.message
se establece en el texto de respuesta sin procesar.error.code
se establece en el propio código de estado.error.response
se establece en la respuesta analizada, utilizandooptions.extract
yoptions.deserialize
como se hace con las respuestas normales.
Esto es útil en muchos casos donde los errores pueden ser gestionados de forma específica. Si desea detectar si una sesión caducó, puede hacer if (error.code === 401) return promptForAuth().then(retry)
. Si alcanza el mecanismo de limitación de una API y devuelve un error con un "timeout": 1000
, podría hacer un setTimeout(retry, error.response.timeout)
.
Indicadores de carga y mensajes de error
Aquí se presenta una versión extendida del ejemplo anterior, que implementa un indicador de carga y un mensaje de error:
var Data = {
todos: {
list: null,
error: '',
fetch: function () {
m.request({
method: 'GET',
url: '/api/v1/todos',
})
.then(function (items) {
Data.todos.list = items;
})
.catch(function (e) {
Data.todos.error = e.message;
});
},
},
};
var Todos = {
oninit: Data.todos.fetch,
view: function (vnode) {
return Data.todos.error
? [m('.error', Data.todos.error)]
: Data.todos.list
? [
Data.todos.list.map(function (item) {
return m('div', item.title);
}),
]
: m('.loading-icon');
},
};
m.route(document.body, '/', {
'/': Todos,
});
Hay algunas diferencias entre este ejemplo y el anterior. Aquí, Data.todos.list
es null
al principio. Además, hay un campo adicional error
para guardar un mensaje de error, y la vista del componente Todos
se modificó para mostrar un mensaje de error si existe uno, o mostrar un icono de carga si Data.todos.list
no es un arreglo.
URLs dinámicas en las solicitudes
Las URLs de las peticiones pueden contener interpolaciones:
m.request({
method: 'GET',
url: '/api/v1/users/:id',
params: { id: 123 },
}).then(function (user) {
console.log(user.id); // registra 123
});
En el código anterior, :id
se completa con los datos del objeto params
, y la petición se convierte en GET /api/v1/users/123
.
Las interpolaciones se ignoran si no existen datos coincidentes en la propiedad params
.
m.request({
method: 'GET',
url: '/api/v1/users/foo:bar',
params: { id: 123 },
});
En el código anterior, la petición se convierte en GET /api/v1/users/foo:bar?id=123
Abortar peticiones
En ocasiones, es deseable abortar una petición. Por ejemplo, en un widget de autocompletar/typeahead, desea asegurarse de que solo se complete la última petición, porque normalmente los autocompletadores activan varias peticiones a medida que el usuario escribe y las peticiones HTTP pueden completarse fuera de orden debido a la naturaleza impredecible de las redes. Si otra petición finaliza después de la última petición enviada, el widget mostraría datos menos relevantes (o potencialmente incorrectos) que si la última petición enviada finalizara en último lugar.
m.request()
expone su objeto XMLHttpRequest
subyacente a través del parámetro options.config
, que le permite guardar una referencia a ese objeto y llamar a su método abort
cuando sea necesario:
var searchXHR = null;
function search() {
abortPreviousSearch();
m.request({
method: 'GET',
url: '/api/v1/users',
params: { search: query },
config: function (xhr) {
searchXHR = xhr;
},
});
}
function abortPreviousSearch() {
if (searchXHR !== null) searchXHR.abort();
searchXHR = null;
}
Carga de archivos
Para subir archivos, primero es necesario obtener una referencia a un objeto File
. La forma más fácil de hacerlo es desde un <input type="file">
.
m.render(document.body, [m('input[type=file]', { onchange: upload })]);
function upload(e) {
var file = e.target.files[0];
}
El fragmento de código anterior renderiza una entrada de archivo. Si un usuario elige un archivo, se activa el evento onchange
, que llama a la función upload
. e.target.files
es una lista de objetos File
.
A continuación, debe crear un objeto FormData
para crear una petición multipart, que es una petición HTTP con formato especial que puede enviar datos de archivo en el cuerpo de la petición.
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
}
A continuación, debe llamar a m.request
y establecer options.method
en un método HTTP que use el cuerpo (por ejemplo, POST
, PUT
, PATCH
) y usar el objeto FormData
como options.body
.
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
});
}
Suponiendo que el servidor esté configurado para aceptar peticiones multipart, la información del archivo se asociará con la clave myfile
.
Subida de múltiples archivos
Es posible subir varios archivos en una sola petición. Hacerlo hará que la subida por lotes sea atómica; es decir, no se procesará ningún archivo si hay un error durante la subida, por lo que no será posible guardar solo una parte de los archivos. Si desea guardar tantos archivos como sea posible en caso de un fallo de red, debería considerar subir cada archivo en una petición separada.
Para subir varios archivos, simplemente añádalos todos al objeto FormData
. Cuando use una entrada de archivo, puede obtener una lista de archivos agregando el atributo multiple
a la entrada:
m.render(document.body, [
m('input[type=file][multiple]', { onchange: upload }),
]);
function upload(e) {
var files = e.target.files;
var body = new FormData();
for (var i = 0; i < files.length; i++) {
body.append('file' + i, files[i]);
}
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
});
}
Seguimiento del progreso
En ocasiones, si una petición es inherentemente lenta (por ejemplo, la subida de un archivo grande), es deseable mostrar un indicador de progreso al usuario para indicar que la aplicación sigue funcionando.
m.request()
expone su objeto XMLHttpRequest
subyacente a través del parámetro options.config
, que le permite adjuntar "oyentes de eventos" al objeto XMLHttpRequest:
var progress = 0;
m.mount(document.body, {
view: function () {
return [
m('input[type=file]', { onchange: upload }),
progress + '% completed',
];
},
});
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
config: function (xhr) {
xhr.upload.addEventListener('progress', function (e) {
progress = e.loaded / e.total;
m.redraw(); // Indica a Mithril.js que los datos han cambiado y se necesita un nuevo renderizado
});
},
});
}
En el ejemplo anterior, se renderiza una entrada de archivo. Si el usuario elige un archivo, se inicia una subida, y en la función de "devolución de llamada" config
, se registra un controlador de eventos progress
. Este controlador de eventos se activa cada vez que hay una actualización de progreso en XMLHttpRequest. Debido a que el evento de progreso de XMLHttpRequest no es manejado directamente por el motor DOM virtual de Mithril.js, se debe llamar a m.redraw()
para indicar a Mithril.js que los datos han cambiado y se requiere un redibujo.
Conversión de la respuesta a un tipo específico
Dependiendo de la arquitectura general de la aplicación, puede ser deseable transformar los datos de respuesta de una petición a una clase o tipo específico (por ejemplo, para analizar uniformemente los campos de fecha o para tener métodos auxiliares).
Puede pasar un constructor como el parámetro options.type
y Mithril.js lo instanciará para cada objeto en la respuesta HTTP.
function User(data) {
this.name = data.firstName + ' ' + data.lastName;
}
m.request({
method: 'GET',
url: '/api/v1/users',
type: User,
}).then(function (users) {
console.log(users[0].name); // registra un nombre
});
En el ejemplo anterior, asumiendo que /api/v1/users
devuelve un arreglo de objetos, el constructor User
se instanciará (es decir, se llamará como new User(data)
) para cada objeto en el arreglo. Si la respuesta devolviera un solo objeto, ese objeto se usaría como argumento body
.
Respuestas que no son JSON
En ocasiones, un punto de acceso del servidor no devuelve una respuesta JSON: por ejemplo, puede estar solicitando un archivo HTML, un archivo SVG o un archivo CSV. Por defecto, Mithril.js intenta analizar una respuesta como si fuera JSON. Para anular ese comportamiento, defina una función options.deserialize
personalizada:
m.request({
method: 'GET',
url: '/files/icon.svg',
deserialize: function (value) {
return value;
},
}).then(function (svg) {
m.render(document.body, m.trust(svg));
});
En el ejemplo anterior, la petición recupera un archivo SVG, no hace nada para analizarlo (porque deserialize
simplemente devuelve el valor tal cual), y luego renderiza la cadena SVG como HTML confiable.
Por supuesto, una función deserialize
puede ser más elaborada:
m.request({
method: 'GET',
url: '/files/data.csv',
deserialize: parseCSV,
}).then(function (data) {
console.log(data);
});
function parseCSV(data) {
// implementación ingenua en aras de mantener el ejemplo simple
return data.split('\n').map(function (row) {
return row.split(',');
});
}
Ignorando el hecho de que la función parseCSV anterior no maneja muchos casos que un analizador CSV adecuado manejaría, el código anterior registra un arreglo de arreglos.
Las cabeceras personalizadas también pueden ser útiles en este sentido. Por ejemplo, si está solicitando un SVG, probablemente desee establecer el tipo de contenido en consecuencia. Para anular el tipo de petición JSON predeterminado, establezca options.headers
en un objeto de pares clave-valor correspondientes a los nombres y valores de las cabeceras de la petición.
m.request({
method: 'GET',
url: '/files/image.svg',
headers: {
'Content-Type': 'image/svg+xml; charset=utf-8',
Accept: 'image/svg, text/*',
},
deserialize: function (value) {
return value;
},
});
Obtención de detalles de la respuesta
Por defecto, Mithril.js intenta analizar xhr.responseText
como JSON y devuelve el objeto analizado. Puede ser útil inspeccionar una respuesta del servidor con más detalle y procesarla manualmente. Esto se puede lograr pasando una función options.extract
personalizada:
m.request({
method: 'GET',
url: '/api/v1/users',
extract: function (xhr) {
return { status: xhr.status, body: xhr.responseText };
},
}).then(function (response) {
console.log(response.status, response.body);
});
El parámetro para options.extract
es el objeto XMLHttpRequest una vez que se completa su operación, pero antes de que se haya pasado a la cadena de promesas devuelta, por lo que la promesa aún puede terminar en un estado rechazado si el procesamiento lanza una excepción.
Realización de solicitudes a direcciones IP
Debido a la forma (muy simplista) en que se detectan los parámetros en las URLs, los segmentos de dirección IPv6 se confunden con interpolaciones de parámetros de ruta, y como los parámetros de ruta necesitan algo para separarlos para que se interpolen correctamente, esto da como resultado que se lance un error.
// Esto no funciona
m.request('http://[2001:db8::990a:cd27:4d9e:79]:8080/some/path', {
// ...
});
Para evitar esto, debe pasar la dirección IPv6 + el par de puertos como un parámetro en su lugar.
m.request('http://:host/some/path', {
params: { host: '[2001:db8::990a:cd27:4d9e:79]:8080' },
// ...
});
Esto no es un problema con las direcciones IPv4, y puede usarlas normalmente.
// Esto funcionará como espera
m.request('http://192.0.2.15:8080/some/path', {
// ...
});
¿Por qué usar JSON en lugar de HTML?
Muchos frameworks del lado del servidor proporcionan un motor de vistas que interpola los datos de la base de datos en una plantilla antes de servir HTML (en la carga de la página o a través de AJAX) y luego emplean jQuery para manejar las interacciones del usuario.
Por el contrario, Mithril.js es un framework diseñado para aplicaciones de cliente pesadas (thick client), que normalmente descargan plantillas y datos por separado y los combinan en el navegador a través de JavaScript. Hacer el trabajo pesado de las plantillas en el navegador puede traer beneficios como reducir los costos operativos al liberar recursos del servidor. Separar las plantillas de los datos también permite que el código de la plantilla se almacene en caché de manera más efectiva y permite una mejor reutilización del código en diferentes tipos de clientes (por ejemplo, escritorio, móvil). Otro beneficio es que Mithril.js permite un paradigma de desarrollo de UI de modo retenido, lo que simplifica enormemente el desarrollo y el mantenimiento de interacciones complejas del usuario.
Por defecto, m.request
espera que los datos de respuesta estén en formato JSON. En una aplicación típica de Mithril.js, esos datos JSON generalmente son consumidos por una vista.
Debe evitar intentar renderizar HTML dinámico generado por el servidor con Mithril. Si tiene una aplicación existente que usa un sistema de plantillas del lado del servidor y desea re-arquitecturarla, primero decida si el esfuerzo es factible para empezar. La migración de una arquitectura de servidor "gruesa" a una arquitectura de cliente "gruesa" suele ser un esfuerzo algo grande e implica refactorizar la lógica de las plantillas en servicios de datos lógicos (y las pruebas que conlleva).
Los servicios de datos se pueden organizar de muchas maneras diferentes según la naturaleza de la aplicación. Las arquitecturas RESTful son populares entre los proveedores de API, y las arquitecturas orientadas a servicios a menudo se requieren donde hay muchos flujos de trabajo altamente transaccionales.
¿Por qué usar XHR en lugar de fetch?
fetch()
es una API web más nueva para obtener recursos de los servidores, similar a XMLHttpRequest
.
m.request
de Mithril.js usa XMLHttpRequest
en lugar de fetch()
por varias razones:
fetch
aún no está completamente estandarizado y puede estar sujeto a cambios en las especificaciones.- Las llamadas
XMLHttpRequest
se pueden abortar antes de que se resuelvan (por ejemplo, para evitar condiciones de carrera en las interfaces de usuario de búsqueda instantánea). XMLHttpRequest
proporciona "ganchos" para "oyentes" de progreso para peticiones de larga duración (por ejemplo, subidas de archivos).XMLHttpRequest
es compatible con todos los navegadores, mientras quefetch()
no es compatible con Internet Explorer y Android más antiguos (anteriores a 5.0 Lollipop).
Actualmente, debido a la falta de compatibilidad con el navegador, fetch()
normalmente requiere un polyfill (https://github.com/github/fetch), que ocupa más de 11 kb sin comprimir, casi tres veces más que el módulo XHR de Mithril.js.
A pesar de ser mucho más pequeño, el módulo XHR de Mithril.js admite muchas características importantes y no tan triviales de implementar, como la interpolación de URL y la serialización de cadenas de consulta, además de su capacidad para integrarse sin problemas en el subsistema de redibujo automático de Mithril.js. El "relleno de compatibilidad" de fetch
no admite nada de eso y requiere bibliotecas y "código repetitivo" adicionales para lograr el mismo nivel de funcionalidad.
Además, el módulo XHR de Mithril.js está optimizado para puntos de acceso basados en JSON, lo que hace que el caso más común sea conciso; es decir, m.request(url)
. En cambio, fetch
requiere un paso explícito adicional para analizar los datos de respuesta como JSON: fetch(url).then(function(response) {return response.json()})
La API fetch()
tiene algunas ventajas técnicas sobre XMLHttpRequest
en algunos casos poco comunes:
- proporciona una API de streaming (en el sentido de transmisión de vídeo, no en el sentido de programación reactiva), lo que permite una mejor latencia y un menor consumo de memoria para respuestas muy grandes (a costa de la complejidad del código).
- se integra con la API de Service Worker, que proporciona una capa adicional de control sobre cómo y cuándo se realizan las peticiones de red. Esta API también permite el acceso a notificaciones "push" y funciones de sincronización en segundo plano.
En escenarios típicos, el streaming no proporcionará beneficios de rendimiento notables porque, en general, no es aconsejable descargar megabytes de datos para empezar. Además, las ganancias de memoria al reutilizar repetidamente pequeños buffers pueden compensarse o anularse si resultan en repintados excesivos del navegador. Por esas razones, elegir el streaming de fetch()
en lugar de m.request
solo se recomienda para aplicaciones extremadamente intensivas en recursos.
Evitar anti-patrones
Las promesas no representan los datos de respuesta
El método m.request
devuelve una Promise
, no los datos de la respuesta. No puede devolver esos datos directamente porque una petición HTTP puede tardar mucho en completarse (debido a la latencia de la red), y si JavaScript esperara, congelaría la aplicación hasta que los datos estuvieran disponibles.
// EVITAR
var users = m.request('/api/v1/users');
console.log('list of users:', users);
// `users` NO es una lista de usuarios, es una promesa
// PREFERIR
m.request('/api/v1/users').then(function (users) {
console.log('list of users:', users);
});