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 val
rejeita o err
do 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
true
quando 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!