request(options)
Descrizione
Effettua richieste XHR (ovvero AJAX) e restituisce una promise.
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)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
options | Object | Sì | Le opzioni da passare per la richiesta. |
options.method | String | No | Il metodo HTTP da utilizzare. Questo valore dovrebbe essere uno dei seguenti: GET , POST , PUT , PATCH , DELETE , HEAD o OPTIONS . Il valore predefinito è GET . |
options.url | String | Sì | L'URL a cui inviare la richiesta, opzionalmente interpolato con valori da options.params . |
options.params | Object | No | I dati da interpolare nell'URL e/o da serializzare nella stringa di query. |
options.body | Object | No | I dati da serializzare nel corpo della richiesta (per metodi diversi da GET ). |
options.async | Boolean | No | Indica se la richiesta deve essere eseguita in modo asincrono. Il valore predefinito è true . |
options.user | String | No | Un nome utente per l'autenticazione HTTP. Il valore predefinito è undefined . |
options.password | String | No | Una password per l'autenticazione HTTP. Il valore predefinito è undefined . Questa opzione è fornita per la compatibilità con XMLHttpRequest , ma è consigliabile evitarne l'uso perché invia la password in testo semplice sulla rete. |
options.withCredentials | Boolean | No | Indica se inviare cookie a domini di terze parti. Il valore predefinito è false . |
options.timeout | Number | No | Il tempo massimo in millisecondi che una richiesta può impiegare prima di essere automaticamente interrotta. Il valore predefinito è undefined . |
options.responseType | String | No | Il tipo previsto per la risposta. Il valore predefinito è "" se extract è definito, altrimenti "json" . |
options.config | xhr = Function(xhr) | No | Espone l'oggetto XMLHttpRequest sottostante per la configurazione a basso livello e la sostituzione opzionale (restituendo un nuovo XHR). |
options.headers | Object | No | Intestazioni da aggiungere alla richiesta prima dell'invio (applicate immediatamente prima di options.config ). |
options.type | any = Function(any) | No | Un costruttore da applicare a ogni oggetto nella risposta. |
options.serialize | string = Function(any) | No | Un metodo di serializzazione da applicare al body . |
options.deserialize | any = Function(any) | No | Un metodo di deserializzazione da applicare a xhr.response o al xhr.responseText normalizzato. |
options.extract | any = Function(xhr, options) | No | Un hook per specificare come leggere la risposta di XMLHttpRequest. Utile per elaborare i dati di risposta, leggere le intestazioni e i cookie. Per impostazione predefinita, questa è una funzione che restituisce options.deserialize(parsedResponse) , generando un'eccezione quando il codice di stato della risposta del server indica un errore o quando la risposta non è sintatticamente valida. Se viene fornito un callback personalizzato, il parametro xhr è l'istanza XMLHttpRequest utilizzata per la richiesta e options è l'oggetto passato alla chiamata m.request . Inoltre, deserialize verrà ignorato e il valore restituito dal callback extract verrà lasciato così com'è quando la promise si risolve. |
options.background | Boolean | No | Se false , ridisegna i componenti montati al termine della richiesta. Se true , non lo fa. Il valore predefinito è false . |
restituisce | Promise | Una promise che si risolve nei dati di risposta, dopo essere stati elaborati dai metodi extract , deserialize e type . Se il codice di stato della risposta indica un errore, la promise viene rifiutata, ma questo può essere evitato impostando l'opzione extract . |
promise = m.request(url, options)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
url | String | Sì | L'URL a cui inviare la richiesta. options.url sovrascrive questo valore quando è presente. |
options | Object | No | Le opzioni di richiesta da passare. |
restituisce | Promise | Una promise che si risolve nei dati di risposta, dopo che sono stati elaborati tramite i metodi extract , deserialize e type . |
Questa seconda forma è per lo più equivalente a m.request(Object.assign({url: url}, options))
, solo che non dipende internamente dalla funzione globale ES6 Object.assign
.
Come funziona
L'utility m.request
è un wrapper semplificato per XMLHttpRequest
e consente di effettuare richieste HTTP a server remoti per salvare e/o recuperare dati da un database.
m.request({
method: 'GET',
url: '/api/v1/users',
}).then(function (users) {
console.log(users);
});
Una chiamata a m.request
restituisce una promise e attiva un ridisegno al termine della sua catena di promise.
Per impostazione predefinita, m.request
presuppone che la risposta sia in formato JSON e la analizza in un oggetto JavaScript (o in un array).
Se il codice di stato della risposta HTTP indica un errore, la promise restituita verrà rifiutata. Fornire un callback extract
impedirà il rifiuto della promise.
Utilizzo tipico
Ecco un esempio di un componente che utilizza m.request
per recuperare dati da un server.
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,
});
Supponiamo che una richiesta all'URL del server /api/items
restituisca un array di oggetti in formato JSON.
Quando m.route
viene chiamato alla fine, il componente Todos
viene inizializzato. Viene chiamato oninit
, che a sua volta chiama m.request
. Questo recupera un array di oggetti dal server in modo asincrono. "Asincrono" significa che JavaScript continua a eseguire altro codice mentre attende la risposta dal server. In questo caso, significa che fetch
restituisce e il componente viene renderizzato con l'array vuoto originale in Data.todos.list
. Una volta completata la richiesta al server, l'array di oggetti items
viene assegnato a Data.todos.list
e il componente viene renderizzato nuovamente, producendo un elenco di <div>
contenenti i titoli di ciascun todo
.
Gestione degli errori
Quando una richiesta (non file:
) restituisce un codice di stato diverso da 2xx o 304, viene rifiutata con un errore. Questo errore è una normale istanza di Error, ma con alcune proprietà speciali.
error.message
è impostato sul testo della risposta grezza.error.code
è impostato sul codice di stato stesso.error.response
è impostato sulla risposta analizzata, utilizzandooptions.extract
eoptions.deserialize
come avviene con le normali risposte.
Questo è utile in molti casi in cui si possono gestire gli errori. Se si desidera rilevare se una sessione è scaduta, si può fare if (error.code === 401) return promptForAuth().then(retry)
. Se si raggiunge il meccanismo di throttling di un'API e questa restituisce un errore con un "timeout": 1000
, si può fare un setTimeout(retry, error.response.timeout)
.
Icone di caricamento e messaggi di errore
Ecco una versione ampliata dell'esempio precedente che implementa un indicatore di caricamento e un messaggio di errore:
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,
});
Ci sono alcune differenze tra questo esempio e il precedente. Qui, Data.todos.list
è inizialmente null
. Inoltre, c'è un campo aggiuntivo error
per contenere un messaggio di errore e la vista del componente Todos
è stata modificata per visualizzare un messaggio di errore se presente, oppure un'icona di caricamento se Data.todos.list
non è un array.
URL dinamici
Gli URL delle richieste possono contenere delle interpolazioni:
m.request({
method: 'GET',
url: '/api/v1/users/:id',
params: { id: 123 },
}).then(function (user) {
console.log(user.id); // logs 123
});
Nel codice sopra, :id
viene sostituito con i dati dall'oggetto params
, e la richiesta diventa GET /api/v1/users/123
.
Le interpolazioni vengono ignorate se non ci sono dati corrispondenti nella proprietà params
.
m.request({
method: 'GET',
url: '/api/v1/users/foo:bar',
params: { id: 123 },
});
Nel codice sopra, la richiesta diventa GET /api/v1/users/foo:bar?id=123
.
Interruzione delle richieste
A volte, può essere utile interrompere una richiesta. Ad esempio, in un widget di autocompletamento/typeahead, si desidera garantire che solo l'ultima richiesta venga completata, poiché in genere gli autocompletatori inviano diverse richieste mentre l'utente digita e le richieste HTTP possono essere completate in ordine sparso a causa della natura imprevedibile delle reti. Se un'altra richiesta termina dopo l'ultima inviata, il widget visualizzerebbe dati meno pertinenti (o potenzialmente errati) rispetto a se l'ultima richiesta inviata fosse completata per ultima.
m.request()
espone il suo oggetto XMLHttpRequest
sottostante tramite il parametro options.config
, che consente di mantenere un riferimento a tale oggetto e chiamare il suo metodo abort
quando necessario:
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;
}
Caricamento di file
Per caricare file, è necessario prima ottenere un riferimento a un oggetto File
. Il modo più semplice per farlo è utilizzare un <input type="file">
.
m.render(document.body, [m('input[type=file]', { onchange: upload })]);
function upload(e) {
var file = e.target.files[0];
}
Lo snippet sopra genera un input file. Se un utente seleziona un file, viene attivato l'evento onchange
, che invoca la funzione upload
. e.target.files
è una lista di oggetti File
.
Successivamente, è necessario creare un oggetto FormData
per generare una richiesta multipart, che è una richiesta HTTP formattata in modo speciale in grado di inviare dati di file nel corpo della richiesta.
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
}
Successivamente, è necessario chiamare m.request
e impostare options.method
su un metodo HTTP che utilizza il corpo (ad es. POST
, PUT
, PATCH
) e utilizzare l'oggetto FormData
come 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,
});
}
Supponendo che il server sia configurato per accettare richieste multipart, le informazioni sul file saranno associate alla chiave myfile
.
Caricamento di più file
È possibile caricare più file in un'unica richiesta. In questo modo, il caricamento in batch sarà atomico, ovvero nessun file verrà elaborato se si verifica un errore durante il caricamento, quindi non sarà possibile salvare solo una parte dei file. Se si desidera salvare il maggior numero possibile di file in caso di errore di rete, è consigliabile caricare ogni file in una richiesta distinta.
Per caricare più file, è sufficiente aggiungerli tutti all'oggetto FormData
. Quando si utilizza un input file, è possibile ottenere una lista di file aggiungendo l'attributo multiple
all'input:
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,
});
}
Monitoraggio dell'avanzamento
A volte, se una richiesta è intrinsecamente lenta (ad es. un caricamento di file di grandi dimensioni), può essere utile visualizzare un indicatore di avanzamento all'utente per segnalare che l'applicazione è ancora in funzione.
m.request()
espone il suo oggetto XMLHttpRequest
sottostante tramite il parametro options.config
, che consente di collegare gestori di eventi all'oggetto 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(); // tell Mithril.js that data changed and a re-render is needed
});
},
});
}
Nell'esempio sopra, viene generato un input file. Se l'utente seleziona un file, viene avviato un caricamento e, nel callback config
, viene registrato un gestore dell'evento progress
. Questo gestore di eventi viene attivato ogni volta che c'è un aggiornamento dell'avanzamento in XMLHttpRequest
. Poiché l'evento di avanzamento di XMLHttpRequest
non è gestito direttamente dal motore DOM virtuale di Mithril.js, è necessario chiamare m.redraw()
per segnalare a Mithril.js che i dati sono cambiati e che è necessario un ridisegno.
Casting della risposta a un tipo
A seconda dell'architettura complessiva dell'applicazione, potrebbe essere utile trasformare i dati di risposta di una richiesta in una classe o tipo specifico (ad esempio, per formattare uniformemente i campi data o per avere metodi di supporto).
È possibile passare un costruttore come parametro options.type
, e Mithril.js lo istanzierà per ogni oggetto nella risposta 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); // logs a name
});
Nell'esempio sopra, supponendo che /api/v1/users
restituisca un array di oggetti, il costruttore User
verrà istanziato (cioè chiamato come new User(data)
) per ogni oggetto nell'array. Se la risposta restituisse un singolo oggetto, tale oggetto verrebbe utilizzato come argomento body
.
Risposte non JSON
A volte un endpoint del server non restituisce una risposta JSON: ad esempio, si potrebbe richiedere un file HTML, un file SVG o un file CSV. Per impostazione predefinita, Mithril.js tenta di analizzare una risposta come se fosse in formato JSON. Per sovrascrivere questo comportamento, definire una funzione options.deserialize
personalizzata:
m.request({
method: 'GET',
url: '/files/icon.svg',
deserialize: function (value) {
return value;
},
}).then(function (svg) {
m.render(document.body, m.trust(svg));
});
Nell'esempio sopra, la richiesta recupera un file SVG, non fa nulla per analizzarlo (poiché deserialize
restituisce semplicemente il valore così com'è) e quindi renderizza la stringa SVG come HTML attendibile.
Naturalmente, una funzione deserialize
può essere più complessa:
m.request({
method: 'GET',
url: '/files/data.csv',
deserialize: parseCSV,
}).then(function (data) {
console.log(data);
});
function parseCSV(data) {
// naive implementation for the sake of keeping example simple
return data.split('\n').map(function (row) {
return row.split(',');
});
}
Ignorando il fatto che la funzione parseCSV
sopra non gestisce molti casi che un parser CSV appropriato dovrebbe gestire, il codice sopra registra un array di array.
Le intestazioni personalizzate possono essere utili anche in questo contesto. Ad esempio, se si richiede un SVG, si vorrà probabilmente impostare il tipo di contenuto di conseguenza. Per sovrascrivere il tipo di richiesta JSON predefinito, impostare options.headers
su un oggetto di coppie chiave-valore corrispondenti ai nomi e ai valori delle intestazioni di richiesta.
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;
},
});
Recupero dei dettagli della risposta
Per impostazione predefinita, Mithril.js tenta di analizzare xhr.responseText
come JSON e restituisce l'oggetto analizzato. Può essere utile esaminare una risposta del server in modo più dettagliato ed elaborarla manualmente. Questo può essere fatto passando una funzione options.extract
personalizzata:
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);
});
Il parametro di options.extract
è l'oggetto XMLHttpRequest
una volta completata la sua operazione, ma prima che sia stato passato alla catena di promise restituita, quindi la promise potrebbe comunque finire in uno stato rifiutato se l'elaborazione genera un'eccezione.
Emissione di fetch a indirizzi IP
A causa del modo (molto semplice) in cui i parametri vengono rilevati negli URL, i segmenti di indirizzo IPv6 vengono confusi con interpolazioni di parametri di percorso e, poiché i parametri di percorso necessitano di un separatore per essere interpolati correttamente, ciò genera un errore.
// This doesn't work
m.request('http://[2001:db8::990a:cd27:4d9e:79]:8080/some/path', {
// ...
});
Per aggirare questo problema, è necessario passare la coppia indirizzo IPv6 + porta come parametro.
m.request('http://:host/some/path', {
params: { host: '[2001:db8::990a:cd27:4d9e:79]:8080' },
// ...
});
Questo non è un problema con gli indirizzi IPv4, che possono essere utilizzati normalmente.
// This will work as you expect
m.request('http://192.0.2.15:8080/some/path', {
// ...
});
Perché JSON invece di HTML
Molti framework lato server forniscono un motore di visualizzazione che interpola i dati del database in un template prima di servire HTML (al caricamento della pagina o tramite AJAX) e quindi utilizzano jQuery per gestire le interazioni dell'utente.
Al contrario, Mithril.js è un framework progettato per applicazioni client pesanti, che in genere scaricano template e dati separatamente e li combinano nel browser tramite JavaScript. Eseguire il templating pesante nel browser può portare vantaggi, come la riduzione dei costi operativi liberando risorse del server. Separare i template dai dati consente anche di memorizzare nella cache il codice del template in modo più efficace e migliora la riusabilità del codice tra diversi tipi di client (ad es. desktop, mobile). Un altro vantaggio è che Mithril.js abilita un paradigma di sviluppo dell'interfaccia utente retained mode, che semplifica notevolmente lo sviluppo e la manutenzione di interazioni utente complesse.
Per impostazione predefinita, m.request
si aspetta che i dati di risposta siano in formato JSON. In una tipica applicazione Mithril.js, tali dati JSON vengono solitamente consumati da una vista.
Si dovrebbe evitare di tentare di renderizzare HTML dinamico generato dal server con Mithril. Se si ha un'applicazione esistente che utilizza un sistema di templating lato server e si desidera ri-architettarla, è importante decidere prima se lo sforzo è fattibile. La migrazione da un'architettura server pesante a un'architettura client pesante è generalmente uno sforzo considerevole e comporta il refactoring della logica dai template in servizi dati logici (e il test correlati).
I servizi dati possono essere organizzati in vari modi a seconda della natura dell'applicazione. Le architetture RESTful sono popolari tra i fornitori di API, mentre le architetture orientate ai servizi sono spesso richieste in presenza di flussi di lavoro altamente transazionali.
Perché XHR invece di fetch
fetch()
è una nuova API Web per recuperare risorse dai server, simile a XMLHttpRequest
.
m.request
di Mithril.js utilizza XMLHttpRequest
invece di fetch()
per vari motivi:
fetch
non è ancora completamente standardizzato e potrebbe essere soggetto a modifiche delle specifiche.- Le chiamate
XMLHttpRequest
possono essere interrotte prima che si risolvano (ad es. per evitare race condition nelle interfacce utente di ricerca istantanea). XMLHttpRequest
fornisce hook per i gestori di avanzamento per le richieste a lunga esecuzione (ad es. caricamenti di file).XMLHttpRequest
è supportato da tutti i browser, mentrefetch()
non è supportato da Internet Explorer e dalle versioni precedenti di Android (prima della 5.0 Lollipop).
Attualmente, a causa della mancanza di supporto del browser, fetch()
richiede generalmente un polyfill, che è di oltre 11kb non compresso, quasi tre volte più grande del modulo XHR di Mithril.js.
Nonostante sia molto più piccolo, il modulo XHR di Mithril.js supporta molte funzionalità importanti e non banali da implementare, come interpolazione URL e serializzazione della query string, oltre alla sua capacità di integrarsi perfettamente nel sottosistema di autoredrawing di Mithril.js. Il polyfill fetch
non supporta nessuna di queste funzionalità e richiede librerie e codice standard aggiuntivi per raggiungere lo stesso livello di funzionalità.
Inoltre, il modulo XHR di Mithril.js è ottimizzato per gli endpoint basati su JSON e rende il caso più comune appropriatamente conciso, ovvero m.request(url)
.
L'API fetch()
presenta alcuni vantaggi tecnici rispetto a XMLHttpRequest
in casi non comuni:
- fornisce un'API di streaming (nel senso di "video streaming", non nel senso di programmazione reattiva), che consente una migliore latenza e un minore consumo di memoria per risposte molto grandi (a costo di complessità del codice).
- si integra con la Service Worker API, che fornisce un ulteriore livello di controllo su come e quando avvengono le richieste di rete. Questa API consente inoltre l'accesso alle notifiche push e alle funzionalità di sincronizzazione in background.
In scenari tipici, lo streaming non fornirà vantaggi significativi in termini di prestazioni, poiché in genere non è consigliabile scaricare megabyte di dati in primo luogo. Inoltre, i guadagni di memoria derivanti dal riutilizzo ripetuto di piccoli buffer possono essere compensati o annullati se comportano eccessive ridisegni del browser. Per questi motivi, la scelta dello streaming fetch()
rispetto a m.request
è consigliata solo per applicazioni estremamente intensive in termini di risorse.
Evitare anti-pattern
Le promise non sono i dati di risposta
Il metodo m.request
restituisce una Promise
, non i dati di risposta. Non può restituire tali dati direttamente perché una richiesta HTTP potrebbe richiedere molto tempo per essere completata (a causa della latenza di rete) e, se JavaScript attendesse, bloccherebbe l'applicazione fino a quando i dati non fossero disponibili.
// EVITARE
var users = m.request('/api/v1/users');
console.log('list of users:', users);
// `users` is NOT a list of users, it's a promise
// PREFERIBILE
m.request('/api/v1/users').then(function (users) {
console.log('list of users:', users);
});