Como promessas loucas meu dia

Achei melhor compartilhar minha primeira postagem do blog com você aqui:

Recentemente, revisei alguns códigos que escrevi para meu primeiro projeto node.js um pouco maior. Eu estava migrando o site para outra plataforma e tive alguns problemas com o upload de arquivos. Então eu tentei descobrir o que deu errado …

O código a seguir basicamente pega um imageStream, redimensiona a imagem com node-gm (dois tamanhos) e os anexa a um documento couchdb. Eu o escrevi no CoffeeScript, então cuidado!

Minha Pirâmide da Perdição pessoal

handleImage = (file, data, callback) ->
if file.size != 0
imageStream
= fs.createReadStream(file.path)
resizeImage imageStream
, '400', (gmErr, thumbStream1) ->
if !gmErr
attachImage data
, thumbStream1, 'flyer.jpg', (attErr, attData) ->
if !attErr
imageStream
= fs.createReadStream(file.path)
resizeImage imageStream
, '250', (gmThumbErr, thumbStream2) ->
if !gmThumbErr
data
.rev = attData.rev
attachImage data
, thumbStream2, 'flyer_thumb.jpg', (attThumbErr, attThumbData) ->
if !attThumbErr
callback
(null, 'OK')
else
callback
('failed to attach image to document ' + JSON.stringify(data))
else
callback
('failed to resize image 250')
else
callback
('failed to attach image to document ' + JSON.stringify(data))
else
callback
('failed to resize image 400')
else
callback
(null, 'OK')

Caramba , que pirâmide legal que construí lá! Além do código de baixa qualidade (sem nenhum comentário!), O inferno de callback impede que você descubra facilmente o que exatamente está acontecendo lá. Felizmente, ter escrito isso em CoffeeScript traz alguma luz para esta selva misteriosa. Está cheio de ifs e elses e não parece nada bonito.

Promessas de resgate!

Tenho feito promessas ultimamente. Eu gosto do belo estilo de código que essa técnica gera e é uma ferramenta poderosa quando se trata de código assíncrono. Não vou entrar nos detalhes da teoria e da história das promessas; Acho que isso é muito mais coberta . Vou apenas enquadrar a parte de como eles me ajudaram no meu problema específico.

Primeiras coisas primeiro

Para usar promessas, é realmente útil contar com algumas bibliotecas de terceiros sobre este assunto. Mais populares são Q , RSVP.js e when.js . Escolhi Q porque ele fez um bom trabalho para mim e está bem documentado.

Vamos:

npm install q

Não se esqueça de exigir:

Q = require('q')

Agora eu poderia começar a usar promessas. Então, eu tinha todas as minhas funções assíncronas com retornos de chamada e outros enfeites, que precisava ser mudado para corresponder ao Promise / Uma proposta . Mais fácil dizer, eles precisam falar a linguagem da promessa.

Felizmente, Q fornece uma maneira de gerar adiamentos (que retornam promessas) de funções de retorno de chamada existentes. Usei um padrão existente para promisificar minhas funções:

promisify = (asyncFunction, context) ->
->
defer
= Q.defer()
args
= Array.prototype.slice.call(arguments)
args
.push((err, val) ->
if err
return defer.reject(err)
defer
.resolve(val)
)
asyncFunction
.apply(context || {}, args)
defer
.promise

Esta função envolve sua função de retorno de chamada assíncrona e retorna uma promessa criada pelo retorno de chamada adiado. Basicamente, ele resolve oe valrejeita o errdo retorno de chamada aplicado ao asyncFunction. Observe que ele só funciona com as funções de retorno de chamada de estilo padrão .callback(err, val)

Agora é hora de encerrar as funções:

resizeImage = promisify((readStream, size, callback) ->
gm
(readStream, 'img.jpg').resize(size, ' ').stream((err, stdout, stderr) ->
if !err
callback
(null, stdout)
else
callback
(err)
)
)

Sim, é tão fácil. Basta adicionar funções de retorno de chamada e pronto. Agora as funções estão retornando promessas e é possível usar o maravilhoso conjunto de ferramentas de Q nelas.promisify()

O caminho prometido

Sem mais delongas, esta é a função resultante realizada com minhas novas funções de retorno de promessa adquiridas:handleImage()

handleImage = (file, data) ->
if file.size != 0
imageStream
= fs.createReadStream(file.path)
Q
.all([
resizeImage
(imageStream, '400'), # bigThumb
resizeImage
(imageStream, '250') # smallThumb
]).spread((bigThumbStream, smallThumbStream) ->
smallThumbStream
.pause() # The stream has to wait for the first attachImage to be done
attachImage
(data, bigThumbStream, 'flyer.jpg').then((bigThumbData) ->
data
.rev = bigThumbData.rev
smallThumbStream
.resume() # Yeah. Node version 0.8.x
attachImage
(data, smallThumbStream, 'flyer_thumb.jpg')
)
)
else return true

Aaah, muito melhor! Algumas notas sobre isso:

  • Q.all()fornece uma maneira de executar várias funções em paralelo . A promessa retornada é resolvida, quando todas as promessas dentro do array são resolvidas e ela é rejeitada imediatamente se uma delas for rejeitada. Se todas as promessas forem resolvidas, uma matriz dos valores resolvidos é passada para a próxima -Função encadeada .then()
  • A função pode ser usada em vez de se a última promessa passar por uma matriz de valores resolvidos. Ele espalha a matriz nos argumentos de sua função interna. A maneira correspondente de fazer isso deve torná-lo um pouco mais claro:spread()then()then()
...
Q
.all([
resizeImage
(imageStream, '400'),
resizeImage
(imageStream, '250')
]).then((thumbStream) -> # thumbStream is an array!
thumbStream
[1].pause()
attachImage
(data, thumbStream[0] 'flyer.jpg').then((bigThumbData) ->
data
.rev = bigThumbData.rev
thumbStream
[1].resume()
attachImage
(data, thumbStream[1], 'flyer_thumb.jpg')
)
)
...
  • A parte deve ser feita em série porque a segunda imagem precisa saber a revisão do documento após o upload da primeira imagem (o que altera a revisão).attachImage()
  • Caso você esteja se perguntando por que estamos apenas retornando um estático truequando não há imagem anexada (file.size === 0), é porque estamos usando para determinar se há algo que precisa ser resolvido no futuro ou não .Q.when
Q.when(handleImage(uploadedFile, data), () ->
# everything's fine.
, (err) ->
# there was an error.
)

when()é usado quando uma função pode ou não retornar um objeto futuro (promessa). A função no segundo argumento é executada se a handleImage()função retornar uma promessa resolvida ou se retornar um valor estático. A função no terceiro argumento é executada se handleImage()retornar uma promessa rejeitada.

(Tudo feito

Estou muito feliz com os resultados (mesmo que haja bastante espaço para otimização, mas acho que está tudo bem por enquanto) e mesmo não tendo conseguido encontrar o erro tão rápido quanto esperava depois, acho que a refatoração valeu a pena Tempo.

Espero, portanto, poder deixar um pouco mais claro o uso das promessas na prática e, se você leu o post até agora, muito obrigado, você tem sido um leitor muito paciente!