Como promisificar XHR nativo?

183

Eu quero usar promessas (nativas) no meu aplicativo front-end para executar solicitações XHR, mas sem toda a bobagem de uma estrutura massiva.

Eu quero o meu xhr para retornar uma promessa, mas isso não funciona (dando-me: Uncaught TypeError: Promise resolver undefined is not a function)

function makeXHRRequest (method, url, done) {
  var xhr = new XMLHttpRequest();
  xhr.open(method, url);
  xhr.onload = function() { return new Promise().resolve(); };
  xhr.onerror = function() { return new Promise().reject(); };
  xhr.send();
}

makeXHRRequest('GET', 'http://example.com')
.then(function (datums) {
  console.log(datums);
});
SomeKittens
fonte
2
Consulte também a referência genérica Como converter uma API de retorno de chamada existente em promessas?
Bergi 24/05

Respostas:

369

Suponho que você saiba como fazer uma solicitação XHR nativa (você pode atualizar aqui e aqui )

Como qualquer navegador que suporte promessas nativas também suportará xhr.onload, podemos pular todas as onReadyStateChangeferramentas do tom. Vamos dar um passo atrás e começar com uma função básica de solicitação XHR usando retornos de chamada:

function makeRequest (method, url, done) {
  var xhr = new XMLHttpRequest();
  xhr.open(method, url);
  xhr.onload = function () {
    done(null, xhr.response);
  };
  xhr.onerror = function () {
    done(xhr.response);
  };
  xhr.send();
}

// And we'd call it as such:

makeRequest('GET', 'http://example.com', function (err, datums) {
  if (err) { throw err; }
  console.log(datums);
});

Viva! Isso não envolve nada terrivelmente complicado (como cabeçalhos personalizados ou dados POST), mas é suficiente para nos levar adiante.

O construtor de promessas

Podemos construir uma promessa como esta:

new Promise(function (resolve, reject) {
  // Do some Async stuff
  // call resolve if it succeeded
  // reject if it failed
});

O construtor da promessa assume uma função que recebe dois argumentos (vamos chamá-los resolvee reject). Você pode pensar neles como retornos de chamada, um para o sucesso e outro para o fracasso. Os exemplos são impressionantes, vamos atualizar makeRequestcom este construtor:

function makeRequest (method, url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.onload = function () {
      if (this.status >= 200 && this.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: this.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: this.status,
        statusText: xhr.statusText
      });
    };
    xhr.send();
  });
}

// Example:

makeRequest('GET', 'http://example.com')
.then(function (datums) {
  console.log(datums);
})
.catch(function (err) {
  console.error('Augh, there was an error!', err.statusText);
});

Agora podemos explorar o poder das promessas, encadeando várias chamadas XHR (e .catchisso desencadeará um erro em qualquer uma das chamadas):

makeRequest('GET', 'http://example.com')
.then(function (datums) {
  return makeRequest('GET', datums.url);
})
.then(function (moreDatums) {
  console.log(moreDatums);
})
.catch(function (err) {
  console.error('Augh, there was an error!', err.statusText);
});

Podemos melhorar ainda mais isso, adicionando parâmetros POST / PUT e cabeçalhos personalizados. Vamos usar um objeto de opções em vez de vários argumentos, com a assinatura:

{
  method: String,
  url: String,
  params: String | Object,
  headers: Object
}

makeRequest agora se parece com isso:

function makeRequest (opts) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open(opts.method, opts.url);
    xhr.onload = function () {
      if (this.status >= 200 && this.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: this.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: this.status,
        statusText: xhr.statusText
      });
    };
    if (opts.headers) {
      Object.keys(opts.headers).forEach(function (key) {
        xhr.setRequestHeader(key, opts.headers[key]);
      });
    }
    var params = opts.params;
    // We'll need to stringify if we've been given an object
    // If we have a string, this is skipped.
    if (params && typeof params === 'object') {
      params = Object.keys(params).map(function (key) {
        return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
      }).join('&');
    }
    xhr.send(params);
  });
}

// Headers and params are optional
makeRequest({
  method: 'GET',
  url: 'http://example.com'
})
.then(function (datums) {
  return makeRequest({
    method: 'POST',
    url: datums.url,
    params: {
      score: 9001
    },
    headers: {
      'X-Subliminal-Message': 'Upvote-this-answer'
    }
  });
})
.catch(function (err) {
  console.error('Augh, there was an error!', err.statusText);
});

Uma abordagem mais abrangente pode ser encontrada na MDN .

Como alternativa, você pode usar a API de busca ( polyfill ).

SomeKittens
fonte
3
Você também pode querer adicionar opções para responseType, autenticação, credenciais timeout... E paramsobjetos devem apoiar blobs / bufferviews e FormDatainstâncias
Bergi
4
Seria melhor retornar um novo erro ao rejeitar?
Prasanthv
1
Além disso, não faz sentido retornar xhr.statuse xhr.statusTextcom erro, pois eles estão vazios nesse caso.
Dqd
2
Esse código parece funcionar como anunciado, exceto por uma coisa. Eu esperava que a maneira correta de passar parâmetros para uma solicitação GET fosse via xhr.send (parâmetros). No entanto, as solicitações GET ignoram quaisquer valores enviados para o método send (). Em vez disso, eles só precisam ser parâmetros de string de consulta no próprio URL. Portanto, para o método acima, se você deseja que o argumento "params" seja aplicado a uma solicitação GET, a rotina precisa ser modificada para reconhecer um GET vs. POST e, em seguida, anexa condicionalmente esses valores ao URL entregue ao xhr .abrir().
hairbo
1
Deve-se usar resolve(xhr.response | xhr.responseText);Na maioria dos navegadores, a repsonse está em responseText enquanto isso.
precisa saber é
50

Isso pode ser tão simples quanto o código a seguir.

Lembre-se de que esse código acionará apenas o rejectretorno de chamada quando onerrorfor chamado ( apenas erros de rede ) e não quando o código de status HTTP significar um erro. Isso também excluirá todas as outras exceções. Lidar com isso deve depender de você, IMO.

Além disso, é recomendável chamar o rejectretorno de chamada com uma instância Errore não o evento em si, mas por uma questão de simplicidade, deixei como está.

function request(method, url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open(method, url);
        xhr.onload = resolve;
        xhr.onerror = reject;
        xhr.send();
    });
}

E invocando poderia ser o seguinte:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });
Peleg
fonte
14
@MadaraUchiha Eu acho que é a versão tl; dr. Dá ao OP uma resposta para sua pergunta e somente isso.
Peleg
para onde vai o corpo de uma solicitação POST?
caub
1
@crl assim como em um XHR regulares:xhr.send(requestBody)
Peleg
sim, mas por que você não permitiu isso no seu código? (desde que você parametrizou o método)
caub
6
Eu gosto dessa resposta, pois fornece um código muito simples para trabalhar imediatamente, que responde à pergunta.
Steve Chamaillard
12

Para quem pesquisa isso agora, você pode usar a função de busca . Tem algum suporte muito bom .

fetch('http://example.com/movies.json')
  .then(response => response.json())
  .then(data => console.log(data));

Em primeiro lugar, usei a resposta de @ SomeKittens, mas depois descobri fetchque isso é feito para mim fora da caixa :)

microo8
fonte
2
Navegadores mais antigos não suportam a fetchfunção, mas o GitHub publicou um polyfill .
bdesham
1
Eu não recomendaria fetch, pois ainda não suporta cancelamento.
James Dunne
2
As especificações para a API de busca agora fornecem cancelamento. Até agora, o suporte foi enviado no Firefox 57 bugzilla.mozilla.org/show_bug.cgi?id=1378342 e Edge 16. Demonstrações: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort -api . E existem bugs abertos do recurso Chrome e Webkit bugs.chromium.org/p/chromium/issues/detail?id=750599 & bugs.webkit.org/show_bug.cgi?id=174980 . Como fazer: developers.google.com/web/updates/2017/09/abortable-fetch & developer.mozilla.org/en-US/docs/Web/API/AbortSignal#Examples
sideshowbarker
A resposta a stackoverflow.com/questions/31061838/... tem canceláveis-fetch exemplo de código que até agora já funciona no Firefox 57+ e 16+ Borda
sideshowbarker
1
@ microo8 Seria bom ter um exemplo simples usando o fetch, e aqui parece um bom lugar para colocá-lo.
jpaugh 6/07
8

Penso que podemos tornar a resposta principal muito mais flexível e reutilizável se não criarmos o XMLHttpRequestobjeto. O único benefício de fazer isso é que não precisamos escrever 2 ou 3 linhas de código para fazê-lo, e isso tem a enorme desvantagem de tirar nosso acesso a muitos dos recursos da API, como definir cabeçalhos. Ele também oculta as propriedades do objeto original do código que deve lidar com a resposta (para sucessos e erros). Para que possamos criar uma função mais flexível e amplamente aplicável, basta aceitar o XMLHttpRequestobjeto como entrada e transmiti-lo como resultado .

Essa função converte um XMLHttpRequestobjeto arbitrário em uma promessa, tratando códigos de status diferentes de 200 como um erro por padrão:

function promiseResponse(xhr, failNon2xx = true) {
    return new Promise(function (resolve, reject) {
        // Note that when we call reject, we pass an object
        // with the request as a property. This makes it easy for
        // catch blocks to distinguish errors arising here
        // from errors arising elsewhere. Suggestions on a 
        // cleaner way to allow that are welcome.
        xhr.onload = function () {
            if (failNon2xx && (xhr.status < 200 || xhr.status >= 300)) {
                reject({request: xhr});
            } else {
                resolve(xhr);
            }
        };
        xhr.onerror = function () {
            reject({request: xhr});
        };
        xhr.send();
    });
}

Essa função se encaixa muito naturalmente em uma cadeia de Promises, sem sacrificar a flexibilidade da XMLHttpRequestAPI:

Promise.resolve()
.then(function() {
    // We make this a separate function to avoid
    // polluting the calling scope.
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://stackoverflow.com/');
    return xhr;
})
.then(promiseResponse)
.then(function(request) {
    console.log('Success');
    console.log(request.status + ' ' + request.statusText);
});

catchfoi omitido acima para manter o código de exemplo mais simples. Você deve sempre ter um, e é claro que podemos:

Promise.resolve()
.then(function() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://stackoverflow.com/doesnotexist');
    return xhr;
})
.then(promiseResponse)
.catch(function(err) {
    console.log('Error');
    if (err.hasOwnProperty('request')) {
        console.error(err.request.status + ' ' + err.request.statusText);
    }
    else {
        console.error(err);
    }
});

E desabilitar a manipulação do código de status HTTP não exige muita alteração no código:

Promise.resolve()
.then(function() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://stackoverflow.com/doesnotexist');
    return xhr;
})
.then(function(xhr) { return promiseResponse(xhr, false); })
.then(function(request) {
    console.log('Done');
    console.log(request.status + ' ' + request.statusText);
});

Nosso código de chamada é mais longo, mas conceitualmente, ainda é simples entender o que está acontecendo. E não precisamos reconstruir toda a API de solicitação da Web apenas para suportar seus recursos.

Também podemos adicionar algumas funções de conveniência para organizar nosso código:

function makeSimpleGet(url) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    return xhr;
}

function promiseResponseAnyCode(xhr) {
    return promiseResponse(xhr, false);
}

Então nosso código se torna:

Promise.resolve(makeSimpleGet('https://stackoverflow.com/doesnotexist'))
.then(promiseResponseAnyCode)
.then(function(request) {
    console.log('Done');
    console.log(request.status + ' ' + request.statusText);
});
jpmc26
fonte
5

A resposta de jpmc26 é quase perfeita na minha opinião. Existem algumas desvantagens:

  1. Ele expõe a solicitação xhr apenas até o último momento. Isso não permitePOST que solicitações configurem o corpo da solicitação.
  2. É mais difícil de ler como o crucial send chamada está oculta dentro de uma função.
  3. Introduz um pouco de clichê ao realmente fazer a solicitação.

O macaco que corrige o objeto xhr trata desses problemas:

function promisify(xhr, failNon2xx=true) {
    const oldSend = xhr.send;
    xhr.send = function() {
        const xhrArguments = arguments;
        return new Promise(function (resolve, reject) {
            // Note that when we call reject, we pass an object
            // with the request as a property. This makes it easy for
            // catch blocks to distinguish errors arising here
            // from errors arising elsewhere. Suggestions on a 
            // cleaner way to allow that are welcome.
            xhr.onload = function () {
                if (failNon2xx && (xhr.status < 200 || xhr.status >= 300)) {
                    reject({request: xhr});
                } else {
                    resolve(xhr);
                }
            };
            xhr.onerror = function () {
                reject({request: xhr});
            };
            oldSend.apply(xhr, xhrArguments);
        });
    }
}

Agora, o uso é tão simples quanto:

let xhr = new XMLHttpRequest()
promisify(xhr);
xhr.open('POST', 'url')
xhr.setRequestHeader('Some-Header', 'Some-Value')

xhr.send(resource).
    then(() => alert('All done.'),
         () => alert('An error occured.'));

Obviamente, isso apresenta uma desvantagem diferente: o patch do macaco prejudica o desempenho. No entanto, isso não deve ser um problema, supondo que o usuário esteja aguardando principalmente pelo resultado do xhr, que a própria solicitação leve ordens de magnitude mais longas que a configuração da chamada e que as solicitações do xhr não sejam enviadas com frequência.

PS: E, claro, se estiver direcionando navegadores modernos, use buscar!

PPS: Foi observado nos comentários que esse método altera a API padrão, o que pode ser confuso. Para maior clareza, pode-se aplicar um método diferente ao objeto xhr sendAndGetPromise().

t.animal
fonte
Evito patch de macacos porque é surpreendente. A maioria dos desenvolvedores espera que os nomes de função da API padrão invoquem a função da API padrão. Esse código ainda oculta a sendchamada real, mas também pode confundir os leitores que sabem que sendnão têm valor de retorno. O uso de chamadas mais explícitas torna mais claro que lógica adicional foi invocada. Minha resposta precisa ser ajustada para lidar com argumentos send; no entanto, provavelmente é melhor usar fetchagora.
jpmc26
Eu acho que depende. Se você devolver / expor a solicitação xhr (que parece duvidosa), você está absolutamente certo. No entanto, não vejo por que alguém não faria isso dentro de um módulo e expunha apenas as promessas resultantes.
usar o seguinte código
Estou me referindo especificamente a qualquer um ter que manter o código que você fazê-lo em.
jpmc26
Como eu disse: depende. Se o seu módulo é tão grande que a função promisify se perde entre o restante do código, você provavelmente tem outros problemas. Se você possui um módulo no qual deseja chamar alguns pontos de extremidade e retornar promessas, não vejo problema.
usar o seguinte código
Não concordo que isso depende do tamanho da sua base de código. É confuso ver uma função API padrão fazer algo diferente de seu comportamento padrão.
precisa saber é o seguinte