Javascript - Criando um módulo Ajax com Promises - Parte 4

Após uma série de 4 posts (aqui, aqui, aqui e aqui), vamos começar a ver nosso módulo finalmente funcionando! Vem comigo :D

Vamos testar as funcionalidades do nosso módulo. Começaremos sempre pelo mais simples: o método get.
Ao requisitar uma URL da nossa API, via GET, esse método deve retornar um objeto.

Vamos ver então como ficaria o teste. Nosso arquivo de testes tem um bloco describe que está testando a interface do nosso módulo. Vamos criar então outro bloco describe, para testar a resposta de cada método, referente aos verbos HTTP, separadamente. Adicione ao arquivo tests/test.ajax.js, logo após o primeiro bloco describe:

1
2
3
4
5
6
7
8
9
describe( 'Test `get` method', function() {
it( 'Should return an object', function( done ) {
var ajax = new Ajax();
ajax.get( 'http://localhost:3000/api/users' ).done(function( response ) {
response.should.be.an( 'object' );
done();
});
});
});

Temos várias coisas acontecendo aqui, vamos por partes:

Primeiro criamos nosso novo bloco de testes, na linha 1, onde testaremos o método get do nosso módulo. Criamos o primeiro teste, na linha 2, onde este deveria retornar, como resposta da requisição bem sucedida, um objeto.

Para testar se realmente funciona, precisamos fazer a asserção com a resposta da requisição. Então instanciamos o nosso módulo Ajax, na linha 3, e logo após, na linha 4, fazemos a requisição para uma URL utilizando o método get. Sabemos que esse método retorna outros dois métodos done e error - usados como Promises - que já testamos acima, garantindo que esses métodos existem.

Então passamos para o método done uma função de callback, que será chamada assim que nossa requisição retornar com sucesso. Essa função de callback recebe um parâmetro response, com os dados da nossa requisição.

Enfim, na linha 5, fazemos a asserção, para verificar se o retorno é realmente um objeto. Se você não entendeu como esses métodos encadeados funcionam, formando uma frase, sugiro consultar a documentação do Chai.

Mas o que é essa função done(), sendo invocada na linha 6?

Quando precisamos testar métodos assíncronos, ou seja, quando não sabemos exatamente o momento da resposta, pois dependemos do retorno de um callback, o Mocha nos dá uma função que passamos como parâmetro na função it(), que pode ser invocada logo após nossa asserção, para dizer ao Mocha quando ele deve realmente verificar se nosso teste passa.

Como as requisições do nosso módulo serão sempre assíncronas (ainda iremos definir isso), precisamos usar a função done() para que o teste não execute antes que resposta da requisição esteja realmente pronta.

Se abrirmos a nossa index.html, que contém os testes, veremos que nosso teste NÃO PASSA. Era o que esperávamos, visto que não implementamos nada ainda!

Mas agora sabemos exatamente o que precisa ser feito: criaremos o processo que faz a requisição e responde ao método done() do nosso módulo :D

Primeiro problema: CORS

Ao acessar a index.html, onde tem os testes, podemos ver no console do nosso navegador que temos um problema com CORS. Vamos resolver isso de forma prática, mas que não deve ser feita para toda aplicação: adicionaremos um header na nossa API, que permite que requisições de domínios diferentes consumam essa API. Vamos liberar para todos os domínios, mas fica o alerta: quando você criar uma API Rest, libere somente requisições para domínios específicos, ou então use um acess token para validar as requisições. Mas foi só um alerta, isso é assunto para um post somente sobre APIs Rest.

Voltando ao que interessa: vamos então adicionar o header à nossa API. No arquivo api/app.js, adicione logo após o objeto users:

1
2
3
4
app.use(function( req, res, next ) {
res.setHeader( 'Access-Control-Allow-Origin', '*' );
next();
});

Isso irá garantir que, antes de toda requisição, o header Access-Control-Allow-Origin seja passado, liberando acesso de qualquer domínio à nossa API :)

Agora vamos ao nosso módulo. Em src/ajax.js, vamos mudar um pouco nosso método get:

1
2
3
4
5
6
7
$public.get = function get( url ) {
var xhr = new XMLHttpRequest();
xhr.open( 'GET', url, true );
xhr.addEventListener( 'readystatechange', $private.handleReadyStateChange, false );
xhr.send();
return $private.promises();
};

Quando falamos em TDD, e desenvolver usando baby steps, sabemos que devemos escrever o mínimo de código possível para que nosso teste passe, e então ir refatorando até que o código fique aceitável. Nesse caso, escrevemos código até demais, mas tem um bom motivo: não estamos criando o XMLHttpRequest. Ele já existe, e precisa de uma quantidade mínima de código para funcionar corretamente. E as linhas adicionadas acima é o que precisamos para que uma requisição seja feita corretamente.

Primeiro passamos para o método get o parâmetro url, pois é à partir dele que iremos requisitar os dados do servidor. Na linha 2, instanciamos o objeto XMLHttpRequest, que será responsável por fazer a requisição. Na linha 3, abrimos uma nova conexão, passando o verbo GET (já que estamos usando o método get() do nosso módulo), depois a URL, e o terceiro parâmetro diz se a requisição será assíncrona ou não. Vamos deixá-lo como true, pois teremos callbacks para nos orientar quando a requisição tiver um retorno.

Na linha 4, atrelamos ao nosso objeto um evento chamado readystatechange, onde poderemos tratar todos os passos da requisição. Na linha 5, invocamos o método send(), que irá enviar a requisição ao servidor, para que comece a brincadeira! :D

No final, retornamos nesse método as Promises, pois queremos utilizar os métodos done() e error(), dependendo da resposta da requisição.

Como você sabe, cada listener de evento recebe como segundo parâmetro uma função de callback, onde será feito o tratamento dos dados quando aquele evento for disparado. Vamos ver como ficará o método $private.handleReadyStateChange(), no nosso módulo src/ajax.js:

1
2
3
$private.handleReadyStateChange = function handleReadyStateChange() {
console.log( this.readyState === 4 && this.status === 200 && JSON.parse( this.responseText ) );
};

Antes de qualquer coisa, vamos verificar se os dados são retornados corretamente. Para isso, o objeto XMLHttpRequest tem uma propriedade somente leitura chamada readyState que, quando está em 4, significa que a requisição está completa. A propriedade status retorna o status HTTP da requisição. 200 significa _OK_, ou seja, os dados foram retornados corretamente.

Em responseText, recebemos uma DOMString, com o resultado da requisição. Como estamos retornando um JSON do servidor, usamos o método JSON.parse() para parsear a string, transformando-a em um objeto do Javascript.

A resposta que temos é essa:

Antes de retornar o objeto, podemos ver dois requests acusando 404. Analisando o erro, vemos que é por causa do assert que testa a interface do método get, se ele retorna os métodos done e error. O erro acontece porque não estamos passando uma URL para o método, e o XMLHttpRequest tenta requisitar uma URL que não existe. Precisamos ajustar isso. Temos dois caminhos: ou simplesmente passamos uma string vazia na chamada get() do nosso objeto, ou validamos dentro do nosso módulo que, se não for passada nenhuma URL, ele passa uma string vazia, considerando a URL atual.

Vamos alterar nosso módulo, para manter retrocompatibilidade. Imagine que outras pessoas já estão usando esse módulo, então não podemos fazer com que a atualização do mesmo quebre os projetos que usam versões mais antigas. No método get() do nosso módulo, em src/ajax.js, vamos adicionar:

1
xhr.open( 'GET', url || '', true );

Dessa forma, se não for passada URL alguma, consideramos a URL atual, passando uma string vazia, e não precisamos mudar as implementações anteriores :)

Hora de fazer nossas promises funcionarem! Nosso método get() retorna os métodos done() e error(), mas não é no exato momento do retorno desse método que as informações estarão disponíveis, mas sim no retorno do callback do evento readystatechange. Então nós precisaremos de um objeto auxiliar, que será usado para fazer com que as nossas Promises conversem com o retorno da requisição.

Parece complicado? Vamos ver na prática como fazer isso! Primeiro, no nosso módulo, criaremos o objeto auxiliar. No início do módulo, em src/ajax.js, logo após as declarações dos objetos $public e $private, adicione:

1
2
3
4
$private.methods = {
done: function() {},
error: function() {}
};

Esse objeto conterá os métodos que nos auxiliarão a criar nossas Promises.

Agora, na função de callback do evento readystatechange, vamos adicionar os métodos done() e error() ao $private.methods, e invocá-los, passando a resposta da requisição para eles. Mudando o método $private.handleReadyStateChange(), vamos agora ter o seguinte:

1
2
3
4
5
6
7
8
9
10
$private.handleReadyStateChange = function handleReadyStateChange() {
var xhr = this;
var DONE = 4;
if( xhr.readyState === DONE ) {
if( xhr.status >= 200 && xhr.status < 300 ) {
return $private.methods.done.call( $private.methods, JSON.parse( xhr.responseText ) );
}
$private.methods.error.call( $private.methods, JSON.parse( xhr.responseText ) );
}
};

Só vamos retornar algo quando a requisição estiver completa, por isso fazemos a verificação do xhr.readyState na linha 4. Na linha 5, verificamos se o status HTTP da requisição está OK, ou seja, se for um status entre 200 e 300, a requisição retornou com sucesso.

Na linha 6 é onde acontece a primeira parte da magia das Promises: usamos o objeto $private.methods, invocando o método done() com o call(), e setamos o próprio objeto para ser o this dentro do método done(). Passamos também como parâmetro o JSON de resposta da requisição, já parseado como objeto Javascript.

Mas tem um porém: esse método $private.methods.done( response ), que recebe esse parâmetro response, deve ser a função de callback que passaremos como parâmetro do método done() da Promise.

Para que isso seja possível, vamos mexer um pouco no método $private.promises() do nosso módulo, fazendo com que, quando os métodos done() e error() do módulo Ajax, quando forem chamados, recebam essa função como callback:

1
2
3
4
5
6
7
8
9
10
11
12
$private.promises = function promises() {
return {
done: function done( callback ) {
$private.methods.done = callback;
return this;
},
error: function error( callback ) {
$private.methods.error = callback;
return this;
}
};
};

Complicou? Pois é, eu disse que seria uma aventura :D

Vou explicar o que acontece: quando invocamos o método get() do nosso módulo, através de:

1
2
var ajax = new Ajax();
ajax.get( 'http://localhost:3000/api/users' );

Nós temos disponíveis outros dois métodos: done() e error(), que são chamados através do return $private.promises() dentro do método get() no nosso módulo.

No método $private.promises() - atualizado acima - nós retornamos esses dois métodos done() e error(), que recebem como parâmetro uma função de callback.

Então, nós atribuímos ao objeto $private.methods o callback passado como parâmetro.

Lá na função de callback do evento readystatechange, você viu que invocamos o $private.methods.done e o $private.methods.error com o call, passando o xhr.responseText como parâmetro desses métodos.

É aproveitando-se da natureza funcional do Javascript que conseguimos fazer as Promises conversarem com o retorno dos métodos no momento certo, retornando a interface que temos agora!

E se você perceber, ainda no método $private.promises(), dentro dos métodos done() e error(), nós retornamos o this, que é o próprio objeto retornado nesse método, para que possamos encadear as respostas, exatamente como vimos no primeiro artigo dessa série, mas com jQuery :D

Por isso, nós podemos utilizar nosso módulo dessa forma:

1
2
3
4
5
6
7
8
var ajax = new Ajax();
ajax.get( 'http://localhost:3000/api/users' )
.done(function( response ) {
// => sucesso
})
.error(function() {
// => erro
});

Vamos executar nosso teste para ver se agora passa:

1
2
Error: Uncaught SyntaxError: Unexpected token < (http://localhost/00-opensource/ajax/:1)
at process.on.global.onerror (http://localhost/00-opensource/ajax/public/mocha.js:6366:10)

Opa! Alguma coisa deu errado! O que aconteceu?

Vamos tentar debugar, para ver se a resposta está vindo corretamente. Em nosso teste, em tests/test.ajax.js, vamos adicionar um console.log( response ):

1
2
3
4
5
6
7
8
it( 'Should return an object', function( done ) {
var ajax = new Ajax();
ajax.get( 'http://localhost:3000/api/users' ).done(function( response ) {
console.log( response );
response.should.be.an( 'object' );
done();
});
});

E vemos que, como esperado, o nosso objeto é retornado com sucesso! Mas está dando um erro de sintaxe no teste!

O que acontece é que, em algum momento, o resultado que é retornado por nossa API como JSON, é convertido para string, com toString() (não sei porque o Mocha faz isso), e depois, quando tentamos fazer o parse com JSON.parse, acontece o erro de sintaxe.

Como o formato da resposta pode variar, temos que garantir que nosso módulo traga a resposta corretamente. Então vamos fazer uma pequena modificação no método $private.handleReadyStateChange do nosso método, em src/ajax.js, para ficar assim:

1
2
3
4
5
6
if( xhr.readyState === DONE ) {
if( xhr.status >= 200 && xhr.status < 300 ) {
return $private.methods.done( $private.parseResponse( xhr.responseText ) );
}
$private.methods.error( $private.parseResponse( xhr.responseText ) );
}

Somente vamos chamar um método $private.parseResponse, passando como parâmetro a resposta da requisição. Nesse método, vamos tratar o parse do JSON:

1
2
3
4
5
6
7
8
9
10
$private.parseResponse = function parseResponse( response ) {
var result;
try {
result = JSON.parse( response );
}
catch( e ) {
result = response;
}
return result;
};

E pronto! Simplesmente colocamos o retorno em um bloco try/catch. Se der erro ao tentar parsear, retornamos somente a resposta, que já deve estar em JSON :D

Agora o nosso teste passou! Ufa!

Agora você está vendo como realmente as Promises funcionam. Não precisamos de nenhuma library ou framework para fazê-las funcionar. Claro que não é um trabalho tão simples, mas conhecendo um pouco de programação funcional, nós conseguimos fazer uma funcionalidade onde antes precisaríamos carregar um lib inteira como a Q ou async só para fazer isso!

Mas ainda não acabou: já temos nosso módulo funcional, mas ainda temos testes a fazer! Precisamos testar agora o próximo método: o post.
Mas como esse artigo já está grande demais, o método post ficará para o próximo artigo! Não perca :D

Ficou com dúvida de algo? Não fique com vergonha! Comente :D

Até o próximo! o/

Sobre o #1postperday: https://blog.da2k.com.br/2014/12/31/um-post-por-dia/

Tem alguma sugestão para os próximos posts do #1postperday? Deixe ela aqui: https://github.com/fdaciuk/fdaciuk.github.io/issues/1