request(options)
Description
Effectue des requêtes XHR (aussi appelées AJAX) et retourne une promesse.
m.request({
method: 'PUT',
url: '/api/v1/users/:id',
params: { id: 1 },
body: { name: 'test' },
}).then(function (result) {
console.log(result);
});
Signature
promise = m.request(options)
Argument | Type | Required | Description |
---|---|---|---|
options | Object | Yes | Les options de la requête à transmettre. |
options.method | String | No | La méthode HTTP à utiliser. Cette valeur doit être l'une des suivantes : GET , POST , PUT , PATCH , DELETE , HEAD ou OPTIONS . Par défaut, la valeur est GET . |
options.url | String | Yes | Le chemin auquel envoyer la requête, éventuellement interpolé avec les valeurs de options.params . |
options.params | Object | No | Les données à insérer dans l'URL et/ou à sérialiser dans les paramètres de la requête. |
options.body | Object | No | Les données à sérialiser dans le corps de la requête (pour les méthodes HTTP qui le permettent). |
options.async | Boolean | No | Indique si la requête doit être asynchrone. Valeur par défaut : true . |
options.user | String | No | Un nom d'utilisateur pour l'authentification HTTP. La valeur par défaut est undefined . |
options.password | String | No | Un mot de passe pour l'authentification HTTP. La valeur par défaut est undefined . Cette option est fournie pour assurer la compatibilité avec XMLHttpRequest , mais son utilisation est déconseillée car elle transmet le mot de passe en clair sur le réseau. |
options.withCredentials | Boolean | No | Indique si les cookies doivent être envoyés à des domaines tiers. La valeur par défaut est false . |
options.timeout | Number | No | Le nombre de millisecondes après lequel une requête est automatiquement abandonnée. La valeur par défaut est undefined . |
options.responseType | String | No | Le type attendu de la réponse. La valeur par défaut est "" si extract est défini, "json" si absent. Si responseType: "json" , JSON.parse(responseText) est effectué en interne. |
options.config | xhr = Function(xhr) | No | Expose l'objet XMLHttpRequest sous-jacent pour une configuration de bas niveau et un remplacement optionnel (en retournant un nouvel XHR). |
options.headers | Object | No | Les en-têtes à ajouter à la requête avant de l'envoyer (appliqués juste avant options.config ). |
options.type | any = Function(any) | No | Un constructeur à utiliser pour chaque objet de la réponse. La valeur par défaut est la fonction identité. |
options.serialize | string = Function(any) | No | Méthode de sérialisation appliquée à body . La valeur par défaut est JSON.stringify , ou si options.body est une instance de FormData ou URLSearchParams , la valeur par défaut est la fonction identité (c'est-à-dire function(value) {return value} ). |
options.deserialize | any = Function(any) | No | Une méthode de désérialisation à appliquer à xhr.response ou à xhr.responseText normalisé. La valeur par défaut est la fonction identité. Si extract est défini, deserialize sera ignoré. |
options.extract | any = Function(xhr, options) | No | Un point d'extension pour spécifier comment la réponse XMLHttpRequest doit être lue. Utile pour traiter les données de réponse, lire les en-têtes et les cookies. Par défaut, il s'agit d'une fonction qui retourne options.deserialize(parsedResponse) , en lançant une exception lorsque le code d'état de la réponse du serveur indique une erreur ou lorsque la réponse est syntaxiquement invalide. Si un callback extract personnalisé est fourni, le paramètre xhr est l'instance XMLHttpRequest utilisée pour la requête, et options est l'objet passé à l'appel m.request . De plus, deserialize est ignoré et la valeur renvoyée par le callback extract est conservée telle quelle lorsque la promesse est résolue. |
options.background | Boolean | No | Si false , redessine les composants montés à la fin de la requête. Si true , il ne le fait pas. La valeur par défaut est false . |
returns | Promise | Une promesse qui est résolue avec les données de réponse, après avoir été traitée par les méthodes extract , deserialize et type . Si le code d'état de la réponse indique une erreur, la promesse est rejetée, ce qui peut être évité en définissant l'option extract . |
promise = m.request(url, options)
Argument | Type | Required | Description |
---|---|---|---|
url | String | Yes | Le chemin auquel envoyer la requête. options.url remplace ceci lorsqu'il est présent. |
options | Object | No | Les options de requête à passer. |
returns | Promise | Une promesse qui renvoie les données de réponse, après avoir été traitée par les méthodes extract , deserialize et type . |
Cette deuxième forme est essentiellement équivalente à m.request(Object.assign({url: url}, options))
, mais elle ne dépend pas de la fonction globale ES6 Object.assign
en interne.
Comment ça marche
L'utilitaire m.request
est un wrapper simple pour XMLHttpRequest
. Il permet d'effectuer des requêtes HTTP vers des serveurs distants afin de sauvegarder ou de récupérer des données d'une base de données.
m.request({
method: 'GET',
url: '/api/v1/users',
}).then(function (users) {
console.log(users);
});
Un appel à m.request
renvoie une promesse et déclenche un redessin à la fin de sa chaîne de promesses.
Par défaut, m.request
suppose que la réponse est au format JSON et l'analyse en un objet JavaScript ou un tableau.
Si le code d'état de la réponse HTTP indique une erreur, la promesse retournée est rejetée. Fournir un callback extract
empêchera le rejet de la promesse.
Utilisation typique
Voici un exemple illustrant l'utilisation de m.request
par un composant pour récupérer des données depuis un serveur.
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,
});
Supposons qu'une requête à l'URL du serveur /api/items
retourne un tableau d'objets au format JSON.
Lorsque m.route
est appelé, le composant Todos
est initialisé. oninit
est appelé, ce qui appelle m.request
. Cela récupère alors un tableau d'objets du serveur de manière asynchrone. "Asynchrone" signifie que JavaScript continue d'exécuter d'autres codes pendant qu'il attend la réponse du serveur. Dans ce cas, cela signifie que fetch
retourne et que le composant est rendu en utilisant le tableau vide original comme Data.todos.list
. Une fois la requête au serveur terminée, le tableau d'objets items
est affecté à Data.todos.list
, et le composant est rendu à nouveau, produisant une liste de <div>
contenant les titres de chaque todo
.
Gestion des erreurs
Lorsqu'une requête (autre que file:
) retourne un statut différent de 2xx ou 304, elle est rejetée avec une erreur. Cette erreur est une instance normale de Error, mais elle possède quelques propriétés spéciales.
error.message
est défini sur le texte brut de la réponse.error.code
est défini sur le code de statut HTTP.error.response
est défini sur la réponse analysée, en utilisantoptions.extract
etoptions.deserialize
comme pour les réponses normales.
Ceci est utile dans de nombreux cas où les erreurs sont des éléments que vous pouvez prendre en compte. Si vous souhaitez détecter si une session a expiré, vous pouvez faire if (error.code === 401) return promptForAuth().then(retry)
. Si vous atteignez le mécanisme de limitation de débit d'une API et qu'elle retourne une erreur avec un "timeout": 1000
, vous pourriez faire un setTimeout(retry, error.response.timeout)
.
Icônes de chargement et messages d'erreur
Voici une version enrichie de l'exemple précédent, intégrant un indicateur de chargement et un message d'erreur :
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,
});
Il existe quelques différences entre cet exemple et le précédent. Ici, Data.todos.list
est initialement null
. De plus, un champ supplémentaire error
a été ajouté pour contenir un message d'erreur, et la vue du composant Todos
a été modifiée pour afficher un message d'erreur s'il existe, ou une icône de chargement si Data.todos.list
n'est pas un tableau.
URLs dynamiques
Les URLs de requête peuvent inclure des interpolations :
m.request({
method: 'GET',
url: '/api/v1/users/:id',
params: { id: 123 },
}).then(function (user) {
console.log(user.id); // logs 123
});
Dans le code ci-dessus, :id
est remplacé par la valeur correspondante dans l'objet params
, et la requête devient alors GET /api/v1/users/123
.
Les interpolations sont ignorées si aucune donnée correspondante n'existe dans la propriété params
.
m.request({
method: 'GET',
url: '/api/v1/users/foo:bar',
params: { id: 123 },
});
Dans le code ci-dessus, la requête devient alors GET /api/v1/users/foo:bar?id=123
Annulation des requêtes
Il est parfois souhaitable d'annuler une requête. Par exemple, dans un widget d'autocomplétion, il est important de s'assurer que seule la dernière requête aboutit, car ces widgets envoient généralement plusieurs requêtes pendant que l'utilisateur tape. Les requêtes HTTP peuvent se terminer dans un ordre imprévisible en raison de la nature des réseaux. Si une requête autre que la dernière aboutit, le widget risque d'afficher des données moins pertinentes, voire erronées.
m.request()
expose l'objet XMLHttpRequest
sous-jacent via le paramètre options.config
, vous permettant de sauvegarder une référence à cet objet et d'appeler sa méthode abort
lorsque nécessaire :
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;
}
Téléchargement de fichiers
Pour télécharger des fichiers, vous devez d'abord obtenir une référence à un objet File
. La manière la plus simple de le faire est d'utiliser un <input type="file">
.
m.render(document.body, [m('input[type=file]', { onchange: upload })]);
function upload(e) {
var file = e.target.files[0];
}
L'extrait ci-dessus génère un champ d'entrée de fichier. Si un utilisateur choisit un fichier, l'événement onchange
est déclenché et appelle la fonction upload
. e.target.files
représente une liste d'objets File
.
Ensuite, vous devez créer un objet FormData
pour générer une requête multipart, qui est une requête HTTP spécialement formatée capable d'envoyer des données de fichier dans le corps de la requête.
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
}
Ensuite, vous devez appeler m.request
et définir options.method
sur une méthode HTTP qui utilise un corps (par exemple, POST
, PUT
, PATCH
), en utilisant l'objet FormData
comme 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,
});
}
Si le serveur est configuré pour accepter les requêtes multipart, les informations du fichier seront associées à la clé myfile
.
Téléchargement de plusieurs fichiers
Il est possible de télécharger plusieurs fichiers dans une seule requête. Cela rend le téléchargement par lot atomique, c'est-à-dire qu'aucun fichier ne sera traité s'il y a une défaillance réseau pendant le téléchargement, rendant impossible l'enregistrement partiel des fichiers. Si vous souhaitez enregistrer autant de fichiers que possible en cas de panne de réseau, envisagez de télécharger chaque fichier dans une requête séparée.
Pour télécharger plusieurs fichiers, ajoutez-les simplement tous à l'objet FormData
. Lorsque vous utilisez un champ d'entrée de fichier, vous pouvez obtenir une liste de fichiers en ajoutant l'attribut multiple
à l'élément :
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,
});
}
Surveillance de la progression
Parfois, si une requête est intrinsèquement lente (par exemple, lors d'un téléchargement de fichier volumineux), il est souhaitable d'afficher un indicateur de progression à l'utilisateur pour signaler que l'application est toujours en cours d'exécution.
m.request()
expose l'objet XMLHttpRequest
sous-jacent via le paramètre options.config
, vous permettant d'attacher des écouteurs d'événements à l'objet 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(); // Indique à Mithril.js que les données ont changé et qu'un nouveau rendu est nécessaire
});
},
});
}
Dans l'exemple ci-dessus, un champ d'entrée de fichier est généré. Si l'utilisateur choisit un fichier, un téléchargement est lancé, et dans le callback config
, un gestionnaire d'événements progress
est attaché. Ce gestionnaire d'événements est déclenché chaque fois qu'il y a une mise à jour de la progression dans l'XMLHttpRequest. Étant donné que l'événement de progression de l'XMLHttpRequest n'est pas directement géré par le moteur DOM virtuel de Mithril.js, il est nécessaire d'appeler m.redraw()
pour signaler à Mithril.js que les données ont changé et qu'un redessin est requis.
Conversion de la réponse à un type
En fonction de l'architecture globale de l'application, il peut être souhaitable de transformer les données de réponse d'une requête en une classe ou un type spécifique, par exemple pour uniformiser l'analyse des champs de date ou pour disposer de méthodes d'assistance.
Vous pouvez passer un constructeur comme paramètre options.type
, et Mithril.js l'instanciera pour chaque objet dans la réponse 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
});
Dans l'exemple ci-dessus, en supposant que /api/v1/users
retourne un tableau d'objets, le constructeur User
sera instancié (c'est-à-dire appelé avec new User(data)
) pour chaque objet dans le tableau. Si la réponse retournait un seul objet, cet objet serait utilisé comme argument body
.
Réponses non-JSON
Parfois, un endpoint de serveur ne retourne pas une réponse JSON : par exemple, vous pouvez demander un fichier HTML, un fichier SVG ou un fichier CSV. Par défaut, Mithril.js tente d'analyser la réponse comme si elle était au format JSON. Pour remplacer ce comportement, définissez une fonction personnalisée pour options.deserialize
:
m.request({
method: 'GET',
url: '/files/icon.svg',
deserialize: function (value) {
return value;
},
}).then(function (svg) {
m.render(document.body, m.trust(svg));
});
Dans l'exemple ci-dessus, la requête récupère un fichier SVG, ne fait rien pour l'analyser (car deserialize
retourne simplement la valeur telle quelle), puis rend la chaîne SVG comme HTML de confiance.
Bien sûr, une fonction deserialize
peut être plus complexe :
m.request({
method: 'GET',
url: '/files/data.csv',
deserialize: parseCSV,
}).then(function (data) {
console.log(data);
});
function parseCSV(data) {
// implémentation naïve dans le but de garder l'exemple simple
return data.split('\n').map(function (row) {
return row.split(',');
});
}
En ignorant le fait que la fonction parseCSV
ci-dessus ne gère pas de nombreux cas qu'un analyseur CSV approprié devrait gérer, le code ci-dessus enregistre un tableau de tableaux.
Les en-têtes personnalisés peuvent également être utiles à cet égard. Par exemple, si vous demandez un SVG, vous souhaiterez probablement définir le type de contenu en conséquence. Pour remplacer le type de requête JSON par défaut, définissez options.headers
comme un objet de paires clé-valeur correspondant aux noms et valeurs des en-têtes de requête.
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;
},
});
Récupération des détails de la réponse
Par défaut, Mithril.js tente d'analyser xhr.responseText
comme JSON et retourne l'objet analysé. Il peut être utile d'examiner une réponse du serveur plus en détail et de la traiter manuellement. Cela peut être réalisé en passant une fonction personnalisée pour options.extract
:
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);
});
Le paramètre de options.extract
est l'objet XMLHttpRequest
une fois son opération terminée, mais avant qu'il ne soit transmis à la chaîne de promesses. La promesse peut donc toujours être rejetée si le traitement génère une exception.
Émettre des requêtes vers des adresses IP
En raison de la méthode (très simpliste) utilisée pour détecter les paramètres dans les URL, les segments d'adresse IPv6 sont interprétés à tort comme des interpolations de paramètres de chemin. Les paramètres de chemin nécessitant un séparateur pour être correctement interpolés, cela provoque une erreur.
// Ceci ne fonctionne pas
m.request('http://[2001:db8::990a:cd27:4d9e:79]:8080/some/path', {
// ...
});
Pour contourner ce problème, vous devez passer la paire adresse IPv6 + port comme paramètre.
m.request('http://:host/some/path', {
params: { host: '[2001:db8::990a:cd27:4d9e:79]:8080' },
// ...
});
Ce n'est pas un problème avec les adresses IPv4, et vous pouvez les utiliser normalement.
// Cela fonctionnera comme prévu.
m.request('http://192.0.2.15:8080/some/path', {
// ...
});
Pourquoi JSON au lieu de HTML
De nombreux frameworks côté serveur proposent un moteur de template qui insère les données de la base de données dans un template avant de servir du code HTML (au chargement de la page ou via AJAX), puis utilisent jQuery pour gérer les interactions utilisateur.
En revanche, Mithril.js est un framework conçu pour les applications client riches, qui téléchargent généralement les modèles et les données séparément, puis les combinent dans le navigateur via JavaScript. Réaliser le gros du travail de templating dans le navigateur peut apporter des avantages, comme la réduction des coûts opérationnels en libérant des ressources serveur. La séparation des modèles et des données permet également de mettre en cache le code du modèle plus efficacement et favorise une meilleure réutilisation du code sur différents types de clients (par exemple, bureau, mobile). Un autre avantage est que Mithril.js permet un paradigme de développement d'interface utilisateur en mode retenu, simplifiant ainsi le développement et la maintenance des interactions utilisateur complexes.
Par défaut, m.request
s'attend à ce que les données de réponse soient au format JSON. Dans une application Mithril.js typique, ces données JSON sont généralement consommées par une vue.
Vous devriez éviter d'essayer de rendre du HTML dynamique généré par le serveur avec Mithril. Si vous avez une application existante utilisant un système de templating côté serveur et que vous souhaitez la ré-architecturer, déterminez d'abord si l'effort est réalisable. La migration d'une architecture serveur riche vers une architecture client riche est généralement un effort considérable, impliquant de refactoriser la logique des modèles vers des services de données logiques (et les tests associés).
Les services de données peuvent être organisés de diverses manières selon la nature de l'application. Les architectures RESTful sont populaires auprès des fournisseurs d'API, et les architectures orientées services sont souvent nécessaires dans les environnements avec de nombreux flux de travail hautement transactionnels.
Pourquoi XHR au lieu de fetch
fetch()
est une API Web plus récente pour récupérer des ressources à partir de serveurs, similaire à XMLHttpRequest
.
Mithril.js utilise XMLHttpRequest
au lieu de fetch()
pour m.request
pour plusieurs raisons :
fetch
n'est pas encore entièrement standardisé et peut être sujet à des modifications de spécification.- Les appels
XMLHttpRequest
peuvent être abandonnés avant qu'ils ne soient résolus (par exemple, pour éviter les conditions de concurrence dans les interfaces utilisateur de recherche instantanée). XMLHttpRequest
fournit des hooks pour les écouteurs de progression pour les requêtes de longue durée (par exemple, les téléchargements de fichiers).XMLHttpRequest
est pris en charge par tous les navigateurs, alors quefetch()
n'est pas pris en charge par Internet Explorer et les anciennes versions d'Android (antérieures à 5.0 Lollipop).
Actuellement, en raison du manque de prise en charge du navigateur, fetch()
nécessite généralement un polyfill, qui fait plus de 11kb non compressé - près de trois fois plus grand que le module XHR de Mithril.js.
Bien qu'il soit beaucoup plus petit, le module XHR de Mithril.js prend en charge de nombreuses fonctionnalités importantes et pas si triviales à implémenter comme l'interpolation d'URL et la sérialisation de chaînes de requête en plus de sa capacité à s'intégrer de manière transparente au sous-système de redessin automatique de Mithril.js. Le polyfill fetch
ne prend en charge aucune de ces fonctionnalités et nécessite des bibliothèques et des modèles supplémentaires pour atteindre le même niveau de fonctionnalité.
De plus, le module XHR de Mithril.js est optimisé pour les endpoints basés sur JSON et rend ce cas le plus courant de manière appropriée concise - c'est-à-dire m.request(url)
- alors que fetch
nécessite une étape explicite supplémentaire pour analyser les données de réponse en tant que JSON : fetch(url).then(function(response) {return response.json()})
L'API fetch()
présente quelques avantages techniques par rapport à XMLHttpRequest
dans quelques cas rares :
- elle fournit une API de streaming (dans le sens du "streaming vidéo", pas dans le sens de la programmation réactive), ce qui permet une meilleure latence et une meilleure consommation de mémoire pour les très grandes réponses (au prix de la complexité du code).
- elle s'intègre à l'API Service Worker, qui fournit une couche de contrôle supplémentaire sur la façon et le moment où les requêtes réseau se produisent. Cette API permet également d'accéder aux notifications push et aux fonctionnalités de synchronisation en arrière-plan.
Dans les scénarios typiques, le streaming n'offrira pas d'avantages de performances notables, car il n'est généralement pas conseillé de télécharger des mégaoctets de données au départ. De plus, les gains de mémoire résultant de la réutilisation répétée de petits tampons peuvent être compensés ou annulés s'ils entraînent des repeintes excessives du navigateur. Pour ces raisons, le choix du streaming fetch()
au lieu de m.request
n'est recommandé que pour les applications extrêmement gourmandes en ressources.
Éviter les anti-patterns
Les promesses ne sont pas les données de réponse
La méthode m.request
renvoie une Promise
, et non les données de réponse elles-mêmes. Elle ne peut pas retourner ces données directement, car une requête HTTP peut prendre beaucoup de temps à se terminer (en raison de la latence du réseau). Si JavaScript l'attendait, cela gèlerait l'application jusqu'à ce que les données soient disponibles.
// À ÉVITER
var users = m.request('/api/v1/users'); // Ceci ne fonctionne pas.
console.log('liste des utilisateurs :', users);
// `users` n'est PAS une liste d'utilisateurs, c'est une promesse.
// À PRÉFÉRER
m.request('/api/v1/users').then(function (users) {
console.log('liste des utilisateurs :', users);
});