request(options)
Descrição
Realiza requisições XHR (também conhecidas como AJAX) e retorna uma Promise.
m.request({
method: 'PUT',
url: '/api/v1/users/:id',
params: { id: 1 },
body: { name: 'test' },
}).then(function (result) {
console.log(result);
});
Assinatura
promise = m.request(options)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
options | Object | Sim | As opções da requisição a serem passadas. |
options.method | String | Não | O método HTTP a ser usado. Este valor deve ser um dos seguintes: GET , POST , PUT , PATCH , DELETE , HEAD ou OPTIONS . O padrão, caso não especificado, é GET . |
options.url | String | Sim | O nome do caminho para enviar a requisição, opcionalmente interpolado com valores de options.params . |
options.params | Object | Não | Os dados a serem interpolados na URL e/ou serializados na query string. |
options.body | Object | Não | Os dados a serem serializados no corpo da requisição (para métodos que suportam corpo, como POST, PUT, etc.). |
options.async | Boolean | Não | Indica se a requisição deve ser assíncrona. O padrão é true . |
options.user | String | Não | Um nome de usuário para autorização HTTP. O padrão, se não especificado, é undefined . |
options.password | String | Não | Uma senha para autorização HTTP. O padrão, se não especificado, é undefined . Esta opção é fornecida para compatibilidade com XMLHttpRequest , mas você deve evitar usá-la, pois ela envia a senha em texto simples pela rede. |
options.withCredentials | Boolean | Não | Indica se os cookies devem ser enviados para domínios de terceiros. O padrão, caso não especificado, é false . |
options.timeout | Number | Não | A quantidade de milissegundos que uma requisição pode levar antes de ser automaticamente encerrada. O padrão é undefined . |
options.responseType | String | Não | O tipo esperado da resposta. O padrão é "" se extract estiver definido, "json" caso contrário. Se responseType: "json" , ele internamente executa JSON.parse(responseText) . |
options.config | xhr = Function(xhr) | Não | Expõe o objeto XMLHttpRequest subjacente para configuração de baixo nível e substituição opcional (retornando um novo XHR). |
options.headers | Object | Não | Cabeçalhos (headers) a serem anexados à requisição antes de enviá-la (aplicados imediatamente antes de options.config ). |
options.type | any = Function(any) | Não | Um construtor a ser aplicado a cada objeto na resposta. O padrão é a função identidade. |
options.serialize | string = Function(any) | Não | Um método de serialização a ser aplicado a body . O padrão é JSON.stringify , ou se options.body for uma instância de FormData ou URLSearchParams , o padrão é a função identidade (ou seja, function(value) {return value} ). |
options.deserialize | any = Function(any) | Não | Um método de desserialização a ser aplicado a xhr.response ou xhr.responseText normalizado. O padrão é a função identidade. Se extract estiver definido, deserialize será ignorado. |
options.extract | any = Function(xhr, options) | Não | Um hook para especificar como a resposta XMLHttpRequest deve ser lida. Útil para processar dados de resposta, ler cabeçalhos e cookies. Por padrão, esta é uma função que retorna options.deserialize(parsedResponse) , lançando uma exceção quando o código de status da resposta indica um erro ou a resposta é sintaticamente inválida. Se um callback extract personalizado for fornecido, o parâmetro xhr é a instância XMLHttpRequest usada para a requisição, e options é o objeto que foi passado para a chamada m.request . Além disso, deserialize será ignorado e o valor retornado do callback extract será usado diretamente para resolver a Promise. |
options.background | Boolean | Não | Se false , redesenha os componentes montados após a conclusão da requisição. Se true , não redesenha. O padrão é false . |
retorna | Promise | Uma Promise que é resolvida com os dados da resposta, após terem sido processados pelos métodos extract , deserialize e type . Se o código de status da resposta indicar um erro, a Promise é rejeitada, mas isso pode ser evitado definindo a opção extract . |
promise = m.request(url, options)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
url | String | Sim | O nome do caminho para enviar a requisição. options.url substitui este valor quando presente. |
options | Object | Não | As opções da requisição a serem passadas. |
retorna | Promise | Uma Promise que é resolvida com os dados da resposta, após terem sido processados pelos métodos extract , deserialize e type . |
Esta segunda forma é praticamente equivalente a m.request(Object.assign({url: url}, options))
, só que não depende do global ES6 Object.assign
internamente.
Como funciona
O utilitário m.request
é um wrapper fino em torno de XMLHttpRequest
, e permite fazer requisições HTTP para servidores remotos para salvar e/ou recuperar dados de um banco de dados.
m.request({
method: 'GET',
url: '/api/v1/users',
}).then(function (users) {
console.log(users);
});
Uma chamada para m.request
retorna uma Promise e aciona um redesenho após a conclusão de sua cadeia de Promise.
Por padrão, m.request
assume que a resposta está no formato JSON e a analisa em um objeto JavaScript (ou array).
Se o código de status da resposta HTTP indicar um erro, a Promise retornada será rejeitada. Fornecer um callback extract
evitará a rejeição da Promise.
Uso típico
Aqui está um exemplo ilustrativo de um componente que usa m.request
para recuperar alguns dados de um 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,
});
Vamos supor que fazer uma requisição para a URL do servidor /api/items
retorna um array de objetos no formato JSON.
Quando m.route
é chamado na parte inferior, o componente Todos
é inicializado. oninit
é chamado, que chama m.request
. Isso recupera um array de objetos do servidor de forma assíncrona. "Assincronamente" significa que o JavaScript continua executando outro código enquanto espera pela resposta do servidor. Neste caso, significa que fetch
retorna, e o componente é renderizado usando o array vazio original como Data.todos.list
. Uma vez que a requisição para o servidor é concluída, o array de objetos items
é atribuído a Data.todos.list
e o componente é renderizado novamente, produzindo uma lista de <div>
s contendo os títulos de cada todo
.
Tratamento de erros
Quando uma requisição não file:
retorna com qualquer status diferente de 2xx ou 304, ela é rejeitada com um erro. Este erro é uma instância normal de Error, mas com algumas propriedades especiais.
error.message
é definido como o texto de resposta bruto.error.code
é definido como o próprio código de status.error.response
é definido como a resposta analisada, usandooptions.extract
eoptions.deserialize
como é feito com respostas normais.
Isso é útil em muitos casos onde os erros são eles mesmos coisas que você pode contabilizar. Se você quiser detectar se uma sessão expirou - você pode fazer if (error.code === 401) return promptForAuth().then(retry)
. Se você atingir o mecanismo de limitação de uma API e ele retornar um erro com um "timeout": 1000
, você pode fazer um setTimeout(retry, error.response.timeout)
.
Ícones de carregamento e mensagens de erro
Aqui está uma versão expandida do exemplo acima que implementa um indicador de carregamento e uma mensagem de erro:
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,
});
Existem algumas diferenças entre este exemplo e o anterior. Aqui, Data.todos.list
é null
no início. Além disso, há um campo extra error
para armazenar uma mensagem de erro, e a view do componente Todos
foi modificada para exibir uma mensagem de erro se existir, ou exibir um ícone de carregamento se Data.todos.list
não for um array.
URLs dinâmicas
URLs de requisição podem conter interpolações:
m.request({
method: 'GET',
url: '/api/v1/users/:id',
params: { id: 123 },
}).then(function (user) {
console.log(user.id);
});
No código acima, :id
é preenchido com os dados do objeto params
, e a requisição se torna GET /api/v1/users/123
.
Interpolações são ignoradas se não existirem dados correspondentes na propriedade params
.
m.request({
method: 'GET',
url: '/api/v1/users/foo:bar',
params: { id: 123 },
});
No código acima, a requisição se torna GET /api/v1/users/foo:bar?id=123
.
Abortando requisições
Às vezes, é desejável abortar uma requisição. Por exemplo, em um widget de autocompletar/typeahead, você quer garantir que apenas a última requisição seja concluída, porque normalmente os autocompletadores disparam várias requisições enquanto o usuário digita e as requisições HTTP podem ser concluídas fora de ordem devido à natureza imprevisível das redes. Se outra requisição terminar após a última requisição disparada, o widget exibirá dados menos relevantes (ou potencialmente errados) do que se a última requisição disparada terminasse por último.
m.request()
expõe seu objeto XMLHttpRequest
subjacente através do parâmetro options.config
, que permite que você salve uma referência a esse objeto e chame seu método abort
quando necessário:
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;
}
Upload de arquivos
Para fazer upload de arquivos, primeiro você precisa obter uma referência a um objeto File
. A maneira mais fácil de fazer isso é a partir de um <input type="file">
.
m.render(document.body, [m('input[type=file]', { onchange: upload })]);
function upload(e) {
var file = e.target.files[0];
}
O trecho acima renderiza um input de arquivo. Se um usuário escolher um arquivo, o evento onchange
é acionado, que chama a função upload
. e.target.files
é uma lista de objetos File
.
Em seguida, você precisa criar um objeto FormData
para criar uma requisição multipart, que é uma requisição HTTP especialmente formatada que é capaz de enviar dados de arquivo no corpo da requisição.
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
}
Em seguida, você precisa chamar m.request
e definir options.method
para um método HTTP que usa body (por exemplo, POST
, PUT
, PATCH
) e usar o 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,
});
}
Assumindo que o servidor esteja configurado para aceitar requisições multipart, as informações do arquivo serão associadas à key myfile
.
Upload de múltiplos arquivos
É possível fazer upload de múltiplos arquivos em uma única requisição. Fazer isso tornará o upload em lote atômico, ou seja, nenhum arquivo será processado se houver um erro durante o upload, então não é possível ter apenas parte dos arquivos salvos. Se você quiser salvar o máximo de arquivos possível em caso de falha de rede, você deve considerar fazer upload de cada arquivo em uma requisição separada.
Para fazer upload de múltiplos arquivos, simplesmente adicione todos eles ao objeto FormData
. Ao usar um input de arquivo, você pode obter uma lista de arquivos adicionando o atributo multiple
ao 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,
});
}
Monitorando o progresso
Às vezes, se uma requisição é inerentemente lenta (por exemplo, um upload de arquivo grande), é desejável exibir um indicador de progresso para o usuário para sinalizar que o aplicativo ainda está funcionando.
m.request()
expõe seu objeto XMLHttpRequest
subjacente através do parâmetro options.config
, que permite que você anexe event listeners ao 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(); // diz ao Mithril.js que os dados mudaram e um redesenho é necessário
});
},
});
}
No exemplo acima, um input de arquivo é renderizado. Se o usuário escolher um arquivo, um upload é iniciado, e no callback config
, um event handler é registrado. Este event handler é disparado sempre que houver uma atualização de progresso no XMLHttpRequest. Como o evento de progresso do XMLHttpRequest não é diretamente manipulado pelo motor de DOM virtual do Mithril.js, m.redraw()
deve ser chamado para sinalizar ao Mithril.js que os dados foram alterados e um redesenho é necessário.
Convertendo a resposta para um tipo
Dependendo da arquitetura geral do aplicativo, pode ser desejável transformar os dados de resposta de uma requisição para uma classe ou tipo específico (por exemplo, para analisar uniformemente campos de data ou para ter métodos auxiliares).
Você pode passar um construtor como o parâmetro options.type
e o Mithril.js o instanciará para cada objeto na resposta 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);
});
No exemplo acima, assumindo que /api/v1/users
retorna um array de objetos, o construtor User
será instanciado (ou seja, chamado como new User(data)
) para cada objeto no array. Se a resposta retornasse um único objeto, esse objeto seria usado como o argumento body
.
Respostas não-JSON
Às vezes, um endpoint do servidor não retorna uma resposta JSON: por exemplo, você pode estar requisitando um arquivo HTML, um arquivo SVG ou um arquivo CSV. Por padrão, o Mithril.js tenta analisar uma resposta como se fosse JSON. Para substituir esse comportamento, defina uma função 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));
});
No exemplo acima, a requisição recupera um arquivo SVG, não faz nada para analisá-lo (porque deserialize
meramente retorna o valor como está), e então renderiza a string SVG como HTML confiável.
Claro, uma função deserialize
pode ser mais elaborada:
m.request({
method: 'GET',
url: '/files/data.csv',
deserialize: parseCSV,
}).then(function (data) {
console.log(data);
});
function parseCSV(data) {
// implementação ingênua para manter o exemplo simples
return data.split('\n').map(function (row) {
return row.split(',');
});
}
Ignorando o fato de que a função parseCSV
acima não lida com muitos casos que um parser CSV adequado lidaria, o código acima exibe um array de arrays.
Cabeçalhos (headers) personalizados também podem ser úteis a este respeito. Por exemplo, se você estiver requisitando um SVG, você provavelmente vai querer definir o tipo de conteúdo de acordo. Para substituir o tipo de requisição JSON padrão, defina options.headers
para um objeto de pares key-value (chave-valor) correspondentes aos nomes e valores dos cabeçalhos de requisição.
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;
},
});
Recuperando detalhes da resposta
Por padrão, o Mithril.js tenta analisar xhr.responseText
como JSON e retorna o objeto analisado. Pode ser útil inspecionar uma resposta do servidor com mais detalhes e processá-la manualmente. Isso pode ser realizado passando uma função 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);
});
O parâmetro para options.extract
é o objeto XMLHttpRequest uma vez que sua operação é concluída, mas antes de ter sido passado para a cadeia de Promise retornada, então a Promise ainda pode acabar em um estado rejeitado se o processamento lançar uma exceção.
Emitindo fetches para endereços IP
Devido à forma (muito simplista) como os parâmetros são detectados em URLs, os segmentos de endereço IPv6 são confundidos como interpolações de parâmetros de caminho, e como os parâmetros de caminho precisam de algo para separá-los para serem interpolados corretamente, isso resulta em um erro sendo lançado.
// Isso não funciona
m.request('http://[2001:db8::990a:cd27:4d9e:79]:8080/some/path', {
// ...
});
Para contornar isso, você deve passar o par endereço IPv6 + porta como um parâmetro.
m.request('http://:host/some/path', {
params: { host: '[2001:db8::990a:cd27:4d9e:79]:8080' },
// ...
});
Este não é um problema com endereços IPv4, e você pode usá-los normalmente.
// Isso funcionará como você espera
m.request('http://192.0.2.15:8080/some/path', {
// ...
});
Por que JSON em vez de HTML
Muitos frameworks do lado do servidor fornecem um motor de view que interpola dados do banco de dados em um template antes de servir HTML (no carregamento da página ou via AJAX) e, em seguida, empregam jQuery para lidar com as interações do usuário.
Por outro lado, o Mithril.js é um framework projetado para aplicativos thick client, que normalmente baixam templates e dados separadamente e os combinam no navegador via JavaScript. Fazer o trabalho pesado de templating no navegador pode trazer benefícios como reduzir os custos operacionais, liberando recursos do servidor. Separar os templates dos dados também permite que o código do template seja armazenado em cache de forma mais eficaz e permite uma melhor reutilização do código em diferentes tipos de clientes (por exemplo, desktop, mobile). Outro benefício é que o Mithril.js permite um paradigma de desenvolvimento de UI retained mode (modo retido), o que simplifica muito o desenvolvimento e a manutenção de interações complexas do usuário.
Por padrão, m.request
espera que os dados de resposta estejam no formato JSON. Em um aplicativo Mithril.js típico, esses dados JSON são então geralmente consumidos por uma view.
Você deve evitar tentar renderizar HTML dinâmico gerado pelo servidor com Mithril. Se você tiver um aplicativo existente que usa um sistema de templating do lado do servidor e deseja re-arquitetá-lo, primeiro decida se o esforço é viável para começar. Migrar de uma arquitetura de servidor thick para uma arquitetura de cliente thick é normalmente um esforço um tanto grande e envolve refatorar a lógica dos templates para serviços de dados lógicos (e os testes que acompanham).
Os serviços de dados podem ser organizados de muitas maneiras diferentes, dependendo da natureza do aplicativo. Arquiteturas RESTful são populares entre os provedores de API, e arquiteturas orientadas a serviços são frequentemente necessárias onde há muitos fluxos de trabalho altamente transacionais.
Por que XHR em vez de fetch
fetch()
é uma API Web mais recente para buscar recursos de servidores, semelhante a XMLHttpRequest
.
O m.request
do Mithril.js usa XMLHttpRequest
em vez de fetch()
por vários motivos:
fetch
ainda não está totalmente padronizado e pode estar sujeito a alterações de especificação.- As chamadas
XMLHttpRequest
podem ser abortadas antes de serem resolvidas (por exemplo, para evitar condições de corrida em UIs de pesquisa instantânea). XMLHttpRequest
fornece hooks para listeners de progresso para requisições de longa duração (por exemplo, uploads de arquivos).XMLHttpRequest
é suportado por todos os navegadores, enquantofetch()
não é suportado pelo Internet Explorer e Android mais antigos (anteriores ao 5.0 Lollipop).
Atualmente, devido à falta de suporte do navegador, fetch()
normalmente requer um polyfill, que tem mais de 11kb não compactado - quase três vezes maior que o módulo XHR do Mithril.js.
Apesar de ser muito menor, o módulo XHR do Mithril.js suporta muitos recursos importantes e não tão triviais de implementar, como interpolação de URL e serialização de query string, além de sua capacidade de se integrar perfeitamente ao subsistema de redesenho automático do Mithril.js. O polyfill fetch
não suporta nenhum desses e requer bibliotecas e boilerplates extras para atingir o mesmo nível de funcionalidade.
Além disso, o módulo XHR do Mithril.js é otimizado para endpoints baseados em JSON e torna esse caso mais comum apropriadamente conciso - ou seja, m.request(url)
- enquanto fetch
requer uma etapa explícita adicional para analisar os dados de resposta como JSON: fetch(url).then(function(response) {return response.json()})
A API fetch()
tem algumas vantagens técnicas sobre XMLHttpRequest
em alguns casos incomuns:
- ele fornece uma API de streaming (no sentido de "vídeo streaming", não no sentido de programação reativa), o que permite melhor latência e consumo de memória para respostas muito grandes (ao custo de complexidade do código).
- ele se integra à API Service Worker, que fornece uma camada extra de controle sobre como e quando as requisições de rede acontecem. Esta API também permite acesso a notificações push e recursos de sincronização em segundo plano.
Em cenários típicos, o streaming não fornecerá benefícios de desempenho perceptíveis porque geralmente não é aconselhável baixar megabytes de dados para começar. Além disso, os ganhos de memória da reutilização repetida de pequenos buffers podem ser compensados ou anulados se resultarem em repinturas excessivas do navegador. Por esses motivos, escolher o streaming fetch()
em vez de m.request
é recomendado apenas para aplicativos extremamente intensivos em recursos.
Evite anti-padrões
Promises não são os dados de resposta
O método m.request
retorna uma Promise
, não os próprios dados de resposta. Ele não pode retornar esses dados diretamente porque uma requisição HTTP pode levar muito tempo para ser concluída (devido à latência da rede) e, se o JavaScript esperasse por ela, congelaria o aplicativo até que os dados estivessem disponíveis.
// EVITE
var users = m.request('/api/v1/users');
console.log('list of users:', users);
// `users` NÃO é uma lista de usuários, é uma Promise
// PREFIRA
m.request('/api/v1/users').then(function (users) {
console.log('list of users:', users);
});