tcxsproject.com.br livros hacker gorpo orko... · erros. aprenderemos a automatizar esse processo....
TRANSCRIPT
ImpressoePDF:978-85-94188-00-7
EPUB:978-85-94188-01-4
MOBI:978-85-94188-02-1
Você pode discutir sobre este livro no Fórum da Casa doCódigo:http://forum.casadocodigo.com.br/.
Casovocêdesejesubmeteralgumaerrataousugestão,acessehttp://erratas.casadocodigo.com.br.
ISBN
Dedico este livro à minha primeira filha, pois, sem ela, eleprovavelmente não teria existido. A motivação de deixarmaterializado nesta obra boa parte do conhecimento que tenhoveio da vontade de torná-lo facilmente acessível para ela, casoalgum dia deseje trilhar o caminho do pai. Boa parte dasexplicações que vocês encontrarão aqui foram construídasenquantovigiavaseusono,pensandoemumadidáticadepaiparafilha.
Porfim,otítulo"CangaceiroJavaScript"nãofoiescolhidoporacaso. Foi uma tentativa de homenagear parte da nossa cultura,seja na história fictícia escrita pormim que inicia cada parte dolivro,comotambémnascitaçõesdeoutrasobrasliterárias.
AGRADECIMENTOS
OSertão,paraosforasteiros,pareceumaterradepoucavida,devegetação rasteira e compoucosanimais àvista,diferentedasgrandes florestas tropicais daZonadaMata, cheias de barulho evida.Masosertanejonãoseengana,sejaplantandonaterraduraouusandoasfolhasdojuazeiro,eletransformaoambientedifíciletira o seu sustento desse lugar que parece impossível de seaproveitar.
O homem e a mulher do Sertão são frutos desse ambienteinóspito, e precisam lidar com a dificuldade de onde estãodiariamente e se acostumaram a seguir em frente independentedosproblemasquesurgem.Masnãopensequesãopessoastristeseduras. Uma sanfona, um triângulo, uma rabeca e uma zabumbaparainspiraradança,juntodeumtachodecanjica,queijocoalhoe trouxinhasdepamonhaenroladasna folhadomilhoevocêvaiverfestapradotônenhumbotardefeito.
Nestelivro,vocêvaicorrerpeloSertãodoJavaScript,seguindoo cangaceiro FlávioAlmeida (@flaviohalmeida) para aprenderosdetalhesesegredosdestalinguagem,construindoumaaplicaçãodeverdade de ponta a ponta. Com o seu editor de código favoritocomo sua confiável peixeira, falando Vixe Maria sempre queencontrarumbugedançandoaqueleforróagarradinhoquandoocódigo vai pra produção, atravessar essa terra inóspita que édesenvolversoftwareseráumagrandeaventura.QuePadimCiçoeFreiDamiãoalumeiemoseucaminho!
PREFÁCIOPORMAURÍCIOLINHARES
Figura1:FlávioAlmeida
Flávio Almeida é desenvolvedor e instrutor na Caelum.Também é instrutor na Alura, contribuindo com mais de 20treinamentosparaestaplataformadeensinoonline.AutordolivroMean: Full stack JavaScript para aplicações web com MongoDB,Express,AngulareNode,possuimaisde15anosdeexperiêncianaáreadedesenvolvimento.BacharelemInformáticacomMBAemGestão de Negócios em TI, tem Psicologia como segundagraduação eprocura aplicaroque aprendeunodesenvolvimentodesoftwareenaeducação.
Atualmente, foca na plataforma Node.js e na linguagemJavaScript, tentando aproximar aindamais o front-end do back-end. Já palestrou e realizou workshops em grandes conferênciascomo QCON e MobileConf, e está sempre ávido por novoseventos.
SOBREOAUTOR
"Mestre não é quem sempre ensina, mas quem de repenteaprende."-GrandeSertão:Veredas
Talveznenhumaoutra linguagemtenhaconseguido invadirocoletivo imagináriodosdesenvolvedorescomoJavaScript fez.Emsua história fabular em busca de identidade, foi a única queconseguiuseenraizarnosnavegadores,enosúltimosanospassouaempoderarservidoresdealtaperformanceatravésdaplataformaNode.js. Com tudo isso, tornou-se uma linguagem em que tododesenvolvedorprecisateralgumníveldeconhecimento.
Comacomplexidadecadavezmaiordosproblemas,soluçõesninjasnão sãomais suficientes; é preciso soluções cangaceiras.Énessecontextoqueestelivroseencaixa,ajudando-oatranscenderseuconhecimento,tornando-oumCangaceiroJavaScript.
Este livro destina-se àqueles que desejam aprimorar amanutenção e legibilidade de seus códigos aplicando osparadigmasdaOrientaçãoaObjetosedaProgramaçãoFuncional,inclusivepadrõesdeprojetos.Porfim,eleajudarátambémaquelesque desejam trabalhar com frameworks Single Page Application(SPA) conceituados do mercado, mas que carecem de umconhecimentomaisaprofundadodalinguagemJavaScript.
INTRODUÇÃO
Aquemsedestinaolivro
Procureiutilizarumalinguagemsimplesparatornarestelivroacessívelaumamploespectrodedesenvolvedores.Noentanto,énecessário que o leitor tenha algum conhecimento, mesmo quebásico, da linguagem JavaScript para que tenha um melhoraproveitamento.
Oprojetodestelivroseráumcadastrodenegociaçõesdebolsade valores. Ele será construído através do paradigma funcional eorientadoaobjetosaomesmotempo,utilizandoomelhordosdoismundos.
Figura1:Previewdaaplicação
Emumprimeiromomento,oescopodanossaaplicaçãopodeparecerbemreduzido,maséosuficientepara lançarmosmãodeumasériede recursosacumuladosna linguagematéa suaversãoECMAScript2017(ES8).
Paraqueoleitorpreparesuasexpectativassobreoqueestáporvir,segueumbreveresumodecadacapítulo:
Visãogeraldanossajornada
Capítulo 01: escreveremos um código rapidamente sem nospreocuparmoscomboaspráticasousuareutilização, sentindonapeleasconsequênciasdessaabordagem.Éapartirdelequetodaatrama do livro dará início, inclusive a motivação para umaestruturaaplicandoomodeloMVC.
Capítulo 02: materializar uma representação de algo domundo real em código é uma das tarefas do desenvolvedor.Veremoscomooparadigmaorientadoaobjetospodenosajudarnaconstruçãodeummodelo.
Capítulo03:doqueadiantaummodeloseousuárionãopodeinteragircomele?Criaremosumcontroller,aponteentreasaçõesdousuárioeomodelo.
Capítulo 04: lidar com data não é uma tarefa trivial emqualquer linguagem, mas se combinarmos recursos da próprialinguagemJavaScriptpodeseralgobemdivertido.
Capítulo05:nãoéapenasumanegociaçãoquepossuiregras,umalistadenegociaçãotambém.Criaremosmaisummodelocomsuasrespectivasregras.
Capítulo 06: do que adianta termos um modelo que émodificado pelo usuário se ele não consegue ver o resultado?Aprenderemosaligaromodelocomaview.
Capítulo07: criar ummodelo, modificá-lo com as ações dousuárioeapresentá-loéumatarefacorriqueira.Seráquepodemosisolaresseprocessoereutilizá-lo?Comcerteza!
Capítulo08: jogar a responsabilidade de atualizar a view nocolododesenvolvedortodavezqueomodelomudarestásujeitoa
erros.Aprenderemosaautomatizaresseprocesso.
Capítulo 09: aplicaremos o padrão de projeto Proxy paraimplementarmos nossa própria solução de data binding, queconsiste na atualização da view toda vez que o modelo sofreralterações.
Capítulo 10: isolaremos a responsabilidade da criação deproxies em uma classe, aplicando o padrão de projeto Factory epermitindoqueelasejareutilizadapelanossaaplicação.
Capítulo11: aprenderemos a lidar com exceções, inclusive acriá-las.
Capítulo 12: trabalharemos com o velho conhecidoXMLHttpRequestparanosintegrarmoscomaAPIfornecidacomoprojeto.
Capítulo 13: combateremos o callback hell, resultado daprogramação assíncrona com callbacks através do padrão deprojetoPromise.IsolaremosacomplexidadedesetrabalharcomoXMLHttpRequestemclassesdeserviços.
Capítulo 14: negociações cadastradas são perdidas toda vezque a página é recarregada ou quando o navegador é fechado.UtilizaremosoIndexedDB,umbancopresenteemtodonavegadorparapersistirnossasnegociações.
Capítulo 15: não basta termos uma conexão com oIndexedDB.Aplicaremosumconjuntodeboaspráticasparalidarcomaconexão, facilitandoassimamanutençãoe legibilidadedocódigo.
Capítulo 16: aplicaremos o padrão de projeto DAO para
esconderdodesenvolvedorosdetalhesdeacessoaobancoetornaraindamaislegívelocódigoescrito.
Capítulo 17: o tendão de Aquiles da linguagem JavaScriptsemprefoioescopoglobaleasdependênciasentrescripts,jogandoa responsabilidade da ordem de carregamento no colo dodesenvolvedor.Aprenderemosausarosistemademódulospadrãodo próprio JavaScript para atacar esses problemas. Durante esseprocesso,aprenderemosalidarcomBabel,otranscompiladormaisfamosodomundoopensource.
Capítulo18:veremoscomoousodepromisescomasync/waittorna nosso código assíncrono mais fácil de ler e de manter.Aplicaremosmaispadrõesdeprojetos.
Capítulo19: aplicaremosopadrãodeprojetoDecoratorcomauxílio do Babel, inclusive aprenderemos a realizar requisiçõesassíncronasatravésdaAPIFetch,simplificandoaindamaisnossocódigo.Nosaprofundaremosnametaprogramaçãocomreflect-metadata.
Capítulo 20: utilizaremos Webpack para agrupar nossosmódulos e aplicar boaspráticas em tempodedesenvolvimento edeprodução.InclusiveaprenderemosafazerodeploydaaplicaçãonoGitHubPages.
Ao final do livro, o leitor terá um arsenal de recursos dalinguagem e padrões de projetos que vão ajudá-lo a resolverproblemasdodiaadia.Acriaçãodeumminiframeworkéapenasparadeixaraaprendizagemmaisdivertida.
Por fim, o leitor poderá acompanhar o autor pelo twitter@flaviohalmeidaepeloblog(http://cangaceirojavascript.com.br).
Agoraquejátemosumavisãogeraldecadacapítulo,veremosodownloaddoprojetoeainfraestruturamínimanecessária.
A infraestrutura necessária para o projeto construído até ocapítulo 10 é apenas oGoogle Chrome. É importante que essenavegador seja usado, pois só atacaremos questões decompatibilidadeapartirdocapítulo11.Aofinaldolivro,sinta-seàvontadeparautilizarqualquernavegador.
Do capítulo 12 em diante, precisaremos do Node.js(https://nodejs.org/en/)instalado.Duranteacriaçãodesteprojeto,foi utilizada a versão 8.1.3.Mas não há problema baixar versõesmais novas, contanto que sejam versões pares, as chamadasversõesLTS(longtermsupport).
Háduasformasdebaixaroprojetobasedestelivro.Aprimeiraé clonar o repositório dohttps://github.com/flaviohenriquealmeida/jscangaceiro.
Você também pode baixar o seu zip no endereçohttps://github.com/flaviohenriquealmeida/jscangaceiro/archive/master.zip.Escolhaaquelaqueformaiscômodaparavocê.
Noentanto,casotenhamoptadopelaversãozip, depois dedescompactá-la, renomeie a pasta jscangaceiro-master parajscangaceiro.
Oprojetopossuiaseguinteestrutura:
Infraestruturanecessária
Downloaddoprojeto
├──client│├──app│├──css│├──index.html└──server
Existemduaspastas,clienteserver.Aprimeiracontémtodoocódigoquerodaránonavegador,inclusiveédentrodapastaclient/appqueficarátodoocódigoquecriaremosatéofimdolivro.Asdemaispastasdentrodeclient possuemos arquivoscssdoBootstrap.
O Bootstrap nada mais é do que uma biblioteca CSS quepermiteaplicarumvisualprofissionalemnossaaplicaçãocompouco esforço, apenas com o uso de classes.Não é necessárioqueoleitortenhaconhecimentopréviodele.
Ainda na pasta client, temos o arquivo index.html, aúnicapáginaqueutilizaremosaologodoprojeto,nossopontodepartida:
<!--client/index.html-->
<!DOCTYPEhtml><html><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width"><title>Negociações</title><linkrel="stylesheet"href="css/bootstrap.css"><linkrel="stylesheet"href="css/bootstrap-theme.css"></head><bodyclass="container">
<h1class="text-center">Negociações</h1>
<formclass="form">
<divclass="form-group"><labelfor="data">Data</label><inputtype="date"id="data"class="form-control"requiredautofocus/></div>
<divclass="form-group"><labelfor="quantidade">Quantidade</label><inputtype="number"min="1"step="1"id="quantidade"class="form-control"value="1"required/></div>
<divclass="form-group"><labelfor="valor">Valor</label><inputid="valor"type="number"class="form-control"min="0.01"step="0.01"value="0.0"required/></div>
<buttonclass="btnbtn-primary"type="submit">Incluir</button></form>
<divclass="text-center"><buttonid="botao-importa"class="btnbtn-primarytext-center"type="button">ImportarNegociações</button><buttonid="botao-apaga"class="btnbtn-primarytext-center"type="button">Apagar</button></div><br><br>
<tableclass="tabletable-hovertable-bordered">
<thead><tr><th>DATA</th><th>QUANTIDADE</th><th>VALOR</th>
<th>VOLUME</th></tr></thead>
<tbody></tbody>
<tfoot></tfoot></table>
</body></html>
Porfim,apastaserversóseráusadaapartirdocapítulo12.Ela disponibiliza um servidor web com as APIs que serãoconsumidaspelanossaaplicação.InclusiveesteservidoréfeitoemNode.js, um dos motivos que tornam essa plataforma umadependência de infraestrutura.As instruções de como levantar oservidorserãoapresentadasassimqueoseuusofornecessário.
VISUALSTUDIOCODE(OPCIONAL)
Durante a criação do projeto, foi utilizado o Visual StudioCode(https://code.visualstudio.com/download),umeditordetexto gratuito e multiplataforma disponível para Windows,LinuxeMAC.Sinta-se livrepara escolher aquele editorquevocêpreferir.
Com o projeto baixado e os requisitos de infraestruturaatendidos, podemos começar nossa jornada através desse sertão
Dandoinícioànossajornada
queéoJavaScript.Masassimcomoemqualquertramacominício,meioefim,nopróximocapítuloteremosumprólogoquemotivaosdemaiscapítulos.
1
2
1112
14
141520232530343740434648
Sumário
Parte1-Ocaminhodocangaceiro
1Prólogo:eraumaveznosertão
1.1Oproblemadonossocódigo1.2OpadrãoMVC(Model-View-Controller)
2Negociarcomocangaceiro,temcoragem?
2.1Opapeldeummodelo2.2AclasseNegociação2.3Construtordeclasse2.4Métodosdeclasse2.5Encapsulamento2.6Asintaxeget2.7Objetosimutáveis2.8Ainstânciaéimutávelmesmo?2.9Programaçãodefensiva2.10MenosverbosidadenoconstructorcomObject.assign2.11Atalhoparapropriedadesdeobjetosliterais2.12Assurpresasdedeclaraçõescomvar
SumárioCasadoCódigo
5153
58
585962677073767785
89
9397
101103
107
107110116
121
121122129
2.13Declaraçãodevariáveiscomlet2.14TemporalDeadZone
3Nocangaço,éaçãoparatodolado
3.1Opapeldeumcontrolador3.2AclasseNegociacaoController3.3Associandométodosdocontrolleràsaçõesdousuário3.4EvitandopercorrerdesnecessariamenteoDOM3.5CriandoumainstânciadeNegociação3.6CriandoumobjetoDateapartirdaentradadousuário3.7Umdesafiocomdatas3.8Resolvendoumproblemacomoparadigmafuncional3.9Arrowfunctions:deixandoocódigoaindamenosverboso
4Doispesos,duasmedidas?
4.1Isolandoaresponsabilidadedeconversãodedatas4.2Métodosestáticos4.3Templateliteral4.4Aboapráticadofail-fast
5Obandodeveseguirumaregra
5.1Criandoumnovomodelo5.2OtendãodeAquilesdoJavaScript5.3Blindandoonossomodelo
6Amodanocangaço
6.1OpapeldaView6.2NossasoluçãodeView6.3Construindoumtemplatedinâmico
CasadoCódigoSumário
135137
140
143146151156157159
162
163
167173177
181
182183187193196197200202206
6.4Totalizandoovolumedenegociações6.5Totalizandocomreduce
7Oplano
7.1Parâmetrodefault7.2CriandoaclasseMensagemView7.3Herançaereutilizaçãodecódigo7.4Classesabstratas?7.5Parasabermais:super7.6Adquirindoumnovohábitocomconst
Parte2-ForçaVolante
8Umcangaceirosabedelegartarefas
8.1EseatualizarmosaViewquandoomodeloforalterado?8.2Driblandoothisdinâmico8.3Arrowfunctioneseuescopoléxico
9Enganaramocangaceiro,será?
9.1OpadrãodeprojetoProxy9.2AprendendoatrabalharcomProxy9.3Construindoarmadilhasdeleitura9.4Construindoarmadilhasdeescrita9.5ReflectAPI9.6Umproblemanãoesperado9.7Construindoarmadilhasparamétodos9.8UmapitadadoES2016(ES7)9.9AplicandoasoluçãoemNegociacaoController
SumárioCasadoCódigo
210
210216218224
226
226227229233235
239
239241248255
263
264265267
270275280284
10Cúmplicenaemboscada
10.1OpadrãodeprojetoFactory10.2Nossoproxyaindanãoestá100%!10.3AssociandomodeloeViewatravésdaclasseBind10.4ParâmetrosREST
11Datadosinfernos!
11.1Oproblemacomoinputdate11.2Ajustandonossoconverter11.3Lidandocomexceções11.4Criandonossaprópriaexceção:primeiratentativa11.5Criandonossaprópriaexceção:segundatentativa
12Pilhandooqueinteressa!
12.1Servidoreinfraestrutura12.2RequisiçõesAjaxcomoobjetoXMLHttpRequest12.3Realizandooparsedaresposta12.4Separandoresponsabilidades
13Lutandoatéofim
13.1CallbackHELL13.2OpadrãodeprojetoPromise13.3CriandoPromises13.4CriandoumserviçoparaisolaracomplexidadedoXMLHttpRequest13.5ResolvendoPromisessequencialmente13.6ResolvendoPromisesparalelamente13.7Ordenandooperíodo
CasadoCódigoSumário
289293
297
298
298304306309311316
322
322326329332
338
345
346347349352353355
13.8Impedindoimportaçõesduplicadas13.9Asfunçõesfilter()esome()
Parte3-Arevelação
14Aalgibeiraestáfurada!
14.1IndexedDB,obancodedadosdonavegador14.2Aconexãocomobanco14.3Nossaprimeirastore14.4Atualizaçãodobanco14.5Transaçõesepersistência14.6Cursores
15Colocandoacasaemordem
15.1AclasseConnectionFactory15.2CriandoStores15.3Garantindoumaconexãoapenasporaplicação15.4OpadrãodeprojetoModulePattern15.5MonkeyPatch:grandespoderestrazemgrandesresponsabilidades
16Entrandonalinha
16.1OpadrãodeprojetoDAO16.2CriandonossoDAOdenegociações16.3Implementandoalógicadeinclusão16.4Implementandoalógicadalistagem16.5CriandoumaDAOFactory16.6Combinandopadrõesdeprojeto
SumárioCasadoCódigo
358360362
366
366367370376377379380382
385
387389393397398401402
407
408410415417
16.7Exibindotodasasnegociações16.8Removendotodasasnegociações16.9Factoryfunction
17Dividirparaconquistar
17.1MódulosdoES2015(ES6)17.2Instalandooloader17.3Transformandoscriptsemmódulos17.4Opapeldeumtranscompilador17.5Babel,instalaçãoebuild-step17.6Sourcemap17.7Compilandoarquivosemtemporeal17.8Barrel,simplificandoaimportaçãodemódulos
18Indoalém
18.1ES2017(ES8)eoaçúcarsintáticoasync/await18.2Parasabermais:generators18.3Refatorandooprojetocomasync/wait18.4GarantindoacompatibilidadecomES201518.5Lidandomelhorcomexceções18.6Debouncepattern:controlandoaansiedade18.7ImplementandooDebouncepattern
19Chegandoaolimite
19.1OpadrãodeprojetoDecorator19.2SuportandoDecoratoratravésdoBabel19.3UmproblemanãoesperadocomnossoDecorator19.4ElaborandoumDOMInjector
CasadoCódigoSumário
418424428432435437440
445
446446448450453456457461469
472473475479480483486487492
19.5Decoratordeclasse19.6SimplificandorequisiçõesAjaxcomaAPIFetch19.7ConfigurandoumarequisiçãocomAPIFetch19.8Validaçãocomparâmetrodefault19.9Reflect-metadata:avançandonametaprogramação19.10AdicionandometadadoscomDecoratordemétodo19.11ExtraindometadadoscomumDecoratordeclasse
20Enfrentamentofinal
20.1Webpack,agrupadordemódulos20.2PreparandooterrenoparaoWebpack20.3Otemívelwebpack.config.js20.4Babel-loader,aponteentreoWebpackeoBabel20.5Preparandoobuilddeprodução20.6Mudandooambientecomcross-env20.7WebpackDevServereconfiguração20.8TratandoarquivosCSScomomódulos20.9ResolvendooFOUC(FlashofUnstyledContent)20.10Resolvemosumproblemaecriamosoutro,mastemsolução!20.11Importandoscripts20.12Lidandocomdependênciasglobais20.13OtimizandoobuildcomScopeHoisting20.14Separandonossocódigodasbibliotecas20.15Gerandoapáginaprincipalautomaticamente20.16Simplificandoaindamaisaimportaçãodemódulos20.17CodesplittingeLazyloading20.18System.importvsimport
SumárioCasadoCódigo
493494496500
20.19Quaissãoosarquivosdedistribuição?20.20DeploydoprojetonoGitHubPages20.21AlterandooendereçodaAPInobuilddeprodução20.22Consideraçõesfinais
CasadoCódigoSumário
Avidatalhouemsuacarnetudooqueelesabia.Masduranteumanoiteacéuaberto,enquantofixavaseuolharnafogueiraqueoaquecia, foi tomado de súbito por uma visão. Nela, Lampiãoaparecia apontando para a direção oposta àquela em que seencontrava.Pormaisfugazqueavisãotenhasido,eleteveacertezade que ainda faltava um caminho a ser trilhado, o caminho docangaceiro. E então, ele juntou suas coisas e tomou seu rumo.Mesmo sabendo que havia cangaceiros melhores e mais bempreparadosdoqueele,nãoesmoreceueaindaapertouopassocomacertezadequeestavanocaminhocerto.Namanhãseguinte,vendoosolnascereolhandoohorizonte,entendeuquetudosódependiadesuavontade.
Parte1-Ocaminhodocangaceiro
CAPÍTULO1
"Quemdesconfiaficasábio."-GrandeSertão:Veredas
Um cangaceiro não pode existir sem uma trama que ocontextualize. Com o nosso projeto, não será diferente. Estecapítuloéoprólogoquepreparaoterrenodoqueestáporvir.Atramagiraemtornodeumaaplicaçãoquepermiteocadastrodenegociaçõesdebolsadevalores.Apesardeodomíniodaaplicaçãoserreduzido,ésuficienteparaaprimorarmosnossoconhecimentodalinguagemJavaScriptatéofinaldolivro.
Para a nossa comunidade, o projeto que baixamos naintrodução possui a página client/index.html , com umformulárioprontoejáestilizadocomBootstrap,comtrêscamposdeentradaparacapturaremrespectivamenteadata,aquantidadeeovalordeumanegociação.Vejamosumdoscampos:
<!--client/index.html--><!--códigoanterioromitido-->
<formclass="form">
PRÓLOGO:ERAUMAVEZNOSERTÃO
2 1PRÓLOGO:ERAUMAVEZNOSERTÃO
<divclass="form-group"><labelfor="data">Data</label><inputtype="date"id="data"class="form-control"requiredautofocus/></div><!--códigoposterioromitido-->
Como cada campo possui umid, fica fácil trazermos umareferência desses elementos para que possamosmanipulá-los emnosso código JavaScript. Vamos escrever todo o código destecapítulo no novo arquivo client/app/index.js . Sempestanejar, vamos importá-lo logo de uma vez emclient/index.html,antesdofechamentodatag</body>:
<!--client/index.html--><!--códigoanterioromitido-->
</table>
<scriptsrc="app/index.js"></script></body></html>
Dentrodoarquivoclient/js/index.js,vamoscriaroarray campos que armazenará uma referência para cada um doselementosdeentradadoformulário.Abuscaseráfeitaatravésde document.querySelector() , uma API do DOM que nospermitebuscarelementosatravésdeseletoresCSS:
//client/app/index.js
varcampos=[document.querySelector('#data'),document.querySelector('#valor'),document.querySelector('#quantidade')];
console.log(campos);//verificandooconteúdodoarray
1PRÓLOGO:ERAUMAVEZNOSERTÃO 3
Vamos recarregar a página no navegador e ver a saída noConsoledonavegador:
Figura1.1:Formulário
Antes demais nada, vamos buscar o elemento<tbody> da<table>,poisénelequevamos inserira<tr> que criaremoscomosdadosdasnegociações:
//client/app/index.js
varcampos=[document.querySelector('#data'),document.querySelector('#valor'),document.querySelector('#quantidade')];
//precisamosdetbody,poiselereceberáatrquevamosconstrui
vartbody=document.querySelector('tabletbody');
Agoraquetemosnossoarraycampospreenchido,precisamoslidar com a ação do usuário. Olhando na estrutura declient/index.html,encontramososeguintebotão:
<buttonclass="btnbtn-primary"type="submit">Incluir</button>
Eleédotiposubmit;issosignificaque,aoserclicado,faráoformuláriodispararoeventosubmit responsável pelo enviodeseusdados.Noentanto,nãoqueremosenviarosdadosparalugaralgum,queremosapenas lerosvalores inseridospelousuárioemcada campo e construir uma nova <tr>. Aliás, como estamos
4 1PRÓLOGO:ERAUMAVEZNOSERTÃO
usandooatributorequiredemcadainput,sóconseguiremosdisparar o evento submit caso todos sejam preenchidos. Onavegador exibirá mensagens padrões de erro para cada camponãopreenchido.
Agora, através de document.querySelector() , vamosbuscar o formulário através da sua classe .form. De posse doelemento em nosso código, vamos associar a função ao eventosubmitatravésdedocument.addEventListener.Éestafunçãoqueseráchamadaquandoousuáriosubmeteroformulário:
//client/app/index.js
varcampos=[document.querySelector('#data'),document.querySelector('#valor'),document.querySelector('#quantidade')];
console.log(campos);//verificandooconteúdodoarray
vartbody=document.querySelector('tabletbody');
document.querySelector('.form').addEventListener('submit',function(event){
alert('oi');});
Por enquanto, vamos exibir um simples pop-up através dafunçãoalertacadasubmissãodoformulário:
1PRÓLOGO:ERAUMAVEZNOSERTÃO 5
Figura1.2:Formulário
Com a certeza de que nossa função está sendo chamada nasubmissão do formulário, já podemos dar início ao código queconstróidinamicamenteumanova<tr>, aquelaqueconteráosdados das negociações do formulário. Através dedocument.createElement,criamosumnovoelemento:
//client/app/index.js
//códigoanterioromitido
vartbody=document.querySelector('tabletbody');
document.querySelector('.form').addEventListener('submit',function(event){
//substituindooalertpelonovocódigovartr=document.createElement('tr');});
TodoarraypossuiafunçãoforEach quenospermite iterarem cada um dos seus elementos. Para cada elemento iterado,vamoscriarsuarespectivatdquefarápartenofinaldatrqueacabamosdecriar:
//client/app/index.js
6 1PRÓLOGO:ERAUMAVEZNOSERTÃO
//códigoanterioromitido
vartbody=document.querySelector('tabletbody');
document.querySelector('.form').addEventListener('submit',function(event){
vartr=document.createElement('tr');
campos.forEach(function(campo){
//criaumatdseminformaçõesvartd=document.createElement('td');
//atribuiovalordocampoàtdtd.textContent=campo.value;
//adicionaatdnatrtr.appendChild(td);
});});
Ocódigoanteriorcriaumacolunacomovalordecadacampodonossoarraycampos,noentanto,aindafaltaumacolunaparaguardar volume. Toda negociação possui um volume, que nadamaisédoqueovalordanegociaçãomultiplicadopelaquantidadenegociadanodia.Nestecaso,aadiçãodestacolunaprecisaráficarforadonossoforEach:
//client/app/index.js
//códigoanterioromitido
vartbody=document.querySelector('tabletbody');
document.querySelector('.form').addEventListener('submit',function(event){
vartr=document.createElement('tr');
campos.forEach(function(campo){
1PRÓLOGO:ERAUMAVEZNOSERTÃO 7
vartd=document.createElement('td');td.textContent=campo.value;tr.appendChild(td);
});
//novatdquearmazenaráovolumedanegociaçãovartdVolume=document.createElement('td');
//asposições1e2doarrayarmazenamoscamposdequantidadeevalor,respectivamentetdVolume.textContent=campos[1].value*campos[2].value;
//adicionandoatdquefaltavaàtrtr.appendChild(tdVolume);});
Agoraquejátemosatrcompleta,podemosadicioná-laemtbody.Nossocódigoatéestepontodeveestarassim:
//client/app/index.js
varcampos=[document.querySelector('#data'),document.querySelector('#valor'),document.querySelector('#quantidade')
];
console.log(campos);//verificandooconteúdodoarray
vartbody=document.querySelector('tabletbody');
document.querySelector('.form').addEventListener('submit',function(event){
vartr=document.createElement('tr');
campos.forEach(function(campo){
vartd=document.createElement('td');td.textContent=campo.value;tr.appendChild(td);
});
8 1PRÓLOGO:ERAUMAVEZNOSERTÃO
vartdVolume=document.createElement('td');tdVolume.textContent=campos[1].value*campos[2].value;tr.appendChild(tdVolume);
//adicionandoatrtbody.appendChild(tr);});
Vamos realizar um teste cadastrando uma nova negociação.Paranossa surpresa,nadaéexibido.Nossa<tr> é até inserida,mascomoasubmissãodoformuláriorecarregounossapágina,elavoltaparaoestadoinicialsem<tr>.
Lembre-se de que não queremos submeter o formulário,queremosapenaspegarosseusdadosnoeventosubmit. Sendoassim, basta cancelarmos o comportamento padrão do eventosubmit através da chamada de event.preventDefault() .
Nosso código continuará a ser executado,mas o formulário nãoserásubmetido:
//client/app/index.js
varcampos=[document.querySelector('#data'),document.querySelector('#valor'),document.querySelector('#quantidade')
];
console.log(campos);//verificandooconteúdodoarray
vartbody=document.querySelector('tabletbody');
document.querySelector('.form').addEventListener('submit',function(event){
//cancelandoasubmissãodoformulárioevent.preventDefault();
//códigoposterioromitido});
1PRÓLOGO:ERAUMAVEZNOSERTÃO 9
Figura1.3:Páginanãorecarregada
Excelente,nossanegociaçãoaparecenalista,noentanto,éumaboapráticalimparoscamposdoformulário,preparando-oparaopróximocadastro.Alémdisso,faremoscomqueocampodadatarecebaofocoatravésdachamadadafunçãofocus, permitindoque o usuário já comece o novo cadastro, sem ter de clicar noprimeirocampo.Nossocódigofinalficaassim:
//client/app/index.js
varcampos=[document.querySelector('#data'),document.querySelector('#valor'),document.querySelector('#quantidade')
];
console.log(campos);//verificandooconteúdodoarray
vartbody=document.querySelector('tabletbody');
document.querySelector('.form').addEventListener('submit',function(event){
event.preventDefault();
vartr=document.createElement('tr');
10 1PRÓLOGO:ERAUMAVEZNOSERTÃO
campos.forEach(function(campo){
vartd=document.createElement('td');td.textContent=campo.value;tr.appendChild(td);
});
vartdVolume=document.createElement('td');tdVolume.textContent=campos[1].value*campos[2].value;tr.appendChild(tdVolume);
tbody.appendChild(tr);
//limpaocampodadatacampos[0].value='';//limpaocampodaquantidadecampos[1].value=1;//limpaocampodovalorcampos[2].value=0;//focanocampodadatacampos[0].focus();});
Apesar de funcionar, nosso código deixa a desejar eentenderemosomotivoaseguir.
Nenhuma aplicação nasce do nada, mas sim a partir deespecificações criadas das necessidades do cliente. No caso danossa aplicação, ainda é necessário totalizarmos o volume norodapéda tabela,alémdegarantirmosqueadatasejaexibidanoformato dia/mês/ano . Também, precisamos garantir quenegociações,depoisdecriadas,nãosejammodificadas.Porfim,alista de negociações não pode ser alterada, ela só poderá recebernovositense,sequisermosumanovalista,precisaremoscriá-ladozero.
1.1OPROBLEMADONOSSOCÓDIGO
1.1OPROBLEMADONOSSOCÓDIGO 11
Damaneiracomonossaaplicação se encontra,não seránadafácilimplementarcomsegurançaasregrasquevimosnoparágrafoanterior.Nosso códigomistura emumúnico lugaro códigoquerepresenta uma negociação com sua apresentação. Para tornaraindamais desanimador, não temos uma estrutura que pode serreaproveitadaentreprojetos.
Será que um ninja se sairia bem dessa? Ou quem sabe umcangaceiropossabotaracasaemordem?
Existem diversos padrões que podem ser aplicados paraorganizar o código da nossa aplicação, e talvez o mais acessíveldeles seja o padrão MVC (Model-View-Controller). Este padrãocriaumaseparaçãomarcanteentreosdadosdaaplicação(model)esua apresentação (view). Ações do usuário são interceptadas porum controller responsável em atualizar o modelo e refletir seuestadomaisatualatravésdesuarespectivaview.
AlémdopadrãoMVC,trabalharemoscomdoisparadigmasdaprogramação que compõem a cartucheira de todo cangaceiro, oparadigmaorientadoaobjetoseoparadigmafuncional.
Caso o leitor nunca tenha aplicado o paradigma orientado aobjetos, não deve se preocupar, pois seus conceitos-chaves serãoensinados ao longo do livro. No entanto, é necessário ter umconhecimentofundamentaldalinguagemJavaScript.
A ideia é transitarmos entre os dois paradigmas, como umespíritoerrantedocangaçoqueviveentredoismundos,utilizando
1.2 O PADRÃO MVC (MODEL-VIEW-CONTROLLER)
12 1.2OPADRÃOMVC(MODEL-VIEW-CONTROLLER)
o que cada um deles oferece de melhor para o problema a serresolvido.Nofinal,issotudoresultaráemumminiframework.
A construçãode umminiframeworknão é a finalidade destelivro,masconsequênciadeaprendizadodaartedeumcangaceirona programação. A ideia é que o leitor possa extrair de cadapassagem desta obra recursos que são interessantes de seremutilizadosemsuasaplicações.
Nosso trabalho de verdade começa no próximo capítulo noqualdaremosinícioacriaçãodomodelodenegociações.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/01
1.2OPADRÃOMVC(MODEL-VIEW-CONTROLLER) 13
CAPÍTULO2
"Vivendoseaprende;masoqueseaprende,mais,ésóafazeroutrasmaioresperguntas."-GrandeSertão:Veredas
No capítulo anterior, vimos que nosso código não realizavaumaseparaçãoclaraentremodeloesuaapresentação,tornando-odifícilde compreenderemanter.Também foipropostoousodomodelo MVC (Model-View-Controller) para melhorar suaestrutura.
Da tríade do modelo MVC, começaremos construindo ummodelodenegociação.Masaliás,oqueseriaummodelo?
Um modelo é uma abstração de algo do mundo real. Umexemplodemodeloé aquele criadoporumanalistademercado,
NEGOCIARCOMOCANGACEIRO,TEMCORAGEM?
2.1OPAPELDEUMMODELO
14 2NEGOCIARCOMOCANGACEIRO,TEMCORAGEM?
permitindo-osimularoperaçõeseatépreverseucomportamento.No contexto de nossa aplicação, criaremos um modelo denegociação.
Nossomodelo precisamaterializar asmesmas regras de umanegociação; emoutraspalavras,deve seguirumaespecificaçãodenegociação.Oparadigmaorientado a objetos trata especificaçõesatravésdousodeclasses.
A partir do ES2015 (ES6), ficou aindamais fácilmaterializaremnossocódigoacriaçãodeclassecoma introduçãoda sintaxeclass.
Antesdecontinuarcomnossoprojeto,vamosremoveroscriptclient/app/index.js de client/index.html , pois não
usaremos mais este código. Com o arquivo removido, podemosdarinícioaconstruçãodanossaclasseNegociacao.
VamoscriaroscriptquedeclararánossaclasseNegociacaonapastaclient/app/domain/negociacao/Negociacao.js.Estesimples ato de criar um novo arquivo já evidencia algumasconvençõesqueutilizaremosatéofinaldolivro.
Apastaclient/domain é aquela que armazenará todos osscriptsquedizemrespeitoaodomíniodaaplicação,nocaso,tudoaquilo sobre negociações. A subpastaclient/app/domain/negociacao não foi criada por engano, é
dentrodelaqueo scriptdanossa classe residirá, inclusive classesde serviços que possuem forte relação com esse domínio. Dessaforma, basta olharmos uma única pasta para saber sobre
2.2ACLASSENEGOCIAÇÃO
2.2ACLASSENEGOCIAÇÃO 15
determinadodomíniodaaplicação.
Por fim, o nome do script segue o padrão PascalCase,caracterizado por arquivos que começam com letra maiúscula,assim como cada palavra que compõe o nome do arquivo. Essaconvençãotornaevidenteparaquemestudaaestruturadoprojetoqueestearquivopossuiadeclaraçãodeumaclasse,poistodaclasseporconvençãosegueomesmopadrãodeescrita.
Assim, alteraremos nosso arquivo client/app/domain/negociacao/Negociacao.js queacabamosdecriar:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
}
Uma classe no mundo da Orientação a Objetos define aestruturaquetodoobjetocriadoapartirdeladeveseguir.Sabemosqueumanegociaçãoprecisa terumadata,umaquantidade eumvalor. É através da função constructor() que definimos aspropriedadesdeumaclasse:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(){
this.data=newDate();//dataatualthis.quantidade=1;this.valor=0.0;
}}
Através de this.nomeDaPropriedade, especificamos que a
16 2.2ACLASSENEGOCIAÇÃO
negociaçãoterá:data,quantidadeevalor,cadapropriedadecomseuvalorpadrão.
Emseguida,noarquivoclient/index.html, vamos incluirumanovatag<script>antesdofechamentodatag</body>.Nosso objetivo será criar duas instâncias de negociação, ou seja,doisobjetoscriadosapartirdaclasseNegociacao.Usaremosasvariáveisn1en2.
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao();console.log(n1);
varn2=newNegociacao();console.log(n2);
</script><!--códigoposterioromitido-->
Vamosrecarregaronavegadoreabriroconsole.VejamquejáaparecemduasNegociacoes:
Figura2.1:Duasnegociaçõesimpressas
Cadainstânciacriadapossuiasmesmaspropriedadesevalores,pois foram criadas a partir da mesma classe que atribui valorespadrõesparacadaumadelas.
2.2ACLASSENEGOCIAÇÃO 17
O uso do operador new não foi por acaso. Vejamos o queacontececasoelesejaomitido:
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao();console.log(n1);
varn2=Negociacao();//<-operadornewremovidoconsole.log(n2);
</script><! códigoposterioromitido
Nonavegador,umamensagemdeerroéexibidanoconsole:
Figura2.2:Mensagemdeerro
Somosinformadosdequeaclasseconstructor()nãopodeser invocada sem operador new. O operador new é bastanteimportante por ser o responsável pela inicialização da variávelimplícitathis de cada instância criada, ou seja, de cadaobjetocriado. Dessa forma, caso o programador esqueça de usar ooperadornewparacriarainstânciadeumaclasse,oquenãofazsentido, ele será avisado com uma bela mensagem de erro noconsole.
Como cada instância criada possui seu próprio contexto emthis,podemosalterarovalordaspropriedadesdecadaobjeto,
18 2.2ACLASSENEGOCIAÇÃO
semqueissointerfiranooutro:
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao();n1.quantidade=10;console.log(n1.quantidade);
varn2=Negociacao();n2.quantidade=20;console.log(n2.quantidade);console.log(n1.quantidade);//continua10
</script><!--códigoposterioromitido-->
Agora, se executarmos o código, no console do navegador,veremosquecadaNegociacaoteráumvalordiferente.
Figura2.3:Negociaçõesimpressas2
Realizamosoprimeirotestecomduasinstânciasquepossuemseusprópriosvaloresdepropriedade,masquecompartilhamumamesmaestruturacomdata,quantidadeevalor.
Antes de continuarmos, vamos apagar uma das negociações,pois uma já é suficiente para os novos testes que realizaremos aseguir:
<!--client/index.html--><!--códigoanterioromitido-->
2.2ACLASSENEGOCIAÇÃO 19
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao();n1.quantidade=10;console.log(n1.quantidade);
</script><!--códigoposterioromitido-->
Veremosa seguiroutra formadecriarmos instânciasdeumaclasse.
Todanegociaçãoprecisa ter umadata, quantidade e valor ounãoseráumanegociação.VejamosumexemploqueconstróiumainstânciadeNegociacaocomtodososseusdadosatribuídos:
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao();n1.data=newDate();n1.quantidade=10;n1.valor=200.50;console.log(n1);
</script><!--códigoposterioromitido-->
Nosso código é funcional,mas se todanegociaçãoprecisa terumadata,quantidadeevalor,que taldefinirmosessesvaloresnamesma instrução que cria uma nova instância? Além de nossocódigo ficar menos verboso, deixamos clara essa necessidade naespecificaçãodaclasse.
Você deve lembrar que toda classe possui um
2.3CONSTRUTORDECLASSE
20 2.3CONSTRUTORDECLASSE
constructor(),umafunçãoespecialqueseráchamadatodavezqueooperadornew forusadoparacriaruma instânciadeumaclasse.Sendoumafunção,elapodereceberparâmetros.
Queremospoderfazerisso:
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
//bastaumainstruçãoapenasemvezde4!varn1=newNegociacao(newDate(),5,700);console.log(n1);
</script><!--códigoposterioromitido-->
NaclasseNegociacao, adicionaremososparâmetrosde seuconstructor():
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
//cadaparâmetrorecebidoseráatribuídoàspropriedadesdaclasse
this.data=data;this.quantidade=quantidade;this.valor=valor;
}}
Veja que a passagem dos parâmetros para a instância deNegociacaosegueamesmaordemdeseusparâmetrosdefinidosem seuconstructor(). Cada parâmetro recebido é repassadoparacadapropriedadedaclasse.
Faremos um teste no console para ver se tudo funcionou
2.3CONSTRUTORDECLASSE 21
corretamente:
Figura2.4:Testenoconsole
Os valores coincidem com os que especificamos no HTML.Porém, ainda não calculamos o volume - a quantidademultiplicadapelovalor.Faremosistoagora:
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);varvolume=n1.quantidade*n1.valor;console.log(volume);
</script><!--códigoposterioromitido-->
Figura2.5:Volume
22 2.3CONSTRUTORDECLASSE
Ovalor3500éoresultadodaoperaçãodemultiplicação.Masestamos fazendo uma programação procedural, e sempre que oprogramadorprecisardovolume,elemesmoteráderealizarestecálculo.
Alémdeseralgotediosoterderepetirocálculodovolume,aschances de cada programador cometer algum erro ao calculá-losãograndes,mesmocomumcódigosimplescomoesse.Aliás,esseéumproblemaqueoparadigmaorientadoaobjetosvemresolver.
NoparadigmadaOrientaçãoaObjetos,háumaforteconexãoentre dado e comportamento, diferente da programaçãoprocedural, na qual temos de um lado o dado e, do outro,procedimentosqueoperamsobreestesdados.Nossaclassepossuiapenas dados, chegou a hora de dotá-la do seu primeirocomportamento,oobtemVolume.
Vamoscriarométodo:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
this.data=data;this.quantidade=quantidade;this.valor=valor;
}
obtemVolume(){
returnthis.quantidade*this.valor;}
2.4MÉTODOSDECLASSE
2.4MÉTODOSDECLASSE 23
}
Funções que definem um comportamento de uma classe sãochamadas de métodos para alinhar o vocabulário com oparadigmadaOrientaçãoaObjetos.Noentanto,nãoéraroalgunscangaceiroscontinuaremautilizarotermofunção.
Agora, basta perguntarmos para a própria instância da classeNegociacaoparaqueobtenhaseuvolumeparanós.Temosum
únicolocalquecentralizaocálculodovolume.Então,alteraremosnossotesteparausarmosométodoqueacabamosdecriar:
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);varvolume=n1.obtemVolume();console.log(volume);
</script><!--códigoposterioromitido-->
Agora, ao executarmos o código, o valor 3500 apareceránovamentenoconsole:
Figura2.6:Volume2
24 2.4MÉTODOSDECLASSE
Ovalorfoicalculadocorretamente.Oobjetosabelidarcomaspropriedades trabalhadas.Logo, a regrade comoobterovolumeestánopróprioobjeto.Voltamosaterumaconexãoentredadoecomportamento.
NósjáconseguimostrabalharcomaclasseNegociacao,masprecisamos implementar outra regra de negócio além do cálculodovolume:apóscriadaanegociação,estanãopoderáseralterada.Noentanto,nossocódigonãoestápreparadoparalidarcomessaregra,comopodemosverificarcomumnovoteste:
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);n1.quantidade=10;console.log(n1.quantidade);
</script><!--códigoposterioromitido-->
No código anterior, estamos atribuindo um novo valor àpropriedade quantidade da nossa instância, para em seguidaimprimirmos seuvalor.Qualvalor será impressonoconsole,5ou10?
Figura2.7:Regraalterada
2.5ENCAPSULAMENTO
2.5ENCAPSULAMENTO 25
Eleimprimiuovalorrecém-adicionado10eistopodecausarproblemas.Nãoéàtoaaregradeumanegociaçãonaqualelanãopodeseralteradadepoisdecriada.
Imaginequeacabamosdefazerumanegociaçãoecombinamosum determinado valor, mas depois alguém decide alterá-la parabenefício próprio.Nosso objetivo é que as propriedades de umanegociaçãosejamsomentedeleitura.
Noentanto,alinguagemJavaScript-atéaatualdata-nãonospermite usar modificadores de acesso, um recurso presente emoutraslinguagensorientadasaobjeto.Nãopodemosfazercomqueuma propriedade seja apenas leitura (ou gravação). O quepodemos fazer é convencionarmosque todapropriedadedeumaclasseprefixadacomumunderline(_)nãodeveseracessadaforadosmétodosdaprópriaclasse:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
this._data=data;this._quantidade=quantidade;this._valor=valor;
}
obtemVolume(){
returnthis._quantidade=this._valor;}
}
Esta seráumaconvençãoque informaráaoprogramadorqueas propriedades que contenham _ (underline) só poderão seracessadas pelos próprios métodos da classe. Isto significa que,
26 2.5ENCAPSULAMENTO
mesmo podendo imprimir a propriedade _quantidade comoutrovalor,nãodeveríamosmaispoderacessá-la.O_ funcionacomoumavisodizendo:"programador,vocênãopodealterarestapropriedade!".
Então, se usamos a convenção de utilizar o prefixo, comofaremosparaimprimirovalordaquantidadedeumanegociação,porexemplo?Seráqueprecisaremosabandonaraconvençãoqueacabamos de aplicar? Não será preciso, pois podemos criarmétodosacessadores.
Métodosacessadoresgeralmentecomeçamcomoprefixogetem seus nomes. No caso da nossa classe Negociacao ,adicionaremos o método getQuantidade() , que retornará o_quantidade . Usaremos também o getData() e o
getValor()queterãofinalidadessemelhantes:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
this._data=data;this._quantidade=quantidade;this._valor=valor;
}
obtemVolume(){
returnthis._quantidade=this._valor;}
getData(){
returnthis._data;}
2.5ENCAPSULAMENTO 27
getQuantidade(){
returnthis._quantidade;}
getValor(){
returnthis._valor;}
}
Como são métodos da própria classe, seguindo nossaconvenção, eles podem acessar às propriedades prefixadas com(_).Equalqueroutrocódigoqueprecisarleraspropriedadesdeum objeto do tipo Negociacao poderá fazê-lo através dosmétodosacessadoresquecriamos.
Vamosatualizarnossocódigoemindex.html,pararefletiroestadoatualdonossocódigo:
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);console.log(n1.getQuantidade());console.log(n1.getData());console.log(n1.getValor());
</script><!--códigoposterioromitido-->
Observequeestamosacessandoosdemaiscampos:
28 2.5ENCAPSULAMENTO
Figura2.8:Valoresnoconsole
Os valores serão impressos corretamente no console. Emseguida,modificaremosoobterVolume()paragetVolumeemNegocicacao js
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
this._data=data;this._quantidade=quantidade;this._valor=valor;
}
//trocamosonomedeobtemVolumeparagetVolumegetVolume(){
returnthis._quantidade=this._valor;}
//códigoposterioromitido}
Vamostestartodososmétodosacessadoresemindex.html:
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
2.5ENCAPSULAMENTO 29
varn1=newNegociacao(newDate(),5,700);console.log(n1.getQuantidade());console.log(n1.getData());console.log(n1.getValor());console.log(n1.getVolume());
</script><!--códigoposterioromitido-->
Nonavegador,veremosqueosvaloresquantidade,data,valorevolumeserãoimpressoscorretamente.
Figura2.9:Quantidadeevolume
Podemos enxugar nossos métodos acessadores através dasintaxeget que a linguagemnos oferece.Vamos alterar nossocódigo:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
this._data=data;this._quantidade=quantidade;this._valor=valor;
}
2.6ASINTAXEGET
30 2.6ASINTAXEGET
getvolume(){
returnthis._quantidade*this._valor;}
getdata(){
returnthis._data;}
getquantidade(){
returnthis._quantidade;}
getvalor(){
returnthis._valor;}
}
Vejamqueasintaxegetvemantesdonomedométodo,quenão possui mais o prefixo get que usamos antes. Por fim,acessamosessesmétodoscomose fossempropriedadesemnossocódigo.
Vamos alterar index.html para vermos em prática aalteração:
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);
console.log(n1.quantidade);console.log(n1.data);console.log(n1.valor);console.log(n1.volume);
</script><!--códigoposterioromitido-->
2.6ASINTAXEGET 31
Estamoscriandoumapropriedadegetterdeacessoà leitura.Emesmo sendo um método, precisamos acessá-lo como umapropriedade. Contudo, por debaixo dos panos, as propriedadesqueacessamoscontinuamsendométodos.
Figura2.10:console
Outracaracterísticademétodoscriadoscomasintaxeget éque, se tentarmos atribuir umnovo valor à propriedade, ele seráignorado.Vejamos:
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);n1.quantidade=1000;//instruçãoéignorada,semmensagemdeerro
console.log(n1.quantidade);console.log(n1.data);console.log(n1.valor);console.log(n1.volume);
</script><!--códigoposterioromitido-->
Ovalor1000 não será repassadoparao_quantidade.Ovalorcontinuarásendo5:
32 2.6ASINTAXEGET
Figura2.11:Valordequantidadeinalterados
Istoacontecequandoapropriedadeéumgetter (ouseja,édeleitura), já que não podemos atribuir a ela um valor.Mas aindapodemosmodificá-lacomn1._quantidade:
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);
//instruçãoéignorada,semmensagemdeerron1.quantidade=1000;
//nadanosimpededefazermosisso,apenasnossaconvençãon1._quantidade=5000;
console.log(n1.quantidade);console.log(n1.data);console.log(n1.valor);console.log(n1.volume);
</script><!--códigoanterioromitido-->
2.6ASINTAXEGET 33
Figura2.12:Quantidadealterada
Como vimos, isso não impede o programador desavisado deainda acessar as propriedades prefixadas com underline. Noentanto, ainda temos um artifício da linguagem que pode evitarqueissoaconteça.
Nós usaremos um artifício existente há algum tempo nalinguagem JavaScript que permite "congelar" um objeto.Propriedades de objetos congeladas não podem receber novasatribuições.ConseguimosessecomportamentoatravésdométodoObject.freeze().
Vamoscongelarainstâncian1emnossoteste:
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);
Object.freeze(n1);n1._quantidade=100000000000;//nãoconseguemodificarenenhumerroacontece
2.7OBJETOSIMUTÁVEIS
34 2.7OBJETOSIMUTÁVEIS
console.log(n1._quantidade);</script><!--códigoposterioromitido-->
Figura2.13:Objetocongelado
No console, veremos o valor 5 ; isto significa que nãoconseguimosmais alterar e o objeto foi congelado.Vale lembrarque quantidade é uma propriedade que chamamos emNegociacao.js, quepor "debaixodospanos" rodará comoum
métodoeretornaráthis._quantidade.
Mesmo colocando n1._quantidade = 100000000000; noindex.html,essevalorparecetersidoignorado.Issoébom,poisagora o desenvolvedor já não conseguirá alterar esta quantidade.Vamos fazer um pequeno teste que nos mostrará o que estácongeladonoconsole:
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);
console.log(Object.isFrozen(n1));Object.freeze(n1);console.log(Object.isFrozen(n1));n1._quantidade=100000000000;
</script>
2.7OBJETOSIMUTÁVEIS 35
Noconsole,veremososeguinteresultado:
Figura2.14:Consolecomfalseetrue
Oobjetoantesnãoeracongeladoe,depois, foi.Por isso,nãoconseguimosalterar_quantidade, o atributoquedefinimos serprivadoporconvenção.Porém,essasoluçãoéprocedural,porquevocêterásempredelembrardecongelarainstância.
Entretanto, queremos que, ao usarmos onewNegociacao,umainstânciacongeladajásejadevolvida.Nóspodemosfazerissocongelando a instância no constructor() . Em
app/domain/negociacao/Negociacao.js , adicionaremosObject.freeze()apósoúltimothis:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
this._data=data;this._quantidade=quantidade;this._valor=valor;Object.freeze(this);
}
36 2.7OBJETOSIMUTÁVEIS
//códigoposterioromitido
Vocês lembram que o this é uma variável implícita? Equando algum método é chamado, temos acesso à instânciatrabalhada. O this do Object.freeze() será o n1 noindex.html.
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);
n1._quantidade=100000000000;
console.log(n1.quantidade);console.log(n1.data);console.log(n1.valor);console.log(n1.volume);
</script><!--códigoanterioromitido-->
Então,oresultadodenewNegociacao(/*parâmetros*/)jádevolveráumainstânciacongelada.Aparentemente,resolvemoso nossoproblema.
Mas seráquenossanegociação é realmente imutável?VamosverificarmaisadiantesedefatoaNegociacaonãoseráalterada.
Pelaregradenegócio,umanegociaçãodeveserimutávelenãopode ser alterada depois de criada. Mas faremos um novo testetentando alterar a data da negociação. Nós fizemos isto com aquantidade. Agora, criaremos a variável outroDia , que seráreferenteàdatadiferentedaatual:
2.8AINSTÂNCIAÉIMUTÁVELMESMO?
2.8AINSTÂNCIAÉIMUTÁVELMESMO? 37
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);
console.log(n1.data);
varoutroDia=newDate();
//setDatepermitealterarodiadeumadataoutroDia.setDate(11);
n1.data=outroDia;
console.log(n1.data);</script><!--códigoposterioromitido-->
Paraverificarseocódigofuncionacorretamente,adicionamoso console.log(). Se Negociacao for realmente imutável, adatanãopoderáseralterada.
Figura2.15:Negociaçãoimutável
Vemosqueelacontinuouimutável.
Agora, faremosumtestediferente.Nãovamosmais trabalharcom a variável outroDia , e substituiremos porn1.data.setDate(11).Veja que estamos chamandoométodosetDatedapropriedadedatadopróprioobjeto:
38 2.8AINSTÂNCIAÉIMUTÁVELMESMO?
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varn1=newNegociacao(newDate(),5,700);
console.log(n1.data);
n1.data.setDate(11);
console.log(n1.data);</script><!--códigoposterioromitido-->
Seráqueadatafoialterada?
Figura2.16:dataalterada
Vejaqueadatafoialterada,poisadataexibidanoconsolefoiodia11.Nossanegociaçãoémutável!Nósnãopodemosdeixarissoacontecer.
Mas se nós utilizamos o Object.freeze() , por que issoaconteceu?OObject.freeze somente evita que seja realizadauma nova atribuição à propriedade do objeto congelado. Nãopodemos fazer, por exemplo, n1.data = new Date() , poisestamos atribuindo um novo valor à propriedade. Porém, éperfeitamentepossível fazermosn1.data.setData(11), pois aspropriedadesdoobjetodefinidoemn1.datanãosãocongeladas.
2.8AINSTÂNCIAÉIMUTÁVELMESMO? 39
Quandooprogramadorchegaaumpontoemquealinguagemnãoofereceumasoluçãonativaparaoproblema,elepodeapelarpara sua criatividade; característica fundamental de todocangaceiroparalidarcomasadversidadesqueencontra.
UmamaneiradetornarmosadatadeumanegociaçãoimutáveléretornarmosumnovoobjetoDatetodavezqueogetterdadatafor acessado. Dessa maneira, qualquer modificação através dasfunçõesquetodainstânciadeDatepossuisómodificaráacópia,enãooobjetooriginal.
Atualmente,ogetdoarquivoNegociacaoestáassim:
//app/domain/negociacao/Negociacao.js//códigoanterioromitido
getdata(){
returnthis._data;}//códigoposterioromitido
Vamosmodificaroretornodogetdata():
//app/domain/negociacao/Negociacao.js//códigoanterioromitido
getdata(){
returnnewDate(this._data.getTime());}//códigoposterioromitido
OgetTime()deumadataretornaráumnúmerolongcomumarepresentaçãodadata.Vejamosnoconsole:
2.9PROGRAMAÇÃODEFENSIVA
40 2.9PROGRAMAÇÃODEFENSIVA
Figura2.17:Retornodadata
Noconstructor()deDate,estenúmeroseráaceitoparaaconstruçãode umanovadata. Então, se tentarmos alterar a dataatravés de n1.data.setDate(11), não estaremos mudando adataquen1guarda,masumacópia. Istoéoquechamamosdeprogramaçãodefensiva.
Vamostestaronossocódigo,apósasalteraçõesfeitasnogetdata()daclasseNegociacao.Depoisderecarregarmosapáginanonavegador,veremosaseguintedatanoconsole:
Figura2.18:Datasnoconsole
Devemos ter o mesmo cuidado com o constructor()também, porque se passamos uma data no constructor() deNegociacao, qualquer mudança na referência da data fora da
classe modificará também sua data. Vejamos o problemaacontecendo:
<!--códigoanterioromitido-->
2.9PROGRAMAÇÃODEFENSIVA 41
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
varhoje=newDate();
varn1=newNegociacao(hoje,5,700);
console.log(n1.data);
//alteraadataquefoiarmazenadaemNegociacaotambémhoje.setDate(11);
console.log(n1.data);</script>
Observe que, em vez de passarmos o new Date() para oconstructor()deNegociacao,passamosavariávelhojequereferenciaumaDate.Quando aNegocicao receber o objetohoje, ele guardará uma referência para o mesmo objeto. Isto
significaque,sealterarmosavariávelhoje,modificaremosadataque estánaNegociacao. Se executarmos o código, a data seráalteradaparaodia11.
Figura2.19:Dataalterada2
Por isso, também usaremos o getTime() noconstructor():
//app/domain/negociacao/Negociacao.js
classNegociacao{
42 2.9PROGRAMAÇÃODEFENSIVA
constructor(data,quantidade,valor){
//criandoumanovadata,umanovareferênciathis._data=newDate(data.getTime());
this._quantidade=quantidade;this._valor=valor;Object.freeze(this);
}//códigoposterioromitido
Aofazermosisto,jánãoconseguiremosalterarareferêncianoindex.html,porqueoqueestamosguardandonoNegociacaonãoémaisumareferênciaparahoje. Então,nósusaremosumnovoobjeto.Quandorecarregarmosapáginanonavegador,adataqueapareceránoconsoleserá10.
Nomomentodacriaçãododesigndasclasses,sejacuidadosocom a mutabilidade, tomando as medidas necessárias paragarantiraimutabilidadequandoforpreciso.
Com isso, finalizamos a blindagem da nossa classe paragarantir a sua imutabilidade. Existem outras soluções maisavançadasnoJSparatentarmosemularoprivacy-aprivacidade-do seu código; entretanto, perdemos em legibilidade eperformance.Então,asoluçãousadaéamaisviável.
Podemos simplificar ainda mais nosso código com umpequenotruquequeveremosaseguir.
VamoslançarnossoolharmaisumaveznoconstructordaclasseNegociacao:
2.10 MENOS VERBOSIDADE NOCONSTRUCTORCOMOBJECT.ASSIGN
2.10MENOSVERBOSIDADENOCONSTRUCTORCOMOBJECT.ASSIGN 43
//app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
this._data=newDate(data.getTime());this._quantidade=quantidade;this._valor=valor;Object.freeze(this);
}//códigoposterioromitido
Cadaparâmetrorecebidonoconstructoréatribuídoaumapropriedade de instância da classe através de instruções queseguemaestruturathis._nomeDaPropriedade=parametro.Setivéssemosumaquantidadedeparâmetrosmuitogrande,ocódigodonossoconstructorcresceriabastante.
Podemos simplificar bastante esse processo de atribuição dosparâmetros recebidos pelo constructor nas propriedades dainstânciada classe comométodoObject.assign(). Primeiro,vamoscompreendercomoelefunciona.
OmétodoObject.assign() éusadoparacopiarosvaloresde todas as propriedades próprias enumeráveis de um ou maisobjetosdeorigemparaumobjetodestino.Ele retornaráoobjetodestino.Vejamosumexemplo:
//EXEMPLOAPENAS,FAÇANOCONSOLEDOCHROME
varorigem1={nome:'Cangaceiro'};varorigem2={idade:20};
varcopia=Object.assign({},origem1,origem2);
console.log(copia);//exibe{nome:'Cangaceiro',idade:20};
O primeiro parâmetro de Object.assign() é o objeto
44 2.10MENOSVERBOSIDADENOCONSTRUCTORCOMOBJECT.ASSIGN
destino que receberá a cópia das propriedades dos objetos deorigem,umobjetosemqualquerpropriedadecomasintaxe{}.Podemospassarquantosobjetosdeorigemdesejarmosapartirdosegundo parâmetro, em nosso caso passamos origem1 eorigem2. O retorno do método é um objeto que contém as
propriedadesdeorigem1eorigem2.
Semudarmosnossoexemploepassarmosa referênciadeumobjeto já existente como parâmetro de destino no métodoObject.assign() , esse objeto será modificado ganhando as
propriedades de um ou mais objetos de origem passados comoparâmetro:
//EXEMPLOAPENAS,FAÇANOCONSOLEDOCHROME
varorigem={idade:20};vardestino={nome:'Cangaceiro'};
Object.assign(destino,origem);console.log(destino);//exibe{nome:'Cangaceiro',idade:20};
OcomportamentoqueacabamosdevernospermiterealizaraseguintealteraçãonoconstructordeNegociacao:
//app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(data,quantidade,valor){
Object.assign(this,{_data:newDate(data.getTime()),_quantidade:quantidade,_valor:valor});
Object.freeze(this);}
//códigoposterioromitido
Estamoscopiandoparathis, que representa a instânciada
2.10MENOSVERBOSIDADENOCONSTRUCTORCOMOBJECT.ASSIGN 45
classeNegociacao, as propriedadesdoobjetodeorigemcriadocomaspropriedades_data,_quantidadee_valorecomosvalores recebidos pelo constructor da classe. Temos apenasumainstruçãodeatribuiçãonolugardetrêsinstruções.
Em um primeiro momento, não parece ser muita vantagemtrocar três instruções por uma apenas com Object.assign(),pois a legibilidade não é lá uma das melhores. Mas secombinarmos este recurso comopróximoqueveremos a seguir,teremosumasoluçãobeminteressante.
O método Object.assign() recebeu como origem umobjetocriadonaformaliteral,istoé,atravésde{}.Vejamosumexemploisoladoquecriaumobjetoatravésdaformaliteral:
//EXEMPLOAPENAS,FAÇANOCONSOLEDOCHROME
varperfil='Cangaceiro';
varobjeto={perfil:perfil};
console.log(objeto);//exibe{perfil:"Cangaceiro"}
No exemplo anterior, a propriedade perfil recebe comovalor uma variável de mesmo nome. Quando o nome dapropriedade tem o mesmo nome da variável que será atribuídacomoseuvalor,podemosdeclararnossoobjetodestaforma:
//EXEMPLOAPENAS,FAÇANOCONSOLEDOCHROME
varperfil='Cangaceiro';
2.11 ATALHO PARA PROPRIEDADES DEOBJETOSLITERAIS
46 2.11ATALHOPARAPROPRIEDADESDEOBJETOSLITERAIS
varobjeto={perfil};//passamosapenasperfil
console.log(objeto);//exibe{perfil:"Cangaceiro"}
Essa novidade introduzida no ES2016 (ES6) nos permitesimplificar o uso de Object.assign() que vimos antes.AlterandooconstructordeNegociacao:
//app/domain/negociacao/Negociacao.js
classNegociacao{
//MUDANÇANONOMEDOSPARÂMETROS,AGORACOMUNDERLINEconstructor(_data,_quantidade,_valor){
//USANDOOATALHOPARADECLARAÇÃODEPROPRIEDADES
Object.assign(this,{_data:newDate(_data.getTime()),_quantidade,_valor});
Object.freeze(this);}
//códigoposterioromitido
Reparem que o nome dos parâmetros recebidos noconstructormudaram dedata,quantidade,valor para_data, _quantidade, _valor . Isso não foi por acaso, a
intenção é que eles tenhamomesmonomedas propriedades doobjetopassadocomoorigemparaObject.assign(). Possuindoo mesmo nome, podemos usar o atalho de declaração depropriedadesquevimosnaseçãoanterior.
Para melhorarmos a legibilidade, vamos convencionar quetoda propriedade recebida pelo constructor que precisa seralteradaantesdaatribuiçãoseráatribuídaindividualmente,semoauxíliodeObject.assign().
Alterandonossocódigo:
2.11ATALHOPARAPROPRIEDADESDEOBJETOSLITERAIS 47
//app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(_data,_quantidade,_valor){
Object.assign(this,{_quantidade,_valor});this._data=newDate(_data.getTime());Object.freeze(this);
}//códigoposterioromitido
Simplificamos bastante a passagem dos parâmetros doconstructorparaaspropriedadesdaclasse.
Antes de continuarmos com o próximo capítulo, precisamosadquirirumnovohábitoaodeclararmosnossasvariáveis.
Temosnossomodelodenegociaçãopronto.Contudo,antesdecontinuarmos, vamos voltar nossa atenção para as variáveisdeclaradascomvar.
Declaraçõescomvarnãocriamescopodeblocoassimcomoemoutraslinguagens.Vejamosumexemplo:
<!--CÓDIGODEEXEMPLO,NÃOENTRAEMNOSSAAPLICAÇÃO--><script>
for(vari=1;i<=100;i++){console.log(i);
}alert(i);//qualseráovalor?</script>
2.12AS SURPRESASDEDECLARAÇÕESCOMVAR
Ausênciadeescopodebloco
48 2.12ASSURPRESASDEDECLARAÇÕESCOMVAR
Ocódigoanteriorimprimirácorretamenteosnúmerosde1a100noconsole.Porém,a instruçãoalert(i) exibirá o valor101:
Figura2.20:Alert
Avariávelicontinuasendoacessívelforadoblocodofor.A únicamaneira de termos um escopo para variáveis declaradascomvaréaodeclará-lasdentrodoblocodeumafunção:
functionexibeNome(){
varnome='FlávioAlmeida';alert(nome);
}exibeNome();//exibeFlávioAlmeidaalert(nome);//undefined
No exemplo que acabamos de ver, a variável nome não éacessívelforadoblocodafunção.
Comvar, podemosdeclarar variáveis comomesmonomesem que isso incomode o interpretador do JavaScript. Porexemplo:
functionminhaFuncao(){
Declaraçõesrepetidas
2.12ASSURPRESASDEDECLARAÇÕESCOMVAR 49
varnome="Flávio";varnome="Almeida";//nãoprecisadevar!
}
Este comportamento não ajuda em nada o desenvolvedor aperceber, nas dezenas de linhas de código, que está declarandonovamenteumavariávelquejáhaviasidodeclarada.
Vejamosmaisumexemplo:
functionminhaFuncao(){
alert(nome);varnome='Flávio';
}
minhaFuncao();
Ocódigoanteriorexibeundefined,nafunçãoalert(),emvez de um erro. Isso acontece porque variáveis declaradas comvarsãoiçadas(hoisting)paraoiníciodoblocodafunçãonaqualforamdeclaradas.
Por debaixo dos panos, o JavaScript executa nosso código daseguintemaneira:
//COMOOINTERPRETADORLÊNOSSOCÓDIGOfunctionminhaFuncao(){
/*Içouadeclaraçãodavariávelparaotopodocontexto,no
caso,iníciodafunção*/varnome;alert(nome);nome='Flávio';
}
Acessoantesdesuadeclaração
50 2.12ASSURPRESASDEDECLARAÇÕESCOMVAR
minhaFuncao();
Esse comportamento permite que códigos como estefuncionem:
functionminhaFuncao(){
if(!nome){varnome='FlávioAlmeida'
}alert(nome);
}
minhaFuncao();
Por debaixo dos panos, nosso código será interpretado destaforma:
//COMOOINTERPRETADORLÊNOSSOCÓDIGO
functionminhaFuncao(){varnome;if(!nome){
nome='FlávioAlmeida'}alert(nome);
}
minhaFuncao();
Dessa maneira, a variável nome testada pelo if já foideclarada,porémcomvalorundefined.AboanotíciaéqueháoutramaneiradedeclararmosvariáveisemJavaScript,assuntodapróximaseção.
A partir do ES2015 (ES6), foi introduzida uma nova sintaxepara declaração de variáveis, a sintaxe let . Vejamos as
2.13DECLARAÇÃODEVARIÁVEISCOMLET
2.13DECLARAÇÃODEVARIÁVEISCOMLET 51
característicasdestenovotipodedeclaração.
Vamos rever o exemplo da seção anterior, mas desta vezusandoletparadeclararavariáveldoblocodofor:
<!--CÓDIGODEEXEMPLO,NÃOENTRAEMNOSSAAPLICAÇÃO--><script>
for(leti=1;1<=100;1++){console.log(i);
}//undefined!
alert(i);</script>
A instrução alert(i) exibirá undefined, pois variáveisdeclaradascomlet,dentrodeumbloco,sósãoacessíveisdentrodessebloco.
Vejamosmaisumteste:
<!--CÓDIGODEEXEMPLO,NÃOENTRAEMNOSSAAPLICAÇÃO--><script>
for(leti=1;i<=100;i++){//sóexistenoblocofor!letnome="Flávio";console.log(i);
}//undefined!alert(i);//undefined!alert(nome);
</script>
Escopodebloco
Nãoépossívelhaverdeclarações repetidasdentrodeummesmobloco
52 2.13DECLARAÇÃODEVARIÁVEISCOMLET
Outropontointeressantedeletéquenãopodemosdeclararduas variáveis com omesmo nome dentro de ummesmo bloco{}.Vejamos:
functionminhaFuncao(){
letnome='Flávio';letnome='Almeida';
}minhaFuncao();
Afunçãoanterior,aoserchamada,resultaránoerro:
UncaughtSyntaxError:Identifier'nome'hasalreadybeendeclared
Contudo,ocódigoseguinteétotalmenteválido:
letnome='Flávio';
functionminhaFuncao(){
letnome='Almeida';alert(nome);
}minhaFuncao();//exibeAlmeidaalert(nome);//exibeFlávio
Há mais um detalhe que precisamos entender a respeito donovotipodedeclaraçãoqueacabamosdeaprender.
Vamos revisar comportamentos de variáveis declaradas comvar.Afunçãodeclaraavariávelnomepormeiodevar:
functionminhaFuncao(){
alert(nome);varnome='Almeida';
}
2.14TEMPORALDEADZONE
2.14TEMPORALDEADZONE 53
minhaFuncao();
Sabemos que, através de hoisting, ela será interpretada destaforma:
//COMOOINTERPRETADORLÊNOSSOCÓDIGO
functionminhaFuncao(){
varnome;alert(nome);nome='Almeida';
}minhaFuncao();
Da maneira que é interpretada pelo JavaScript, temos adeclaraçãovarnome;, quenãopossuiqualquer atribuição.Porissooalert(nome)exibeundefinedemvezdeumerroalegandoque a variável não existe. Contudo, quando usamos let ,precisamos ficar atentos para oTemporalDead Zone. O nomepodeparecerumtantoassustadorinicialmente.
Variáveisdeclaradascomlet tambémsão içadas (hoisting).Contudo, seu acesso só é permitido após receberem umaatribuição; caso contrário, teremos um erro.Vejamos o exemploanterior,destavezusandolet:
functionminhaFuncao(){
alert(nome);letnome='Flávio';
}
minhaFuncao();
Quando executamos nosso código, receberemos amensagemdeerro:
UncaughtReferenceError:nomeisnotdefined
54 2.14TEMPORALDEADZONE
Como foi dito, variáveis declaradas com let também sãoiçadas,entãonossocódigoestaráassimquandointerpretado:
//COMOOINTERPRETADORLÊNOSSOCÓDIGO
functionminhaFuncao(){
letnome;alert(nome);nome='Flávio';
}
minhaFuncao();
Éentreadeclaraçãodavariável e suaatribuiçãoque temosoTemporalDeadZone.Qualqueracessofeitoàvariávelnesseespaçode tempo resultará em ReferenceError. Contudo, é um errobenéfico, pois acessar uma variável antes de sua declaração éraramenteintencionalemumaaplicação.
Nadanosimpedededeliberadamentefazermosisso:
functionminhaFuncao(){letnome=undefined;alert(nome);nome='Flávio';
}
minhaFuncao();
O resultado dealert seráundefined. Faz todo sentido,porqueacessamosavariávelnome após receberuma atribuição,istoé,foradoTemporalDeadZone.
Agoraquejásabemosdosdesdobramentosdousodeletemnossa aplicação, vamos alterar client/index.html parautilizarmosessanovadeclaração:
<!--códigoanterioromitido-->
2.14TEMPORALDEADZONE 55
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><script>
lethoje=newDate();
letn1=newNegociacao(hoje,5,700);
console.log(n1.data);
hoje.setDate(11);
console.log(n1.data);</script>
Se executarmos o código, tudo continuará funcionandonormalmente.
Figura2.21:Dataconsole
Nestecapítulo,criamosummodelodenegociaçãoaplicandooparadigma da Orientação a Objetos. Contudo, implementamosapenas o M do modelo MVC. Precisamos implementar asinterações do usuário com o modelo, papel realizado pelocontrollerdomodeloMVC,assuntodopróximocapítulo.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/02
56 2.14TEMPORALDEADZONE
2.14TEMPORALDEADZONE 57
CAPÍTULO3
"Orealnãoestánoinícionemnofim,elesemostrapragenteénomeiodatravessia..."-GrandeSertão:Veredas
No capítulo anterior, criamos nosso modelo de negociaçãoatravésda classeNegociacao e, através do console, realizamosdiversos testes com instâncias dessa classe. Para que nossaaplicação seja útil para o usuário, essas instâncias devemconsiderar informações prestadas pelo usuário através doformulário definido client/index.html . Resolveremos essaquestãocomauxíliodeumControllerdomodeloMVC.
NomodeloMVC,oControllerfazapontedeligaçãoentreasaçõesdousuárionaView comoModel.NaView, temosnosso client/index.html (ainda nos aprofundaremos), e noModeltemosinstânciasdeNegociacao.Dessaforma,mantendoaseparaçãoentreModeleView,alteraçõesnaView(comopor
NOCANGAÇO,ÉAÇÃOPARATODOLADO
3.1OPAPELDEUMCONTROLADOR
58 3NOCANGAÇO,ÉAÇÃOPARATODOLADO
exemplo,trocarde<table>para<ul>)nãoafetamoModel.Issoéinteressante,poispodemosterváriasrepresentaçõesvisuaisdeummesmomodelosemqueestasforcemqualqueralteraçãonomodelo.
Chegouomomentodecriarmosnossoprimeirocontrolador.
Vamos criar o arquivoclient/app/controllers/NegociacaoController.js,noqual
declararemosaclassedonossocontroller:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
}
Maisumavez,paranãocorrermosoriscodeesquecermosdeimportar o script que acabamos de criar, vamos importá-lo emclient/index.html , como o último script. Aliás, vamos
removera tag<script> queusamospara realizarnossos testescomaclasseNegociacao:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domian/negociacao/Negociacao.js"></script>
<!--REMOVEUATAGCOMNOSSOSTESTES-->
<scriptsrc="app/controllers/NegociacaoController.js"></script>
<!--códigoposterioromitido-->
Vamosagoracriarumnovométodochamadoadiciona.Ele
3.2ACLASSENEGOCIACAOCONTROLLER
3.2ACLASSENEGOCIACAOCONTROLLER 59
será chamado toda vez que o usuário submeter o formulárioclicandonobotão"Incluir".
Por enquanto, vamos apenas cancelar o evento padrão desubmissãodoformulário(nãoqueremosqueapáginarecarregue)eexibirumalerta:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
adiciona(event){
//cancelandoasubmissãodoformulárioevent.preventDefault();
alert('Chameiaçãonocontroller');}}
Como faremos a ligação do método adiciona com asubmissão do formulário? Uma prática comum entreprogramadores JavaScript é realizar essa associação através dopróprioJavaScript,buscandooelementodoDOMeassociando-ocomumafunçãooumétodoatravésdainterfacedeeventos.
Vamoscriaroarquivoclient/app/app.js,noqualfaremosaassociaçãocitadanoparágrafoanterior.Aliás,jávamosimportá-lo emclient/index.html antesde escrever qualquer linhadecódigo:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/app.js"></script>
<!--códigoposterioromitido-->
60 3.2ACLASSENEGOCIACAOCONTROLLER
Não foi engano o nome do arquivo começar com letraminúscula, uma vez que ele não declara uma classe. Entendamapp.js como o ponto de entrada da nossa aplicação, isto é,
aquelequefazainicializaçãodetodososobjetosnecessáriosparanossaaplicação.Nocasodanossaaplicação,sóprecisamosdeumcontrollerporenquanto.
Em client/app/app.js , criaremos uma instância deNegociacaoController , para em seguida buscarmos o
formulário da página. Por fim, vamos associar o métodoadicionadainstânciacriadaaoeventosubmitdoformulário:
//client/app/app.js
//criouainstânciadocontrollerletcontroller=newNegociacaoController();
//associaoeventodesubmissãodoformulárioàchamadadométodo "adiciona"
document.querySelector('.form').addEventListener('submit',function(event){
controller.adiciona(event);});
Coma alteraçãoque fizemos,basta recarregarmos apágina epreenchermos o formulário. Com os dados da negociação todospreenchidos, ao clicarmos no botão "Incluir" o alerta queprogramamosemnossocontrollerseráexibido.
Apesarde funcional,estecódigopodeserreduzido.AfunçãoaddEventListeneresperareceberdoisparâmetros.Oprimeiroéo nomedoeventoqueestamostratandoeosegundoafunçãoquedesejamos chamar. Mas, se o método controller.adiciona
3.2ACLASSENEGOCIACAOCONTROLLER 61
também é uma função e funções podem ser passadas comoparâmetroparaoutrasfunções,podemossimplificardestaforma:
//client/app/app.js
letcontroller=newNegociacaoController();
//passamosdiretamentecontroller.adicionadocument
.querySelector('.form').addEventListener('submit',controller.adiciona);
Tudo continua funcionando como antes. Então, a ação docontroller foi chamada.Mas o nosso objetivo não é exibir a
mensagem,nósqueremoscriarumanegociação.
Ao clicarmos no botão Incluir do formulário, jáconseguimos executar as ações do controller . Agora,precisamos capturar os dados preenchidos. Mas já fizemos issoantes,noprólogo, atravésdedocument.querySelector(), querecebe como parâmetro um seletor CSS que permite identificarqualelementodesejamosbuscar:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
adiciona(event){
event.preventDefault();
//buscandooselementosletinputData=document.querySelector('#data');letinputQuantidade=document.querySelector('#quantidad
e');
3.3 ASSOCIANDO MÉTODOS DOCONTROLLERÀSAÇÕESDOUSUÁRIO
62 3.3ASSOCIANDOMÉTODOSDOCONTROLLERÀSAÇÕESDOUSUÁRIO
letinputValor=document.querySelector('#valor');
console.log(inputData.value);console.log(inputQuantidade.value);console.log(inputValor.value);
}}
Odocument.querySelector()éoresponsávelpelabuscanoDOMdoselementoscomid#data,#quantidadee#valor-observe que conseguimos utilizar os seletores CSS. Aliás, umcangaceiroJavaScripttemumbomconhecimentodessesseletores.
Vamosexecutarocódigoparaversetodososdadosaparecemnoconsoledonavegador.Preencheremosoformuláriocomadata11/12/2016,aquantidade1eovalor100.
Figura3.1:Dadosdoconsole
Os dados aparecem no console, no entanto, os valores daspropriedades _quantidade e _valor são strings. Podemosverificarissoatravésdainstruçãotypeof:
//client/app/controllers/NegociacaoController.js
3.3ASSOCIANDOMÉTODOSDOCONTROLLERÀSAÇÕESDOUSUÁRIO 63
classNegociacaoController{
adiciona(event){
event.preventDefault();
//buscandooselementosletinputData=document.querySelector('#data');letinputQuantidade=document.querySelector('#quantidad
e');letinputValor=document.querySelector('#valor');
console.log(inputData.value);console.log(inputQuantidade.value);
//exibestringconsole.log(typeof(inputQuantidade.value));
console.log(inputValor.value);
//exibestringconsole.log(typeof(inputValor.value));
}}
Isso acontece porque o retorno da propriedade value doselementos dosDOMdo tipoinput retorna seus valores comostring. Precisamos convertê-los para números com as funçõesparseInt()eparseFloat(),respectivamente:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
adiciona(event){
event.preventDefault();
//buscandooselementosletinputData=document.querySelector('#data');letinputQuantidade=document.querySelector('#quantidad
e');
64 3.3ASSOCIANDOMÉTODOSDOCONTROLLERÀSAÇÕESDOUSUÁRIO
letinputValor=document.querySelector('#valor');
console.log(inputData.value);console.log(parseInt(inputQuantidade.value));console.log(parseFloat(inputValor.value));
}}
Nosso formulário é pequeno, mas se tivesse mais de dezcampos, teríamos de repetir a instruçãodocument.querySelector dez vezes. Podemos enxugar essa
sintaxe.
Vamoscriaravariável$ (umahomenagemao jQuery!)quereceberácomovalordocument.querySelector,algototalmentepossível em JavaScript, pois funções podem ser atribuídas avariáveis. Com isso, criamos um alias , um atalho para ainstrução:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
adiciona(event){
event.preventDefault();
//aideiaéque$sejaoquerySelectorlet$=document.querySelector;
letinputData=$('#data');letinputQuantidade=$('#quantidade');letinputValor=$('#valor');
console.log(inputData.value);console.log(parseInt(inputQuantidade.value));console.log(parseFloat(inputValor.value));
}}
3.3ASSOCIANDOMÉTODOSDOCONTROLLERÀSAÇÕESDOUSUÁRIO 65
Destaforma,ficoumenostrabalhosoescreverocódigo.Masseo executarmos como está, veremos uma mensagem de erro nonavegador:
UncaughtTypeError:IllegalinvocationatHTMLFormElement.adiciona(NegociacaoController.js:9)
A atribuição do document.querySelector na variável $parecenãoterfuncionado.Porquenãofuncionou?
Odocument.querySelector() é uma funçãoquepertenceao objeto document - chamaremos tal função de método.Internamente, o querySelector tem uma chamada para othis, que é o contexto no qual ométodo é chamado, no casodocument.
Noentanto,quandoatribuímosodocument.querySelectorà variável $ , ele passa a ser executado fora do contexto dedocument, e isto não funciona. O que devemos fazer, então?
PrecisamostrataroquerySelectorcomoumafunçãoseparada,masqueaindamantenhaodocumentcomoseucontexto.
A boa notícia é que conseguimos isso facilmente através debind(),umafunção,pormaisestranhoquepareça,presenteemqualquerfunçãoemJavaScript:Assim,alteraremosnossocódigo:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
adiciona(event){
event.preventDefault();
//realizandoobind,$mantémdocumentcomoseucontextothis
let$=document.querySelector.bind(document);
66 3.3ASSOCIANDOMÉTODOSDOCONTROLLERÀSAÇÕESDOUSUÁRIO
letinputData=$('#data');letinputQuantidade=$('#quantidade');letinputValor=$('#valor');
console.log(inputData.value);console.log(parseInt(inputQuantidade.value));console.log(parseFloat(inputValor.value));
}}
Agora,estamosinformandoqueoquerySelectorvaiparaavariável$,mas aindamanterádocument como seu contexto.Destavez,ocódigofuncionarácorretamentenonavegador.
Temos "amoringa e a cartucheira" emmãos para podermoscriarumainstânciadenegociaçãocomosdadosdoformulário.Noentanto, o código anterior, apesar de ninja, não é um código decangaceiro. Podemos melhorá-lo ainda mais e faremos isso aseguir.
O código funciona, mas se adicionarmos dez negociações eclicarmosdezvezesemIncluir,oquerySelectorbuscaráoselementos identificadospor#data,#quantidade e #valordezvezestambém.Contudo,devemosevitaraomáximopercorrero DOMporquestõesdeperformance.
Nossocódigoestáassim:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
3.4 EVITANDO PERCORRERDESNECESSARIAMENTEODOM
3.4EVITANDOPERCORRERDESNECESSARIAMENTEODOM 67
adiciona(event){
event.preventDefault();
let$=document.querySelector.bind(document);
letinputData=$('#data');letinputQuantidade=$('#quantidade');letinputValor=$('#valor');
console.log(inputData.value);console.log(parseInt(inputQuantidade.value));console.log(parseFloat(inputValor.value));
}}
Pormaisqueoimpactodeperformancebuscandoapenastrêselementos pelo seu ID não seja tão drástico em nossa aplicação,vamospensarcomocangaceiroseaplicarboaspráticas.
Paramelhoraraperformance,adicionaremosoconstructoremoveremosos elementosde entradaparadentrodele.Mas emvez de criarmos variáveis, criaremos propriedades em this .Sendoassim,abuscapeloselementosdoDOMsóserãorealizadasumaúnicaveznoconstructor(),enãomaisacadachamadadométodoadiciona().
Comoessaspropriedadessófazemsentidoaoseremacessadaspela própria classeNegociacaoControler, usaremos o prefixo_(aconvençãoparapropriedadesprivadas):
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');
68 3.4EVITANDOPERCORRERDESNECESSARIAMENTEODOM
this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
}
adiciona(event){
event.preventDefault();
//precisamosacessaraspropriedadesatravésdethisconsole.log(this._inputData.value);console.log(parseInt(this._inputQuantidade.value));console.log(parseFloat(this._inputValor.value));
}}
Agora,mesmo que incluamos 300 negociações, os elementosdo DOM serão buscados uma única vez, quando a classeNegociacaoController for instanciada. No entanto, as coisas
nãosaemcomoesperado.
Depois de clicarmos no botão Incluir e verificarmos oconsoledoChrome,vemosaseguintemensagemdeerro:
UncaughtTypeError:Cannotreadproperty'value'ofundefinedatHTMLFormElement.adiciona(NegociacaoController.js:15)
Temos o mesmo problema quando associamos odocument.querySelectoràvariável$,queperdeuareferênciade this para o document . Vamos olhar o arquivoclient/app/app.js:
//client/app/app.js
letcontroller=newNegociacaoController();document
.querySelector('.form').addEventListener('submit',controller.adiciona);
Temos o mesmo problema aqui. Quando passamoscontroller.adicionaparaométodoaddEventListener,seu
3.4EVITANDOPERCORRERDESNECESSARIAMENTEODOM 69
this deixou de ser o controller e passou a ser o próprioelemento doDOM, no caso, o formulário. Isso acontece porquetodafunção/métodopossuiumthisdinâmicoqueassumecomovalorocontextonoqualfoichamado.Masjáaprendemosalidarcomisso.
Vamoslançarmãomaisumavezdafunçãobind:
//client/app/app.js
letcontroller=newNegociacaoController();
//bindaqui!document
.querySelector('.form').addEventListener('submit',controller.adiciona.bind(controller));
Se executarmos o código, veremos que eles serão exibidoscorretamentenoconsole.
Figura3.2:Dadosnoconsole
Agora que o usuário pode chamar ométodo adiciona da
3.5 CRIANDO UMA INSTÂNCIA DENEGOCIAÇÃO
70 3.5CRIANDOUMAINSTÂNCIADENEGOCIAÇÃO
nossa classe NegociacaoController podemos construir umanegociaçãocombasenosdadosdoformulário:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoanterioromitido
adiciona(event){
event.preventDefault();
letnegociacao=newNegociacao(this._inputData.value,parseInt(this inputQuantidade value),parseFloat(this._inputValor.value)
);
console.log(negociacao);}
}
Passamososparâmetrosdata,quantidadeevalorparaoconstructordeNegociacao.Comosdadosdoformulário,nósjátemosumanegociação.Masseráquetemosmesmo?
OlhandooconsoledoChrome,vemosaseguintemensagemdeerro:
Negociacao.js:5UncaughtTypeError:Cannotreadproperty'getTime'ofundefined
atnewNegociacao(Negociacao.js:5)atNegociacaoController.adiciona(NegociacaoController.js:15)
Recebemos uma mensagem de erro, mas não foi emNegociacaoController.OerroocorreunaclasseNegociacao.Noconsole,vemosaindaemquallinhafoioproblema,alémdeelenos avisar quedata.getTime não é uma função. Como isso épossível?
3.5CRIANDOUMAINSTÂNCIADENEGOCIAÇÃO 71
Vamosdarumaolhadinhanadefiniçãodanossaclasse:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
constructor(_data,_quantidade,_valor){
Object.assign(this,{_quantidade,_valor});this._data=newDate(_data.getTime());Object.freeze(this);
}
//códigoposterioromitido
Quer dizer então que a data que estamos passando para oconstructornãoéumaDate?Quetipodedadoelaéentão?
Para descobrirmos o tipo da data capturada no formulário,podemosusarafunçãotypeof():
//client/app/controllers/NegociacaoController.js
adiciona(event){
event.preventDefault();
//verificandootipoconsole.log(typeof(this._inputData.value));
letnegociacao=newNegociacao(this._inputData.value,parseInt(this._inputQuantidade.value),parseFloat(this._inputValor.value)
);
console.log(negociacao);}
Atravésdoconsole,vemosqueavalordadatadoelementoédotipostring:
72 3.5CRIANDOUMAINSTÂNCIADENEGOCIAÇÃO
Figura3.3:String
Encarar conversões de datas nas mais diversas linguagensrequer determinação. É por isso que precisamos da nossacartucheiraedonossochapéudecangaceiroaseguir.
Existem váriasmaneiras de construirmos uma data passandoparâmetrosparaoconstructordeDate.Umadasformasquejávimoséque,sejátemosumadata,podemospassaroresultadodogetTime() dela para o constructor de outra. Será quefuncionasepassarmosastringdiretamente?Vamostentar:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
adiciona(event){
event.preventDefault();
//oconstructorestárecebendoumastring.Seráquefunciona
letdata=newDate(this._inputData.value)console.log(data);
}
//códigoposterioromitido
3.6 CRIANDO UM OBJETO DATE A PARTIRDAENTRADADOUSUÁRIO
3.6CRIANDOUMOBJETODATEAPARTIRDAENTRADADOUSUÁRIO 73
Realizando um teste com a última alteração, não temossucesso:
Figura3.4:DataerradanoConsole
Adataqueapareceunoconsolenãofoiamesma.Emvezdisso,vemosqueodiaé11.Então,nãofuncionou.Temosdeencontraroutrojeitodetrabalharcomessadata.
Outramaneirade trabalharmoscomDate é passarmosumarray para seu construtor com os valores do ano, mês e dia,respectivamente.Sedigitarmosnoconsole:
newDate(['2016','11','12']);
Oretornoseráadatacorreta.
Estaéumaformaderesolvermos.Precisamosentãoencontraruma maneira de transformar a string retornada porthis._inputData.valueemumarraydestrings.AboanotíciaéquestringssãoobjetosemJavaScripte,comotodoobjeto,sabemexecutartarefasparanós.
É através do método split de uma string que podemostransformá-la em um array. No entanto, para que o métodoidentifique cada elemento, precisamos informar um separador*.Comoa string retornadaporthis._inputData.value segueopadrãoano-mês-dia,utilizaremosohífencomoseparador.
74 3.6CRIANDOUMOBJETODATEAPARTIRDAENTRADADOUSUÁRIO
QuetalumtesteantesnoconsoledoChrome?
dataString='2016-12-11';lista=dataString.split('-')console.log(lista);//exibe["2016","12","11"]
Ele nos retornou a data correta, perfeito. Então, no métodoadiciona da nossa classe NegociacaoController , faremos
assim:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
//agoraoDaterecebeumarrayletdata=newDate(this._inputData.value.split('-'));console.log(data);}//códigoposterioromitido
Veja que, em uma única instrução, capturamos dethis._inputData.value , realizamos o split e passamos o
resultado final para Date . Resolvemos o problema da data!Vejamosoresultadonoconsole!
SatNov12201600:00:00GMT-0200(-02)
Masnossa soluçãonãoparaporaqui,háoutras formasde seresolver o mesmo problema. Por exemplo, quando um Daterecebeumarray,internamenteelereagrupatodososelementosdoArrayemumaúnicastringusandoumavírgulacomoseparador.
Oreagrupamentointernoéfeitoatravésdafunçãojoin,querecebe como parâmetro o separador utilizado. Podemos simularestecomportamentonoconsole:
3.6CRIANDOUMOBJETODATEAPARTIRDAENTRADADOUSUÁRIO 75
dataString='2016-12-11';lista=dataString.split('-');//separouosnúmeroslista=lista.join(',')//juntouosnúmerosusandovírgulacomoseparadorconsole.log(lista);
Noentanto,podemosemnossocódigosimplesmentetrocaroshifensporvírgulasepassarastringresultanteparaonovoDate.Para isso,podemospedirajudaà funçãoreplace, presente emtodas as strings. Passaremos como parâmetro uma expressãoregular,pedindoqueseja trocadoohífende todasasocorrênciasdastring(ouseja,global)porvírgula:replace(/-/g,',').
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
//outraformaderesolverletdata=newDate(this._inputData.value.replace(/-/g,','))
;console.log(data);
}
Eoresultadonoconsolefica:
SatNov12201600:00:00GMT-0200(-02)
A data ficou correta. Vimos que existem várias formas deresolver a questão da data. Mas que tal escolhermos uma outramaneira para testar nossas habilidades de cangaceiro? É o queveremosaseguir.
HámaisumaformaaceitapeloDatenaconstruçãodeuma
3.7UMDESAFIOCOMDATAS
76 3.7UMDESAFIOCOMDATAS
novadata.NoconsoledoChrome,escreveremosaseguintelinha:
data=newDate(2016,11,12)console.log(data);
No entanto, se imprimirmos esta data, veremos o seguinteretorno:
MonDec12201600:00:00GMT-0200(-02)
AdataimpressaéMonDec122016,ouseja,12/12/2016.Porqueeleimprimiudezembro,sepassamosomês11?Porquenestaformaomêsdeveserpassadode0(janeiro)a11 (dezembro).Então, se queremos que a data seja em novembro, precisamosdecrementar o valor do mês. Vamos fazer um novo teste noconsole,digitando:
data=newDate(2016,10,12)console.log(data);
Agora,adatajáaparececorreta.
SatNov12201600:00:00GMT-0200(-02)
Todas as soluções que vimos anteriormentepara se construirumadata foramsoluçõesninjas.Noentanto, cangaceirosgostamdedesafios,eporissoescolheremosestaúltimaformaquevimos.
Então,voltandoaoproblema,comodeveserfeitaaconversãoda string no formato 2016-11-12 , capturada dethis._inputData.valueparaumDate, usando a formaque
acabamosdeaprender?Éissoqueveremosaseguir.
3.8 RESOLVENDO UM PROBLEMA COM OPARADIGMAFUNCIONAL
3.8RESOLVENDOUMPROBLEMACOMOPARADIGMAFUNCIONAL 77
NossoobjetivoéqueoDate receba em seuconstructorumano,mêsedia.Vamosalterarnossocódigolançandomãodafunçãosplit():
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
letdata=newDate(this._inputData.value.split('-'));console.log(data);
}
//códigoposterioromitido
Atravésdosplit,astring2016-11-12setornaráumarrayde três elementos. Sabemos que se passarmos da forma como jáestá, conseguiremos o resultado desejado. Porém, não queremosqueoDaterecebaumarray,queremosqueelerecebacadaitemdoarraycomocadaparâmetrodoseuconstructor.Queremosque ele receba a primeira posição do array como a primeiraposição do constructor , e que o processo se repita com osegundoeterceiroelementodoarray.
ApartirdoES2015(ES6),podemosutilizarospreadoperator,que permite tratar cada elemento do array como um elementoindividualmente.Leiaatentamenteaalteraçãonocódigo:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
//usamososfamosostrêspontinhos!letdata=newDate(...this._inputData.value.split('-'));
78 3.8RESOLVENDOUMPROBLEMACOMOPARADIGMAFUNCIONAL
console.log(data);}
//códigoposterioromitido
Adicionamos o spread operator ... (reticências) antes doparâmetro recebidopeloconstructor deDate.Comele, nolugardeoconstructorreceberoarraycomoúnicoparâmetro,ele receberá cada elemento do array individualmente. Oselementosserãopassadosnamesmaordemparaoconstructordaclasse.
Nossocódigoestásintaticamentecorretoeostrêsparâmetrospara o constructor de Date serão passados. Porém, o mêsficará incorretosenãooajustarmos, subtraindo1de seuvalor.VejamosseuresultadonoconsoledoChrome:
MonDec12201600:00:00GMT-0200(-02)
Ospreadoperatoroperatorfuncionou,masaindaprecisamosencontrar uma forma de, antes de reposicionarmos cadaparâmetrodoarrayparaoconstructordeDate,decrementarmos1 do valor do mês. Para isto, trabalharemos com a funçãomap(),bemconhecidanomundoJavaScriptequenospermitirárealizaroajustequenecessitamos.
Primeiro,chamaremosafunçãomap()semrealizarqualquertransformação,apenasparaentendermosemqual lugardonossocódigoeledeveentrar:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
3.8RESOLVENDOUMPROBLEMACOMOPARADIGMAFUNCIONAL 79
letdata=newDate(...this._inputData.value.split('-').map(function(item){
returnitem;})
);console.log(data);
}
//códigoposterioromitido
A lógica passada para map() atuará em cada elemento doarray,retornandoumnovoarraynofinal.Masdojeitoqueestá,oarray resultante será idêntico ao array que estamos querendotransformar.Precisamosagoraaplicaralógicaquefaráoajustedomês, subtraindo 1 de seu valor. Mas como conseguiremosidentificar seestamosnoprimeiro, segundoou terceiroelementoda lista? Essa informação é importante, porque queremos alterarapenasosegundo.
Aboanotíciaéqueafunçãomap()podedisponibilizarparanósaposiçãodoelementoiteradonosegundoparâmetropassadoparaela:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
letdata=newDate(...this._inputData.value.split('-').map(function(item,indice){
//lembre-sedequearrayscomeçamcomíndice0,//logo,1éosegundoelemento!
80 3.8RESOLVENDOUMPROBLEMACOMOPARADIGMAFUNCIONAL
if(indice==1){//oresultadoaquiseráumnúmero,conversãoimp
lícitareturnitem-1;
}returnitem;
}));console.log(data);
}
//códigoposterioromitido
Quandoindice for1, significa que estamos iterando nosegundoelemento,aquelequecontémovalordomêsequeprecisaserajustado.Nacondiçãoif, realizamos facilmenteesseajuste.No entanto, sabemos que os elementos do array que estamositerando são do tipo string , sendo assim, qual a razão daoperaçãodesubtraçãofuncionar?
Quando o interpretador do JavaScript detecta operações demultiplicação, divisão e subtração envolvendo uma string e umnúmero,pordebaixodospanostentaráconverterimplicitamenteastringemumnúmero.Casoastringnãorepresenteumnúmero,oresultadoseráNaN(NotaNumber).
Por fim, o resultado bem-sucedido da operação resultará emum novo número. Operações de soma não são levadas emconsideraçãona conversão implícita, porque o interpretador nãoterácomosabersedeveconcatenarouconverteronúmero.
Veremos se o código vai funcionar. No formulário,preencheremosocampodadatacom12/11/2016.Destavez,adataqueapareceránoconsoleestarácorreta.
3.8RESOLVENDOUMPROBLEMACOMOPARADIGMAFUNCIONAL 81
Figura3.5:Datascorrespondentes
Nossocódigofuncionaperfeitamenteeconseguimosresolveroproblema.Porém,cangaceiros falampouco, sãohomensdeação.Será que podemos escrever menos código e conseguir o mesmoresultado?Sim,atravésdomódulodoíndice.
Namatemática,módulo é o resto da divisão. No console doChrome,vamosrealizarumteste:
2%2//omóduloé0,poisesteéorestodadivisãode2por2
Vejamosoutroexemplo:
3%2//omóduloserá1,poiséorestodadivisãode3por2
Osíndicesdonossoarraysão0,1e2, respectivamente.Sendo assim, se formos calcular omódulo de cadaumdeles pordois,temososmódulos:
Recordaréviver
82 3.8RESOLVENDOUMPROBLEMACOMOPARADIGMAFUNCIONAL
0%2//móduloé01%2//móduloé12%2//móduloé0
Vejaqueéjustamenteíndicedomêsquepossuioresultado1,justamenteovalorqueprecisamossubtrair.Porfim,nossocódigoficaráassim:
//códigoanterioromitido
adiciona(event){
event.preventDefault();
letdata=newDate(...this._inputData.value.split('-').map(function(item,indice){returnitem-indice%2;
}));
console.log(data);}
//códigoposterioromitido
Seestamosnaprimeiraposiçãodoarray,ovalordeindiceé0.Porisso,oresultadodeindice%2seráiguala0também.Se subtrairmos este valor de item, nadamudará.Mas quandoestivermosna segundaposiçãodo array, oindice será igual a1.Agora,quandocalcularmos1módulode2,oresultadoserá1.Equandoestivermosnaterceiraposiçãodoarray,2módulode2,tambémseráiguala0.Nãodiminuiremosnadadovalordoitem.Dessaformaconseguimosevitaracriaçãodeumif:
3.8RESOLVENDOUMPROBLEMACOMOPARADIGMAFUNCIONAL 83
Figura3.6:Valoresdosmódulos
Atéaqui,onossocódigoficouassim:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
}
adiciona(event){
event.preventDefault();
letdata=newDate(...this._inputData.value.split('-').map(function(item,indice){returnitem-indice%2;
}));
console.log(data);
84 3.8RESOLVENDOUMPROBLEMACOMOPARADIGMAFUNCIONAL
}}
Seeleforexecutado,veremosqueadataquesurgiránoconsoleserá12denovembrode2016.
Podemosaindadeixaronossocódigomenosverboso,usandouma forma diferente de se declarar funções, usada a partir daversãoES2015(ES6).Falamosdasarrowfunctions.
O termo arrow traduzido para o português significa flecha.Com estas "funções flecha", podemos eliminar a palavrafunctiondocódigo.Massesimplesmenteapagarmosapalavraetentarmosexecutarocódigo,teremosumerrodesintaxe,queseráapontado no navegador. Para emitirmos o termo, teremos deadicionar=>(olhaaflechaaqui!).Elasinalizaráquetemosumaarrowfunctionquereceberádoisparâmetros.
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
letdata=newDate(...this._inputData.value.split('-').map((item,indice)=>{
returnitem-indice%2;})
);
console.log(data);
3.9 ARROW FUNCTIONS: DEIXANDO OCÓDIGOAINDAMENOSVERBOSO
3.9ARROWFUNCTIONS:DEIXANDOOCÓDIGOAINDAMENOSVERBOSO 85
}
//códigoposterioromitido
Quandousamosasintaxedaflecha,jásabemosquesetratadeumaarrowfunction.Seatualizarapáginanonavegador,veremosquetudofunciona.
Agora, uma pergunta: no bloco da arrow functions, quantasinstruçõesnós temos?Apenasuma:returnitem-indice%2.Alinhaestádentrodafunçãomap():
//analisandoumtrechodocódigo
.map((item,indice)=>{returnitem-indice%2;
});
Quandonossaarrowfunctionpossuiduasoumaisinstruções,somosobrigadosautilizarobloco{}, inclusiveaadicionarumreturn. No entanto, como nosso código possui apenas uma
instrução,épermitidoremoverobloco{},inclusiveainstruçãoreturn. Isso pois ela é adicionada implicitamente pela arrow
function.Nossocódigosimplificadoficaassim:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
letdata=newDate(...this._inputData.value.split('-').map((item,indice)=>item-indice%2)
);
console.log(data);}
86 3.9ARROWFUNCTIONS:DEIXANDOOCÓDIGOAINDAMENOSVERBOSO
//códigoposterioromitido
Vejamcomoocódigoficoumaissimples!
Se rodarmos o código no navegador, o formulário estáfuncionandonormalmente,eadataaparecerácomodesejamosnoconsole. Nós conseguimos reduzir a "verbosidade" do códigousando uma arrow function. Agora, só nos resta criar umainstânciadeNegociacaocomosdadosdoformulário:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
letdata=newDate(...this._inputData.value.split('-').map((item,indice)=>item-indice%2)
);
letnegociacao=newNegociacao(data,parseInt(this._inputQuantidade.value),parseFloat(this._inputValor.value)
);
console.log(negociacao);}//códigoposterioromitido
Nossa saga em lidar com datas ainda não terminou. Somoscapazes de criar um novo objeto Date a partir dos dados doformulário,mascomofaremossequisermosexibirumDateparao usuárionoformatodia/mês/ano?Preparadosparaopróximocapítulo?
3.9ARROWFUNCTIONS:DEIXANDOOCÓDIGOAINDAMENOSVERBOSO 87
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/03
88 3.9ARROWFUNCTIONS:DEIXANDOOCÓDIGOAINDAMENOSVERBOSO
CAPÍTULO4
"Conto ao senhor é o que eu sei e o senhor não sabe; masprincipalquerocontaréoqueeunãoseisesei,equepodeserqueosenhorsaiba."-GrandeSertão:Veredas
Jásomoscapazesde instanciarumanegociaçãocombasenosdados do formulário. Em breve, precisaremos exibir seus dadosdentro da <table> de client/index.html. Mas será que aexibiçãodadataseráamigável?
Vamos alterar o método adiciona() deNegociacaoController para que seja impressa a data da
negociaçãocriadaemvezdoobjetointeiro:
//client/src/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
//códigoanterioromitido
//acessandoogetterdadataexibindo-anoconsole
DOISPESOS,DUASMEDIDAS?
4DOISPESOS,DUASMEDIDAS? 89
console.log(negociacao.data);}
//códigoposterioromitido
Agora,paraadata12/11/2016 informada,suaapresentaçãonoconsoleficanesteformato:
SatNov12201600:00:00GMT-0200(BRST)
Apesar de ser uma informação válida, ficaria muito maissimples e direto exibirmos para o usuário a data no formatodia/mês/ano . É um problema que precisamos resolver, pois
muito embreve incluiremosnegociações emnossa tabela, eumadas colunas se refere à propriedade data de uma negociação.Faremosumexperimento.
Aindanométodoadiciona(),vamosimprimiradatadestamaneira:
//client/src/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
//códigoanterioromitido
letdiaMesAno=negociacao.data.getDate()+'/'+negociacao.data.getMonth()+'/'+negociacao.data.getFullYear();
console.log(diaMesAno);}
//códigoposterioromitido
Com o getDate() retornaremos o dia e depois, com ogetMonth(), retornaremos omês e, com getFullYear(), o
ano.Todosforamconcatenadoscomadata.
90 4DOISPESOS,DUASMEDIDAS?
Masadataqueapareceránoconsoleaindanãoseráacorreta:
Figura4.1:Dataerradanoconsole
Noconsole,vemosomês10.Issoaconteceporqueeleveiodeumarrayquevaide0a11.Então,seadatagravadafornomês11,eleseráimpressonomês10.
Umatentativaparasolucionarmosoproblemaésomando+1aoretornodegetMonth():
//client/src/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
//códigoanterioromitido
letdiaMesAno=negociacao.data.getDate()+'/'+negociacao.data.getMonth()+1+'/'+negociacao.data.getFullYear();
4DOISPESOS,DUASMEDIDAS? 91
console.log(diaMesAno);}
//códigoposterioromitido
Figura4.2:Dataesquisita
Contudo,o resultadoaindanão éo esperado e aindaexibeomêscomo"101",umastring!Aliás,esseéumproblemaclássicodeconcatenaçãocomquetodoprogramadorJavaScriptsedeparaquandoestácomeçandoseuprimeirocontatocomalinguagem.
Para resolvermos o problema da precedência dasconcatenações, basta adicionarmos parênteses na operação quequeremos que seja processada primeiro, antes de qualquerconcatenação:
//client/src/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
//códigoanterioromitido
letdiaMesAno=negociacao.data.getDate()+'/'+(negociacao.data.getMonth()+1)+'/'+negociacao.data.getFullYear();
console.log(diaMesAno);}
//códigoposterioromitido
92 4DOISPESOS,DUASMEDIDAS?
Básico de JavaScript. Um teste demonstra o texto correto deexibição da data. Podemos tornar ainda melhor nosso código,principalmentequandooassuntoélidarcomdatas.
Empoucas linhas, garantimos a conversão de uma string emobjetoDate, inclusive o caminho inverso, umDate em umastringno formatodia/mês/ano.No entanto, em todo lugardanossaaplicaçãoqueprecisarmosrealizaressasoperações, teremosderepetirumcódigoquejáescrevemos.
Vamos criar o novo arquivoclient/app/ui/converters/DateConverter.js . Como
sempre, já vamos importar o arquivo em client/index.htmlparanãocorrermosoriscodeesquecermosdeimportá-lo:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/app.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script>
<!--códigoposterioromitido-->
No arquivo que acabamos de criar, vamos declarar a classeDateConvertercomoesqueletodedoismétodosquerealizarãoaconversão:
//client/app/ui/converters/DateConverter.js
classDateConverter{
4.1 ISOLANDO A RESPONSABILIDADE DECONVERSÃODEDATAS
4.1ISOLANDOARESPONSABILIDADEDECONVERSÃODEDATAS 93
paraTexto(data){
}
paraData(texto){
}}
Teremos pouco esforço para implementá-lo, pois podemoscopiaralógicadeconversãoespalhadapelométodoadicionadeNegociacaoController,sófaltandoadequarocódigoaonome
dosparâmetrosrecebidospelosmétodos:
//client/app/ui/converters/DateConverter.js
classDateConverter{
paraTexto(data){
returndata.getDate()+'/'+(data.getMonth()+1)+'/'+data.getFullYear();
}
paraData(texto){
returnnewDate(...texto.split('-').map((item,indice)=>item-indice%2));
}}
Agora, precisamos alterar a classe NegociacaoController.No seu método adiciona() , usaremos a nova classe queacabamosdecriar:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
94 4.1ISOLANDOARESPONSABILIDADEDECONVERSÃODEDATAS
event.preventDefault();
letconverter=newDateConverter();
letdata=converter.paraData(this._inputData.value);
letnegociacao=newNegociacao(data,parseInt(this._inputQuantidade.value),parseFloat(this._inputValor.value)
);
letdiaMesAno=converter.paraTexto(negociacao.data);
console.log(diaMesAno);}
//códigoposterioromitido
4.1ISOLANDOARESPONSABILIDADEDECONVERSÃODEDATAS 95
Figura4.3:Negociaçãonocontroller
Adatatambéméimpressacorretamentenosdoisformatos.Noentanto, talvez vocês possam estar se perguntando como ooperadornew funcionoucomDateConverter, umavezque aclassenãopossuiumconstructor.Nãoaconteceunenhumerroporque classes quenãodefinemumconstructor ganham umpadrão.
96 4.1ISOLANDOARESPONSABILIDADEDECONVERSÃODEDATAS
NósconseguimosisolarocódigodentrodoHelper.Masseráquepodemosmelhorá-loaindamais?
Aprendemos que instâncias de uma classe podem terpropriedadesequecadainstânciapossuiseuprópriovalor.Masseprocurarmos por propriedades de instância na classeDateConverter , não encontraremos nenhuma. Sendo assim,
estamos criandouma instânciadeDateConverter apenasparapodermos chamar seusmétodos que não dependem de nenhumdadodeinstância.
Quando temos métodos que não fazem sentido seremchamados de uma instância, como no caso do métodoparaTexto() que criamos, podemos chamá-losdiretamenteda
classe na qual foram declarados, adicionando o modificadorstaticantesdonomedométodo:
//client/app/ui/converters/DateConverter.js
classDateConverter{
staticparaTexto(data){
returndata.getDate()+'/'+(data.getMonth()+1)+'/'+data.getFullYear();
}
staticparaData(texto){
returnnewDate(...texto.split('-').map((item,indice)=>item-indice%2));
}}
4.2MÉTODOSESTÁTICOS
4.2MÉTODOSESTÁTICOS 97
Agora, podemos chamar os métodos diretamente da classe.AlterandoocódigodeNegociacaoController:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();
//CHAMANDOOMÉTODOESTÁTICO
letnegociacao=newNegociacao(DateConverter.paraData(this._inputData.value),parseInt(this._inputQuantidade.value),parseFloat(this inputValor value)
);
console.log(negociacao.data);
//CHAMANDOOMÉTODOESTÁTICO
letdiaMesAno=DateConverter.paraTexto(negociacao.data);console.log(diaMesAno);
}
//códigoposterioromitido
98 4.2MÉTODOSESTÁTICOS
Figura4.4:Negociaçãonoconsole
Vimos uma novidade em termos de Orientação a Objeto: aclasse DateConverter tem métodos estáticos, o que tornadesnecessáriaacriaçãodeumainstância.
Apesarde funcional,nada impedeoprogramadordesavisadode criar uma instância de DateConverter e, a partir dessainstância,tentaracessaralgummétodoestático.Issoresultarianoerrodemétodooufunçãonãodefinida.
A boa notícia é que podemos pelo menos avisar aodesenvolvedor que determinada classe não deve ser instanciadaescrevendoumconstrutorpadrãoquelanceumaexceçãocomumamensagemclaradequeelenãodeveriafazerisso:
//client/app/ui/converters/DateConverter.js
classDateConverter{
4.2MÉTODOSESTÁTICOS 99
constructor(){
thrownewError('Estaclassenãopodeserinstanciada');}
//códigoposterioromitido
NopróprioconsoledoChromepodemoscriaruma instânciade DateConverter e vermos o resultado. No console,digitaremos:
//dáerro!x=newDateConverter()
Veremososeguinteretorno:
Figura4.5:Mensagemdeerro
Aoverestamensagem,oprogramadorsaberáquetrabalhamoscom métodos estáticos. Se clicarmos no erro, veremos qual é aclasseeondeestáoproblema.
Ainda podemos melhorar um pouco mais a legibilidade danossa classe com um recurso simples, porém poderoso, que
100 4.2MÉTODOSESTÁTICOS
veremosaseguir.
Vamos voltar ao arquivo DateConverter.js e analisar ométodoparaTexto.Nele,realizamosconcatenaçõesenvolvendostringsenúmeros:
//client/app/ui/converters/DateConverter.js
//códigoanterioromitido
staticparaTexto(data){
returndata.getDate()+'/'+(data.getMonth()+1)+'/'+data.getFullYear();
}
//códigoposterioromitido
Inclusivetivemosdeenvolveraexpressão(data.getMonth()+1)entreparênteses,paraqueelativesseprecedência.Noentanto,apartirdoES2015 (ES6), foi introduzidana linguagem templateliteral que nada mais são do que uma forma diferente de sedeclararstringsequeevitaoprocessodeconcatenação.
Atravésdoconsole,vamosrelembraraconcatenaçãoclássica.Primeiramente,digitaremosnele:
nome='Flávio';idade=18;console.log('Aidadede'+nome+'é'+idade+'.');
O primeiro passo é alterarmos a linha do nosso últimoconsole.log,queficaráassim:
nome='Flávio';
4.3TEMPLATELITERAL
4.3TEMPLATELITERAL 101
idade=18;console.log(`Aidadedenomeéidade.`)
Observe que a string foi declara entre ` (backtick), mas seexecutarmosocódigodestaforma,seráexibidaafrase:Aidadedenomeé 18.. Ele não entendeu que o conteúdo da variávelnome deve ser considerado na template literal. Para que isso
aconteça, precisamos colocar todas as variáveis que desejamosdentrodaexpressão${}:
nome-'Flávio';idade=18;console.log(`Aidadede${nome}é${idade}.`)
Com o uso de ${} dentro da template literal, ele fará omecanismo de interpolação. A expressão vai interpolar oconteúdodas variáveisnome eidade na string.Essa forma émenos sujeita a erro do que a anterior, que continha váriasconcatenações, principalmente se a quantidade de variáveisenvolvidasfosseaindamaior.
Agora,podemostransporoconhecimentoaquiadquiridoparanossa classe DateConverter. Seu método paraTexto ficaráassim:
//client/app/ui/converters/DateConverter.js
classDateConverter{
constructor(){
thrownewError('Estaclassenãopodeserinstanciada');}
staticparaTexto(data){
return`${data.getDate()}/${data.getMonth()+1}/${data.getFullYear()}`;
102 4.3TEMPLATELITERAL
}
//códigoposterioromitido
Desta vez, nem foi necessário adicionar parênteses, porquecadaexpressão seráavaliada individualmenteprimeiro,para logoemseguidaseuresultadoserinterpoladocomastring.
A terminologia "Template Literal" substitui o termo"TemplateString"nofinaldaimplementaçãodaespecificaçãodoESCMASCRIPT2015(ES6).
Este foi o nosso primeiro contato com o template literal.Veremosqueesteéumrecursomuitopoderosoaolongodonossoprojeto, colocando-o ainda mais à prova. Porém, ainda nãoacabou.
Nossa classe DateConverter está cada vez melhor.Entretanto, o que acontecerá se passarmos para seu métodoparaData a string11/12/2017, quenão segueopadrão com
hífenestipulado?Vamostestarnoconsole:
//testandonoconsoledate=DateConverter.paraData('11/12/2017')
Seráimpresso:
InvalidDate
Como o método internamente depende da estrutura que
4.4ABOAPRÁTICADOFAIL-FAST
4.4ABOAPRÁTICADOFAIL-FAST 103
recebe uma data no formato dia/mês/ano, trocar o separadorfará com que o split usado internamente por paraDataretorneumvalorinválidoparaoconstructordeDate.Demossorte, já pensou se ele devolvesse uma Data, mas diferente daqualdesejamos?
Podemosusaraboapráticadofail-fast,emquevalidamososparâmetrosrecebidospelométodoejáfalhamosantecipadamenteantes que eles sejamusados pela lógica do programa.Mas claro,fornecendo uma mensagem de erro clara para o desenvolvedorsobreoproblemaocorrido.
Vamos apelar um pouquinho para as expressões regularesparaverificarseotextorecebidopelométodoparaDatasegueopadrãodia/mês/ano.Aexpressãoqueusaremoséaseguinte:
/aexpressãoqueutilizaremos^\d{4}-\d{2}-\d{2}$
Nela,estamosinteressadosemqualquertextoquecomececomquatro dígitos, seguido de um hífen e de um número com doisdígitos.Porfim,elaverificaseotextoterminacomumnúmerodedoisdígitos.
//client/app/ui/converters/DateConverter.js
classDateConverter{
//códigoanterioromitido
staticparaData(texto){
if(!/^\d{4}-\d{2}-\d{2}$/.test(texto)){thrownewError('Deveestarnoformatoaaaa-mm-dd');
}
returnnewDate(...texto.split('-').map((item,indice)=>
104 4.4ABOAPRÁTICADOFAIL-FAST
item-indice%2));}
//códigoposterioromitido
A linha comothrownew só será executada se oif forfalse.Porisso,usamososinalde!.Aindapodemosnoslivrardoblocodoifdestaforma:
//client/app/ui/converters/DateConverter.js
classDateConverter{
//códigoanterioromitido
staticparaData(texto){
if(!/^\d{4}-\d{2}-\d{2}$/.test(texto))thrownewError('Deveestarnoformatoaaaa-mm-dd');
returnnewDate(...texto.split('-').map((item,indice)=>item-indice%2));
}
//códigoposterioromitido
Apesardeoif não termais um bloco, o interpretador doJavaScript entende que a próxima instrução imediatamente apósif será executada apenas se a condição fortrue. As demais
instruçõesnãosãoconsideradascomofazendopartedoblocodoif.Épor issoquea instruçãothrownew... foi indentada,
paradarumapistavisualparaoprogramadordequeelapertenceaoif.
Depoisderecarregarmosnossapágina,podemosrealizartestesnoconsole.Primeiro,comumastringválida:
DateConverter.paraData('2017-11-12')SunNov12201700:00:00GMT-0200(BRST)
4.4ABOAPRÁTICADOFAIL-FAST 105
A data exibida está correta. Agora, forçaremos o erro noconsoleparavermosoqueacontece.
DateConverter.paraData('2017/11/12')UncaughtError:Deveestarnoformatoaaaa-mm-dd(..)
Perfeito, mas o que acontecerá se, no campo da data,digitarmosumanocommaisdequatrodígitos?Amensagemdeerro continuará sendo impressa no console. Mais tarde,aprenderemos a lidar com este tipo de erro, exibindo umamensagemmaisamigávelparaousuário.Porhora,ocódigoqueescrevemos é suficiente para podermos avançar com nossaaplicação.
Agora que já lidamos com as conversões de data, podemosatacaroutrorequisitodaaplicação,alistadenegociações.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/04
106 4.4ABOAPRÁTICADOFAIL-FAST
CAPÍTULO5
"Naturezadagentenãocabeemnenhumacerteza." -GrandeSertão:Veredas
Todas as negociações cadastradas pelo usuário precisam serarmazenadas em uma lista que só deve permitir inclusões enenhumoutrotipodeoperaçãoalémdeleitura.Essarestriçãonãonos permite usar um simples array do JavaScript, devido àampla gama de operações que podemos realizar com ele. Noentanto, o array é uma excelente estrutura de dados paratrabalharmos.Eagora?
Umasoluçãoécriarmosumaclassequeencapsuleoarraydenegociações. O acesso ao array só poderá ser feito através dosmétodos dessa classe, pois estes implementam as regras quequeremosseguir.Emsuma,criaremosummodelodenegociações.
OBANDODEVESEGUIRUMAREGRA
5.1CRIANDOUMNOVOMODELO
5OBANDODEVESEGUIRUMAREGRA 107
Vamos criar o arquivoclient/app/domain/negociacao/Negociacoes.js . A nova
classecriadaterácomopropriedadeumarraydenegociaçõessemqualqueritem:
//client/app/domain/negociacao/Negociacoes.js
classNegociacoes{
constructor(){
this._negociacoes=[];}
adiciona(negociacao){
this._negociacoes.push(negociacao);}
}
Observequeusamosoprefixo_ para indicar que a lista sópodeseracessadapelosmétodosdaprópriaclasse.Devidoaessarestrição, adicionamos ométodo adiciona, que receberá umanegociação. Precisamos tambémde ummétodo quenos permitaleralistadenegociações.
VamoscriarométodoparaArray que, ao ser chamado eminstâncias de Negociacoes , retornará o array internoencapsuladopelaclasse:
classNegociacoes{
constructor(){
this._negociacoes=[];}
adiciona(negociacao){
108 5.1CRIANDOUMNOVOMODELO
this._negociacoes.push(negociacao);}
paraArray(){
returnthis._negociacoes;}
}
Depois,noindex.html,temosdeimportaranossaclasse.
<!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/app.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script>
<!--novoscriptaqui!-->
<scriptsrc="app/domain/negociacao/Negociacoes.js"></script>
<!--códigoposterioromitido-->
De volta ao NegociacaoController , adicionaremos apropriedade negociacoes que receberá uma instância deNegociacoes:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');this._negociacoes=newNegociacoes();
}//códigoposterioromitido
Agora, no método adiciona do nosso controller, vamos
5.1CRIANDOUMNOVOMODELO 109
adicionar a negociação criada com os dados do formulário emnossalistaatravésdométodothis._negociacoes.adiciona().Vamos aproveitar e remover o código que exibia a data danegociaçãoformatada,poiselenãoémaisnecessário:
//client/app/controllers/NegociacaoController.js
//códigoposterioromitidoadiciona(event){
event.preventDefault();
letnegociacao=newNegociacao(DateConverter.paraData(this._inputData.value),parseInt(this._inputQuantidade.value),parseFloat(this._inputValor.value)
);
//incluianegociaçãothis._negociacoes.adiciona(negociacao);
//imprimealistacomonovoelementoconsole.log(this._negociacoes.paraArray());
}//códigoposterioromitido
Pefeito,tudonolugar,jápodemostestar.
A cada inclusão de uma nova negociação, exibiremos noconsole o estado atual da lista encapsulada por Negociacoes.Contudo, essa nova solução parece não ter funcionado, poisqualquertesterealizadoexibiránoconsoleamensagem:
UncaughtReferenceError:NegociacoesisnotdefinedatnewNegociacaoController(NegociacaoController.js:9)atapp.js:1
5.2OTENDÃODEAQUILESDOJAVASCRIPT
110 5.2OTENDÃODEAQUILESDOJAVASCRIPT
O problema é que, em app.js , estamos criando umainstância de NegociacaoController. Esta classe, por sua vez,dependedaclasseNegociacoesqueaindanãofoicarregada!Fazsentido, pois se olharmos a ordem de importação de scripts emclient/index.html,aimportaçãodeNegociacoes.jsfoifeitaapósapp.js:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/app.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script>
<!--precisavirantesdeapp.js--><scriptsrc="app/domain/negociacao/Negociacoes.js"></script>
<!--códigoposterioromitido-->
EssadependênciaentrescriptséumdostendõesdeAquilesdoJavaScript. Isso faz com que o desenvolvedor assuma aresponsabilidadedeimportarosscriptsdesuaaplicaçãonaordemcorreta. Aprenderemos a solucionar esse problema através dosmódulosdoES2015(ES6),masaindanãoéahoradeabordarmosesse assunto, pois temos muita coisa a aprender antes. Porenquanto,vamosnospreocuparcomaordemdeimportaçãodosscripts.
Vamos ajustar o client/index.html, movendo app.jscomoúltimoscriptaserimportado:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script>
5.2OTENDÃODEAQUILESDOJAVASCRIPT 111
<scriptsrc="app/domain/negociacao/Negociacoes.js"></script><!--app.jsagorasempreseráoúltimoscriptporqueeleéoresponsáveleminstanciarNegociacaoControllerqueporconseguintedependedeoutrasclasses-->
<scriptsrc="app/app.js"></script><!--códigoposterioromitido-->
Agora nosso teste exibe a lista no console assim quecadastramosumanovanegociação:
Figura5.1:Negociaçãonoconsole
Tudo funciona, mas os dados do formulário continuampreenchidos depois do cadastro. É umaboa prática limparmos oformuláriopreparando-oparaapróximainclusão.
Vamos criar o método privado _limpaFormulario(), queconteráalógicaquelimparánossoformulário.
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
112 5.2OTENDÃODEAQUILESDOJAVASCRIPT
//códigoanterioromitido
_limpaFormulario(){
this._inputData.value='';this._inputQuantidade.value=1;this._inputValor.value=0.0this._inputData.focus();
}}
Agora, vamos chamar o método depois de adicionarmos anegociaçãonalista:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoanterioromitido
adiciona(event){
//códigoanterioromitido
//limpandooformuláriothis._limpaFormulario();
}
//coódigoposterioromitido}
Podemos incluir umanovanegociação, queo console exibirácorretamentealistacomseusdoisitens:
5.2OTENDÃODEAQUILESDOJAVASCRIPT 113
Figura5.2:Exibiçõesnoarray
Nossocódigo funciona,maspodemos torná-lo aindamelhor.Quetalisolarmosocódigoqueinstanciaumanegociaçãocombasenosdadosdo formulárioemummétodoprivadodaclasse?Comele,deixaremosclaranossaintençãodecriarumanovanegociação,jáqueseunomedeixaissobastanteexplícito.
NossaclasseNegociacaoControllernofinalficaráassim:
//client/app/controller/NegociacaoController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');this._negociacoes=newNegociacoes();
114 5.2OTENDÃODEAQUILESDOJAVASCRIPT
}
adiciona(event){
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());//mo
dificação!console.log(this._negociacoes.paraArray());this._limpaFormulario();
}
_limpaFormulario(){
this._inputData.value='';this._inputQuantidade.value=1;this._inputValor.value=0.0this._inputData.focus();
}
_criaNegociacao(){
//retornaumainstânciadenegociaçãoreturnnewNegociacao(
DateConverter.paraData(this._inputData.value),parseInt(this._inputQuantidade.value),parseFloat(this._inputValor.value)
);}
}
Depois de concluída as alterações, um novo teste exibe asinformaçõesesperadasnoconsole:
5.2OTENDÃODEAQUILESDOJAVASCRIPT 115
Figura5.3:Negociaçõescomoesperado
Tudoestáfuncionandocorretamente.Masseráqueinstânciasde Negociacoes realmente só permitem adicionar novasnegociaçõesequenenhumaoutraoperaçãoépermitida?Faremosumtesteaseguirparadescobrirmos.
Para sabermos se as negociações encapsuladas porNegociacao são imutáveis, podemos realizar um rápido teste,
apagandotodosos itensda lista.Para isso,vamosacessaroarrayretornado pelo método paraArray() de Negociacoes ,atribuindo0àpropriedadelengthquetodoarraypossui.Issoserásuficienteparaesvaziá-la:
//client/app/controller/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());
5.3BLINDANDOONOSSOMODELO
116 5.3BLINDANDOONOSSOMODELO
//seráqueconseguimosapagaralista?this._negociacoes.paraArray().length=0;
//qualoresultado?console.log(this._negociacoes.paraArray());this._limpaFormulario();
}
//códigoposterioromitido
Executandonossoteste:
Figura5.4:Arrayvazio
Conseguemidentificaroproblema?Qualquertrechodonossocódigo que acessar o array de negociações devolvido através dométodo paraArray terá uma referência para o mesmo arrayencapsulado de Negociacoes . Já vimos esse problema antes,quando trabalhamos com datas. Inclusive, aplicamos umaprogramação defensiva, uma estratégia que caimuito bem nestasituação.
A ideia é retornarmos um novo array, ou seja, uma novareferênciacombasenositensdoarraydenegociaçõesencapsuladopela classeNegociacoes. Criaremos um novo array através deum truque: faremos return com array vazio, seguido pelométodo concat() que receberá a lista cujos itens desejamos
5.3BLINDANDOONOSSOMODELO 117
copiar:
//client/app/domain/negociacoes/Negociacoes.js
classNegociacoes{
constructor(){
this._negociacoes=[];}
adiciona(negociacao){
this._negociacoes.push(negociacao);}
paraArray(){
//retornaumanovareferênciacriadacomositensdethis._negociacoes
return[].concat(this._negociacoes);}
}
Aopassarmosothis._negociacoesdentrodoconcat(),o retornoseráumanovalista,umnovoarray.Agorasetentarmosutilizarainstruçãothis._negociacoes.paraArray().length=0 , nada acontecerá com a lista encapsulada pela instância deNegociacoes,porqueestamosmodificandoumacópiadoarray,enãooarrayoriginal.
118 5.3BLINDANDOONOSSOMODELO
Figura5.5:Arrayblindado
Agora que já temos tudo no lugar, vamos alterarNegociacaoController e remover do método adiciona o
códigoque tenta esvaziar a lista e todas as chamadas ao console,deixando apenas as três instruções essenciais para seufuncionamento:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._limpaFormulario();
}
//códigoposterioromitido
Dessa forma, garantimos que nossa lista de negociação sópoderá receber novas negociações e nenhuma outra operaçãopoderáserrealizada.
Agoraquetemosnossocontrollerenossosmodelos,jáestamospreparadospara apresentarosdadosdonossomodelonapáginaindex.html.Esteéoassuntodopróximocapítulo.
5.3BLINDANDOONOSSOMODELO 119
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/05
120 5.3BLINDANDOONOSSOMODELO
CAPÍTULO6
"Feitoflecha,feitofogo,feitofaca."-GrandeSertão:Veredas
Criamos as classes Negociacao e Negociacoes paramodelar as regras de negócio solicitadas. Inclusive criamosNegociaoControllerparaqueousuáriopudesseinteragircom
instâncias dessas classes.No entanto, aindanão apresentamos osestadosdessesmodelosemclient/index.html.
Precisamoscriarumasoluçãoquesejacapazde traduzirumainstânciadeNegociacoesemalgumaapresentaçãoqueousuárioentenda, por exemplo, uma <table> do HTML. Estamosquerendo implementar uma solução de View, o V do modeloMVC.
NomodeloMVC,aView éaestruturanaqualapresentamosmodelos. Um modelo pode ter diferentes apresentações, ealterações nessas apresentações não afetam o modelo. Essaseparaçãoévantajosa,porqueainterfacedousuáriomudaotempo
AMODANOCANGAÇO
6.1OPAPELDAVIEW
6AMODANOCANGAÇO 121
todo e, quanto menos impacto as alterações na View causarem,mais fácil será a manutenção da aplicação. A questão agora édefinirmosaestratégiadeconstruçãodasnossasViews.
A estratégia de criação de View que adotaremos seráescrevermos a apresentação dos nossos modelos em HTML nopróprioJavaScript.
Vamos criar o arquivoclient/app/ui/views/NegociacoesView.js . Nele, vamos
declarar a classe NegociacoesView com o único método,chamadotemplate():
//client/app/ui/views/NegociacoesView.js
classNegociacoesView{
template(){
}}
A finalidade dométodo template será o de retornar umastring com a apresentação HTML que desejamos utilizar pararepresentar em breve omodelo Negociacoes. É por isso que,agora, vamos mover a <table> declarada emclient/index.html para dentro do método. Sabemos que, se
simplesmente movermos o código, o runtime do JavaScriptindicaráumasériedeerros.Éporissoquetemplate()retornaráamarcaçãoHTMLcomoumatemplateliteral:
//client/app/ui/views/NegociacoesView.js
6.2NOSSASOLUÇÃODEVIEW
122 6.2NOSSASOLUÇÃODEVIEW
classNegociacoesView{
template(){
return`<tableclass="tabletable-hovertable-bordered">
<thead><tr>
<th>DATA</th><th>QUANTIDADE</th><th>VALOR</th><th>VOLUME</th>
</tr></thead>
<tbody></tbody>
<tfoot></tfoot>
</table>`
}}
Como retiramos o trecho do código referente à tabela, noindex.html,elajánãoserámaisexibidaabaixodoformulário.
Figura6.1:Formuláriosemtabela
6.2NOSSASOLUÇÃODEVIEW 123
Paranãocorrermosoriscodeesquecermos,vamosimportaronovo arquivo client/index.html . Mas atenção,
NegociacaoController dependerá da declaração deNegociacoesView,entãoonovoscriptdeveserimportadoantesdo script NegociacaoController.js . Nossoclient/index.htmlficaráassim:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script>
<!--importouantes!--><scriptsrc="app/ui/views/NegociacoesView.js"></script>
<scriptsrc="app/app.js"></script><!--códigoposterioromitido-->
Agora, em NegociacaoControler , adicionaremos apropriedade this._negociacoesView , que receberá umainstânciadeNegociacaoView:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');this._negociacoes=newNegociacoes();this._negociacoesView=newNegociacoesView();
}//códigoposterioromitido
124 6.2NOSSASOLUÇÃODEVIEW
Após recarregarmos a página, vamos digitar no console asinstruções:
view=newNegociacoesView()view.template()//exibeotemplatedaview
Vemos no console o template como string, definido emNegociacoesView:
Figura6.2:Stringnoconsole
Com certeza não queremos exibir o template no console,queremosqueelesejaexibidoparaousuárioquandoelevisualizarclient/index.html.Comofaremosisso?
O primeiro passo é indicarmos o local emclient/index.html,noqualotemplatedanossaviewdeveser
exibido. Indicaremos através de uma <div> com oid="negociacoes":
<!--códigoanterioromitido--><br><divid="negociacoes"></div><!--entrounomesmolocalondetínhamosa<table>--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><!--códigoposterioromitido-->
6.2NOSSASOLUÇÃODEVIEW 125
Excelente!Porém, comouma instânciadeNegociacaoViewsaberáquedeveserassociadaaoelementodoDOMda<div>queacabamosdeadicionar?
Adicionaremosumconstructor() emNegociacoesViewque receba um seletorCSS. A partir desse seletor, buscamos oelemento do DOM guardando-o na propriedadethis._elemento:
//client/app/ui/views/NegociacoesView.js
classNegociacoesView{
constructor(seletor){
this.elemento=document.querySelector(seletor);}
template(){
//códigodotemplateomitido}
}
Agora, em NegociacaoController , passaremos o seletor#negociacoes para o constructor() da instância deNegociacoesViewqueestamoscriando:
//client/app/ui/views/NegociacoesView.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');this._negociacoes=newNegociacoes();
126 6.2NOSSASOLUÇÃODEVIEW
//passamosparaoconstrutoroseletorCSSdeIDthis._negociacoesView=newNegociacoesView('#negociacoes
);}
//códigoposterioromitido
O código que escrevemos ainda não soluciona a questão decomo transformar o templateNegociacoesView em elementosdoDOM.Estes precisam ser inseridos como elementos filhosdoelemento indicado pelo seletor CSS passado para o seuconstructor() . A solução mora no método update que
criaremos:
//client/app/ui/views/NegociacoesView.js
classNegociacoesView{
constructor(seletor){
this._elemento=document.querySelector(seletor);}
//novométodo!update(){
this._elemento.innerHTML=this.template();}
template(){
//códigoomitido}
}
Ométodoupdate,quandochamado,atribuiráàpropriedadeinnerHTML o script devolvido pelo método template(). OinnerHTMLnaverdadeéumsetter,ouseja,quandoelerecebeseu valor, por debaixo dos panos um método é chamadoconvertendoastringemelementosdoDOM.
6.2NOSSASOLUÇÃODEVIEW 127
Agora, em NegociacaoController , precisamos chamarthis._negociacoesView.update() para que o template da
nossaviewsejarenderizado:
//client/app/controllers/NegociacoesController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');this._negociacoes=newNegociacoes();this._negociacoesView=newNegociacoesView('#negociacoes
);//atualizandoaviewthis._negociacoesView.update();
}//códigoposterioromitido
Apósasúltimasalterações,quandorecarregarmosapáginanonavegador,atabelajáserávisualizada.
Figura6.3:Tabelanapágina
Afunçãoupdate()funcionaeatabelajápodeservisualizada
128 6.2NOSSASOLUÇÃODEVIEW
abaixodo formulário.Porém,osdadosdomodeloaindanãosãolevados em consideração na construção dinâmica da tabela.Podemos cadastrar quantas negociações foremnecessárias, que atabelacriadanãoexibiráosdadosdanossalista.Precisamosdeumtemplatedinâmico!
Para tornarmos nosso template dinâmico, o métodoupdate()passaráarecebercomoparâmetroomodelonoqualafunçãotemplate()devesebasear:
//client/app/ui/views/NegociacoesView.js
classNegociacoesView{
constructor(seletor){
this._elemento=document.querySelector(seletor);}
//recebeomodelupdate(model){
//repassaomodelparathis.template()this._elemento.innerHTML=this.template(model);
}
//PARÂMETROAQUI!//deveretornarotemplatebaseadonomodeltemplate(model){
//códigodotemplateomitido}
}
6.3 CONSTRUINDO UM TEMPLATEDINÂMICO
6.3CONSTRUINDOUMTEMPLATEDINÂMICO 129
Alteraremos NegociacaoController para passarmosthis._negociacoes como parâmetro para
this._negociacoesView.update():
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');this._negociacoes=newNegociacoes();this._negociacoesView=newNegociacoesView('#negociacoes
);//recebeinicialmenteomodeloqueencapsulaumalistav
aziathis._negociacoesView.update(this._negociacoes);
}//códigoposterioromitido
Sónosrestaimplementaralógicadométodotemplate()deNegociacoesView.Cadaobjetodanossalistadenegociaçõesserátransformadoemuma stringque correpondaauma<tr> comsuas <td>. Aliás, como o modelo recebido por template()possuiométodoparaArrayquenosdevolveoarrayencapsuladopor ele, podemos utilizar a poderosa funçãomap, que realizaráum"depara",deobjetoparastring,comalógicaquedefiniremos.
A transformação deve ser feita dentro de <tbody>. Sendoassim, vamos acessar omodelo através da expressão${}. Paraefeito de brevidade, temos apenas o trecho do template quemodificaremosaseguir:
<tbody>${model.paraArray().map(negociacao=>{
130 6.3CONSTRUINDOUMTEMPLATEDINÂMICO
//precisaconverterantesderetornar!returnnegociacao;
})}</tbody>
Não podemos simplesmente retornar a negociação que estásendoiteradapelafunçãomap,poiscontinuaremoscomumarraydenegociações.Precisamosdeumarraydestring,emquecadaelementosejaumastringcomarepresentaçãodeuma<tr>.
Para resolvermos esta questão, para cada negociação iterada,vamos retornar uma nova template literal, representando uma<tr>equeinterpolaosdadosdanegociação:
<tbody>${model.paraArray().map(negociacao=>{
return`<tr>
<td>${DateConverter.paraTexto(negociacao.data)}</td><td>${negociacao.quantidade}</td><td>${negociacao.valor}</td><td>${negociacao.volume}</td>
</tr>`
})}</tbody>
Estamosquase lá!Agoraque temosumnovoarrayde string,precisamosconcatenarcadaelementoemumaúnica string.Esta,no final, será aplicada na propriedadeinnerHTML do elementoque associamos à view. Podemos fazer isso facilmente através dafunçãojoin(),presenteemtodoarray:
<tbody>${model.paraArray().map(negociacao=>{
return`<tr>
<td>${DateConverter.paraTexto(negociacao.data)}</td><td>${negociacao.quantidade}</td><td>${negociacao.valor}</td>
6.3CONSTRUINDOUMTEMPLATEDINÂMICO 131
<td>${negociacao.volume}</td></tr>`
}).join('')}</tbody>
A função join() aceita um separador, no entanto, nãoqueremos separador algum, por isso passamos uma string embranco para ele. Por fim, não podemos nos esquecer dechamarmosthis._negociacoesView.update(this._negociacoes) logo
apósa inclusãodanegociaçãona lista,paraquenossa tabela sejarenderizadalevandoemconsideraçãoosdadosmaisatuais.
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._negociacoesView.update(this._negociacoes);this._limpaFormulario();
}
//códigoposterioromitido
Vamos ver o que será exibido no navegador, após opreenchimentodoformulário:
Figura6.4:Tabelacomvalores
Emseguida,adicionaremosumanovanegociaçãoeseusdadostambémserãoexibidosnatabela.
132 6.3CONSTRUINDOUMTEMPLATEDINÂMICO
Figura6.5:Tabelacomduasnegociações
Se realizarmos novas inclusões, nossa tabela será renderizadanovamente,representandooestadoatualdomodelo.
Atualmente,oinnerHTMLébemperformático.Noentanto,sea quantidade de dados a ser renderizada for exorbitante, asoluçãode incluircada<tr> individualmente sedestacaria,apesar de mais verbosa. Porém, é uma boa prática utilizarpaginação quando trabalhamos com uma massa de dadosconsiderável, o que minimizaria possíveis problemas deperformancedoinnerHTML.
De maneira elegante, usando apenas recursos do JavaScript,conseguimos fazer um template engine.Mas que tal enxugarmosumpoucomaisocódigodonossotemplate?
Todaarrowfunctionquepossuiapenasumainstruçãopodeterseubloco{}removido,inclusiveainstruçãoreturn,poiselajáassume como retorno o resultado da instrução. Alterando nossocódigo:
<tbody>${model.paraArray().map(negociacao=>`
<tr><td>${DateConverter.paraTexto(negociacao.data)}</td><td>${negociacao.quantidade}</td>
6.3CONSTRUINDOUMTEMPLATEDINÂMICO 133
<td>${negociacao.valor}</td><td>${negociacao.volume}</td>
</tr>`).join('')}</tbody>
Agora, com nossa alteração, a única instrução da arrowfunction é a criação da template literal, que é retornadaautomaticamente.
Porfim,nossométodotemplatecompletoficouassim:
//client/app/ui/views/NegociacoesView.js//códigoanterioromitido
template(model){
return`<tableclass="tabletable-hovertable-bordered">
<thead><tr>
<th>DATA</th><th>QUANTIDADE</th><th>VALOR</th><th>VOLUME</th>
</tr></thead>
<tbody>${model.paraArray().map(negociacao=>`
<tr><td>${DateConverter.paraTexto(negociacao.data
)}</td><td>${negociacao.quantidade}</td><td>${negociacao.valor}</td><td>${negociacao.volume}</td>
</tr>`).join('')}
</tbody>
<tfoot></tfoot>
134 6.3CONSTRUINDOUMTEMPLATEDINÂMICO
</table>`
}
//códigoposterioromitido
Nossa solução é uma caricatura da estratégia utilizada pelabiblioteca React (https://facebook.github.io/react/) pararenderização da apresentação de componentes através do JSX(https://facebook.github.io/react/docs/jsx-in-depth.html).Vejamosumexemplo:
//EXEMPLOAPENAS
letminhaLista=React.createClass({render:function(){return(<ul>{this.props.list.map(function(listValue){return<li>{listValue}</li>;
})}</ul>
)}
});
Voltandoparaanossaaplicação,sevocêachaqueterminamoscom nosso template, se enganou. Ainda precisamos completá-locomoutrainformação.
Precisamos totalizar o volume no rodapé da tabela do nossotemplate.Primeiro,naclasseNegociacoes, adicionaremosumapropriedadegetter,chamadavolumeTotal. Ela percorrerá a
6.4 TOTALIZANDO O VOLUME DENEGOCIAÇÕES
6.4TOTALIZANDOOVOLUMEDENEGOCIAÇÕES 135
lista encapsulada pela classe, tornando o somatório dos volumesdasnegociações.
//client/app/domain/negociacao/Negociacoes.js
classNegociacoes{
constructor(){
this._negociacoes=[]}
//códigoanterioromitido
getvolumeTotal(){
lettotal=0;
for(leti=0;i<this._negociacoes.length;i++){total+=this._negociacoes[i].volume;
}
returntotal;}
}
Agora, vamos adicionar amarcação completa do<tdfoot>do nosso template e utilizar a expressão${} para interpolar ovolumetotal:
<tfoot><tr>
<tdcolspan="3"></td><td>${model.volumeTotal}</td>
</tr></tfoot>
Um teste demonstra que nossa solução funcionou comoesperado.
136 6.4TOTALIZANDOOVOLUMEDENEGOCIAÇÕES
Figura6.6:Tabelacomovolumetotal
Apesardefuncionar,alógicadogettervolumeTotalpodeserescritadeoutramaneira.
No mundo JavaScript, arrays são muito espertos. Podemosrealizar conversões através demap(), iterar em seus elementosatravés de forEach() , e concatenar seus elementos comjoin(). Inclusive, podemos reduzir todos os seus elementos a
umúnicovalorcomafunçãoreduce().
Emnossa aplicação,reduce() reduzirá todosos elementosdoarrayaumúnicovalor,nocaso,ovolumetotal.Estenadamaisé do que a soma do volume de todas as negociações.Vejamos anovaimplementação:
//client/app/domain/negociacao/Negociacoes.js//códigoanterioromitido
getvolumeTotal(){
returnthis._negociacoes.reduce(function(total,negociacao){returntotal+negociacao.volume
},0);}
//códigoposterioromitido
Antesdeentrarmosnosdetalhesdafunçãoreduce(),vamos
6.5TOTALIZANDOCOMREDUCE
6.5TOTALIZANDOCOMREDUCE 137
lançar mão de uma arrow function para simplificar ainda maisnossocódigo:
//client/app/domain/negociacao/Negociacoes.js//códigoanterioromitido
getvolumeTotal(){
//agoracomarrowfunctions,semousodeblocoreturnthis._negociacoes
.reduce((total,negociacao)=>total+negociacao.volume,0);
}
//códigoposterioromitido
A função reduce() recebe dois parâmetros: a lógica dereduçãoeovalorinicialdavariávelacumuladora,respectivamente.A funçãocoma lógicade reduçãonosdáacessoaoacumulador,que chamamos de total , e ao item que estamos iterando,chamadodenegociacao.
Para cada item iterado, será retornada a soma do total atualcomovolumedanegociação.Ototalacumuladoserápassadoparaapróximachamadadafunçãoreduce,eesseprocessoserepetiráatéotérminodaiteração.Nofinal,reduce() retornaráo valorfinaldetotal.
Ao executarmos o código, vemos que ele funcionaperfeitamente:
Figura6.7:Tabelacomtotal
138 6.5TOTALIZANDOCOMREDUCE
Terminamosotemplatedatabela.Acadanegociaçãoincluída,a informação será exibida para o usuário com base nasinformações da lista. Mas será que podemos generalizar nossasolução?Éissoqueveremosnopróximocapítulo.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/06
6.5TOTALIZANDOCOMREDUCE 139
CAPÍTULO7
"O correr da vida embrulha tudo, a vida é assim: esquenta eesfria,apertaedaíafrouxa,sossegaedepoisdesinquieta.Oqueelaquerdagenteécoragem."-GrandeSertão:Veredas
Já somos capazes de exibir novas negociações na tabela denegociações, no entanto, podemos melhorar a experiência dousuário exibindo uma mensagem indicando que a operação foirealizadacomsucesso.
Nolugardeusarmosumastringparaestafinalidade,criaremosa classeMensagem, que representará umamensagem em nossosistema. Ela não faz parte do domínio da aplicação que é o denegociações,porissovamoscriá-laemclient/app/ui/models:
//client/app/ui/models/Mensagem.js
classMensagem{
constructor(){
this._texto;
}
OPLANO
140 7OPLANO
gettexto(){
returnthis._texto;}
}
Antes de continuarmos, importaremos o script emclient/index.html.Nãopodemosnosesquecerde importá-lo
antes de NegociacaoController.js , pois há uma ordem dedependência, algo que já vimos quando importamosNegociacoesView.
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script>
<!--importouaqui!--><scriptsrc="app/ui/models/Mensagem.js"></script>
<scriptsrc="app/app.js"></script><!--códigoposterioromitido-->
Usamos a convençãodoprefixo_ paramanter o_textoprivado, e criamos um getter para a propriedade. Mas e sequisermosalterarotextodamensagem?Paraisso,criaremosnossoprimeirosetter:
//client/app/ui/models/Mensagem.js
classMensagem{
constructor(){
this._texto;}
gettexto(){
7OPLANO 141
returnthis._texto;}
settexto(texto){
this._texto=texto;}
}
Damesmamaneiraqueacessamosumgettercomosefosseumapropriedade,podemosatribuirumvaloraosettertambémdamesmamaneira.Recarregandonossapágina,podemosrealizaro seguintetestenoconsole:
mensagem=newMensagem();mensagem.texto='x';
//NOSSOTESTEALTEROUAPROPRIEDADE!console.log(mensagem.texto);
Podemosaindaadicionarumparâmetronoconstructor()daclasse,contendootextodamensagem.
//client/app/ui/models/Mensagem.js
classMensagem{
constructor(texto){
this._texto=texto;}
gettexto(){
returnthis._texto;}
settexto(texto){
this._texto=texto;}
142 7OPLANO
}
Vamosfazerumnovotestenoconsole:
mensagem=newMensagem('FlávioAlmeida');
//PASSOUCORRETAMENTEOTEXTOPELOCONSTRUCTOR()console.log(mensagem.texto);
Noentanto,oqueacontecerásenãopassarmosumvalorparao constructor()?
mensagem=newMensagem();
//ORESULTADOÉUNDEFINED!console log(mensagem texto)
Nãopodemosdeixarqueissoaconteça.
Que talassumirmosumastringembranco,casooparâmetronãosejapassado?
//client/app/ui/models/Mensagem.js
classMensagem{
constructor(texto){
if(!texto){texto='';
}
this._texto=texto;}
gettexto(){
returnthis._texto;}
7.1PARÂMETRODEFAULT
7.1PARÂMETRODEFAULT 143
settexto(texto){
this._texto=texto;}
}
Podemosnoslivrardoifcomoseguintetruque:
//client/app/ui/models/Mensagem.js
classMensagem{
constructor(texto){
this._texto=texto||'';}
//códigoposterioromitido
No código anterior, se o valor do parâmetro texto fordiferentedenull,undefined,0 e não for uma string embranco, ele será atribuído a this._texto ; caso contrário,receberáastringembranco.
Podemosenxugaraindamaisindicandonopróprioparâmetrodoconstructor()seuvalorpadrão,algopossívelapenasapartirdoES2015(ES6)emdiante:
//client/app/ui/models/Mensagem.js
classMensagem{
//senãoforpassado,seráumastringembrancoconstructor(texto=''){
this._texto=texto}
//códigoposterioromitido
144 7.1PARÂMETRODEFAULT
Este é um recurso interessante, porque podemos definir umparâmetrodefault,tantonoconstructor()comoemmétodos.
Com a classe criada, vamos adicionar a propriedadethis._mensagememNegociacaoController.js.ElaguardaráumainstânciadeMensagem:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');this._negociacoes=newNegociacoes();this._negociacoesView=newNegociacoesView('#negociacoes
);this._negociacoesView.update(this._negociacoes);
//instanciandoomodelo!this._mensagem=newMensagem();
}
//códigoposterioromitido
Quando uma nova negociação for realizada, vamos atribuirumanovamensagemàpropriedadethis._mensagem.texto.
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
adiciona(event){
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._mensagem.texto='Negociaçãoadicionadacomsucesso';this._negociacoesView.update(this._negociacoes);this._limpaFormulario();
7.1PARÂMETRODEFAULT 145
}//códigoposterioromitido
Se preenchermos o formulário, os dados serão inseridos natabela, mas a mensagem não, porque ela ainda não possui umaapresentação,istoé,suaView.Chegouahoradecriá-la.
Vamos criar a View que representará nosso modeloMensagememclient/app/ui/views/MensagemView.js:
classMensagemView{
constructor(seletor){
this._elemento=document.querySelector(seletor);}
template(model){
return`<pclass="alertalert-info">${model.texto}</p>`;}
}
Usaremosoalertalert-info doBootstrap, seguidopelaexpressão ${model.texto} . Logo abaixo, adicionaremos ométodoupdate()quereceberáomodel.
//client/app/ui/views/MensagemView.js
classMensagemView{
constructor(seletor){
this._elemento=document.querySelector(seletor);}
7.2CRIANDOACLASSEMENSAGEMVIEW
146 7.2CRIANDOACLASSEMENSAGEMVIEW
template(model){
return`<pclass="alertalert-info">${model.texto}</p>`;}
//métodoupdateupdate(model){
this._elemento.innerHTML=this.template(model);}
}
Agora vamos importar o arquivo que acabamos de criar emclient/index.html:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script>
<!--importou!--><scriptsrc="app/ui/views/MensagemView.js"></script>
<scriptsrc="app/app.js"></script>
<!--códigoposterioromitido-->
Ainda emclient/index.html, vamos adicionar a<div>com IDmensagemView para indicar o local de renderização deMensagemView:
<!--client/index.html-->
<!DOCTYPEhtml><html><head>
<metacharset="UTF-8"><metaname="viewport"content="width=device-width">
7.2CRIANDOACLASSEMENSAGEMVIEW 147
<title>Negociações</title><linkrel="stylesheet"href="css/bootstrap.css"><linkrel="stylesheet"href="css/bootstrap-theme.css">
</head><bodyclass="container">
<h1class="text-center">Negociações</h1>
<!--novatag!--><divid="mensagemView"></div>
<formclass="form"><!--códigoanterioromitido-->
EmNegociacoesController,vamosadicionarapropriedade_mensagemViewquereceberáumainstânciadeMensagemView.NãopodemosnosesquecerdepassaroseletorCSSnoconstrutorda classe, para que ela encontre o elemento do DOM onde seutemplate deve ser renderizado. Inclusive, já vamos chamar seumétodo update , passando o modelo da mensagem comoparâmetro.
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
let$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');this._negociacoes=newNegociacoes();this._negociacoesView=newNegociacoesView('#negociacoes
);this._negociacoesView.update(this._negociacoes);this._mensagem=newMensagem();
//novapropriedade!this._mensagemView=newMensagemView('#mensagemView');this._mensagemView.update(this._mensagem);
}
148 7.2CRIANDOACLASSEMENSAGEMVIEW
//códigoposterioromitido
AindaemNegociacaoController,vamosalterarseumétodoadiciona() para que chame a atualização da View logo após
receberamensagemquedefinimos:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
adiciona(event){
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._mensagem.texto='Negociaçãoadicionadacomsucesso';this._negociacoesView.update(this._negociacoes);
//atualizaaviewcomotextodamensagemqueacabamosdeatribuir
this._mensagemView.update(this._mensagem);this._limpaFormulario();
}
//códigoposterioromitido
Vamosversealgojáéexibidononavegador.
Figura7.1:Barraazul
7.2CRIANDOACLASSEMENSAGEMVIEW 149
Agora aparece uma barra com um fundo azul. Isto é umamensagemdoBootstrapvazia,evamosmelhorarissoembreve.Ocadastrodeusuáriospassaaexibirmensagensparaousuário.
Figura7.2:Cadastrocomsucesso
Conseguimos adicionar os dados à tabela, e a mensagem desucesso apareceu corretamente. Veja que conseguimos usar omesmo mecanismo de criação da View para lidarmos com asmensagens do sistema. Agora, vamos resolver o problema damensagemvazia.
Uma solução seria removermos a instruçãothis._mensagemView.update(this._mensagem) do
constructor()deNegociacaoController.Noentanto,vamosmanter dessa forma para padronizar nossa solução.Convencionamosqueafunçãoupdatedevesersemprechamadatodavezquecriarmosnossaview.Sendoassim,vamosàsegundasolução.
No template de MensagemView , podemos usar um ifternário que retornará um parágrafo vazio, sem as classes doBootstrap,casoamensagemestejaembranco.
150 7.2CRIANDOACLASSEMENSAGEMVIEW
Vamosalterarclient/app/ui/views/MensagemView.js:
//client/app/ui/views/MensagemView.js
classMensagemView{
//códigoanterioromitido
template(model){
returnmodel.texto?`<pclass="alertalert-info">${model.texto}</p>`:`<p></p>`;
}
//códigoposterioromitido}
Lembre-se deque, para o interpretador do JavaScript, umastring em branco, 0, undefine e null são consideradosfalse.Semodel.texto tiverqualquervalordiferentedosque
foramlistados,seráconsideradotrue.
Conseguimos resolver a parte dasmensagens para o usuário.Masseráqueconseguimosmelhoraraindamaisocódigo?
Temos duas Views criadas: MensagemView eNegociacoesView . Se observarmos, ambas possuem umconstructor() que recebe um seletor, além de possuírem a
propriedadeelemento.Asduastêmosmétodostemplate()eupdate().Ométodoupdate()éidênticonasduasclasses,jáotemplate() tem uma implementação diferente, apesar de
receberumúnicoparâmetroedevolversempreumastring.
7.3HERANÇAEREUTILIZAÇÃODECÓDIGO
7.3HERANÇAEREUTILIZAÇÃODECÓDIGO 151
Para cada nova View criada em nossa aplicação, teremos derepetir boa parte do código que já escrevemos - com exceção dafunçãotemplate(),queédiferenteparacadaclasse.Nãopareceserumtrabalhodecangaceiro ficarrepetindocódigo.Existeumasolução?
Para evitarmos a repetição, criaremos uma classe chamadaView,quecentralizarátodoocódigoqueécomumentretodasasViewsdanossaaplicação(nocaso,oconstructor()eométodoupdate()).
Vamoscriaressaclasseemclient/app/ui/views/View.js:
//client/app/ui/views/View.js
classView{
constructor(seletor){
this._elemento=document.querySelector(seletor);}
update(model){
this._elemento.innerHTML=this.template(model);}
}
Antes de continuarmos, vamos importar a classe emclient/index.html. Entretanto, ela não pode ser importada
comopenúltimoscriptdanossapágina,antesdeapp.js, comofizemoscomosdemaisscripts.TodosnossosarquivosdeViewquevamos criar dependerão diretamente da classe View , e ointerpretadorJavaScriptindicaráumerrocasoasclassesnãosejamcarregadasantesdasdemais.
Vamos importar a nova classe antes do script de
152 7.3HERANÇAEREUTILIZAÇÃODECÓDIGO
NegociacoesView.js:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script>
<!--importouaqui-->
<scriptsrc="app/ui/views/View.js"></script>
<!--adeclaraçãodeNegociacoesViewdependerádadeclaraçãodeView--><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/app.js"></script>
A classe View recebeu tudo o que as Views tinham emcomum: um constructor(elemento) - que guardaráinternamenteumelemento-eupdate().Vale lembrarqueométodotemplatenãopôdeserdefinidonaclasse,porquecadaViewemnossaaplicaçãopossuiráumcódigodiferente.
Agora que centralizamos o código comum em uma classe,vamos remover o construtor() e o método update() deNegociacaoViewedeMensagemView.Elasficarãoassim:
//client/app/ui/views/NegociacoesView.js
classNegociacoesView{
template(model){
//códigodotemplateomtido}
}
7.3HERANÇAEREUTILIZAÇÃODECÓDIGO 153
//client/app/ui/views/MensagemView.js
classMensagemView{
template(model){
returnmodel.texto?`<pclass="alertalert-info">${model.texto}</p>`:`<p></p>`;
}}
Do jeito que está, nossa aplicação não funcionará, pois tantoNegociacoesView comoMensagemView dependemdo código
quebuscaoelementodoDOM(definidonoconstructor())eafunçãoupdate(),queforamremovidos.
Paraasclassesquealteramosvoltemafuncionar,faremoscomqueherdemocódigodaclasseView,aquelaqueagoracentralizaemumúnicolugaroconstrutor(),inclusiveafunçãoupdate.
É através da instruçãoextends que podemos herdar outraclasse.VamosalterarNegociacaoVieweMensagemView:
//client/app/ui/views/NegociacoesView.js
classNegociacoesViewextendsView{
template(model){
//códigodotemplateomtido}
}
//client/app/ui/views/MensagemView.js
classMensagemViewextendsView{
template(model){
154 7.3HERANÇAEREUTILIZAÇÃODECÓDIGO
returnmodel.texto?`<pclass="alertalert-info">${model.texto}</p>`:`<p></p>`;
}}
Secadastrarmosumanovanegociação,veremosqueestátudofuncionando.
Figura7.3:Formuláriofuncionandocorretamente
A solução de template dentro domodeloMVC adotada peloautor foi a mais acessível e didática que ele encontrou paracolocar em prática recursos da linguagem JavaScript, semgastar páginas e mais páginas detalhando a criação de umframework, o que desvirtuaria a proposta do livro. Contudo,não será mera coincidência encontrar diversos conceitosapresentadosemframeworksebibliotecasdomercado.
Conseguimosevitarcódigoduplicadoegarantimosoreúsodecódigo através de herança. Contudo, ainda podemos melhorarnossocódigoumpouquinhomais.
7.3HERANÇAEREUTILIZAÇÃODECÓDIGO 155
Na programação orientada a objetos, existe o conceito declasses abstratas. Essas classes podem ter um ou mais métodosabstratos, que nadamais são do quemétodos que não possuemimplementação, apenas a assinatura que consiste no nome dométodoeoparâmetroquerecebe.
Esses métodos devem ser implementados pelas classes filhasqueherdamdaclasseabstrata.Nocasodométodotemplate(),éimpossívelquenossaclasseViewtenhaumaimplementaçãoparaserherdada,poiselanãotemcomosaberdeantemãoqualseráatemplateliteralretornadapelassuasclassesfilhas.
No JavaScript, não há classes abstratas, mas podemos tentaremulá-lasdaseguintemaneira.
Na classe View, vamos adicionar ométodo template(),cujaimplementaçãoconsistenolançamentodeumaexceção:
//client/app/ui/views/View.js
classView{
constructor(seletor){
this._elemento=document.querySelector(seletor);}
update(model){
this._elemento.innerHTML=this.template(model);}
//novométodo!template(model){
thrownewError('Vocêprecisaimplementarométodotempla
7.4CLASSESABSTRATAS?
156 7.4CLASSESABSTRATAS?
te');}
}
Do jeito que está, todas as classes filhas que herdarem deViewganharamométodotemplate()padrão.Contudo,seessemétodonão forsobrescritopelaclasse filha,essecomportamentopadrãoresultaráemumerro,deixandoclaroparaoprogramadorqueeledeveimplementá-lo,casotenhaesquecido.
Podemosfazerumsimplestesteremovendotemporariamenteo métodotemplate()deMensagemViewerecarregandonossaaplicação.Receberemosnoconsoleaseguintemensagem:
UncaughtError:VocêprecisaimplementarométodotemplateatMensagemView.template(View.js:15)atMensagemView.update(View.js:10)atnewNegociacaoController(NegociacaoController.js:14)atapp.js:1
Diferente de outras linguagens com tipagem estática (comoJava, Rust ou C#), a exceção só será lançada em tempo deexecução. Entretanto, ainda assim é uma maneira de ajudar osdesenvolvedoresfornecendoumamensagemclaradoerroemseucódigo,casonãoimplementeosmétodos"abstratos"daclassefilha.
Emnossaaplicação,tantooconstructordeViewquantooconstructordesuasclassesfilhasrecebemamesmaquantidadede parâmetros. Contudo, quando a classe filha recebe umaquantidadedeparâmetrosdiferente,énecessárioescrevermosumpoucomaisdecódigo.
Vejamos um exemplo hipotético envolvendo as classes
7.5PARASABERMAIS:SUPER
7.5PARASABERMAIS:SUPER 157
FuncionarioeGerente.
classFuncionario{
constructor(nome){
this._nome=nome;}
getnome(){
returnthis._nome;}
setnome(nome){
this._nome=nome;}
}
classGerenteextendsFuncionario{}
No exemplo anterior, a classe Gerente herdou deFuncionario e, por conseguinte, herdou seuconstrutor().
MasoqueaconteceráseGerente receber, alémdonome,umasenhadeacesso?
classGerenteextendsFuncionario{
constructor(nome,senha){/*oquefazer?*/
}}
AclasseGerenteprecisoudoseupróprioconstructor(),pois recebe dois parâmetros. Nesse caso, precisamos chamar oconstructor()daclassepai,passandoosparâmetrosdequeeleprecisa:
classGerenteextendsFuncionario{
158 7.5PARASABERMAIS:SUPER
constructor(nome,senha){
super(nome);//chamaoconstructor()dopai
//propriedadeexclusivadeGerentethis._senha=senha;
}
//gettersesettersexclusivosdoGerentegetsenha(){
returnthis._senha;}
setsenha(senha){
returnthis._senha=senha;}
}
É importante que a chamada de super() seja a primeirainstrução dentro do constructor() da classe filha; casocontrário, o this da classe filha não será inicializado e ainstruçãothis._senha=senha resultará em um erro. Agoraque já fizemos o teste, podemos voltar com o métodotemplate()emMensagemView.
Aprendemosa favorecerousodelet emvezdevar nasdeclarações de variáveis, inclusive entendemos como funciona otemporaldeadzone.Noentanto,podemoslançarmãodeconst.Variáveis declaradas atravésdeconst obrigatoriamente devemser inicializadasenãopodemrecebernovasatribuições.Vejamosumexemploisolado:
7.6 ADQUIRINDO UM NOVO HÁBITO COMCONST
7.6ADQUIRINDOUMNOVOHÁBITOCOMCONST 159
constnome='Flávio';nome='Almeida';//resultanoerroUncaughtTypeError:Assignmenttoconstantvariable
No exemplo anterior, não podemos usar novamente ooperador= para atribuir um novo valor a variável. O fato deusarmos essa declaração específica não quer dizer que a variávelsejaimutável.Vejamosoutroexemplo:
constdata=newDate();data.setMonth(10);//funcionaperfeitamenteconsole.log(data.getMonth());data=newDate()//resultanoerroUncaughtTypeError:Assignmenttoconstantvariable
Neste último exemplo, pormais que tenha declarado datacom const , através do método data.setMonth(10) ,conseguimos alterar o mês da data. Isso é a prova real de queconstnãocriaobjetosimutáveis.
Porfim,valemasseguintesrecomendaçõescangaceiras:
Useconstsemprequepossível.Utilize let apenas se a variável precisar recebernovas atribuições, por exemplo, uma variáveltotalizadora.Nãousevar, pois a únicamaneira da variável terescopoéquandodeclaradadentrodeumafunção.
Dessa forma, vamos alterar NegociacaoController parafazerusodeconst:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
160 7.6ADQUIRINDOUMNOVOHÁBITOCOMCONST
//nãovamosatribuiroutorvaloràvariávelconst$=document.querySelector.bind(document);
//códigoposterioromitido}
//códigoposterioromitido
Podemos até alterar client/app/app.js para utilizarconstnadeclaraçãodavariávelcontroller:
//client/app/app.js
//USANDOCONSTconstcontroller=newNegociacaoController();
document.querySelector('.form')
.addEventListener('submit',controller.adiciona.bind(controller));
Umapequenamelhoriaqueutilizaremosaolongodoprojeto.
Será que podemos melhorar ainda mais nosso código, porexemplo, enxugando-o e removendo cada vez mais aresponsabilidadedoprogramador?Éissoqueveremosnopróximocapítulo.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/07
7.6ADQUIRINDOUMNOVOHÁBITOCOMCONST 161
ProcuradopelaForçaVolante, famintoeabatido, ele chegouapensaremdesistirváriasvezes.Porém,avontadeinquebrantáveldedescobriroqueoesperavanofimeramaiordoqueseusofrimento.Então, ele recuperou suas forças e se pôs a andar. Durante suacaminhada,ajudouefoiajudado,lutousemserderrubado,correuefoi arrastado. Suas cicatrizes eram a marca da experiência queacumulava. Ele tinha muito o que aprender ainda, mas haviaespaçoemsuapeleparanovasexperiências.
Parte2-ForçaVolante
CAPÍTULO8
"Temhoras emquepensoquea gente carecia, de repente, deacordar de alguma espécie de encanto." - Grande Sertão:Veredas
Nossa aplicação já funciona e totaliza o volume no fim databela.Masaindafaltaimplementarmosumaregradenegócionomodelo Negociacoes . Precisamos criar um método que nospermitaesvaziaralistadenegociaçõesencapsulada.Vamoscriá-lo.
Primeiro, vamos adicionar o método esvazia() emNegociacoes:
//client/app/domain/negociacao/Negociacoes.js
classNegociacoes{
constructor(){
this._negociacoes=[];}
adiciona(negociacao){
UMCANGACEIROSABEDELEGARTAREFAS
8UMCANGACEIROSABEDELEGARTAREFAS 163
this._negociacoes.push(negociacao);}
paraArray(){
return[].concat(this._negociacoes);}
getvolumeTotal(){
returnthis._negociacoes.reduce((total,negociacao)=>
total+negociacao.volume,0);}
//novométodo!esvazia(){
this._negociacoes=[];}
}
O novo método, quando chamado, atribuirá um novo arraysem elementos à propriedade this._negociacoes . Agora,precisamosqueestenovométodosejachamadoquandoousuárioclicar no botão "Apagar" . Para isso, criaremos primeiro ométodoapaga()emNegociacaoController, aquelequeseráchamadoatravésdobotão"Apagar"deindex.html:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
apaga(){
this._negociacoes.esvazia();this._negociacoesView.update(this._negociacoes);this._mensagem.texto='Negociaçõesapagadascomsucesso';this._mensagemView.update(this._mensagem);
}
164 8UMCANGACEIROSABEDELEGARTAREFAS
//códigoposterioromitido
Porfim,precisamosassociarocliquedobotão"Apagar"comachamadadométodoqueacabamosdecriaremnossocontroller.Isso não é novidade para nós, já fizemos algo semelhante emclient/app/app.js:
//client/app/app.js
constcontroller=newNegociacaoController();
document.querySelector('.form')
.addEventListener('submit',controller.adiciona.bind(controller));
//buscandooelementopeloseuIDdocument
.querySelector('#botao-apaga').addEventListener('click',controller.apaga.bind(controller))
;
Quandoclicarmosem"Apagar",atabelaficarávazia.AViewfoi atualizada com os dados do modelo da lista de negociaçõesatualizada.Inclusive,amensagemfoiexibida:
Figura8.1:Listaapagada
Apesar de funcionar como esperado, podemos melhorar um
8UMCANGACEIROSABEDELEGARTAREFAS 165
pouco mais nosso modelo Negociacoes. Nele, a propriedade_negociacoesnãoestácongeladapermitindoquerecebanovas
atribuições. Aprendemos que através de Object.freeze
podemos impedir novas atribuições às propriedades da instânciade umobjeto.Que tal lançarmosmãodeste recurso também emNegociacoes?Alterandoaclasse:
//client/app/domain/negociacao/Negociacoes.js
classNegociacoes{
constructor(){
this._negociacoes=[];
//CONGELOUAINSTÂNCIAObject.freeze(this);
}
//códigoanterioromitido}
Excelente, mas como nossa instância não aceita novasatribuições à propriedade this._negociacoes o métodoesvazia()daclassenãofuncionará:
//client/app/domain/negociacao/Negociacoes.js
//códigoanterioromitido
esvazia(){
//NÃOACEITARÁANOVAATRIBUIÇÃOthis._negociacoes=[];
}
//códigoposterioromitido
Para solucionarmos este problema, podemos alterar aimplementaçãodonossométodoesvazia()para:
166 8UMCANGACEIROSABEDELEGARTAREFAS
//client/app/domain/negociacao/Negociacoes.js
//códigoanterioromitido
esvazia(){
this._negociacoes.length=0;}
//códigoposterioromitido
Quando atribuímos 0 à propriedade length de um array,eliminamostodososseusitens.
Nossaaplicaçãofunciona,mastodavezquetivermosdealterarnossos modelos, precisaremos lembrar de chamar o métodoupdate()dasuarespectivaView.Achancedeesquecermosdechamar o método é alta, ainda mais se novas Views foremintroduzidasemnossaaplicação.SeráqueexistealgumamaneiradeautomatizarmosaatualizaçãodaView?
A alteração do modelo é um ponto inevitável em nossaaplicação.Quetalcriarmosumaestratégianaqual,todavezqueomodeloforalterado,asuarespectivaViewsejaatualizada?
Para que isso seja possível, o constructor() dos nossosmodelosprecisará receberuma estratégiade atualizaçãoque seráchamada toda vez que algum de seus métodos que alteram seuestado interno(suaspropriedades) forchamado.VamoscomeçarporNegociacoes:
//client/app/domain/negociacoes/Negociacoes.js
8.1ESEATUALIZARMOSAVIEWQUANDOOMODELOFORALTERADO?
8.1ESEATUALIZARMOSAVIEWQUANDOOMODELOFORALTERADO? 167
classNegociacoes{
//agorarecebeumparâmetro!constructor(armadilha){
this._negociacoes=[];this._armadilha=armadilha;Object.freeze(this);
}
//códigoposterioromitido
Nossaclasseagorarecebeoparâmetroarmadilha, que seráguardado em uma propriedade da classe. Esse parâmetro nadamais é do queuma função coma lógica de atualizaçãodaView.Agora,nosmétodosadiciona()eesvazia(), chamaremos afunçãoarmazenadaemthis._armadilha:
//client/app/domain/negociacoes/Negociacoes.js
classNegociacoes{
constructor(armadilha){
this._negociacoes=[];this._armadilha=armadilha;Object.freeze(this);
}
adiciona(negociacao){
this._negociacoes.push(negociacao);
//chamandoafunçãothis._armadilha(this);
}
//códigoanterioromitido
esvazia(){
168 8.1ESEATUALIZARMOSAVIEWQUANDOOMODELOFORALTERADO?
this._negociacoes.length=0;
//chamandoafunçãothis._armadilha(this);
}}
Veja que a função armazenada em this._armadilha échamadarecebendocomoparâmetrothis.Issopermitiráqueafunçãotenhaumareferênciaparaainstânciaqueestáexecutandoaoperação,nocaso,instânciasdeNegociacoes.
Agora, em NegociacaoController , vamos passar umaestratégiadeatualizaçãoparaoconstructor() domodeloqueacabamosdealterar.Porenquanto,usaremosumafunctionemvezdeumaarrowfunction:
//client/app/domain/negociacoes/Negociacoes.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//passandoaestratégiaaserutilizada
this._negociacoes=newNegociacoes(function(model){
this._negociacoesView.update(model);});
this._negociacoesView=newNegociacoesView('#negociacoes);
//precisacontinuar,paraatualizar//nainicializaçãodocontroller
8.1ESEATUALIZARMOSAVIEWQUANDOOMODELOFORALTERADO? 169
this._negociacoesView.update(this._negociacoes);
this._mensagem=newMensagem();this._mensagemView=newMensagemView('#mensagemView');this._mensagemView.update(this._mensagem);
}
//códigoposterioromitido
Agora,todavezqueosmétodosadiciona()eesvazia()de Negociacoes forem chamados, a estratégia da armadilhapassadanoconstructor()seráchamada.Vejaqueaarmadilhapassada comoparâmetro receberáthis ao ser chamada, que éumareferênciaàinstânciadeNegociacoes.Éestareferênciaqueacessaremos como model na função que passamos paraNegociacoes, emNegociacaoController.Vamos relembrar
dessetrechodecódigo:
//client/app/domain/negociacoes/Negociacoes.js//códigoanterioromitido
//modelseráainstânciadeNegociacoes//estiverchamandoadiciona()ouesvazia()this._negociacoes=newNegociacoes(function(model){
this._negociacoesView.update(model);});
//códigoposterioromitido
Podeparecerumtantoestranhoquea funçãodeclaradatenteutilizarthis._mensagemView antes de ter sido declarada, masnão há problema nenhum nisso. Isso porque ela não seráexecutada imediatamente,sóquandoosmétodosadiciona() eesvazia()foremchamados.
Agora, tanto em adiciona() quanto em apaga() deNegociacaoController , removeremos a instrução que faz a
170 8.1ESEATUALIZARMOSAVIEWQUANDOOMODELOFORALTERADO?
atualização da view de Negociacoes. Os dois métodos devemficarassim:
//client/app/controllers/NegociacaoController.js//códigoposterioromitido
adiciona(event){
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());
//nãochamamaisoupdatedenegociacoesView
this._mensagem.texto='Negociaçãoadicionadacomsucesso';this._mensagemView.update(this._mensagem);this limpaFormulario()
}
//códigoposterioromitido
apaga(){
this._negociacoes.esvazia();
//nãochamamaisoupdatedenegociacoesView
this._mensagem.texto='Negociaçõesapagadascomsucesso';this._mensagemView.update(this._mensagem);
}
Agora que temos todo o código em seu devido lugar, vamosrecarregarnossapáginaetestaroresultado.Nenhumaatualizaçãoéfeitaeaindarecebemosnoconsoleaseguintemensagemdeerro:
UncaughtTypeError:Cannotreadproperty'update'ofundefinedatNegociacoes._armadilha(NegociacaoController.js:12)atNegociacoes.adiciona(Negociacoes.js:12)atNegociacaoController.adiciona(NegociacaoController.js:25)
Pelo console, ao procurarmos a instrução que causou o erro,encontramosestetrechodecódigodeNegociacaoController:
8.1ESEATUALIZARMOSAVIEWQUANDOOMODELOFORALTERADO? 171
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
this._negociacoes=newNegociacoes(function(model){
//CAUSADOERROthis._negociacoesView.update(model);
});
//códigoposterioromitido
Estávamos esperandoque a função armadilhapassadaparaoconstructor() de Negociacoes fosse capaz de ter acesso à
propriedade this._negociacoesView deNegociacaoController.Contudo,devidoànaturezadinâmica
de toda function() , o valor de this emthis._negociacoesView.update(model) passou a ser a
instância de Negociacoes, que não possui a propriedade queestamosacessando.
Sósabemosocontexto(this)deuma funçãoemtempodeexecução(runtime)donossocódigo.Podemosfazerumtesteparavermos esse comportamento com mais clareza adicionando umconsole.log(this)nafunçãoquepassamoscomoarmadilha:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
this._negociacoes=newNegociacoes(function(model){
//imprimiráNegociacoesnoconsoleemvezdeNegociacaoController
console.log(this);
this._negociacoesView.update(model);});
//códigoposterioromitido
172 8.1ESEATUALIZARMOSAVIEWQUANDOOMODELOFORALTERADO?
Verificandonoconsole,temos:
Negociacoes//exibiuNegociacoes!UncaughtTypeError:Cannotreadproperty'update'ofundefined
Elemostraquethis é oNegociacoes. É assimporque afunçãoestásendoexecutadanocontextodeNegociacoes. Seráquetemoscomomudarocontextodethis?
Umamaneiradesolucionarmosoproblemaéguardandoumareferência para o this de NegociacaoController em umavariável(porexemplo,self),eutilizandoessavariávelnafunçãodearmadilha.Vejamos:
//client/app/domain/negociacoes/Negociacoes.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//selféNegociacaoControllerconstself=this;
this._negociacoes=newNegociacoes(function(model){
//continuasendoNegociacoesconsole.log(this);
//acessamosself//temoscertezadequeéNegociacaoController
self._negociacoesView.update(model);});
8.2DRIBLANDOOTHISDINÂMICO
8.2DRIBLANDOOTHISDINÂMICO 173
//códigoposterioromitido
Essasolução funciona,noentanto,nosobrigaadeclararumavariável extra para guardarmos uma referência para o valor dethis que desejamos usar em nossa função de armadilha.
Podemosresolverdeoutramaneira.
Vamos voltar nosso código para o estágio anterior, que nãoestáfuncionandocomoesperado:
//client/app/domain/negociacoes/Negociacoes.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
this._negociacoes=newNegociacoes(function(model){
console.log(this);this._negociacoesView.update(model);
});
//códigoposterioromitido
Para que nossa função passada como armadilha para oconstructor()deNegociacoespasseaconsiderarainstânciade NegociacaoController como this , e não a instânciaNegociacoes , passaremos mais um parâmetro para oconstructor() de Negociacoes além da armadilha. Seu
primeiro parâmetro será o this , aquele que se refere aNegociacaoController:
//client/app/controller/NegociacaoController.js
174 8.2DRIBLANDOOTHISDINÂMICO
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//othispassadoaindaapontapara//umainstânciadeNegociacaoController
this._negociacoes=newNegociacoes(this,function(model){
console.log(this);this._negociacoesView.update(model);
});
//códigoposterioromitido
PrecisamosalterarNegociacoesparaquerecebaeguardeoparâmetrorecebido:
//client/app/domain/negociacoes/Negociacoes.js
classNegociacoes{
constructor(contexto,armadilha){
this._negociacoes=[];this._armadilha=armadilha;this._contexto=contexto;Object.freeze(this);
}
//códigoposterioromitido
Agora, vamos alterar a chamada de this._armadilha nosmétodosadiciona() e esvazia(). Faremos isso através dachamada da função call() que toda função em JavaScript
8.2DRIBLANDOOTHISDINÂMICO 175
possui. Ela recebe dois parâmetros.Oprimeiro é o contexto queserá usado como this na chamada da função e o segundo, oparâmetrorecebidoporestafunção:
//client/app/domain/negociacoes/Negociacoes.js
classNegociacoes{
constructor(contexto,armadilha){
this._negociacoes=[];this._contexto=contexto;this._armadilha=armadilha;Object.freeze(this);
}
adiciona(negociacao){
this._negociacoes.push(negociacao);
//ALTERAÇÃOAQUIthis._armadilha.call(this._contexto,this);
}
//códigoanterioromitido
esvazia(){
this._negociacoes.length=0;
//ALTERAÇÃOAQUIthis._armadilha.call(this._contexto,this);
}}
Seexecutarmosocódigo,ocadastrovoltaráafuncionar:
176 8.2DRIBLANDOOTHISDINÂMICO
Figura8.2:Formuláriofuncionando
Estaéumasegundaformaderesolveroproblemageradopelodinamismo de this Contudo há uma terceira maneira desolucionarmosoproblema,porémmenosverbosaqueasduasqueexperimentamos.Veremosestanovaformaaseguir.
A primeira mudança que faremos para escrevemos menoscódigo é removermos o parâmetro contexto doconstructor()deNegociacoes. Inclusive,voltaremosparaa
chamadadiretadethis._armadilhanosmétodosadiciona()eesvazia():
classNegociacoes{
//nãorecebemaiscontextoconstructor(armadilha){
this._negociacoes=[];this._armadilha=armadilha;Object.freeze(this);
}
8.3 ARROW FUNCTION E SEU ESCOPOLÉXICO
8.3ARROWFUNCTIONESEUESCOPOLÉXICO 177
adiciona(negociacao){
this._negociacoes.push(negociacao);
//chamadadiretadafunção,semcall()this._armadilha(this);
}
//códigoposterioromitido
esvazia(){
this._negociacoes.length=0;
//chamadadiretadafunção,semcall()this._armadilha(this);
}}
Precisamos alterar no constructor() deNegociacaoController os parâmetros passados para
Negociacoes,quepassouaserapenasafunçãodearmadilha:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//nãorecebemaisthiscomoparâmetro,//somenteafunção!this._negociacoes=newNegociacoes(function(model){
console.log(this);this._negociacoesView.update(model);
});
//códigoposterioromitido
178 8.3ARROWFUNCTIONESEUESCOPOLÉXICO
Do jeito que está, nosso código não funcionará. Mas setrocarmos a function passada para o constructor() deNegociacoesporumaarrowfunction,teremosumasurpresa:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//passouumarrowfunction!
this._negociacoes=newNegociacoes(model=>{
console.log(this);this._negociacoesView.update(model);
});
//códigoposterioromitido
Umtestedemonstraquenossocódigofuncionou,sendomuitomenos verboso do que o anterior. Como isso é possível? Istoocorreporqueaarrowfunctionnãoéapenasumamaneirasucintade escrevermos uma função, ela também tem uma característicapeculiar: o escopo de seu this é léxico (estático) em vez dedinâmico.
Othisdeumaarrowfunctionobtémseuvalordo"códigoaoredor", mantendo esse valor independente do lugar onde échamado.ÉporissoqueothisdafunçãodaarmadilhapassadaapontaparaainstânciadeNegociacaoController.
Nossasoluçãoé funcional,maspodemosmelhorá-la.Éoque
8.3ARROWFUNCTIONESEUESCOPOLÉXICO 179
veremosnopróximocapítulo.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/08
180 8.3ARROWFUNCTIONESEUESCOPOLÉXICO
CAPÍTULO9
"Jagunçonãoseescabreiacomperdanemderrota-quasetudoparaeleéoigual."-GrandeSertão:Veredas
Para liberarmos o desenvolvedor da responsabilidade deatualizar programaticamente a View sempre que o modelo foratualizado,nóscolocamosalógicadeatualizaçãoem"armadilhas",funçõesqueeramchamadasemmétodosquealteravamoestadodomodelo.
No entanto, esta soluçãodeixa a desejar porquenos obriga areceberaarmadilhaemtodasasnossasclassesdemodelo,alémdetermos de lembrar de chamá-la em cada método que altera oestadodomodelo.
Apropriedadethis._armadilhadeNegociacoesnãotemqualquerrelaçãocomodomínioqueaclasserepresenta.Ouseja,elaestáláapenasporumaquestãodeinfraestrutura.Relembremosdocódigo:
ENGANARAMOCANGACEIRO,SERÁ?
9ENGANARAMOCANGACEIRO,SERÁ? 181
//APENASRELEMBRANDO!
//client/app/domain/negociacoes/Negociacoes.js
classNegociacoes{
constructor(armadilha){
this._negociacoes=[]
//estranhononinho,//semrelaçãocomodomíniodenegociaçõesthis._armadilha=armadilha;
Object.freeze(this);}
Ummodelododomínioéumadasclassesmaisreutilizáveisdeumsistema,justamentepornãoconternenhumamágicaextraquenãodigarespeitoaoproblemadodomínioqueresolve.Contudo,nossocódigoparecenãoseguiressarecomendação.
Em nossa aplicação, se mais tarde quisermos usar nossosmodeloscomframeworksfamososdomercado,comoAngularouReact, a propriedadearmadilha não serámais necessária,masainda fará parte da definição da classe domodelo, obrigando osdesenvolvedoresalidarcomela.Eagora?
De um lado, queremos preservar o modelo, do outro,queremos facilitar a vida do desenvolvedor. Será que podemosconseguiromelhordosdoismundos?
Vimosquenãoé interessantepoluirmosnossomodelocomafunçãoqueatualizarásuarespectivaViewequeaindaprecisamos
9.1OPADRÃODEPROJETOPROXY
182 9.1OPADRÃODEPROJETOPROXY
deumasoluçãoqueatualizeaViewautomaticamentetodavezqueo modeloforalterado.Existeumpadrãodeprojetoquepodenosajudaraconciliaressesdoisopostos,oProxy.
O padrão de projeto Proxy, em poucas palavras, trata-se de"um farsante". Lidamos com um Proxy como se ele fosse ainstância do objeto que estamos querendo manipular, porexemplo, uma instânciadeNegociacoes. Ele é uma casquinhaque envolve a instância que desejamos manipular e, para cadapropriedade emétodo presente nessa instância, o Proxy terá umcorrespondente.
Sechamarmosométodoadiciona()noProxy,eledelegaráachamada domesmométodo na instância que encapsula. Entre achamada do método do Proxy e a chamada do métodocorrespondentenainstância,podemosadicionarumaarmadilha-nocasodanossaaplicação,umcódigoqueatualizaránossaView.Dessaforma,nãoprecisamosmodificaraclassedomodelo,apenascriarumProxyquesaibaexecutarocódigoquedesejamos.Comoimplementá-lo?
Aboanotíciaéquenãoprecisamos implementaressepadrãode projeto.OECMAScript, a partir da versão 2015, já possui naprópria linguagem um recurso de Proxy. Vamos aprender autilizá-loaseguir.
O primeiro passo é editarmos NegociacaoController ecomentarmos temporariamente o trecho do código que não
9.2 APRENDENDO A TRABALHAR COMPROXY
9.2APRENDENDOATRABALHARCOMPROXY 183
funciona mais, aquele que passa a armadilha para oconstructor()deNegociacoes:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//códigocomentado/*this._negociacoes=newNegociacoes(model=>{
console.log(this);this._negociacoesView.update(model);
});*/
//códigoposterioromitido
Em seguida, vamos remover a armadilha passada para oconstructor()deNegociacoes.Alémdisso,nãopodemosnosesquecer de remover as chamadas das armadilhas dos seusmétodos.Aclassenofinalficaráassim:
//client/app/domain/negociacao/Negociacoes.js
classNegociacoes{
constructor(){
this._negociacoes=[]Object.freeze(this);
}
adiciona(negociacao){
184 9.2APRENDENDOATRABALHARCOMPROXY
this._negociacoes.push(negociacao);}
paraArray(){
return[].concat(this._negociacoes);}
getvolumeTotal(){
returnthis._negociacoes.reduce((total,negociacao)=>
total+negociacao.volume,0)}
esvazia(){
this._negociacoes.length=0;}
}
Com a alteração que acabamos de fazer, nossa aplicaçãodeixará de funcionar e receberemos no console a seguintemensagemdeerro:
UncaughtTypeError:Cannotreadproperty'paraArray'ofundefined
Não se preocupe com ela. Deixaremos nossa aplicaçãoquebrada enquanto aprendemos um novo recurso que ajudará arecompô-la.
Agora, faremos uma série de experimentos no console doChromeparaaprendermosa trabalharcomProxy.Vamosdeixarumpouco de lado a classeNegociacoes para trabalhar com aclasseNegociacao,poisessatrocafacilitaránossaaprendizagem.
Com o console do Chrome aberto, vamos executar as duasinstruçõesaseguir:
//NOCONSOLEDOCHROME
9.2APRENDENDOATRABALHARCOMPROXY 185
negociacao=newNegociacao(newDate(),1,100);proxy=newProxy(negociacao,{});
Aprimeira instrução cria uma instância deNegociacao.AsegundacriaumainstânciadeumProxy.Oconstructor() doProxy recebe como primeiro parâmetro a instância deNegociacao que queremos encapsular e, como segundo, um
objetonoformatoliteral{},quecontémocódigodasarmadilhasque desejamos executar. Por enquanto, não definimos nenhumaarmadilha.
O Proxy criado possui todas as propriedades e métodos daclasse que encapsula, tanto que se acessarmos no console apropriedade proxy.valor , estaremos por debaixo dos panosacessandoamesmapropriedadenainstânciadeNegociacaoqueencapsula:
//NOCONSOLEDOCHROME
console.log(proxy.valor);//imprime100console.log(proxy.quantidade);//imprime1console.log(proxy.volume);//imprime100
Noentanto,nãoqueremosqueninguémtenhaacessodiretoàinstância encapsulada pelo nosso Proxy. Logo, podemos limparnossoconsoleecomeçardozerocomessainstrução:
//NOCONSOLEDOCHROME
negociacao=newProxy(newNegociacao(newDate(),1,100),{};
Usamoscomonomedevariávelnegociacao paramascararnosso Proxy. Em seguida, como primeiro parâmetro doconstructor()donossoProxy,instanciamosdiretamenteuma
instânciadeNegociacao.
186 9.2APRENDENDOATRABALHARCOMPROXY
De nada adianta nosso Proxy se ele não é capaz de executarnossasarmadilhas.Precisamosdefini-lasnoobjetopassadocomosegundoparâmetroparaoProxy,umobjetochamadohandler.
Como vamos executar uma série de operações, será maiscômodo para nós escrever todas essas instruções dentro de umanova tag <script></script> , que adicionaremostemporariamenteemclient/index.html.Eladeveseraúltimatag<script>dapágina.
<!--client/index.html--><!--códigoposterioromitido--><!--últimatagscript!-->
<script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
});</script>
<!--códigoposterioromitido-->
Agoraquetemostudonolugar,vamosaprenderacriarnossasarmadilhasnohandlerpassadoparanossoProxy.
Para iniciarmos nossa jornada no mundo do Proxy,começaremos com algo bem modesto. Toda vez que aspropriedades do Proxy forem lidas, faremos com que ele exibaumamensagemindicandoonomedapropriedade.
Para isso, em nosso handler, adicionaremos a propriedade
9.3 CONSTRUINDO ARMADILHAS DELEITURA
9.3CONSTRUINDOARMADILHASDELEITURA 187
get que receberá como parâmetro a função que desejamosexecutarnaleituradaspropriedadesdonossoProxy.Vejamqueapropriedadeget não foi por acaso, ela indica claramente queestamosplanejandoumcódigoparaumacessodeleitura:
<!--client/index.html--><!--códigoposterioromitido-->
<script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
get:function(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
}});
console.log(negociacao.quantidade);console.log(negociacao.valor);</script>
<!--códigoposterioromitido-->
A função passada para a propriedade get recebe trêsparâmetrosespeciais.Oprimeiro,target,éumareferênciaparao objetoencapsuladopeloProxy,oobjetoverdadeiro.Osegundoparâmetro,prop,éumastringcomonomedapropriedadequeestásendoacessada.Porfim,receiveréumareferênciaparaopróprioProxy.
Podemossimplificarumpoucomaisnossocódigo.ApartirdoES2015 (ES6), podemos declarar propriedades de objetos queequivalemafunçõesdessaforma:
<!--client/index.html--><!--códigoposterioromitido-->
188 9.3CONSTRUINDOARMADILHASDELEITURA
<script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
//AQUI!nãoémaisget:function(...){}
get(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
}});
console.log(negociacao.quantidade);console.log(negociacao.valor);</script>
<!--códigoposterioromitido-->
Recarregandoapágina,vemosasaídadoConsole:
//EXIBIDONOCONSOLEDOCHROME
apropriedade"quantidade"caiunaarmadilha!undefinedapropriedade"valor"caiunaarmadilha!undefined
A mensagem da nossa armadilha funcionou perfeitamente.Entretanto, as impressões de console.log() resultaram emundefinedemvezdeexibirosvaloresdaspropriedades.Porqueissoaconteceu?
Todaarmadilha,quandoexecutada,deveseresponsabilizarpordefinir o valor que será retornado para quem acessar apropriedade.Comonãodefinimosretornoalgum,oresultado foiundefined.
Façamos um teste para entendermos ainda melhor a
9.3CONSTRUINDOARMADILHASDELEITURA 189
necessidadedesseretorno:
<!--client/index.html--><!--códigoposterioromitido-->
<script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
get(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
//MUDANÇAAQUI!:)return'Flávio';
}});
console.log(negociacao.quantidade);console.log(negociacao.valor);</script>
<!--códigoposterioromitido-->
Noexemploanterior,pormaisestranhoqueissopossaparecer,estamosretornandoastring"Flávio".Olhandono consoledoChrome, as instruções console.log() exibirão a string"Flávio", tanto para negociacao.quantidade quanto emnegociacao.valor:
Figura9.1:ReturnFlávio
190 9.3CONSTRUINDOARMADILHASDELEITURA
Nosso handler retorna algo, mas ainda não é o esperado.Queremos que sejam retornados os respectivos valores daspropriedadesquantidadeevalordoobjetoencapsuladopeloProxy.
Aboanotíciaéquetemostodososelementosnecessáriosparaisso. Quando temos um objeto JavaScript, podemos acessar suaspropriedadesatravésde.(ponto),ouutilizandoumcolchetequerecebeastringcomonomedapropriedadequedesejamosacessar.Se temos target (a referência para nossa instância) e temosprop (a string com o nome da propriedade que está sendo
acessada) podemosfazertarget[prop]
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
get(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
//modificaçãoaquireturntarget[prop];
}});
console.log(negociacao.quantidade);console.log(negociacao.valor);
</script><!--códigoposterioromitido-->
9.3CONSTRUINDOARMADILHASDELEITURA 191
Figura9.2:AcessandoapropriedadedoobjetoencapsuladopeloProxy
Nós vimos como executar um código toda vez que aspropriedadesdeumobjetoforemlidas.Contudo,issonãoresolveo problemadeatualizaçãoautomáticadaView,porquequeremosexecutar uma armadilha apenas quando as propriedades foremmodificadas.
Para realizarmos um teste de escrita, não podemossimplesmente atribuir valores para negociacao.quantidade enegociacao.valor,porqueeles sãogetters.Qualquer valor
atribuído para eles é ignorado. Sendo assim, aplicaremos uma"licençapoética"paraacessardiretamenteaspropriedadesprivadas_quantidadee_valor.Estamosquebrandoaconvenção,masépor uma boa causa, para podermos entender como Proxyfunciona:
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
get(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
returntarget[prop];}
192 9.3CONSTRUINDOARMADILHASDELEITURA
});
//mesmoacessandoaspropriedadessomenteleitura//aarmadilhanãoédisparadanegociacao._quantidade=10negociacao._valor=100;
</script><!--códigoposterioromitido-->
Realmente,nadaacontece.Damesmaformaqueaprendemosaexecutar armadilhas para leitura, aprendemos a executararmadilhasparaescrita.
Paracriarmosarmadilhasdeescrita,emvezdeusarmosgetemnossohandler,usamosset.Eleébemparecidocomoget,adiferençaestánoparâmetroextravaluequeelerecebe:
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
get(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
returntarget[prop];},
set(target,prop,value,receiver){
/*nossalógicaentraráaqui*/}
});
9.4 CONSTRUINDO ARMADILHAS DEESCRITA
9.4CONSTRUINDOARMADILHASDEESCRITA 193
negociacao._quantidade=10negociacao._valor=100;
</script><!--códigoposterioromitido-->
O parâmetro value é o valor que está sendo atribuído àpropriedade que está sendo acessada no Proxy. Sendo assim,podemosimplementarnossaarmadilhadessaforma:
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
get(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
returntarget[prop];},
set(target,prop,value,receiver){
console.log(`${prop}guarda${target[prop]},receberá${value}`);
//efetivaapassagemdovalor!target[prop]=value;
}});
negociacao._quantidade=10negociacao._valor=100;
</script><!--códigoposterioromitido-->
Um ponto que faz parte da especificação Proxy do ES2015(ES6)éanecessidadederetornarmostrue emnossaarmadilha
194 9.4CONSTRUINDOARMADILHASDEESCRITA
paraindicarqueaoperaçãofoibem-sucedida.
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100),{
get(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
returntarget[prop];},
set(target,prop,value,receiver){
console.log(`${prop}guarda${target[prop]},receberá${value}`);
//seforumobjetocongelado,seráignoradotarget[prop]=value;
//retornatrueseconseguiualterarapropriedadedoobjeto
returntarget[prop]==value;}
});
//armadilhaserádisparada,masosvalores//nãoserãoalterados,poisestãocongelados
negociacao._quantidade=10negociacao._valor=100;
</script><!--códigoposterioromitido-->
A instrução target[prop] = value será ignorada parapropriedades congeladas do objeto, é por isso que retornamos oresultado de target[prop] == value para sabermos se a
9.4CONSTRUINDOARMADILHASDEESCRITA 195
atribuiçãofoirealizadacomsucesso.
Verificandooconsole,temos:
Figura9.3:Novovalor
Podemosescreverumpoucomenoscomauxíliodeumrecursoqueaprenderemosaseguir.
Podemossimplificarumpoucomaisnossocódigocomauxílioda API Reflect (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect)introduzidanoECMAScript2015(ES6).EstaAPIcentralizaumasérie de métodos estáticos que permitem a leitura, escrita echamada de métodos e funções dinamicamente, um deles é oReflect.set().
OReflect.set() recebe três parâmetros. O primeiro é ainstância do objeto alvo da operação e o segundo o nome dapropriedadequeseráalterada.Oúltimoparâmetroéonovovalordapropriedade.Ométodoretornarátrueoufalsecasotenhaconseguidoalterarounãoapropriedadedoobjeto:
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacao=newProxy(newNegociacao(newDate(),2,100
9.5REFLECTAPI
196 9.5REFLECTAPI
),{
get(target,prop,receiver){
console.log(`apropriedade"${prop}"caiunaarmadilha!`);
returntarget[prop];},
set(target,prop,value,receiver){
console.log(`${prop}guarda${target[prop]},receberá${value}`);
returnReflect.set(target,prop,value);}
});
negociacao._quantidade=10negociacao._valor=100;
</script><!--códigoposterioromitido-->
A Reflect API possui outros métodos como oReflect.get() . No entanto, sua introdução tornaria nosso
código um pouco mais verboso, diferente do uso deReflect.set() que nos poupou uma linha de código. Por
exemplo, a instrução return target[prop] poderia ter sidosubstituída por return Reflect.get(target, prop), mas aforma atual nos permite escrever uma quantidade de caracteresmenor.
Agora que já compreendemos como um Proxy funciona,podemos voltar para Negociacoes , aliás, modelo cuja Viewprecisamosatualizar.
9.6UMPROBLEMANÃOESPERADO
9.6UMPROBLEMANÃOESPERADO 197
Agoraquejáconsolidamosnossoconhecimentosobrecriaçãode Proxy, deixaremos o modelo Negociacao de lado paraabordarmos o modelo Negociacoes , aquele cuja Viewprecisamos atualizar automaticamente. Vamos realizar um testecriando um Proxy e, através dele, chamaremos o métodoesvazia():
<!--client/index.html--><!--códigoposterioromitido--><script>
//começandodozero,apagandoocódigoanteriorqueexistia
constnegociacoes=newProxy(newNegociacoes(),{
set(target,prop,value,receiver){
console.log(`${prop}guarda${target[prop]},receberá${value}`);
returnReflect.set(target,prop,value);}
});
negociacoes.esvazia();
</script><!--códigoposterioromitido-->
Recarregando nossa página e observando o resultado noconsole,vemosapenasamensagemdeerroquejáeraexibidaantesporaindanãotermosterminadonossaaplicação.Nenhumaoutramensagemé exibida,ou seja,nossa armadilhanão foi executada!Seráqueomesmoacontececomométodoadiciona()?Vamostestá-lo.
Aindaemclient/index.html,vamossubstituirachamadade negociacoes.esvazia() por uma chamada denegociacoes.adiciona(),passandoumanovanegociação:
198 9.6UMPROBLEMANÃOESPERADO
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacoes=newProxy(newNegociacoes(),{
set(target,prop,value,receiver){
console.log(`${prop}guarda${target[prop]},receberá${value}`);
returnReflect.set(target,prop,value);}
});
//agora,chamandoométodoadicionanegociacoes.adiciona(newNegociacao(newDate(),1,100));
</script><!--códigoposterioromitido-->
Recarregando nossa página e observando o resultado noconsole,nadaacontece.
Isso acontece porque nossa armadilha só é disparada apenasquando houver atribuição de valor nas propriedades do objetoencapsuladopeloProxy.Osmétodosesvazia()eadiciona()não realizam qualquer atribuição. O primeiro fazthis._negociacoes.length=0eosegundoadicionaumnovoelementoemthis._negociacoes atravésdométodopush()dopróprioarray.Vamosrelembrardosmétodos!
//RELEMBRADOOCÓDIGO,NÃOHÁMODIFICAÇÃOAQUI
//client/app/domain/negociacoes/Negociacoes.js
classNegociacoes{
//códigoomitido
adiciona(negociacao){
9.6UMPROBLEMANÃOESPERADO 199
//nãoháatribuiçãoaqui!this._negociacoes.push(negociacao);
}
//códigoomitido
esvazia(){
//nãoháatribuiçãoaqui!this._negociacoes.length=0;
}
//códigoposterioromitido
Aliás,nãofazsentidothis._negociacoesreceberumanovaatribuição, pois é umapropriedade congelada.E agora? Seráqueexistealgumasoluçãoparaesseproblema?
Uma solução é sermos capazes de indicar quais métodos daclassedevemdispararnossas armadilhas.Comessa estratégia, osmétodosadiciona()eesvazia()nãoprecisammaisdependerde uma atribuição a alguma propriedade da instância deNegociacoes para que suas respectivas armadilhas sejam
disparadas.
Vamosdarinícioànossasoluçãocangaceiraparasolucionaroproblemadaseçãoanterior.Oprimeiropontoéentendermosque,pordebaixodospanos,quandooJavaScriptchamadaummétodo,elerealizaumgetparaobterumareferênciaaométodo,edepoisumapplyparapassarseusparâmetros.Porenquanto,vamosnosateraopapeldoget.
9.7 CONSTRUINDO ARMADILHAS PARAMÉTODOS
200 9.7CONSTRUINDOARMADILHASPARAMÉTODOS
VamossubstituirosetquetemosnoProxyquecriamosemclient/index.htmlporumget.Lembre-sedequegetnãorecebeoparâmetrovalue:
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacoes=newProxy(newNegociacoes(),{
get(target,prop,receiver){
/*eagora?*/}
});
negociacoes.adiciona(newNegociacao(newDate(),1,100));
</script><!--códigoposterioromitido-->
Opróximopassoéverificarmosseapropriedadequeestamosinterceptandoéumafunção/método,poisaestratégiaqueestamosconstruindo é para lidar com esse tipo de dado. Fazemos issoatravésdafunçãotypeof():
<!--client/index.html--><!--códigoposterioromitido-->
<!--NÃOTESTEOCÓDIGOAINDA!NÃOFUNCIONARÁ,NÃOTERMINAMOS--><script>
constnegociacoes=newProxy(newNegociacoes(),{
get(target,prop,receiver){
if(typeof(target[prop])==typeof(Function){
/*faltacódigoainda*/}
}
9.7CONSTRUINDOARMADILHASPARAMÉTODOS 201
});
negociacoes.adiciona(newNegociacao(newDate(),1,100));
</script><!--códigoposterioromitido-->
Se deixarmos do jeito que está, aplicaremos nossa estratégiapara qualquer método, e não é isso o que queremos. Não fazsentido o método paraArray() de Negociacoes dispararatualizaçõesdeView,apenasmétodosquealteramoestado.
Alémdeverificarmosotipodapropriedade,vamostestarseelaestáincluídanalistademétodosquedesejamosinterceptar:
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacoes=newProxy(newNegociacoes(),{
get(target,prop,receiver){
if(typeof(target[prop])==typeof(Function)&&['adiciona','esvazia']
.includes(prop)){
/*faltacódigoainda*/}
}});
negociacoes.adiciona(newNegociacao(newDate(),1,100));
</script><!--códigoposterioromitido-->
Através dométodoincludes() do array, verificamos se a
9.8UMAPITADADOES2016(ES7)
202 9.8UMAPITADADOES2016(ES7)
propriedade que está sendo interceptada consta em nossa lista.Aliás, ométodoincludes() foi introduzidonoES2016 (ES7),umaversãoposterioraoES2015(ES6).
A especificação ES2016 (ES7) defineArray.prototype.includes e o operador exponencial
apenas.
Agora, se apropriedadenão forummétodo/funçãodanossalista,deixamosocursoacontecernaturalmente.
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacoes=newProxy(newNegociacoes(),{
get(target,prop,receiver){
if(typeof(target[prop])==typeof(Function)&&['adiciona','esvazia']
.includes(prop)){
/*faltacódigoainda*/
}else{
//realizandoumgetpadrãoreturntarget[prop];
}}
});
negociacoes.adiciona(newNegociacao(newDate(),1,100));
</script><!--códigoposterioromitido-->
9.8UMAPITADADOES2016(ES7) 203
Agora que temos tudo encaixado, precisamos implementar ocódigo que será executado pela armadilha dos métodos quedecidimosconsiderar.Éaquiqueentraopulodogato,eminglês,catjump.
Paraosmétodoslistadosemnossoarraydenomesdemétodosa serem interceptados, vamos fazer com que sua leitura retorneuma nova function em vez de uma arrow function, com ocódigo que desejamos executar, inclusive aquele que aplicará osparâmetrosdafunção(casoexistam):
<!--client/index.html--><!--códigoposterioromitido--><script>
constnegociacoes=newProxy(newNegociacoes(),{
get(target,prop,receiver){
if(typeof(target[prop])==typeof(Function)&&['adiciona','esvazia']
.includes(prop)){
//retornaumanovafunçãocomcontextodinâmico
//antesqueoapplyaplicadoporpadrãopelo
//interpretadorsejachamado.
returnfunction(){
console.log(`"${prop}"disparouaarmadilha`);
target[prop].apply(target,arguments);}
}else{//seépropriedade,retornaovalornormalmentereturntarget[prop];
}}
204 9.8UMAPITADADOES2016(ES7)
});
negociacoes.adiciona(newNegociacao(newDate(),1,100));
</script><!--códigoposterioromitido-->
Vamosrepassarpelamágicaquefizemos.Lembrem-sedequeo interpretadordoJavaScriptprimeirorealizaumgetparalerométodo,edepoisexecutaumapplypordebaixodospanosparaexecutá-lo, passando seus parâmetros. A function() queestamos retornando substituirá o método original antes que oapply entre em ação. Nela, adicionamos o código da nossa
armadilha.
Atravésdetarget[prop].apply(nãoconfundacomoapplyque o interpretador fará), estamos chamando o método usandocomo contexto o próprio target . O método apply() éparecidocomométodocall()quejávimos,adiferençaéqueoprimeirorecebeseusparâmetrosemumarray.Perfeito,mastemosumproblema.
Nossa function() substituta precisa saber se o métodooriginalpossuiounãoparâmetrosparapassá-losna chamadadetarget[prop].apply.Conseguimosessa informaçãoatravésdearguments.
Avariávelimplícitaargumentspossuiestruturasemelhanteaumarray.Elaestápresenteemtodométodooufunção,edáacessoaosparâmetrospassados,mesmoqueelesnãosejamexplicitados.Vejamosumexemploatravésdoconsole:
//TESTARNOCONSOLE,EXEMPLOAPENAS
9.8UMAPITADADOES2016(ES7) 205
//NÃORECEBEPARÂMETROALGUM
functionexibeNomeCompleto(){alert(`Nomecompleto:${arguments[0]}${arguments[1]}`);
}
//funciona!exibeNomeCompleto('Flávio','Almeida');
Oarguments foi fundamental paranossa solução, poisnãoteríamos como prever quantos parâmetros cada métodointerceptado receberia. Agora que temos nossa solução pronta,vamosalterarNegociacaoControllerparaquefaçausodela.
VamosalteraraclasseNegociacaoController atribuindo àpropriedadethis._negociacoesumProxy,usandoalógicaquejáimplementamosemclient/index.html.Épraticamenteumcorta e cola, a diferença é que fazemos uma chamada a
this._negociacoesView.update() logo abaixo doconsole.log()emqueométododisparouaarmadilha:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//agoraéumproxy!this._negociacoes=newProxy(newNegociacoes(),{
9.9 APLICANDO A SOLUÇÃO EMNEGOCIACAOCONTROLLER
206 9.9APLICANDOASOLUÇÃOEMNEGOCIACAOCONTROLLER
get(target,prop,receiver){
if(typeof(target[prop])==typeof(Function)&&['adiciona','esvazia']
.includes(prop)){
returnfunction(){
console.log(`"${prop}"disparouaarmadilha`);
target[prop].apply(target,arguments);
//targetéainstânciarealdeNegociacoes
//contudo,TEREMOSPROBLEMASAQUI!this._negociacoesView.update(target);
}
}else{
returntarget[prop];}
}});
//códigoposterioromitido}
Não precisamos mais da tag <script> com nossos testes,podemos removê-la. Por fim, quando executamos uma novanegociação,recebemosestamensagemdeerronoconsole:
//EXIBIDONOCONSOLEDOCHROME
UncaughtTypeError:Cannotreadproperty'update'ofundefined
Isso acontece porque o this dethis._negociacoesView.update(target) é o Proxy, e nãoNegociacaoController. Se tivéssemos usado arrow function,
9.9APLICANDOASOLUÇÃOEMNEGOCIACAOCONTROLLER 207
resolveríamos esse problema, mas precisamos que o this dafunção seja dinâmico; caso contrário, nossa solução nãofuncionará.
Umasoluçãoque jávimosantes éguardarumareferênciadethisnavariávelself,paraquepossamosutilizá-ladepoisna
armadilha:
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//guardandoumareferência//paraainstânciadeNegociacaoControllerconstself=this;
this._negociacoes=newProxy(newNegociacoes(),{
get(target,prop,receiver){
if(typeof(target[prop])==typeof(Function)&&['adiciona','esvazia']
.includes(prop)){
returnfunction(){
console.log(`"${prop}"disparouaarmadilha`);
target[prop].apply(target,arguments);
//AGORAUSASELF!self._negociacoesView.update(target);
}
}else{
208 9.9APLICANDOASOLUÇÃOEMNEGOCIACAOCONTROLLER
returntarget[prop];}
}});
this._negociacoesView=newNegociacoesView('#negociacoes);
this._negociacoesView.update(this._negociacoes);
this._mensagem=newMensagem();this._mensagemView=newMensagemView('#mensagemView');this._mensagemView.update(this._mensagem);
}//códigoposterioromitido
Nossasoluçãofunciona,mas imagineaplicá-laparaomodeloMensagem? Teríamos de repetir muito código, inclusive sua
manutenção não será lá uma das mais fáceis. Então, antes degeneralizarmosnossasolução,nopróximocapítulovamospediraajudadeoutropadrãodeprojetoparacolocarosertãoemordem.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/09
9.9APLICANDOASOLUÇÃOEMNEGOCIACAOCONTROLLER 209
CAPÍTULO10
"... o mais importante e bonito, do mundo, é isto: que aspessoasnãoestãosempreiguais,aindanãoforamterminadas– mas que elas vão sempremudando.Afinamou desafinam.Verdademaior.Éoqueavidameensinou."-GrandeSertão:Veredas
Nocapítuloanterior,aplicamosopadrãodeprojetoProxyparadeixar clara uma separação entre o modelo e o código deinfraestrutura que criamos para mantê-lo sincronizado com aView. No entanto, vimos que podemos aplicar outro padrão deprojeto para tornar nossa solução mais genérica. Aplicaremos opadrãodeprojetoFactory.
O padrão de projeto Factory auxilia na criação de objetoscomplexos, encapsulando os detalhes de criação desses objetos,comonocasodosnossosproxies.
CÚMPLICENAEMBOSCADA
10.1OPADRÃODEPROJETOFACTORY
210 10CÚMPLICENAEMBOSCADA
Vamoscriaroarquivoclient/app/util/ProxyFactory.js.Apastautil não éumerrodedigitação.Éumapastanaqualficarão os arquivos que não fazem parte do domínio nem dainterface(UI)daaplicação.
AclasseProxyFactoryteráométodoestáticocreate,seuúnicométodo:
//client/app/util/ProxyFactory.js
classProxyFactory{
staticcreate(objeto,props,armadilha){
}
}
Ométodo create recebe três parâmetros. O primeiro é oobjetoalvodoproxy,osegundoéumarraycomosmétodosquedesejamos interceptar e, por fim, o último é a função armadilhaque desejamos executar para os métodos presentes no arrayprops.
Antesdemaisnada,vamosimportaroscriptquedevevirantesda importação de NegociacaoController , emclient/index.html:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script>
10.1OPADRÃODEPROJETOFACTORY 211
<!--importouoProxyFactory--><scriptsrc="app/util/ProxyFactory.js"></script>
<scriptsrc="app/app.js"></script><!--códigoposterioromitido-->
Agora,vamosmoverocódigodeNegociacaoControler,quecria nosso proxy, para dentro do método create . Então,realizaremos os ajustes necessários para que ele leve emconsideraçãoosparâmetrosdométodo:
//client/app/util/ProxyFactory.js
classProxyFactory{
staticcreate(objeto,props,armadilha){
//recebeobjetocomoparâmetro
returnnewProxy(objeto,{
get(target,prop,receiver){//usaoarraypropspararealizaroincludesif(typeof(target[prop])==typeof(Function)
&&props.includes(prop)){
returnfunction(){
console.log(`"${prop}"disparouaarmadilha`);
target[prop].apply(target,arguments);
//executaaarmadilhaquerecebe//oobjetooriginal
armadilha(target);}
}else{
returntarget[prop];}
212 10.1OPADRÃODEPROJETOFACTORY
}});
}}
Nãoprecisamosmaisdatag<script>,naqualrealizávamosnossostestes.Removaatagdeclient/index.html.
AgoraquetemosnossoProxyFactory,vamosutilizá-loparacriar nosso Proxy em NegociacaoController . Veja que nãoprecisaremos usar o self para termos acesso à instância deNegociacaoController emnossa armadilha, porquedessa vez
estamosusandoarrowfunction:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//criandooproxycomauxíliodanossafábrica!
this._negociacoes=ProxyFactory.create(newNegociacoes(),['adiciona','esvazia'],model=>this._negociacoesView.update(model)
);
this._negociacoesView=newNegociacoesView('#negociacoes);
this._negociacoesView.update(this._negociacoes);
this._mensagem=newMensagem();this._mensagemView=newMensagemView('#mensagemView');this._mensagemView.update(this._mensagem);
}
10.1OPADRÃODEPROJETOFACTORY 213
//códigoposterioromitido
Vejacomoconseguimosmelhoraremmuitoa legibilidadedonossocódigoe,porconseguinte,suamanutenção.
Agora,faremosamesmacoisacomomodeloMensagem.Masnocasodessemodelo,queremosmonitoraroacessoàpropriedadetexto:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
this._negociacoes=ProxyFactory.create(newNegociacoes(),['adiciona','esvazia'],model=>this._negociacoesView.update(model)
);
this._negociacoesView=newNegociacoesView('#negociacoes);
this._negociacoesView.update(this._negociacoes);
//criandooproxycomauxíliodanossafábrica!
this._mensagem=ProxyFactory.create(newMensagem(),['texto'],model=>this._mensagemView.update(model)
);
this._mensagemView=newMensagemView('#mensagemView');this._mensagemView.update(this._mensagem);
}
214 10.1OPADRÃODEPROJETOFACTORY
//códigoposterioromitido
Porfim,vamosalterarosmétodosadiciona()eapaga(),removendoainstruçãoquerealizaoupdatemanualmentedaViewdeMensagem:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
//códigoomitido}
adiciona(event){
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._mensagem.texto='Negociaçãoadicionadacomsucesso
;
//nãochamamaisoupdatedaviewdeMensagem
this._limpaFormulario();}
//códigoposterioromitido
apaga(){
this._negociacoes.esvazia();this._mensagem.texto='Negociaçõesapagadascomsucesso'
;//nãochamamaisoupdatedaviewdeMensagem
}}
Se recarregarmos a página no navegador, veremos que oformulário funciona corretamente, mas não atualizou amensagem.Oqueseráqueaconteceu?
10.1OPADRÃODEPROJETOFACTORY 215
OProxyFactorysóestápreparadoparainterceptarmétodos;elenãoestápreparadoparainterceptarpropriedades.
Na classe Mensagem , get texto() é um getter e oProxyFactorynãoentendequeprecisainterceptarcomoget.Jáqueosgettersesetterssãoacessadoscomopropriedades,temostambémdecolocarnoProxyFactoryumcódigopara lidarmoscompropriedades.Paraisto,adicionaremosumsetnohandlerdonossoproxy:
//client/app/util/ProxyFactory.js
classProxyFactory{
staticcreate(objeto,props,armadilha){
//recebeumobjetocomoparâmetroreturnnewProxy(objeto,{
get(target,prop,receiver){
//códigoomitido!},
//NOVIDADE!set(target,prop,value,receiver){
constupdated=Reflect.set(target,prop,value);
//SÓEXECUTAMOSAARMADILHA//SEFIZERPARTEDALISTADEPROPS
if(props.includes(prop))armadilha(target);
returnupdated;}
10.2NOSSOPROXYAINDANÃOESTÁ100%!
216 10.2NOSSOPROXYAINDANÃOESTÁ100%!
});}
}
Vamos recarregar a página no navegador e preencher oscampos do formulário. Tudo estará funcionando em nossaaplicação.
AindapodemosrealizarumamelhorianalegibilidadedonossoProxyFactory de último segundo. Nele temos a lógica que
determina se a propriedade é uma função, dentro do if .Podemos isolar essa comparação em um novo método estáticochamadoehFuncao(),eutilizá-lonacondiçãoparadeixarclaranossaintenção.
Comestaalteração,nossoProxyFactoryfinalestaráassim:
//client/app/util/ProxyFactory.js
classProxyFactory{
staticcreate(objeto,props,armadilha){
returnnewProxy(objeto,{
get(target,prop,receiver){
//USANDOOMÉTODOESTÁTICO
if(ProxyFactory._ehFuncao(target[prop])&&props.includes(prop)){
returnfunction(){
console.log(`"${prop}"disparouaarmadilha`);
target[prop].apply(target,arguments);armadilha(target);
}
10.2NOSSOPROXYAINDANÃOESTÁ100%! 217
}else{
returntarget[prop];}
},
set(target,prop,value,receiver){
constupdated=Reflect.set(target,prop,value);
if(props.includes(prop))armadilha(target);returnupdated;
}
});}
//NOVOMÉTODOESTÁTICOstatic_ehFuncao(fn){
returntypeof(fn)==typeof(Function);}
}
Só alteramos amaneira pela qual organizamos nosso código,sem modificar seu comportamento. Mas aí vem aquela mesmaperguntadesempre:seráquepodemosmelhorarnossocódigo?
No capítulo anterior, criamos nosso ProxyFactory queencapsulougrandeparteda complexidadepara criarmosproxies.Isso possibilitou o reúso de todo o código que escrevemos semgrandescomplicações.Noentanto,temosdoispontosquedeixamadesejar.
Oprimeiropontoéprecisarmos lembrarderealizaroupdate
10.3 ASSOCIANDO MODELO E VIEWATRAVÉSDACLASSEBIND
218 10.3ASSOCIANDOMODELOEVIEWATRAVÉSDACLASSEBIND
manual da View na inicialização do constructor() deNegociacoesController.Vejamos:
//REVISANDOOCÓDIGOAPENAS!
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
this._negociacoes=ProxyFactory.create(newNegociacoes(),['adiciona','esvazia'],model=>this._negociacoesView.update(model)
);
this._negociacoesView=newNegociacoesView('#negociacoes);
//PRECISACHAMAROUPDATE
this._negociacoesView.update(this._negociacoes);
this._mensagem=ProxyFactory.create(newMensagem(),['texto'],model=>this._mensagemView.update(model)
);
this._mensagemView=newMensagemView('#mensagemView');
//PRECISACHAMAROUPDATEthis._mensagemView.update(this._mensagem);
}
Alémdisso, se observarmos atentamenteo código anterior, oupdatedaViewpassadoparaaarmadilhadoProxyépraticamente
10.3ASSOCIANDOMODELOEVIEWATRAVÉSDACLASSEBIND 219
idêntico, sómudando aView que chamará ométodo update.Vamosnoslivrardessasresponsabilidades.
O que fizemos até agora se assemelha a um mecanismo deDatabinding(quetraduzidoparaoportuguês,significa"ligaçãodedados"). Associamos um modelo com a View, de modo quemudançasnomodeloatualizamautomaticamenteaView.VamoscriaraclasseBind, que será a responsável emexecutar as duasaçõesqueoprogramadoraindaprecisarealizar.
Criaremos o arquivo client/app/util/Bind.js , ondevamos declarar a classe Bind . Em seu constructor() ,receberemos três parâmetros. O primeiro será o modelo daassociação, o segundo, a View que deve ser atualizada a cadamudança do modelo e, por fim, o terceiro será o array com osnomes de propriedades e métodos que devem disparar aatualização.
//client/app/util/Bind.js
classBind{
constructor(model,view,props){
}}
Agora,noconstructor()deBind,vamoscriarumproxyatravés do nosso ProxyFactory , repassando os parâmetrosrecebidosparaométodocreate()dafactory:
//client/app/util/Bind.js
classBind{
constructor(model,view,props){
220 10.3ASSOCIANDOMODELOEVIEWATRAVÉSDACLASSEBIND
constproxy=ProxyFactory.create(model,props,model=>{view.update(model)
});}
}
Sabemosque todaViewemnossa aplicaçãopossuiométodoupdate() que recebeummodelo, por isso, estamos chamandoview.update(model) na armadilha do proxy. Retiramos essa
responsabilidadedodesenvolvedor.
Agora,assimqueoproxyécriado,vamosforçarumachamadaao método update() da View, removendo mais umaresponsabilidadededesenvolvedor!
//client/app/util/Bind.js
classBind{
constructor(model,view,props){
constproxy=ProxyFactory.create(model,props,model=>{view.update(model)
});
view.update(model);}
}
Porfim,pormaisestranhoqueissopossaparecer,casooleitortenha vindo de outras linguagens como Java ou C#, faremos oconstructor() de Bind retornar o proxy que criamos. Em
JavaScript,umconstructor()poderetornarumobjetodeumtipodiferentedaclasseàqualpertence:
//client/app/util/Bind.js
classBind{
constructor(model,view,props){
10.3ASSOCIANDOMODELOEVIEWATRAVÉSDACLASSEBIND 221
constproxy=ProxyFactory.create(model,props,model=>{view.update(model)
});
view.update(model);
returnproxy;}
}
Vamos importar o script que declara Bind emclient/index.html . Mas muito cuidado, pois
NegociacaoController depende de Bind, que depende deProxyFactory.Chegaráummomentoneste livro emque você
serálibertadodessegrandeproblemanalinguagemJavaScript,masaindanãoéahora.
<!--client/index.html--><!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script><scriptsrc="app/util/ProxyFactory.js"></script>
<!--importouaqui!--><scriptsrc="app/util/Bind.js"></script>
<scriptsrc="app/app.js"></script><!--códigoposterioromitido-->
Para terminar, alteraremos NegociacaoController eusaremos nossa classe Bind para fazer a associação entre omodeloeaView:
//client/app/controllers/NegociacaoController.js
222 10.3ASSOCIANDOMODELOEVIEWATRAVÉSDACLASSEBIND
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
this._negociacoes=newBind(newNegociacoes(),newNegociacoesView('#negociacoes'),['adiciona','esvazia']
);
this._mensagem=newBind(newMensagem(),newMensagemView('#mensagemView'),['texto']
);}
//códigoposterioromitido
Recarregando a página, tudo continua funcionando comoantes, só que dessa vez simplificamos bastante a quantidade decódigo no constructor() de Negociacoes . Não foi maisnecessário declarar as propriedades _negociacaoView nem_mensagemView,nemmesmofoinecessáriochamaroupdate()manualmente das Views na inicialização deNegociacaoController.
AtravésdeBind,passamosainstânciadomodelo,ainstânciada View e as propriedades ou métodos que desejamos ativar aatualizaçãoautomática.Nemmesmofoinecessárioescrevermosalinhaquerealizaaatualizaçãodaviewapartirdomodelo.
Para fechar com chave de ouro, que tal enxugarmos uma
10.3ASSOCIANDOMODELOEVIEWATRAVÉSDACLASSEBIND 223
gotinhanocódigoqueacabamosdeescrever?
EmnossoBind,onomedaspropriedadesedosmétodosquedesejamos executar nossas armadilhas foram passados dentro deum array. Apesar de ainda não funcionar, vamos passar cadaparâmetroindividualmente,abdicandodoarray:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//nãopassamosmais"adiciona"e"esvazia"dentrodeumarray
this._negociacoes=newBind(newNegociacoes(),newNegociacoesView('#negociacoes'),'adiciona','esvazia'
);
//nãopassamosmais"texto"dentrodeumarraythis._mensagem=newBind(
newMensagem(),newMensagemView('#mensagemView'),'texto'
);}
Agora,paraquenossocódigofuncione,vamosalteraraclasseBind adicionando três pontos ( ... ) antes do parâmetroprops:
10.4PARÂMETROSREST
224 10.4PARÂMETROSREST
//client/app/util/Bind.js
classBind{
//...antesdepropsconstructor(model,view,...props){
constproxy=ProxyFactory.create(model,props,model=>{view.update(model)
});
view.update(model);
returnproxy;}
}
Quando adicionamos ... antes do nome do últimoparâmetro,estamosindicandoquetodososparâmetrosapartirdoterceiro, inclusive,serãoconsideradoscomofazendopartedeumarray.Issotornareflexívelaquantidadedeparâmetrosqueum
métodooufunçãopodereceber.Masatenção,sópodemosusaroREST operator no último parâmetro. Aliás, este recurso foiintroduzidonoES2015(ES6)emdiante.
Até aqui criamos um mecanismo de data binding parasimplificar aindamaisnosso código.Podemosdar a tarefa comoconcluída. Todavia, há um problema antigo que voltará a nosassombrarnopróximocapítulo,oproblemacomdatas!
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/10
10.4PARÂMETROSREST 225
CAPÍTULO11
"O correr da vida embrulha tudo, a vida é assim: esquenta eesfria,apertaedaíafrouxa,sossegaedepoisdesinquieta.Oqueelaquerdagenteécoragem."-GrandeSertão:Veredas
Nossa aplicação continua funcionando como esperado, masprecisamosresolveraindaumproblemarelacionandoaocampodadatadonossoformulário.
O input do tipo date que utilizamos não faz parte daespecificação da W3C, sendo um tipo de elemento criado pelaequipedoGoogleChrome.Comooinputdotipodatenãofazpartedaespecificação,nãotemosagarantiadequeestejapresentenosmaisdiversosnavegadoresdomercado.Aliás, esse éoúnicomotivoquenosprendefortementeaestenavegador.
Precisamos alterar nossa aplicação para que utilize em seulugar um input do tipo text . Primeiro, vamos alterarclient/index.htmlemudarotipodoinput:
DATADOSINFERNOS!
11.1OPROBLEMACOMOINPUTDATE
226 11DATADOSINFERNOS!
<!--client/index.html--><!--códigoanterioromitido--><divclass="form-group">
<labelfor="data">Data</label>
<!--eradate,passouasertext-->
<inputtype="text"id="data"class="form-control"requiredautofocus/></div><!--códigoposterioromitido-->
Contudo, essa alteração não é suficiente. Temos de alterar alógicadométodoparaData()daclasseDateConverter.Atéomomento,suaimplementaçãoestáassim:
//client/app/ui/converters/DateConverter.js
classDateConverter{
//códigoomitido
staticparaData(texto){
if(!/^\d{4}-\d{2}-\d{2}$/.test(texto))thrownewError('Deveestarnoformatoaaaa-mm-dd');
returnnewDate(...texto.split('-').map((item,indice)=>item-indice%2));
}}
Comonãoestamosmaisusandooinputdotipodate,nãorecebemos mais a string no formato aaaa-mm-dd. Recebemosexatamentecomoousuáriodigitounoformulário.
Nossoprimeiropassoseráajustarnossaexpressãoregularea
11.2AJUSTANDONOSSOCONVERTER
11.2AJUSTANDONOSSOCONVERTER 227
mensagemdeerro,casootextoaserconvertidonãouseopadrão:
//client/app/ui/converters/DateConverter.js
classDateConverter{
//códigoomitido
staticparaData(texto){
//NOVAEXPRESSSÃOREGULAR//AMENSAGEMDEERROTAMBÉMMUDOU
if(!/\d{2}\/\d{2}\/\d{4}/.test(texto))thrownewError('Adatadeveestarnoformatodd/mm/a
aa');
returnnewDate(...texto.split('-').map((item,indice)=>item-indice%2));
}}
A alteração ainda não é suficiente, precisamos modificar alógica que desmembra nossa string.O primeiro passo é trocar oseparadorparasplit('/').Agora,precisamosinverteraordemdos elementos para ano, mês e dia. Antes não era necessário,porque a string recebida já estava nessa ordem. Resolvemos issofacilmenteatravésdafunçãoreverse()quetodoarraypossui:
//client/app/ui/converters/DateConverter.js
classDateConverter{
//códigoomitido
staticparaData(texto){
//NOVAEXPRESSSÃOREGULAR//AMENSAGEMDEERROTAMBÉMMUDOU
if(!/\d{2}\/\d{2}\/\d{4}/.test(texto))thrownewError('Adatadeveestarnoformatodd/mm/a
228 11.2AJUSTANDONOSSOCONVERTER
aa');
//MUDOUOSEPARADORPARA/EUSOUREVERSE()
returnnewDate(...texto.split('/').reverse().map((item,indice)=>
item-indice%2));}
}
Pronto,issoésuficienteparaquenossaaplicaçãofuncioneemoutrosnavegadoresquenãosuporteoinputdotipodate.Masainda precisamos resolver um problema que já existia mesmoquandousávamosesteinput.
Sedigitarmosumadatainválida,nossovalidadorlançaráumaexceção que será apresentada apenas no console do navegador.Nenhuma mensagem informando o usuário sobre a entrada dadata errada é exibida para o usuário, e ele fica sem saber o queaconteceu.Chegouahoradelidarmoscomessaquestão.
Podemos capturar exceções em JavaScript lançadas comthrowsatravésdoblocotry,etratá-lasnoblococatch.No
bloco da instrução try , temos uma ou mais instruções quepotencialmentepodemlançarumaexceção.Casoalgumaexceçãoseja lançada, o fluxo do código é direcionado para o bloco docatchquerecebecomoparâmetroumobjetocominformações
daexceçãolançada.
Vamos alterar o método adiciona() deNegociacoesControllerparausarmosessanovasintaxe:
11.3LIDANDOCOMEXCEÇÕES
11.3LIDANDOCOMEXCEÇÕES 229
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
adiciona(event){
try{
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._mensagem.texto='Negociaçãoadicionadacomsucesso
;this._limpaFormulario();
}catch(err){
console.log(err);this._mensagem.texto=err.message;
}}//códigoposterioromitido
Vejaque,noblocodocatch,aprimeiracoisaquefazemosélogarainformaçãodoerroquefoilançado,paralogoemseguidaatribuirmosotextodoerroàthis._mensagem.texto,atravésdeerr.message.
Setentarmosincluirumanovadatainválida,seráexibidaumamensagem de erro. Essa validação poderia ser feita de diversasformas,mas a forma atual possibilita introduzir certos conceitosimportantes para quem trilha o caminho de um cangaceiro emJavaScript.
Tudo muito bonito, mas há um detalhe que pode passardesapercebido por um ninja, mas nunca por um cangaceiro.Vamos alterar a última instrução do método adiciona()
forçandoumerrodedigitação:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
230 11.3LIDANDOCOMEXCEÇÕES
adiciona(event){
try{
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._mensagem.texto='Negociaçãoadicionadacomsucesso
;
//ATENÇÃO,FORÇANDOUMERRO//CHAMANDOUMMÉTODOQUENÃOEXISTEthis._limpaForm();
}catch(err){
console.log(err);this._mensagem.texto=err.message;
}}
//códigoposterioromitido
Oqueaconteceráaoincluirmosnegociações?Amensagemdeerroseráexibidaparaousuário:
Figura11.1:Mensagemdeerroquenãodeveriaaparecer
Só faz sentido exibir para o usuáriomensagens de alto nível,nãomensagemde erro de sintaxe.O problema é que não temoscomosaberotipodoerronoblocodocatch.
Umasoluçãoécriarmosnossasprópriasexceçõese,comajuda
11.3LIDANDOCOMEXCEÇÕES 231
dainstruçãoinstanceof,perguntaríamosseaexceçãolançadaédo tipo em que estamos interessados. Vamos alterar o métodoadiciona():
//client/app/controllers/NegociacoesController.js
//códigoposterioromitidoadiciona(event){
try{
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._mensagem.texto='Negociaçãoadicionadacomsucesso
;this._limpaForm();
}catch(err){
console.log(err);console.log(err.stack);
//TESTASEOTIPODOERROÉDEDATA,//SEFOR,EXIBEMENSAGEMPARAOUSUÁRIO
if(errinstanceofDataInvalidaException){
this._mensagem.texto=err.message;
}else{
//mensagemgenéricaparaqualquerproblemaquepossaacontecer
this._mensagem.texto='Umerronãoesperadoaconteceu. Entreemcontatocomosuporte';
}}
}
Ocódigo anteriornão funcionará, pois ainda não criamos aclasseDataInvalidaException.Porém,issonãonosimpedede
232 11.3LIDANDOCOMEXCEÇÕES
analisá-lo. No bloco catch , verificamos através da instruçãoinstanceof se a exceção é uma instância de
DataInvalidaException,eexibimosamensagemdeerroparaousuário.Seoerronãofordaclassequeestamostestando,apenasexibimosamensagemnoconsole.
Agoraquesabemoscomonossocódigosecomportará,chegouahoradecriarmosnossaclasseDataInvalidaException,porquequemsabefazhoraenãoesperaacontecer!
Vamos criar nossa própria classe de erroDataInvalidaException herdando de Error . Criaremos o
arquivoclient/app/ui/converters/DataInvalidaException.js , no
qualdeclararemosnossaclasse,quecomeçaráassim:
//client/app/ui/converters/DataInvalidaException.js
classDataInvalidaExceptionextendsError{
constructor(){
super('Adatadeveestarnoformatodd/mm/aaaa');}}
Como a classe filha DataInvalidaException possui umconstructor()diferentedaclassepaiError,somosobrigadosa chamar super(), passando a informação necessária para oconstructor() do pai, a mensagem "A data deve estar no
formatodd/mm/aaaa".
11.4 CRIANDO NOSSA PRÓPRIA EXCEÇÃO:PRIMEIRATENTATIVA
11.4CRIANDONOSSAPRÓPRIAEXCEÇÃO:PRIMEIRATENTATIVA 233
Agora, vamos importar o script da classe que criamos emclient/index.htmlcomoúltimoscript:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script><scriptsrc="app/util/ProxyFactory.js"></script><scriptsrc "app/util/Bind js" </script
<!--importouaqui--><scriptsrc="app/ui/converters/DataInvalidaException.js"></script>
<scriptsrc="app/app.js"></script>
<!--códigoposterioromitido-->
Agora,precisamosalterara classeDateConverter paraqueseumétodoparaData()lanceaexceçãoqueacabamosdecriar:
//client/app/ui/converters/DateConverte.js
classDateConverter{
//códigoposterioromitido
staticparaData(texto){
//LANÇAANOVAEXCEÇÃO!
if(!/\d{2}\/\d{2}\/\d{4}/.test(texto))thrownewDataInvalidaException();
returnnewDate(...texto.split('/').reverse()
234 11.4CRIANDONOSSAPRÓPRIAEXCEÇÃO:PRIMEIRATENTATIVA
.map((item,indice)=>item-indice%2));
}}
Vamos experimentar preencher os dados do formulário,mascomumadatainválida.Issofarácomqueamensagemdeerrosejaexibidaparaousuário.
Agora, se cadastramos uma negociação com data válida,teremoso errode chamadadeummétodoquenão existe sendoimpressoapenasnoconsole.Será?
Seolharmosasduasinformaçõesdeerroquesãoimpressasnoconsole,asegundaqueexibeerr.stackindicaqueoerrofoideError,enãoDataInvalidaException.Precisamosrealizaresseajusteatribuindoàpropriedadethis.nameherdadadeErrorovalordethis.constructor.name:
//client/app/ui/converters/DataInvalidaException.js
classDataInvalidaExceptionextendsError{
constructor(){
super('Adatadeveestarnoformatodd/mm/aaaa');
//ajustaonamedoerro!this.name=this.constructor.name;
}}
Umnovo testedemonstraqueháumaparidadedo resultadodeconsole.log(err)econsole.log(err.stack).
11.5 CRIANDO NOSSA PRÓPRIA EXCEÇÃO:SEGUNDATENTATIVA
11.5CRIANDONOSSAPRÓPRIAEXCEÇÃO:SEGUNDATENTATIVA 235
Apesar de a nossa solução funcionar, não é muito intuitivopara o programador ter de lembrar sempre de adicionar ainstruçãothis.name=this.constructor.nameparaajustaronome da exceção na stack. Hoje temos apenas um tipo de errocriadopornós,masesetivéssemosumadezenadeles?
Paraevitarqueodesenvolvedorprecise lembrarde realizaroajuste que vimos, vamos criar uma nova classe chamadaApplicationException . Todas as exceções que criarmos
herdarãodessaclasseemvezdeError.
Criando o arquivo emclient/app/util/ApplicationException.js:
//client/app/util/ApplicationException.js
classApplicationExceptionextendsError{
constructor(msg=''){
super(msg);this.name=this.constructor.name;
}}
Vamos importar o script em client/index.html . Masatenção: como haverá uma dependência entreDataInvalidaException eApplicationException, a última
deveserimportadaprimeiro:
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script>
236 11.5CRIANDONOSSAPRÓPRIAEXCEÇÃO:SEGUNDATENTATIVA
<scriptsrc="app/util/ProxyFactory.js"></script><scriptsrc="app/util/Bind.js"></script>
<!--importouaqui,precisaserantesdeDataInvalidaException-->
<scriptsrc="app/util/ApplicationException.js"></script><scriptsrc="app/ui/converters/DataInvalidaException.js"></script>
<scriptsrc="app/app.js"></script>
Agora,alterandoDataInvalidaException paraquepasse aherdardeApplicationException:
//client/app/ui/converters/DataInvalidaException.js
classDataInvalidaExceptionextendsApplicationException{
constructor(){
super('Adatadeveestarnoformatodd/mm/aaaa');}}
Ao recarregarmos a página e verificarmos as mensagens deerros impressasno consoleparaumadata inválida, vemosqueonomedoerroestácorretonaimpressãodastack.
Por fim, podemos voltar o nome correto da chamada dométodothis._limpaFormulario():
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
adiciona(event){
try{
event.preventDefault();this._negociacoes.adiciona(this._criaNegociacao());this._mensagem.texto='Negociaçãoadicionadacomsucesso
11.5CRIANDONOSSAPRÓPRIAEXCEÇÃO:SEGUNDATENTATIVA 237
;
//VOLTOUCOMONOMECORRETODOMÉTODOthis._limpaFormulario();
}catch(err){//códigoomitido
}}
//códigoposterioromitido
Agora que já temos informações mais amigáveis para ousuário, podemos continuar com nossa aplicação. Aliás, umaaplicaçãonãoésófeitadeconversãodedatas,elatambéméfeitadeintegrações,assuntodopróximocapítulo.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/11
238 11.5CRIANDONOSSAPRÓPRIAEXCEÇÃO:SEGUNDATENTATIVA
CAPÍTULO12
"Nocentrodosertão,oqueédoideiraàsvezespodeserarazãomaiscertaedemaisjuízo!"-GrandeSertão:Veredas
Nós aplicamos diversos recursos da linguagem JavaScriptduranteaconstruçãodanossaaplicação.Entretanto,paraqueelafiquecompleta,precisará importarnegociaçõesdeumaAPIweb.OprojetobaixadojápossuiumservidorcriadocomNode.js,quedisponibilizaaAPInecessáriaparanossaaplicação.
Vejamosa infraestruturamínimanecessáriaparaquesejamoscapazesdelevantaroservidor.
Para que seja possível levantar o servidor disponibilizado, éprecisoqueoNode.js (https://nodejs.org)esteja instaladoemsuamáquina.Masatenção,utilizesempreasversõespares,evitandoasversõesímparesdevidoàinstabilidadedaúltima.Aversão8.1.3do
PILHANDOOQUEINTERESSA!
12.1SERVIDOREINFRAESTRUTURA
12PILHANDOOQUEINTERESSA! 239
Node.jsfoiutilizadaduranteaescritadestelivro.
Para levantaroservidor,énecessárioummínimodetraquejocom o terminal (ou prompt de comando, noWindows) do seusistemaoperacional.Comoterminalaberto,vamosnosdirigiratéa pasta jscangaceiro/server . Com a certeza de estarmosdentrodapastacorreta,executaremosocomando:
npmstart
Istofarácomqueumservidorrodeesejaacessívelpormeiodoendereço http://localhost:3000 . Acessando esse endereço,automaticamenteapáginaindex.htmlqueestamostrabalhandoao longo do livro será carregada. Se preferir, pode digitarhttp://localhost:3000/index.html.
Apartirdeagora, sópodemosacessarindex.html atravésdo nosso servidor; caso contrário, não poderemos utilizarvários recursos que aprenderemos ao longo do livro quedependemderequisiçõesassíncronasrealizadasdoJavaScriptparaoservidor.
Além de servir nossa aplicação, o servidor disponibiliza trêsendereçosespeciais.Sãoeles:
localhost:3000/negociacoes/semanalocalhost:3000/negociacoes/anteriorlocalhost:3000/negociacoes/retrasada
Ao acessarmos o endereçohttp://localhost:3000/negociacoes/semana , sairemos da
240 12.1SERVIDOREINFRAESTRUTURA
páginaindex.html e será exibida uma estrutura de dados noformatoJSON(JavaScriptObjectNotation):
Figura12.1:Dadosnoformatotexto
No entanto, não queremos exibir o arquivo JSON na telasaindo da página index.html. Nosso objetivo é clicarmos nobotão "Importar Negociações" , buscar os dados usandoJavaScript e inseri-los na tabela de negociações. Comoimplementamos nossomecanismo dedata binding, a tabela serárenderizadaautomaticamente.
Não usaremos jQuery ou nenhum outro atalho que blinde odesenvolvedordosdetalhesdecriaçãoderequisiçõesassíncronas.ComoestamostrilhandoocaminhodeumcangaceiroJavaScript,precisaremoslidardiretamentecomoobjetoXMLHttpRequest,ocentro nervoso do JavaScript para realização de requisiçõesassíncronas.
Precisamosfazerobotão"ImportarNegociações" chamaruma ação em nosso controller. Vejamos o HTML do botão emclient/index.html:
<!--client/index.html--><!--códigoanterioromitido-->
<divclass="text-center"><buttonid="botao-importa"class="btnbtn-primarytext-center
12.2 REQUISIÇÕES AJAX COM O OBJETOXMLHTTPREQUEST
12.2REQUISIÇÕESAJAXCOMOOBJETOXMLHTTPREQUEST 241
type="button">ImportarNegociações
</button><buttonid="botao-apaga"class="btnbtn-primarytext-center"type="button">
Apagar</button></div>
<!--códigoposterioromitido-->
Vamos criar o método importaNegociacoes() emNegociacaoController que, por enquanto, exibirá na tela um
alerta:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoposterioromitido
importaNegociacoes(){
alert('Importandonegociações');}
}
Agora, alteraremos client/app/app.js e associaremos oeventoclickcomachamadadométodoqueacabamosdecriar:
//client/app/app.js
constcontroller=newNegociacaoController();
document.querySelector('.form')
.addEventListener('submit',controller.adiciona.bind(controller));
document.querySelector('#botao-apaga').addEventListener('click',controller.apaga.bind(controller))
;
242 12.2REQUISIÇÕESAJAXCOMOOBJETOXMLHTTPREQUEST
//NOVIDADEAQUI!//associandooeventoàchamadadométodo
document.querySelector('#botao-importa')
.addEventListener('click',controller.importaNegociacoes.bind(controller));
Excelente, mas estamos repetindo várias vezes a instruçãodocument.querySelector . Faremos a mesma coisa que já
fizemosemNegociacaoController,vamoscriaroalias$para a instrução, tornando nosso código menos verboso. Nossoapp.jsficaráassim:
//client/app/app.js
constcontroller=newNegociacaoController();
//criouoalias
const$=document.querySelector.bind(document);
$('.form').addEventListener('submit',controller.adiciona.bind(controller));
$('#botao-apaga').addEventListener('click',controller.apaga.bind(controller))
;
//associandooeventoàchamadadométodo$('#botao-importa').addEventListener('click',controller.importaNegociacoes.bind(controller));
Serecarregarmosapáginanonavegadoredepoisclicarmosem"ImportarNegociações",oalertseráexibido.
12.2REQUISIÇÕESAJAXCOMOOBJETOXMLHTTPREQUEST 243
Figura12.2:Alertnonavegador
Comacertezadequerealizamosaassociaçãodoeventocomométododocontroller,substituiremosoalertporumainstânciadeXMLHttpRequest, umobjeto domundo JavaScript capaz derealizarrequisições:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoposterioromitido
importaNegociacoes(){
constxhr=newXMLHttpRequest();}
}
Agora, vamos solicitar à instânciaque acabamosde criarqueabraumaconexãocomoservidorparanós:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoposterioromitido
importaNegociacoes(){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
244 12.2REQUISIÇÕESAJAXCOMOOBJETOXMLHTTPREQUEST
}}
Ométodoopen() recebeudois parâmetros: oprimeiro é otipoderequisiçãoaserrealizada(GET),osegundoéoendereçodoservidor.SetrabalhássemoscomoutroendereçodoserviçonaWeb,serianecessárioutilizaroendereçocompleto.Comoestamostrabalhandolocalmente,sóusamosnegociacoes/semana.
A requisição ainda não está pronta. Será preciso realizaralgumasconfiguraçõesantesdoenvio.Issoéfeitoatravésdeumafunção atribuída à propriedade onreadystatechange dainstânciadexhr:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoposterioromitido
importaNegociacoes(){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
//realizaremosnossasconfiguraçõesaqui
};
}}
Todarequisiçãocomxhr(ouAJAX,termocomumutilizado)passa por estados - um deles nos dará os dados retornados doservidor.Essesestadossão:
12.2REQUISIÇÕESAJAXCOMOOBJETOXMLHTTPREQUEST 245
0:requisiçãoaindanãoiniciada;
1:conexãocomoservidorestabelecida;
2:requisiçãorecebida;
3:processandorequisição;
4:requisiçãoestáconcluídaearespostaestápronta.
Oestado4éaquelequenosinteressa,porquenosdáacessoàresposta enviada pelo servidor. Precisamos descobrir em qualestado estamos quando o onreadystatechange for chamado.Paraisto,adicionaremosumif:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoposterioromitido
importaNegociacoes(){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
}};
}}
Oestado4indicaquearequisiçãofoiconcluídacomarespostapronta,contudonãopodemosconfiarnele.Podemosreceberumaresposta de erro do servidor que continua sendo uma resposta
246 12.2REQUISIÇÕESAJAXCOMOOBJETOXMLHTTPREQUEST
válida.
AlémdetestarmosoreadyState da requisição, precisamostestar se xhr.status é igual a 200, um código padrão queindicaqueaoperaçãofoirealizadacomsucesso:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoposterioromitido
importaNegociacoes(){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
console.log('Obtendoasnegociaçõesdoservidor.');}
}};
}}
Agora, toda vez que o readyState for 4 e o status fordiferentede200,exibiremosnoconsoleumamensagemindicandoquenão foi possível realizar a requisição. Inclusive, comoúltimainstrução do método, executaremos nossa requisição através dexhr.send():
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
12.2REQUISIÇÕESAJAXCOMOOBJETOXMLHTTPREQUEST 247
//códigoposterioromitido
importaNegociacoes(){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
console.log('Obtendoasnegociaçõesdoservidor.');
}else{
console.log('Nãofoipossívelobterasnegociaçõesdasemana.');
}}
};
xhr.send();//executaarequisiçãoconfigurada}
}
Aquestãoagoraé:comoteremosacessoaosdadosqueforamretornadospeloservidor?
Temosacessoaosdadosretornadospelarequisiçãoatravésdapropriedade xhr.responseText , inclusive é na mesmapropriedadequerecebemosmensagensdeerros:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoposterioromitido
12.3REALIZANDOOPARSEDARESPOSTA
248 12.3REALIZANDOOPARSEDARESPOSTA
importaNegociacoes(){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
console.log('Obtendoasnegociaçõesdoservidor.');
console.log(xhr.responseText);}else{
console.log(xhr.responseText);this._mensagem.texto='Nãofoipossívelobt
erasnegociaçõesdasemana';}
}};
xhr.send();}
}
Em seguida, recarregaremos a página e veremos o que éexibidonoconsole:
Figura12.3:Textoexibidonoconsole
Vemosqueéumtextosendoexibido,enãoumarray.Comooprotocolo HTTP só trafega texto, o JSON é uma representaçãotextualdeumobjetoJavaScript.ComoauxíliodeJSON.parse(),convertemos o JSON de string para objeto, permitindo assimmanipulá-lofacilmente:
12.3REALIZANDOOPARSEDARESPOSTA 249
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
//códigoposterioromitido
importaNegociacoes(){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
console.log('Obtendoasnegociaçõesdoservidor.');
//realizandooparseconsole.log(JSON.parse(xhr.responseText));
}else{console.log(xhr.responseText);
this._mensagem.texto='Nãofoipossívelobterasnegociaçõesdasemana';
}}
};
xhr.send();}
}
No console, vemos a saída deJSON.parse(), um array deobjetosJavaScript:
250 12.3REALIZANDOOPARSEDARESPOSTA
Figura12.4:Arraynoconsole
Pormais que tenhamos convertido o resultado da requisiçãoem um array de objetos, não podemos simplesmente adicionarcada item do array na lista de negociações através dethis._negociacoes.adiciona().OmétodoesperareceberumainstânciadeNegociacao e osdadosque recebemos são apenasobjetos com as propriedades data, quantidade e valor.Precisamos converter cada objeto para uma instância deNegociacaoantesdeadicioná-lo.
Realizaremosaconversãoatravésdafunçãomap() do arraydeobjetosqueconvertemos.Emseguida,comoarrayconvertido,vamos iterar em cada instância de Negociacao pela funçãoforEach,adicionandocadaumaemthis._negociacoes:
12.3REALIZANDOOPARSEDARESPOSTA 251
//client/app/controllers/NegociacaoController.js//códigoposterioromitido
importaNegociacoes(){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
//convertecadaobjetoparaumainstânciadeNegociacao
JSON.parse(xhr.responseText)
.map(objeto=>newNegociacao(objeto.data,objeto.quantidade,objeto.valor))
.forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensage.texto='Negociaçõesimportadascomsucesso!';
}else{console.log(xhr.responseText);
this._mensagem.texto='Nãofoipossívelobterasnegociaçõesdasemana';
}}
};
xhr.send();}
//códigoposterioromitido
Nosso código está quase completo, porque se tentarmosadicionar umanova negociação, receberemos umamensagemdeerro.
252 12.3REALIZANDOOPARSEDARESPOSTA
Agora, se acessarmos o endereçolocalhost:3000/negociacoes/semana,veremosqueadataestáemumformatodestringumpoucodiferente.
Figura12.5:Datanoformatoestranho
Nósestamospassandoobjeto.datanarepresentaçãotextualdiretamentenoconstructor() deNegociacao. Emvez isso,passaremos uma nova instância de Date que receberá em seuconstructor() a datano formato string, pois é uma estrutura
válida:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
//vejaqueagoratemosnewDate(object.data)
JSON.parse(xhr.responseText)
.map(objeto=>newNegociacao(newDate(objeto.data),objeto.quantidade,objeto.valor))
.forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensagem.texto='Negociaçõesimportadascomsucesso!';
//códigoposterioromitido
Figura12.6:Negociaçõesimportadascomsucesso
12.3REALIZANDOOPARSEDARESPOSTA 253
Amensagemseráexibidacorretamenteepodemosconsideraresta primeira parte finalizada. A seguir, faremos uma pequenabrincadeira para vermos o que acontece em um caso de erro.Mudaremos o endereço no importaNegociacoes() paranegociacoes/xsemana:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitidoimportaNegociacoes(){
constxhr=newXMLHttpRequest();
//FORÇANDOUMERRO,UMENDEREÇOQUENÃOEXISTExhr.open('GET','negociacoes/xsemana');
//códigoposterioromitido
Neste caso, quando tentarmos recarregar a página, o usuárioveráaseguintemensagemdeerro:
Figura12.7:Mensagemdeerro
Sónãoesqueçamdevoltarcomoendereçocorretodepoisdoteste!.
Conseguimos realizar requisições assíncronas (Ajax) usandoJavaScript padrão, sem apelarmos para bibliotecas como jQuery.
254 12.3REALIZANDOOPARSEDARESPOSTA
Noentanto,nossocódigopodeficaraindamelhor.
Já somos capazes de importamos negociações, mas o queaconteceria se tivéssemos outros controllers e estes precisassemimportar negociações? Teríamos de repetir toda a lógica deinteração com o servidor. Além disso, o acesso de APIs não éresponsabilidadedeNegociacaoController,apenasacapturadeaçõesdousuárioerealizaraponteentreomodeleaview.
Vamos isolar a responsabilidade de interação como servidorem uma classe que a centralizará. Criaremos o arquivoclient/app/domain/negociacao/NegociacaoService.js:
//client/app/domain/negociacao/NegociacaoService.js
classNegociacaoService{
obterNegociacoesDaSemana(){
}}
Vamos importar o script que acabamos de criar emclient/index.html . Entretanto, como ele será uma
dependência no constructor() de NegociacaoController,deve ser importado antes deapp.js, pois é nesse arquivo quecriamosumainstânciadeNegociacaoController.
A classe terá um único método chamadoobterNegociacaoDaSemana() . Dentro desse método,
moveremos o código que escrevermos no métodoimportaNegociacoes()deNegociacaoController:
12.4SEPARANDORESPONSABILIDADES
12.4SEPARANDORESPONSABILIDADES 255
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script><scriptsrc="app/util/ProxyFactory.js"></script><scriptsrc="app/util/Bind.js"></script><scriptsrc="app/util/ApplicationException.js"></script><scriptsrc="app/ui/converters/DataInvalidaException.js"></script>
<!--importouaqui!--><scriptsrc="app/domain/negociacao/NegociacaoService.js"></script>
<scriptsrc="app/app.js"></script><!--códigoposterioromitido-->
Agora, vamos mover todo o código que já escrevemos dométodo importarNegociacoes de NegociacaoControllerparadentrodométododanossanovaclasse:
//client/app/domain/negociacao/NegociacaoService.js
classNegociacaoService{
obterNegociacoesDaSemana(){
//CÓDIGOMOVIDO!
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
256 12.4SEPARANDORESPONSABILIDADES
if(xhr.status==200){
JSON.parse(xhr.responseText)
.map(objeto=>newNegociacao(newDate(objeto.data),objeto.quantidade,objeto.valor))
.forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensagem.texto='Negociaçõesimportadascomsucesso!';
}else{console.log(xhr.responseText);
this._mensagem.texto='Nãofoipossívelobterasnegociaçõesdasemana';
}}
};
xhr.send();}
}
Como movemos o código, por enquanto o métodoimportaNegociacoes() ficará vazio em
NegociacaoController.
Agora, no constructor() de NegociacaoController ,vamosguardarnapropriedadethis._serviceumainstânciadeNegociacaoService , para que possamos acessar dentro de
qualquermétododaclasse:
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
//códigoanterioromitido
this._service=newNegociacaoService();
12.4SEPARANDORESPONSABILIDADES 257
}
Em seguida, ainda em nosso controller, no métodoimportaNegociacoes() que se encontra vazio, chamaremos o
métodoobterNegociacoesDaSemana()doserviço:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
importaNegociacoes(){
this._service.obterNegociacoesDaSemana();}
//códigoposterioromitido
Do jeito que nosso código se encontra, não conseguiremosimportar as negociações, porque o métodoobterNegociacoesDaSemana() faz referência a elementos deNegociacaoController,eambosdevemserindependentes.
O primeiro passo para resolvermos esse problema éremovermosdométodoobterNegociacoesDaSemana()todasasreferênciasàspropriedadesdeNegociacaoController.Eleficaráassim:
//client/app/domain/negociacao/NegociacaoService.js
classNegociacaoService{
obterNegociacoesDaSemana(){
//CÓDIGOMOVIDO!
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
258 12.4SEPARANDORESPONSABILIDADES
if(xhr.readyState==4){
if(xhr.status==200){
//NÃOFAZMAISOFOREACHJSON
.parse(xhr.responseText).map(objeto=>newNegociacao(newDate(ob
jeto.data),objeto.quantidade,objeto.valor));
//NÃOEXIBEMENSAGEM
}else{console.log(xhr.responseText);
//NÃOEXIBEMENSAGEM}
}};
xhr.send();}
}
Do jeito que deixamos nossa aplicação, não recebemos maismensagensdeerroaoimportamosnegociações,mastambémelasnão são inseridas na tabela. Inclusive, não temos qualquermensagem para o usuário de sucesso ou fracasso. De um lado,temos o método importaNegociacoes() deNegociacaoController que depende da lista de negociações
retornadas do servidor e, do outro, temos a classeNegociacaoService que sabebuscarosdados,masnão sabeo
quefazercomeles.
Para solucionarmos esse impasse, quem chama o métodothis._service.obterNegociacoesDaSemana()precisarápassarumafunçãocomalógicaqueseráexecutadaassimqueométodotrazer os dados do servidor. Para entendermos melhor essa
12.4SEPARANDORESPONSABILIDADES 259
estratégia, primeiro vamos passar a função sem antes alterar ométodoobterNegociacoesDaSemana():
//client/app/controller/NegociacaoController.js//códigoanterioromitido
importaNegociacoes(){
this._service.obterNegociacoesDaSemana((err,negociacoes)=>{
if(err){this._mensagem.texto='Nãofoipossívelobternasne
gociaçõesdasemana';return;
}
negociacoes.forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensagem.texto='Negociaçõesimportadascomsucesso';
});}
//códigoposterioromitido
A funçãoquepassamos éumcallback (uma funçãoque seráchamada posteriormente), e ela segue o padrão Error-First-Callback.Essaabordagemémuitocomumquandoqueremoslidarcomcódigoassíncrono.Casoafunçãoquerecebeuocallbacknãoconsigaexecutarsuaoperação,noprimeiroparâmetrodacallbackrecebemosumerroe,nosegundo,null.
Caso a função que recebeu o callback consiga executar suaoperação,noprimeiroparâmetrodocallbackreceberemosnulle,nosegundo,osdadosresultantesdaoperação(emnossocaso,alista de negociações). A partir da ausência ou não de valor em
260 12.4SEPARANDORESPONSABILIDADES
err,lidamoscomosucessooufracassodaoperação.
Agora, em NegociacaoService , vamos implementar ocallback(cb):
//client/app/domain/negociacao/NegociacaoService.js
classNegociacaoService{
obterNegociacoesDaSemana(cb){
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
constnegociacoes=JSON.parse(xhr.responseText)
.map(objeto=>newNegociacao(newDate(objeto.data),objeto.quantidade,objeto.valor));
//OPERAÇÃOCONCLUÍDA,SEMERROcb(null,negociacoes);
}else{console.log(xhr.responseText);
//ERRONAOPERAÇÃO!cb('Nãofoipossívelobternasnegociaçõesd
asemana',null);}
}};
xhr.send();}
}
Se ocorrer um erro, executaremos o cb exibindo uma
12.4SEPARANDORESPONSABILIDADES 261
mensagemdealtonível,einformandoparaousuárioquenãofoipossível obter as negociações. Ao recarregarmos a página epreenchermos os dados do formulário, veremos a mensagem desucesso.
Figura12.8:Negociaçõesforamimportadascomsucesso
Nosso código funciona como esperado. Mas será que ele sesustenta ao longo do tempo? Há outra maneira de deixarmosnossocódigoaindamelhor?Veremosnopróximocapítulo.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/12
262 12.4SEPARANDORESPONSABILIDADES
CAPÍTULO13
"Cavaloqueamaodonoatérespiradomesmojeito"-GrandeSertão:Veredas
O padrão Error-First-Callback é bastante comum,principalmente no mundo Node.js para lidar com códigoassíncrono.Contudo,sua implementaçãopoluia interfacedeusodemétodosefunções.
Hoje,sequisermosobterumalistadenegociações,chamamoso método obtemNegociacoesDaSemana() deNegociacaoService . Porém, o método só recebe o callback
comoparâmetroparatermosacessoaoresultadoouaoerrodesuachamada.O parâmetro de obtemNegociacoesDaSemana() nãofaz parte do domínio de negociações, é um parâmetro mais deinfraestruturadonossocódigo.
A mesma coisa acontece com o método get() deHttpService.Elerecebeaurldoservidoremquebuscaráas
informações. Já o segundo parâmetro é um callback paracapturarmosoretornodesucessoouerrodaoperação.
LUTANDOATÉOFIM
13LUTANDOATÉOFIM 263
Alémdoquevimos,precisamossempretestarpelaausênciaounão do valor de err para sabermos se a operação foi bem-sucedida.Parapiorar,podemoscairnoCallbackHELL.
OutropontodessaabordageméquepodemoscairnoCallbackHELL, uma estrutura peculiar em nosso código resultante deoperações assíncronas que lembra uma pirâmide deitada.Vejamos um exemplo no qual realizamos quatro requisiçõesget() com HttpService, para exibirmos no final uma lista
consolidadacomosdadosdetodasasrequisições:
//EXEMPLO,NÃOENTRAEMNOSSOCÓDIGO
constservice=newHttpService();
letresultado=[];
service.get('http://wwww.endereco1.com',(err,dados1)=>{resultado=resultado.concat(dados1);service.get('http://wwww.endereco2.com',(err,dados2)=>{resultado=resultado.concat(dados2);service.get('http://wwww.endereco3.com',(err,dados3)=>{resutado=resultado.concat(dados3);service.get('http://wwww.endereco4.com',(err,dados4)=>{
resultado=resultado.concat(dados4);console.log('Listacompleta');console.log(resultado);
});});
});});
Não precisamos meditar muito para percebemos que essaestrutura não é lá uma das melhores para darmos manutenção.Será que existe outro padrão que podemos utilizar para
13.1CALLBACKHELL
264 13.1CALLBACKHELL
escrevermosumcódigomaislegívelefácildemanter?
Há o padrão de projeto Promise criado para lidar com acomplexidade de operações assíncronas.Aliás, a partir da versãoES2015 (ES6), este padrão tornou-se nativo na linguagemJavaScript.
Uma Promise é o resultado futuro de uma ação; e quandodizemos futuro, é porque pode ser fruto de uma operaçãoassíncrona. Por exemplo, quando acessamos um servidor, arequisiçãopodedemorarmilésimosousegundos,tudodependerádeváriosfatoresaolongodarequisição.
Antes de refatorarmos (alterarmos sem mudarcomportamento) por completo o nosso código, vamos alterarapenasNegociacaoController,maispropriamenteseumétodoimportaNegociacoes(),paraqueconsidereumaPromisecomoretorno do métodothis._service.obtemNegociacoesDaSemana().
Alterandonossocódigo:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitidoimportaNegociacoes(){
//apagouocódigoanterior,começandodozeroconstpromise=this._service.obtemNegociacoesDaSemana();}
Duas coisas notáveis se destacam nessa linha de código. Aprimeira é a ausência da passagem do callback para o método
13.2OPADRÃODEPROJETOPROMISE
13.2OPADRÃODEPROJETOPROMISE 265
this._service.obtemNegociacoesDaSemana(). O segundo, oresultado da operação é uma Promise. É através da funçãothen()queinteragimoscomaPromise.
A função then() recebe duas funções (callbacks) comoparâmetros. A primeira função nos dá acesso ao retorno daoperação assíncrona, já o segundo a possíveis erros que possamocorrerduranteaoperação.Alterandonossocódigo:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
importaNegociacoes(){
constpromise=this._service.obtemNegociacoesDaSemana();
promise.then(negociacoes=>{negociacoes.forEach(negociacao=>this._negociacoes.a
diciona(negociacao));this._mensagem.texto='Negociaçõesimportadascomsu
cesso';},err=>this._mensagem.texto=err
);}
//códigoposterioromitido
Podemos simplificar o código anterior eliminando a variávelpromise:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
importaNegociacoes(){
this._service.obtemNegociacoesDaSemana().then(
266 13.2OPADRÃODEPROJETOPROMISE
negociacoes=>{negociacoes.forEach(negociacao=>this._negociaco
es.adiciona(negociacao));this._mensagem.texto='Negociaçõesimportadasco
msucesso';},err=>this._mensagem.texto=err
);}
//códigoposterioromitido
Vamos recapitular!Métodos que retornamumaPromise nãoprecisamreceberumcallback,evitandopoluirsuainterfacedeuso.Afunçãothen()evitaacondiçãoifpara tratarosucessooufracasso,pois trataambosseparadamentenas funçõesdecallbackque recebe. A primeira função é para obtermos o resultado daoperaçãoe,asegunda,errosquepossamacontecerequeimpedemqueaoperaçãosejaconcluída.
Excelente. Porém, nosso código ainda não funciona, pois ométodo obtemNegociacoesDaSemana() deNegociacaoServiceaindanãoretornaumaPromise.Chegoua
horadealterá-lo.
O primeiro passo para convertermos o métodoobtemNegociacoesDaSemana() de NegociacaoServide é
adicionar todo seu código existente dentro do bloco de umaPromise.Primeiro,vejamosaestruturadeumaPromisequandoaestamoscriando:
//EXEMPLO,NÃOENTRANAAPLICAÇÃOAINDA
newPromise((resolve,reject)=>{
13.3CRIANDOPROMISES
13.3CRIANDOPROMISES 267
//lógicadapromise!
});
O constructor() de uma Promise recebe uma funçãopreparada para receber dois parâmetros. O primeiro é umareferência para uma função que deve receber o resultado daoperação assíncronaque encapsula. Já o segundo, possíveis errosquepossamacontecer.
Vamos alterar o método obtemNegociacoesDaSemana()envolvendo todo o código já existente dentro do bloco de umanovaPromise:
//client/domain/negociacao/NegociacaoService.js//códigoanterioromitido
obtemNegociacoesDaSemana(cb){
returnnewPromise((resolve,reject)=>{
/*iníciodoblocodaPromise*/
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
constnegociacoes=JSON.parse(xhr.responseText)
.map(objeto=>newNegociacao(newDate(objeto.data),objeto.quantidade,objeto.valor));
cb(null,negociacoes);}else{
268 13.3CRIANDOPROMISES
cb('Nãofoipossívelobternasnegociações',null);
}}
};
xhr.send();
/*fimdoblocodaPromise*/
});}//códigoposterioromitido
Afunçãopassadaparaoconstructor()dePromisepossuidois parâmetros, resolve e reject . O primeiro é umareferênciaparaumafunçãonaqualpassamosovalordaoperação.Nosegundo,passamosacausadofracasso.
Agora,noslocaisqueusamoscb,utilizaremosresolve()ereject().Ométodoficaráassim:
//client/domain/negociacao/NegociacaoService.js//códigoanterioromitido
//NÃORECEBEMAISOCALLBACKobtemNegociacoesDaSemana(){
returnnewPromise((resolve,reject)=>{
constxhr=newXMLHttpRequest();xhr.open('GET','negociacoes/semana');
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
constnegociacoes=JSON.parse(xhr.responseText).map(objeto=>newNegociacao(newDate(ob
13.3CRIANDOPROMISES 269
jeto.data),objeto.quantidade,objeto.valor));
//CHAMOURESOLVEresolve(negociacoes);
}else{
//CHAMOUREJECTreject('Nãofoipossívelobternasnegociaçõe
s');}
}};
xhr.send();
});}//códigoposterioromitido
Perfeito!Aimportaçãodasnegociaçõescontinuafuncionandocom o novo padrão de organização de código com Promise.Noentanto,podemosdeixarnossocódigoaindamelhor.
O método obtemNegociacoesDaSemana() deNegociacaoService , apesar de funcional, mistura
responsabilidades.Nele,temosocódigoresponsávelemlidarcomo objetoXMLHttpRequest eoque trata a resposta específicadalistadenegociações.
Oproblemada abordagemanterior é aduplicaçãodo códigodeconfiguraçãodoXMLHttpRequest em todososmétodosqueprecisarem realizar este tipo de requisição. Para solucionarmosesse problema, vamos isolar em uma classe de serviço acomplexidade de se lidar com XMLHttpRequest, que também
13.4CRIANDOUMSERVIÇOPARAISOLARACOMPLEXIDADEDOXMLHTTPREQUEST
270 13.4CRIANDOUMSERVIÇOPARAISOLARACOMPLEXIDADEDOXMLHTTPREQUEST
utilizaráopadrãoPromise:
Vamoscriaroarquivoclient/app/util/HttpService.js:
//client/app/util/HttpService.js
classHttpService{
get(url){
returnnewPromise((resolve,reject)=>{
constxhr=newXMLHttpRequest();
xhr.open('GET',url);
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
if(xhr.status==200){
//PASSOUORESULTADOPARARESOLVE//JÁPARSEADO!
resolve(JSON.parse(xhr.responseText));}else{
console.log(xhr.responseText);
//PASSOUOERROPARAREJECTreject(xhr.responseText);
}}
};
xhr.send();
});}}
Agora, não podemos nos esquecer de importar o script em
13.4CRIANDOUMSERVIÇOPARAISOLARACOMPLEXIDADEDOXMLHTTPREQUEST 271
client/index.html . Atenção para a ordem de importação.ComooscriptseráumadependêncianoNegociacaoService,eledeveserimportadoantes:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script><scriptsrc="app/util/ProxyFactory.js"></script><scriptsrc="app/util/Bind.js"></script><scriptsrc="app/util/ApplicationException.js"></script><scriptsrc="app/ui/converters/DataInvalidaException.js"></script>
<scriptsrc="app/domain/negociacao/NegociacaoService.js"></script>
<!--importouaqui!--><scriptsrc="app/util/HttpService.js"></script>
<scriptsrc="app/app.js"></script>
<!--códigoposterioromitido-->
Vejamosumexemplodenossoserviçofuncionando:
//EXEMPLOAPENAS,AINDANÃOENTRANAAPLICAÇÃO
consthttp=newHttpService();http.get('negociacoes/semana').then(dados=>console.log(dados),
err=>console.log(err));
272 13.4CRIANDOUMSERVIÇOPARAISOLARACOMPLEXIDADEDOXMLHTTPREQUEST
Agora, precisamos integrar HttpService comNegociacaoService. Durante essa integração, novos detalhes
sobrePromisessaltarãoaosolhos.
O primeiro passo é adicionarmos uma instância deHttpService como dependência de NegociacaoService ,
atravésdapropriedadethis._http:
//client/app/domain/service/NegociacaoService.js
classNegociacaoService{
constructor(){
//NOVAPROPRIEDADE!this._http=newHttpService();
}
obtemNegociacoesDaSemana(){
//códigoanterioromitido}}
Vamos alterar o método obtemNegociacoesDaSemana() ,paraquefaçausodonossonovoserviço:
//client/app/domain/service/NegociacaoService.js
classNegociacaoService{
constructor(){
this._http=newHttpService();}
obtemNegociacoesDaSemana(){
//ATENÇÃO,RETURNAQUI!
returnthis._http
13.4CRIANDOUMSERVIÇOPARAISOLARACOMPLEXIDADEDOXMLHTTPREQUEST 273
.get('negociacoes/semana').then(
dados=>{
constnegociacoes=dados.map(objeto=>newNegociacao(newDate(objeto.data),objeto.qu
antidade,objeto.valor));
//ATENÇÃOAQUI!returnnegociacoes;
},err=>{
//ATENÇÃOAQUI!thrownewError('Nãofoipossívelobterasnegociaç
ões');}
);}}
Não criamos uma nova Promise, e sim retornamos uma jáexistente, no caso, a Promise criada pelo método get() deHttpService.APromise retornada já sabe lidarcomosucesso
oufracassodarequisição.Alémdessedetalhe,hámaisdoispontosnotáveisdonossocódigo.
Oprimeiroéoreturnnegociacoesdocallbackdesucesso,passado para then(). Por regra, toda Promise que possui umreturn disponibilizará o valor retornadona próxima chamada
encadeada à função then() . Sendo assim, quem chamar ométodo obtemNegociacoesDaSemana() pode ter acesso aoreturnnegociacoes,dessamaneira:
//EXEMPLO,NÃOENTRANAAPLICAÇÃOAINDA
constservice=newNegociacaoService();service.obtemNegociacoesDaSemana()//AQUITEMOSACESSOAORETURNDAPROMISE.then(negociacoes=>console.log(negociacoes));
274 13.4CRIANDOUMSERVIÇOPARAISOLARACOMPLEXIDADEDOXMLHTTPREQUEST
Por fim, ainda nométodo obtemNegociacoesDaSemana(),quandoaPromisecriadapelométodoget()deHttpServicefalha, lançamosumaexceçãoatravésdethrownewError('Nãofoi possível obter as negociações') . Isso é necessárioquandoqueremosdevolverumanovarespostadeerro:
//EXEMPLO,NÃOENTRANAAPLICAÇÃOAINDA
constservice=newNegociacaoService();service.obtemNegociacoesDaSemana().then(negociacoes=>console.log(negociacoes),//AQUIACESSAMOSOERRODOTHROWSerr=>console.log(err);
);
Nosso código continua com o mesmo comportamento, sóalteramos sua organização. Continuamos importando asnegociações da semana clicando no botão "Importar
Negociações"doformulário.
Nossaaplicação,alémde importarasnegociaçõesda semana,deve importar as negociações da semana anterior. Inclusive, háumaAPInoservidordisponibilizadoqueretornaessainformação.No fim, queremos exibir todas asnegociações comoum todonamesmatabela.
Em NegociacaoService , criaremos o métodoobtemNegociacoesDaSemanaAnterior() que terá um código
idêntico ao método que já temos. A única coisa que muda é o
13.5 RESOLVENDO PROMISESSEQUENCIALMENTE
13.5RESOLVENDOPROMISESSEQUENCIALMENTE 275
endereçodaAPIeamensagemdeerro:
//client/app/domain/negociacao/NegociacaoService.js//codigoposterioromitido
obtemNegociacoesDaSemanaAnterior(){
returnthis._http.get('negociacoes/anterior').then(
dados=>{
constnegociacoes=dados.map(objeto=>newNegociacao(newDate(objeto.data),objeto.
quantidade,objeto.valor));
returnnegociacoes;},err=>{
//ATENÇÃOAQUI!thrownewError('Nãofoipossívelobterasnegoci
açõesdasemanaanterior');}
);}
//códigoanterioromitido
Agora, vamos alterar ométodoimportaNegociacoes() deNegociacaoControllerparaimportarasnegociaçõesdasemanaanterior. Vejamos o código primeiro, para em seguidacomentarmosaseurespeito:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
importaNegociacoes(){
constnegociacoes=[];
this._service.obtemNegociacoesDaSemana()
276 13.5RESOLVENDOPROMISESSEQUENCIALMENTE
.then(semana=>{
//USANDOSPREADOPERATORnegociacoes.push(...semana);
//QUANDORETORNAMOSUMAPROMISE,SEURETORNO//ÉACESSÍVELAOENCADEARUMACHAMADAÀTHEN
returnthis._service.obtemNegociacoesDaSemanaAnterior();
}).then(anterior=>{
negociacoes.push(...anterior);negociacoes.forEach(negociacao=>this._negociacoes.a
diciona(negociacao));this._mensagem.texto='Negociaçõesimportadascomsu
cesso';}).catch(err=>this._mensagem.texto=err);
}
Criamosoarraynegociacoesvazioparaquepossareceberasnegociações da semana e da semana anterior. Começamoschamando o métodothis._service.obtemNegociacoesDaSemana() que retorna
uma Promise. Através do método then() , temos acesso aoretornodaoperação-nenhumanovidadeatéagora.
Ainda em then() , realizamos umnegociacoes.push(...anterior) usando o spread operator,
quedesmembrarácadaelementodoarrayanterior, passando-os como parâmetro para a função push(). Ela está preparadaparalidarcomumoumaisparâmetros.
Depois de incluirmos as negociações da semana no arraynegociacoes , executamos a enigmática instrução return
this._service.obtemNegociacoesDaSemanaAnterior() . O
13.5RESOLVENDOPROMISESSEQUENCIALMENTE 277
returnnãoestáláporacaso,todafunçãothen()queretornaumaPromisetornaoresultadodessaPromiseacessívelnapróximachamadaàthen().Éporissoquetemos:
//REVISANDOAPENAS!//códigoanterioromitido
.then(anterior=>{
negociacoes.push(...anterior);negociacoes.forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensagem.texto='Negociaçõesimportadascomsucesso';})//códigoposterioromitido
Nessa chamada, anterior são as negociações das semanasanteriores, resultadodaPromiseque foi retornada.Vejaqueessaestratégia evita o Callback HELL, pois não temos chamadas defunçõesaninhadas.
Por fim, temos uma chamada inédita à função catch() ,presenteemtodaPromise.ElapermitecentralizarotratamentodeerrosdasPromisesemumúnicolugar.IssoevitaquetenhamosdepassarocallbackparatrataroerroemcadaPromise.
Recarregando nossa página e clicando no botão "ImportarNegociações",tudocontinuafuncionando,excelente.MasaindafaltamaisummétodoemNegociacaoService.
Precisamos importar também as negociações da semanaretrasada. Para isso, vamos criar o método
obtemNegociacoesDaSemanaRetrasada() emNegociacaoService . Assim como o método que criamos
anteriormente, ele será idêntico aoobtemNegociacoesDaSemana(),apenasoendereçodaAPIesua
278 13.5RESOLVENDOPROMISESSEQUENCIALMENTE
mensagemdeerroserãodiferentes:
//client/app/domain/negociacao/NegociacaoService.js//códigoanterioromitido
obtemNegociacoesDaSemanaRetrasada(){
returnthis._http.get('negociacoes/retrasada').then(
dados=>{
constnegociacoes=dados.map(objeto=>newNegociacao(newDate(objeto.data),objeto.
quantidade,objeto.valor));
returnnegociacoes;},err=>{thrownewError('Nãofoipossívelobterasnegoci
açõesdasemanaretrasada');}
);}
Agora, repetiremos o que já fizemos anteriormente eincorporaremos a chamada do nosso método emimportaNegociacoes()deNegociacaoController:
//client/app/domain/negociacao/NegociacaoService.js//códigoanterioromitido
importaNegociacoes(){
constnegociacoes=[];
this._service.obtemNegociacoesDaSemana().then(semana=>{
negociacoes.push(...semana);
returnthis._service.obtemNegociacoesDaSemanaAnterior();
13.5RESOLVENDOPROMISESSEQUENCIALMENTE 279
}).then(anterior=>{
negociacoes.push(...anterior);returnthis._service.obtemNegociacoesDaSemanaRetrasad
a()}).then(retrasada=>{
negociacoes.push(...retrasada);negociacoes.forEach(negociacao=>this._negociacoes.a
diciona(negociacao));this._mensagem.texto='Negociaçõesimportadascomsu
cesso';}).catch(err=>this._mensagem.texto=err);
}
//códigoposterioromitido
NossométodoresolvenossasPromisessequencialmente,istoé,ele só tentará resolver a segunda caso a primeira tenha sidoresolvida, e só resolverá a terceira caso a segunda tenha sidoresolvida. Todavia, em alguns cenários, pode ser interessanteresolvermos todas as Promises em paralelo, principalmentequando a ordem de resolução não é importante. É isso queveremosaseguir.
PodemosresolvernossasPromisesparelamentecomauxíliodePromise.all() , que recebe um array de Promises como
parâmetro. Vamos recomeçar do zero o métodoimportaNegociacoes()deNegociacaoController.
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
13.6 RESOLVENDO PROMISESPARALELAMENTE
280 13.6RESOLVENDOPROMISESPARALELAMENTE
importaNegociacoes(){
//RECEBEUMARRAYDEPROMISES
Promise.all([this._service.obtemNegociacoesDaSemana(),this._service.obtemNegociacoesDaSemanaAnterior(),this._service.obtemNegociacoesDaSemanaRetrasada()
]).then(periodo=>console.log(periodo)).catch(err=>this._mensagem.texto=err);
}
A função Promise.all() resolverá todas as Promises emparalelo, o que pode reduzir drasticamente o tempo gasto comessasoperações.Quandotodastiveremsidoresolvidas,achamadaàfunçãothen()nosdevolveráumarraydearrays, istoé, cadaposição do array terá a lista de negociações retornada por cadaPromise, na mesma ordem em que foram passadas paraPromise.all(). Podemos verificar essa informação olhando a
saídadeconsole.log(periodo).
Nãopodemossimplesmenteiterarnessearrayeadicionarcadanegociação em this._negociacoes, pois cada item do array,como vimos, é um array. Precisamos tornar periodo em umarray de única dimensão, ou seja, queremos achatar (flaten) estearray.
Podemos conseguir este resultado facilmente com auxílio dafunçãoreduce(),quejáutilizamosnoscapítulosanteriores:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
importaNegociacoes(){
Promise.all([this._service.obtemNegociacoesDaSemana(),
13.6RESOLVENDOPROMISESPARALELAMENTE 281
this._service.obtemNegociacoesDaSemanaAnterior(),this._service.obtemNegociacoesDaSemanaRetrasada()
]).then(periodo=>{
//periodoaindaéumarraycom3elementosquesãoarray
periodo=periodo.reduce((novoArray,item)=>novoArray.concat(item),[]);
//umarraycomnomeelementosconsole.log(periodo);
}).catch(err=>this._mensagem.texto=err);
}
Comestaalteração,periodopassaaserumarraycomnoveelementosemvezdeumcomtrêselementosdotipoarray.Agora,podemos adicionar cada item do novo array em nossa lista denegociações:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
importaNegociacoes(){
Promise.all([this._service.obtemNegociacoesDaSemana(),this._service.obtemNegociacoesDaSemanaAnterior(),this._service.obtemNegociacoesDaSemanaRetrasada()
]).then(periodo=>{
periodo.reduce((novoArray,item)=>novoArray.concat(item),
[]).forEach(negociacao=>this._negociacoes.adiciona(neg
ociacao));
this._mensagem.texto='Negociaçõesimportadascomsucesso';
})
282 13.6RESOLVENDOPROMISESPARALELAMENTE
.catch(err=>this._mensagem.texto=err);}
//códigoposterioromitido
Maisumcódigomelhorado,maseleaindafereumprincípio.Alógica de buscar as negociações doperíodonãodeveria estar emNegociacaoController,masemNegociacaoService.VamoscriarnestaclasseométodoobtemNegociacoesDoPeriodo():
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
obtemNegociacoesDoPeriodo(){
//ACESSAAOSPRÓPRIOSMÉTODOSATRAVÉSDETHIS
returnPromise.all([this.obtemNegociacoesDaSemana(),this.obtemNegociacoesDaSemanaAnterior(),this.obtemNegociacoesDaSemanaRetrasada()
]).then(periodo=>{
//NÃOFAZMAISOFOREACHreturnperiodo
.reduce((novoArray,item)=>novoArray.concat(item),[]);
}).catch(err=>{
console.log(err);thrownewError('Nãofoipossívelobterasnegociaçõesdo
período')});
}
Por fim, em NegociacaoController , vamos chamar ométodoqueacabamosdecriar:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
13.6RESOLVENDOPROMISESPARALELAMENTE 283
importaNegociacoes(){
this._service.obtemNegociacoesDoPeriodo().then(negociacoes=>{
negociacoes.forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensagem.texto='Negociaçõesdoperíodoimportadascomsucesso';
}).catch(err=>this._mensagem.texto=err);
}
//códigoposterioromitido
Excelente.VimosquePromise.all() recebeumarraycomtodasasPromisesquedesejamosresolverparalelamente.Aposiçãodas Promises no array é importante, pois ao serem resolvidas,teremos acesso a um array com o resultado de cada uma delas,atravésdethen,seguindoamesmaordemderesoluçãodoarray.
AsAPIsque acessamos são "boazinhas", cadauma retornaasdatas emordemdecrescente.Contudo, não podemos confiar emumaAPIquenão foi criadapornós, razãopela qual precisamosgarantiremnossocódigoaordenaçãoquedesejamos.
O método obtemNegociacoesDoPeriodo deNegociacaoServicedevolveumaPromiseque,aoserresolvida,devolve um array com um período de negociações. Podemosordenar esse array damaneira que desejarmos através da funçãosort(),presenteemtodoarray.
13.7ORDENANDOOPERÍODO
284 13.7ORDENANDOOPERÍODO
Afunçãosort() recebeuma estratégiade comparaçãopormeio de uma função. Esta função é chamada diversas vezes,comparando cada elemento com o próximo. A lógica decomparação deve retornar 0 se os elementos são iguais; 1 ousuperior, seoprimeiroelementoémaiorqueosegundo;e -1ouinferiorseoprimeiroelementoémenordoqueosegundo.
Alterandoométodo:
//client/app/domain/negociacao/NegociacaoService.js//códigoanterioromitido
obtemNegociacoesDoPeriodo(){
returnPromise.all([this.obtemNegociacoesDaSemana(),this.obtemNegociacoesDaSemanaAnterior(),this.obtemNegociacoesDaSemanaRetrasada()
]).then(periodo=>{
returnperiodo.reduce((novoArray,item)=>novoArray.concat(item),
[]).sort((a,b)=>a.data.getTime()-b.data.getTime());
}).catch(err=>{
console.log(err);thrownewError('Nãofoipossívelobterasnegociaçõesdo
período')});
}
//códigoposterioromitido
Oresultadodeb.data.getTime() retornaumnúmeroquerepresenta uma data, algo bem oportuno para nossa lógica, poispodemossubtrairoresultadodoprimeiroelementopelosegundo.Seforemiguais,oresultadoserásempre0;seoprimeiroformaior
13.7ORDENANDOOPERÍODO 285
queosegundo, teremosumnúmeropositivo;e seoprimeiro formenorqueosegundo, teremosumnúmeronegativo.Atendemosplenamenteaexigênciadométodosort().
No entanto, queremos uma ordem descendente, por issoprecisamossubtrairb.data.getTime()dea.data.getTime():
//client/app/domain/negociacao/NegociacaoService.js//códigoanterioromitido
obtemNegociacoesDoPeriodo(){
returnPromise.all([this.obtemNegociacoesDaSemana(),this.obtemNegociacoesDaSemanaAnterior(),this.obtemNegociacoesDaSemanaRetrasada()
]).then(periodo=>{
returnperiodo.reduce((novoArray,item)=>novoArray.concat(item),
[]).sort((a,b)=>b.data.getTime()-a.data.getTime());
}).catch(err=>{
console.log(err);thrownewError('Nãofoipossívelobterasnegociaçõesdo
período')});
}
//códigoposterioromitido
Agora temos a garantia de que o período importado semprenosretornaráumalistaordenadacomordenaçãodescendentepeladata das negociações. Daí, vem aquela velha pergunta: será quepodemossimplificaraindamaisnossocódigo?
Vejamos o código que escrevemos por último, aquele que
286 13.7ORDENANDOOPERÍODO
ordenaoperíodo:
//ANALISANDOOCÓDIGO.then(periodo=>{
returnperiodo.reduce((novoArray,item)=>novoArray.concat(item),[]).sort((a,b)=>b.data.getTime()-a.data.getTime());
})
Perceba que temos uma única instrução dentro do bloco dothen que retorna o novo array ordenado. Podemos remover o
blocodaarrowfunction,inclusiveainstruçãoreturn,poisarrowfunctions possuem uma única instrução sem bloco, mas jáassumemumreturnimplícito.
//ANALISANDOOCÓDIGO.then(periodo=>periodo.reduce((novoArray,item)=>novoArray.concat(item),[]).sort((a,b)=>b.data.getTime()-a.data.getTime()))
Aliás, se olharmos os outros métodos da classeNegociacaoService,todospossuemapenasumaúnicainstruçãonoblocodothen().
Com todas as simplificações, nossa classeNegociacaoServicenofinalestaráassim:
//client/app/domain/negociacao/NegociacaoService.js
classNegociacaoService{
constructor(){
this._http=newHttpService();}
obtemNegociacoesDaSemana(){
13.7ORDENANDOOPERÍODO 287
returnthis._http.get('negociacoes/semana').then(
dados=>dados.map(objeto=>newNegociacao(newDate(objeto.data),obj
eto.quantidade,objeto.valor)),err=>{
thrownewError('Nãofoipossívelobterasnegociaçõesdasemana');
});
}
obtemNegociacoesDaSemanaAnterior(){
returnthis._http.get('negociacoes/anterior').then(
dados=>dados.map(objeto=>newNegociacao(newDate(objeto.data),objeto.
quantidade,objeto.valor)),err=>{
thrownewError('Nãofoipossívelobterasnegociaçõesdasemanaanterior');
});
}
obtemNegociacoesDaSemanaRetrasada(){
returnthis._http.get('negociacoes/retrasada').then(
dados=>dados.map(objeto=>newNegociacao(newDate(objeto.data),objeto.
quantidade,objeto.valor)),err=>{thrownewError('Nãofoipossívelobterasne
gociaçõesdasemanaretrasada');
288 13.7ORDENANDOOPERÍODO
});
}
obtemNegociacoesDoPeriodo(){
returnPromise.all([this.obtemNegociacoesDaSemana(),this.obtemNegociacoesDaSemanaAnterior(),this.obtemNegociacoesDaSemanaRetrasada()
]).then(periodo=>periodo
.reduce((novoArray,item)=>novoArray.concat(item),[])
.sort((a,b)=>b.data.getTime()-a.data.getTime())).catch(err=>{
console.log(err);thrownewError('Nãofoipossívelobterasnegociaçõe
sdoperíodo')});
}}
Excelente!Todavia,sevocêssãobonsobservadores,devemterreparado que podemos importar várias vezes as mesmasnegociações.Que tal evitarmos que isso aconteça?A ideia é nãoimportarnovamentenegociaçõesdeumamesmodia,mêseano.
Precisamos adotar algum critério para que possamosdeterminarseanegociaçãoqueestamosimportandojáhaviasidoimportada ou não. Em outras palavras, precisamos de algumalógicaque indique seumanegociaçãoé igualaoutra.Seráqueooperador== pode nos ajudar?Vamos realizar alguns testes no
13.8 IMPEDINDO IMPORTAÇÕESDUPLICADAS
13.8IMPEDINDOIMPORTAÇÕESDUPLICADAS 289
Consoledonavegador.
n1=newNegociacao(newDate(),1,100);n2=newNegociacao(newDate(),1,100);n1==n2//oretornoéfalse!
Não podemos simplesmente usar o operador == paracomparar objetos que não sejam dos tipos literais string ,number,boolean,null eundefined, pois ele testa se as
variáveisapontamparaomesmoendereçonamemória.
Sequisermosquen1sejaigualan2,podemosfazerissonoconsole:
n1=n2n1==n2//oretornoétrue
No exemplo anterior, a variáveln1 passa a apontar para omesmoobjeton2.Comon1en2agorareferenciamomesmoobjeto, o operador == retornará true na comparação entreambos.
Que tal realizarmos uma comparação entre as datas dasnegociações? Cairíamos no mesmo problema, pois estaríamoscomparandodoisobjetosdotipoDate,eooperador==apenastestaria se as propriedades do objeto apontam para o mesmoobjetonamemória.Hásaída?Claroquehá.
Vamosrealizaracomparaçãoentredatasdessamaneira:
n1.data.getDate()==n2.data.getDate()&&n1.data.getMonth()==n2.data.getMonth()&&n1.data.getFullYear()==n2.data.getFullYear()//resultadoétrue!
Os métodos Date que usamos devolvem números e, porpadrão, o operador== comeste tipo literal comparao valor, e
290 13.8IMPEDINDOIMPORTAÇÕESDUPLICADAS
não a referência na memória. Excelente, conseguimos saber seduasnegociaçõessãoiguais.Será?
Precisamosmelhorarocritériodecomparação,poispodemosternegociaçõesdeumamesmadataemnossalistadenegociações.Que tal considerarmos tambémna comparaçãoaquantidade eovalor?
Testandonoconsoledonavegador:
n1.data.getDate()==n2.data.getDate()&&n1.data.getMonth()==n2.data.getMonth()&&n1.data.getFullYear()==n2.data.getFullYear()&&n1.quantidade==n2.quantidade&&n1.valor==n2.valor//retornatrue!
Não é nada orientado a objetos termos de repetir essa lógicaem todos os lugares em que precisamos testar a igualdade entrenegociações, uma vez que ela faz parte do domínio de umanegociação. Vamos alterar a classe Negociacao e adicionar ométodoequals()queestarápresenteemtodasasinstânciasdaclasse:
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
//códigoanterioromitido
equals(negociacao){
returnthis.data.getDate()==negociacao.data.getDate()&&this.data.getMonth()==negociacao.data.getMonth()&&this.data.getFullYear()==negociacao.data.get
FullYear()&&this.quantidade==negociacao.quantidade
&&this.valor==negociacao.valor;}
}
13.8IMPEDINDOIMPORTAÇÕESDUPLICADAS 291
Ométodo compara a própria instância que está chamandoométodoequals()comoutranegociaçãorecebidanométodo.
Recarregando nossa página e testando nosso método noconsoledonavegador,teremos:
n1=newNegociacao(newDate(),1,100);n2=newNegociacao(newDate(),1,100);n1.equals(n2);//retornatrue
Perfeito,maspodemosenxugarbastanteométodoequals()queacabamosdecriar.
Quandoo critériode comparação entreobjetos ébaseadonovalor de todas as propriedades que possui, podemos usar oseguintetruquecomJSON.stringify():
//client/app/domain/negociacao/Negociacao.js
classNegociacao{
//códigoanterioromitido
equals(negociacao){
returnJSON.stringify(this)==JSON.stringify(negociacao);
}}
A função JSON.stringify() converte um objeto em umarepresentaçãotextual,ouseja,umastring.Convertendososobjetosenvolvidos na comparação para string, podemos usartranquilamenteooperador==paratestaraigualdadeentreeles.Bem mais enxuto, não? Porém, essa solução só funciona se ocritériodecomparaçãoforosvaloresdetodasaspropriedadesqueo objetopossui.
292 13.8IMPEDINDOIMPORTAÇÕESDUPLICADAS
Agora, precisamos alterar a lógica do métodoimportaNegociacoes de NegociacaoController para que
importeapenasnovasnegociações.
Precisamosfiltraralistadenegociaçõesretornadasdoservidor,considerando apenas aquelas que ainda não constam no arrayretornadoporthis._negociacoes.paraArray().Aboanotíciaé que arrays já possuem uma função com essa finalidade, afilter().
Vejamosisoladamentenoconsolecomoelafunciona:
lista=['A','B','C','D'];listaFiltrada=lista.filter(letra=>letra=='B'||letra=='C);
Afunçãofilter()recebeumafunçãocomaestratégiaqueserá aplicada em cada elemento da lista. Se a função retornartrue para um elemento, ele será adicionado na nova lista
retornadaporfilter(). Se retornarfalse, o elemento seráignorado.
No exemplo anterior, listaFiltrada conterá apenas asletras"B"e"C".
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
importaNegociacoes(){
this._service.obtemNegociacoesDoPeriodo().then(negociacoes=>{
13.9ASFUNÇÕESFILTER()ESOME()
13.9ASFUNÇÕESFILTER()ESOME() 293
negociacoes.filter(novaNegociacao=>console.log('lógicadofilt
roaqui')).forEach(negociacao=>this._negociacoes.adiciona(neg
ociacao));
this._mensagem.texto='Negociaçõesdoperíodoimportadascomsucesso';
}).catch(err=>this._mensagem.texto=err);
}//códigoposterioromitido
Analisandoocódigo,vemosqueoencadeamentodachamadada função forEach() trabalhará com a lista filtrada. Porém,aindanãotemosqualquerlógicadefiltroaplicada.
Para cada item processado pelo nosso filtro, precisamosverificarseelejáexisteemthis_negociacoes.paraArray().Senãoexistir,oitemfarápartedanovalista.Podemosimplementaressa lógica de diversas formas,mas esse é omomento oportunoparaaprendemosautilizarafunçãosome().
Afunçãosome()iteraemcadaelementodeumalista.Assimque encontrar algum (some) elemento de acordo com algumalógicaqueretornetrue,pararáimediatamentedeiterarnoarrayretornandotrue. Se nenhum elemento atender ao critério decomparação,oarrayterásidopercorridoatéofimeoretornodesome()seráfalse.
Que tal combinarmos some() com filter() para nãoimportarmos negociações duplicadas? Primeiro, vejamos comonossocódigoficará,paraemseguidatecermoscomentáriossobreele:
//client/app/controllers/NegociacaoController.js
294 13.9ASFUNÇÕESFILTER()ESOME()
//códigoanterioromitido
importaNegociacoes(){
this._service.obtemNegociacoesDoPeriodo().then(negociacoes=>{
negociacoes.filter(novaNegociacao=>
!this._negociacoes.paraArray().some(negociacaoExistente=>
novaNegociacao.equals(negociacaoExistente)))
.forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensagem.texto='Negociaçõesdoperíodoimportadascomsucesso';
}).catch(err=>this._mensagem.texto=err);
}}
//códigoposterioromitido
Alógicadafunçãofilter()dalistaquedesejamosimportaré o retorno da função some() aplicada na lista já existente.Lembre-se de que some() pode retornar true ou false,valores que satisfazem a funçãofilter(). No caso, queremosfiltrarnossa listadenegociaçõesa importarconsiderandoapenasasquenãofazempartedalistadenegociaçõesjáimportadas.
Através de some() aplicada na lista de negociações jáimportadas, verificamos se a negociação que desejamos importarnão existe na lista.Mas como isso, a função retornariafalse,então precisamos inverter esse valor para true , pois apenasretornandotrueafunçãofilter()consideraráoelementona
13.9ASFUNÇÕESFILTER()ESOME() 295
nova lista.Vejaqueutilizamosa funçãoequals() que criamosemnossomodelodeNegociacao.
No final danossa cadeiade funções,forEach() adicionaráapenasasnegociaçõesqueaindanãoforamimportadas.
Tudomuito bonito,mas o que acontece se fecharmos nossonavegadordepoisde incluirmosduasoumaisnegociações?Comcerteza perderemos todo nosso trabalho, porque não hápersistência de dados em nossa aplicação. Aliás, este será nossopróximoassunto.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/13
296 13.9ASFUNÇÕESFILTER()ESOME()
Exausto,elenãoaguentouedesabou.Duranteseuesvaecimento,teve uma nova visão. Nela, Lampião tirava o chapéu, sentava aolado da fogueira e apontava para a direção oposta. Depois deacordar,foientãoqueelesedeucontadequeobutimdocaminhodocangaceiroéoprópriocaminho.Então,eledeuumúltimogoleemsuamoringaecomeçouatrilharocaminhoparalugarnenhume, aomesmo tempo, todo lugar, cada vezmais preparado para assurpresasdavida.
Parte3-Arevelação
CAPÍTULO14
"Rir,antesdahora,engasga."-GrandeSertão:Veredas
Nossa aplicação é funcional. No entanto, se fecharmos eabrirmosnovamente o navegador, perdemos todos os dados quehavíamos cadastrado. Uma solução é enviarmos os dados daaplicação para umaAPI de algum servidor que os armazene emum banco de dados, para que mais tarde possamos reavê-losconsumindooutraAPI.
Contudo,nãousaremosessaabordagem.Gravaremososdadosemumbancodedadosquetodonavegadorpossui,oIndexedDB.
O IndexedDBéumbancodedados transacional especificadopela W3C (https://www.w3.org/), presente nos mais diversosnavegadores do mercado. Diferente de bancos relacionais que
AALGIBEIRAESTÁFURADA!
14.1 INDEXEDDB, O BANCO DE DADOS DONAVEGADOR
298 14AALGIBEIRAESTÁFURADA!
utilizam SQL, IndexedDB não se pauta em esquemas (estruturaspré-definidas de tabelas, colunas e seus tipos), mas em objetosJavaScript que podem ser persistidos e buscados com menosburocracia. Este tipo de banco faz muito pouco para validar osdados persistidos, a validação é de responsabilidade da aplicaçãoqueoutiliza.
Nãopodemossair integrandooIndexedDBemnossoprojetoantesdesabermoscomoelefunciona.Éporissoquecriaremosoarquivo db.html, para que possamos praticar primeiro, e sódepoisointegraremosemnossaaplicação.Vamoscriaroarquivoclient/db htmlcomaseguinteestrutura
<!--client/dn.html-->
<!DOCTYPEhtml><html><head>
<metacharset="UTF-8"><title>AprendendoIndexedDB</title></head>
<body><script>
/*nossostestesentraramaqui!*/
</script></body></html>
OprimeiropassoérequisitarmosumaaberturadeconexãoaoIndexedDB.ÉpeloobjetoglobalmentedisponívelindexedDBeseumétodoopen()querealizamosessepasso:
<!DOCTYPEhtml><html><head><metacharset="UTF-8">
14.1INDEXEDDB,OBANCODEDADOSDONAVEGADOR 299
</head><body><script>
constopenRequest=indexedDB.open("jscangaceiro",1);
</script></body>
Ométodoopen() recebe dois parâmetros. O primeiro é onomedobancoemquedesejamosnosconectar,eosegundo,umnúmerodefinidopornósqueindicaaversãodobanco-nocaso,ovalor1 foi escolhido.Por enquanto, esse segundoparâmetronãoterá grande importância para nós, por isso o deixaremostemporariamentedeladonasexplicações.
O retorno do método open() é uma instância deIDBOpenDBRequest, ou seja, uma requisição de abertura dobanco. Toda vez que realizamos uma requisição de abertura,precisaremoslidarcomumatríadedeeventos,sãoeles:
onupgradeneeded
onsuccess
onerror
300 14.1INDEXEDDB,OBANCODEDADOSDONAVEGADOR
ATENÇÃO
Só recarregue a página no navegador quando esseprocedimentoforaqui indicado.Casoorecarregamentosejafeito antes de pontos-chaves do nosso código, você teráproblemas de atualização de banco que só serão resolvidosmaistarde.
<!--client/dn.html--><! códigoanterioromitido
<body><script>
constopenRequest=indexedDB.open('jscangaceiro',1);
//lidandocomatríadedeeventos!
openRequest.onupgradeneeded=function(e){
console.log('Criaoualteraumbancojáexistente');};
openRequest.onsuccess=function(e){
console.log('Conexãoobtidacomsucesso!');};
openRequest.onerror=function(e){
console.log(e.target.error);};
</script></body>
<!--códigoposterioromitido-->
14.1INDEXEDDB,OBANCODEDADOSDONAVEGADOR 301
Atribuímos a openRequest.onupgradeneeded uma funçãoque será chamada somente se o banco que estivermos nosconectando não existir, ou quando estivermos atualizando umbanco já existente, assunto que abordaremos em breve. Emseguida, em openRequest.onsuccess(), atribuímos a funçãoque será chamada quando uma conexão for obtida com sucesso.Porfim,emopenRequest.onerror,atribuímosafunçãoqueseráchamada caso algum erro ocorra no processo de abertura daconexão.Seráatravésdee.target.errorque teremosacessoàcausadoerro.
Antesdecontinuarmos quetalusarmosarrowfunctionsparatermos um código menos verboso? Não há problema nenhumutilizarestetipodefunçãoparaessestrêseventos:
<!--client/dn.html--><!--códigoanterioromitido-->
<body><script>
constopenRequest=indexedDB.open('jscangaceiro',1);
openRequest.onupgradeneeded=e=>{
console.log('Criaoualteraumbancojáexistente');};
openRequest.onsuccess=e=>{
console.log('Conexãoobtidacomsucesso!');};
openRequest.onerror=e=>{
console.log(e.target.error);};
</script>
302 14.1INDEXEDDB,OBANCODEDADOSDONAVEGADOR
</body>
<!--códigoposterioromitido-->
Recarregue sua página no navegador para que possamosverificar no console se as mensagens de onupgradeneeed eonsuccess são exibidas. Lembre-se de que a primeira será
exibida porque estamos criando o banco pela primeira vez e asegunda,porqueteremosobtidoumaconexão.
Naprimeiravez,veremosasseguintesmensagens:
Figura14.1:Duasmensagensnoconsole
Serecarregarmosmaisumavezapágina,veremosapenasumadasmensagens.Fazsentido,poisobancojáfoicriado:
Figura14.2:Umamensagemnoconsole
14.1INDEXEDDB,OBANCODEDADOSDONAVEGADOR 303
Pormaisqueasmensagensanteriores indiquemqueobancofoicriado,umcangaceirosóacreditarávendo.Podemosvisualizaro banco criado no Chrome através de seu console, na abaApplication. No canto esquerdo, na seção Storage, temosIndexedDB.Clicandonesteitem,eleexpandiráexibindoobancojscangaceiro:
Figura14.3:VisualizandoobanconoChrome
Agoraquejáentendemosopapeldatríadedeeventos,chegouahoradeinteragirmoscomobancoqueacabamosdecriar.
Conexõessãoobtidasatravésdoeventoonsuccess.Oúnicoparâmetrorecebidopelafunçãoassociadaaoeventonosdáacessoa uma instância de IDBDatabase, aquela que representa nossa
14.2ACONEXÃOCOMOBANCO
304 14.2ACONEXÃOCOMOBANCO
conexão.Acessandoaconexãoatravésdee.target.result:
<!--client/dn.html--><!--códigoanterioromitido-->
<body><script>
constopenRequest=indexedDB.open('jscangaceiro',1);
openRequest.onupgradeneeded=e=>{
console.log('Criandoouatualizandoobanco');};
openRequest.onsuccess=e=>{
console.log('Conexãorealizadacomsucesso');
//e.target.resultéumainstânciadeIDBDatabase
e.target.result;};
openRequest.onerror=e=>{
console.log(e.target.error);};
</script></body><!--códigoposterioromitido-->
Já sabemos onde se encontra a conexão, mas precisamosarmazená-la em alguma variável para podermos utilizá-la emdiferentes ocasiões em nosso programa. Para isso, vamos criar avariávelconnection antesdo códigoque já temos.Estandonoescopoglobal,elapodeguardaroresultadodee.target.result:
<!--client/dn.html--><!--códigoanterioromitido-->
14.2ACONEXÃOCOMOBANCO 305
<body><script>
//PRECISASERLETPARAACEITARUMANOVAATRIBUIÇÃOletconnection=null;
constopenRequest=indexedDB.open('jscangaceiro',1);
openRequest.onupgradeneeded=e=>{
console.log('Criandoouatualizandoobanco');};
openRequest.onsuccess=e=>{
console.log('Conexãorealizadacomsucesso');
//avariávelguardaumareferênciaparaaconexão
connection=e.target.result;};
openRequest.onerror=e=>{
console.log(e.target.error);};
</script></body><!--códigoposterioromitido-->
Excelente,temosumaconexão.Porém,antesdepensarmoseminteragir comobanco, precisamos criarumanovaObject Store.EntendaumastorecomoalgoanálogoàstabelasdomundoSQL.
Criamos novas stores através de uma conexão com o bancoduranteoeventoonupgradeneeded.Porém,quandoesteeventoédisparado,ovalordavariávelconnectionaindaénull,pois
14.3NOSSAPRIMEIRASTORE
306 14.3NOSSAPRIMEIRASTORE
elasóreceberáseuvalorduranteoeventoonsuccess.Eagora?
A boa notícia é que podemos obter uma conexão no eventoonupgradeneeded da mesma maneira que obtemos em
onsuccess:
<!--client/dn.html--><!--códigoanterioromitido-->
<body><script>
letconnection=null;
constopenRequest=indexedDB.open('jscangaceiro',1);
openRequest.onupgradeneeded=e=>{
console.log('Criaoualteraumbancojáexistente');
//obtendoaconexão!connection=e.target.result;};
openRequest.onsuccess=e=>{
console.log('Conexãorealizadacomsucesso');connection=e.target.result;};
openRequest.onerror=e=>{
console.log(e.target.error);};
</script></body><!--códigoposterioromitido-->
Excelente! Da maneira que estruturamos nosso código, aconexão pode vir tanto de onupgradeneeded quanto deonsuccess. Já temos amoringa e a cartucheira emmãos para
14.3NOSSAPRIMEIRASTORE 307
criarmosnossostore.
Usaremos a seguinte lógica de criação da store. Primeiro,verificaremos se a store que estamos tentando criar já existe; seexistir,vamosapagá-laantesdecriá-la.Arazãodessaabordageméque o evento onupgradeneed também pode ser disparadoquandoestivermosatualizandonossobanco.
Para sabermos se uma store existe ou não durante umaatualização, usamos o métodoconnection.objectStoreNames.contains() passandoonome
dastore.Paraapagarmosumastoreecriarmosumanova,usamosconnection.deleteObjectStore() e
connection.createObjectStore(),respectivamente:
<!--client/dn.html--><!--códigoanterioromitido--><body>
<script>
letconnection=null;
constopenRequest=indexedDB.open('jscangaceiro',1);
openRequest.onupgradeneeded=e=>{
console.log('Criaoualteraumbancojáexistente');connection=e.target.result;
//podeserqueoeventoestejasendodisparadoduranteumaatualização,
//nessecaso,verificamosseastoreexiste,seexistir
//apagamosastoreatualantesdecriarmosumanova
if(connection.objectStoreNames.contains('negociacoes')){
connection.deleteObjectStore('negociacoes');}
308 14.3NOSSAPRIMEIRASTORE
connection.createObjectStore('negociacoes',{autoIncrement:true});
};
//códigoposterioromitido
</script></body><!--códigoposterioromitido-->
Um ponto a destacar é o segundo parâmetro deconnection.createObjectStore().Nele, passamosumobjeto
JavaScript com a propriedade autoIncrement: true . Ela éimportante,poiscriaráinternamenteumidentificadorúnicoparaosobjetosquesalvarmosnastore.
Ainda sobre a lógica de atualização, ela poderia ser maisrebuscada,masapagarastoreantigaecriarumanovaésuficienteparanossaaplicação.Contudo,enfrentaremosumproblema.
Recarregandonossapágina, apenaso eventoonsuccess échamado. O evento onupgradeneeded só é chamado quandoestamos criandoumnovo banco ou atualizandoum já existente.Como já temos um banco criado, precisamos indicar para oIndexedDB que desejamos realizar uma atualização. Comoresolver?
Para que o evento onupgradeneeded seja disparadonovamente, a versão do banco que estamos criando deve sersuperioràversãodobancoexistentenonavegador.Sendoassim,basta passarmos como segundo parâmetro para a funçãoindexedDB.open()qualquervalorquetemoscertezadequeseja
14.4ATUALIZAÇÃODOBANCO
14.4ATUALIZAÇÃODOBANCO 309
superioràversãodobancopresentenonavegador.Emnossocaso,o valor2ésuficiente:
<!--client/db.html--><!--códigoanterioromitido-->
<body><script>letconnection=null;
//INCREMENTAMOSONÚMERODAVERSÃOconstopenRequest=indexedDB.open('jscangaceiro',2);
//códigoposterioromitido</script></body<!--codigoposterioromitido-->
Desta forma, recarregando a página, o eventoonupgradeneeded será disparado e criará nossa storage
negociacoes.
Atenção, feche e abra o navegador antes de realizar o novoteste.AabaApplicationfazumcachedasúltimasinformaçõesqueexibeeaversãodobancocontinuará1,mesmocomanossamudança.Fechareabrirobrowser realizaumrefreshnestaárea,exibindocorretamenteanovaversãodonossobanco.Éalgoquepodeconfundiroleitor.
Por fim, ao acessarmos a abaApplication, veremos que aversãodobancomudou e que aObject Storenegociacoes foicriada,masaindaestávazia.
310 14.4ATUALIZAÇÃODOBANCO
Figura14.4:Objectstorenegociacoes
Temosnossa storeprontinhaparapersistirdados, assuntodapróximaseção.
Chegou ao momento de persistirmos algumas negociações.Paraisso,precisamosimportaroscriptNegociacao.jsparaquepossamoscriarinstânciasdessaclasse:
<!--client/db.html--><!DOCTYPEhtml><html><head>
<metacharset="UTF-8"><title>AprendendoIndexedDB</title></head><body>
<!--códigoanterioromitido--><scriptsrc="app/domain/negociacao/Negociacao.js"></script></body>
14.5TRANSAÇÕESEPERSISTÊNCIA
14.5TRANSAÇÕESEPERSISTÊNCIA 311
</html>
Agora,vamoscriarafunçãoadiciona()quemaistardeseráchamadapornósparaadicionarmosnegociaçõesnobanco:
<!--client/db.html--><!DOCTYPEhtml><html><head>
<metacharset="UTF-8"><title>AprendendoIndexedDB</title></head>
<body><script>letconnection=null;
constopenRequest=indexedDB.open('jscangaceiro',2);
openRequest.onupgradeneeded=e=>{//códigoomitido
};
openRequest.onsuccess=e=>{//códigoomitido};
openRequest.onerror=e=>{//códigoomitido};
//novafunçãoparaadicionarnegociaçõesfunctionadiciona(){
}</script><scriptsrc="js/app/domain/negociacao/Negociacao.js"></script>
</body></html>
Quandoafunçãoforchamada,elaterádesercapazdegravarumainstânciadenegociaçãonoIndexedDB.
312 14.5TRANSAÇÕESEPERSISTÊNCIA
Oprimeiropassonoprocessodepersistênciaéobtermosumatransação de escrita para, em seguida, através dessa transação,termosacessoàstorenaqualdesejamospersistirobjetos.Primeiro,vejamosocódigo:
//client/dn.html//códigoanterioromitido
functionadiciona(){
//NOVAINSTÂNCIADENEGOCIACAOconstnegociacao=newNegociacao(newDate(),200,1);
consttransaction=connection.transaction(['negociacoes'],'readwrite');conststore=transaction.objectStore('negociacoes');}
//códigoposterioromitido
O método connection.transaction() recebe comoprimeiroparâmetroumarraycomonomedastorecujatransaçãoqueremoscriare,comosegundoparâmetro,oseutipo.Passamosreadwrite para uma transação que permita escrita. Porém, se
quiséssemossomenteleitura,bastariatrocarmosparareadonly.
Por intermédio da store transacional, podemos realizaroperaçõesdepersistência,comoinclusão,alteraçãoeremoção.
//client/dn.html//códigoanterioromitido
functionadiciona(){
constnegociacao=newNegociacao(newDate(),200,1);
consttransaction=connection.transaction(['negociacoes'],'readwrite');
conststore=transaction.objectStore('negociacoes');
14.5TRANSAÇÕESEPERSISTÊNCIA 313
//ATRAVÉSDASTORE,REQUISITAMOSUMAINCLUSÃOconstrequest=store.add(negociacao);
}
//códigoposterioromitido
Após criarmos uma instância de Negociacao, utilizamos ométodostore.add()parapersisti-la.Noentanto,essaoperaçãoé apenas uma requisição de inclusão. Precisamos saber se ela foibem-sucedida ou não através das funções de callback quepassaremosparaoseventosonsucceseonerrordarequisição:
//client/dn.html//códigoanterioromitido
functionadiciona(){
constnegociacao=newNegociacao(newDate(),200,1);
consttransaction=connection.transaction(['negociacoes'],'readwrite');
conststore=transaction.objectStore('negociacoes');constrequest=store.add(negociacao);
request.onsuccess=e=>{
console.log('negociaçãosalvacomsucesso');};
request.onerror=e=>{
console.log('Nãofoipossívelsavaranegociação')};
}
//códigoposterioromitido
Agora sónos resta chamarmosométodoadiciona() peloconsoledoChrome:
314 14.5TRANSAÇÕESEPERSISTÊNCIA
Figura14.5:Chamandoadiciona()peloconsole
Comaexecuçãodométodo,temosaseguintesaída:
Figura14.6:Resultadodeadiciona()noconsole
Excelente, agora vamos verificar na aba Application osdadosdastorenegociacoes,clicandodiretamentenastore:
Figura14.7:Dadosdastore
Podemos simplificar ainda mais nosso código evitandodeclarar variáveis intermediárias, encadeando as chamadas defunção e removendo o bloco de algumas arrow functions.Nossafunçãoadiciona()ficaráassim:
//client/db.html
14.5TRANSAÇÕESEPERSISTÊNCIA 315
//códigoanterioromitido
functionadiciona(){
constnegociacao=newNegociacao(newDate(),200,1);
//ENCADEOUASCHAMADASconstrequest=connection.transaction(['negociacoes'],'readwrite').objectStore('negociacoes').add(negociacao);
//REMOVEUBLOCOrequest.onsuccess=e=>console.log('negociaçãosalvacomsucesso');
//REMOVEUBLOCOrequest.onerror=e=>console.log('Nãofoipossívelsavaranegociação');
}
//códigoposterioromitido
Maisenxuto!
Conseguimos gravar a nossa primeira negociação! Maisadiante,seremoscapazesdelistarosdados.
Somos capazes de persistir negociações, mas faz parte dequalqueraplicaçãolistá-las.Vamoscriarimediatamenteabaixodafunçãoadiciona()afunçãolistaTodos():
<!--client/db.html--><!--códigoposterioromitido-->
<body><script>//códigoanterioromitido
14.6CURSORES
316 14.6CURSORES
functionadiciona(){
//codigoomitido}
functionlistaTodos(){
}
</script><scriptsrc="app/domain/negociacao/Negociacao.js"></script></body>
<!--códigoposterioromitido-->
O processo é bem parecido com a inclusão de negociações.Precisamos de uma transação, mas em vez de chamarmos ométodo add() , em seu lugar chamaremos o métodoopenCursor(),quenosretornaráumareferênciaparaumobjetoquesabeiterarporcadaobjetogravadonastore:
//client/db.html//códigoanterioromitido
functionlistaTodos(){
constcursor=connection.transaction(['negociacoes'],'readwrite').objectStore('negociacoes').openCursor();
cursor.onsuccess=e=>{
};
cursor.onerror=e=>{
//error.name,paraficarmaissucintaainformação
console.log('Error:'+e.target.error.name);};
}
14.6CURSORES 317
//códigoposterioromitido
O evento onsuccess do nosso cursor será chamado naquantidade de vezes correspondente ao número de negociaçõesarmazenadasemnossaobjectstore.Naprimeirachamada,teremosacesso a um objeto ponteiro que sabe acessar a primeiranegociação da store. Adicionaremos a negociação no arraynegociacoesquereceberátodasasnegociaçõesqueiterarmos.
Assimqueadicionarmosaprimeiranegociação,solicitamosaoobjetoponteiroqueváparaapróxima.Esseprocessodisparaumanovachamadaàfunçãoatribuídaaonsuccess.Tudoserepetiráaté que tenhamos percorrido todos os objetos da store.No final,teremosnossoarraycomtodasasnegociaçõeslidas.
Nossocódigoficaráassim:
//client/db.html//códigoanterioromitido
functionlistaTodos(){
constnegociacoes=[];
constcursor=connection.transaction(['negociacoes'],'readwrite').objectStore('negociacoes').openCursor();
cursor.onsuccess=e=>{
//objetoponteiroparaumanegociaçãoconstatual=e.target.result;
//sefordiferentedenull,éporqueaindahádado
if(atual){
//atual.valueguardaosdadosdanegociação
318 14.6CURSORES
negociacoes.push(atual.value);
//vaiparaapróximaposiçãochamandoonsuccessnovamente
atual.continue();
}else{
//quandoatualfornull,éporquenãohámaisdados
//imprimimosnoconsolealistadenegociaçõesconsole.log(negociacoes);
}};
}
Antesde testarmosnossométodo,que tal adicionarmosmaisduasnegociaçõesparatermospelomenostrêselementosemnossastore? Para isso, basta abrirmos o console do Chrome echamarmosduasvezesmaisafunçãoadiciona().
Por fim, podemos ver nossa listagem chamando o métodolistaTodos()tambématravésdoterminal.Oresultadoserá:
Figura14.8:Listandonegociações
14.6CURSORES 319
Será impresso no console um array com todas as nossasnegociações. Contudo, se escrutinarmos cada item do array,veremos que é apenas um objeto com as propriedades _data,_quantidade e _valor . Isso acontece porque, quando
gravamosumobjetoemumastore, apenas suaspropriedades sãosalvas.
Sendoassim, antesde adicionarmosnoarraydenegociações,precisamos criar umanova instância deNegociacao com basenosdados:
//client/db.html//códigoanterioromitido
cursor.onsuccess=e=>{
constatual=e.target.result;
if(atual){
//criaumanovainstânciaantesdeadicionarnoarrayconstnegociacao=newNegociacao(
atual.value._data,atual.value._quantidade,atual.value._valor);
negociacoes.push(negociacao);atual.continue();
}else{
console.log(negociacoes);}
};
cursor.onerror=e=>console.log(e.target.error.name);
//códigoposterioromitido
Perfeito.Seolharmosasaídanoconsole,vemosquetemosum
320 14.6CURSORES
arraydeinstânciasdaclasseNegociacao.
Com tudo o que vimos, temos o pré-requisito para integrarnossaaplicaçãocomoIndexedDB.Entretanto,vocêdevetervistoquenossocódigonãoéláumdosmelhores.Nopróximocapítulo,veremos como organizá-lo ainda melhor, recorrendo a algunspadrõesdeprojetodomercado.patterns.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/14
14.6CURSORES 321
CAPÍTULO15
"Amorteéparaosquemorrem."-GrandeSertão:Veredas
No capítulo anterior, aprendemos a utilizar o IndexedDB, epersistimos e buscamos negociações. No entanto, gastamosbastante linhasdecódigo lidandocomaaberturadaconexão.Semais tarde precisarmos damesma conexão emoutros pontos danossaaplicação,teremoscódigorepetidoedifícildemanter.
Umasoluçãoéisolaracomplexidadedecriaçãodaconexãoemumaclasse.Aliás,utilizamosopadrãodeprojetoFactory pararesolverumproblemaparecidocomacomplexidadedecriaçãodeProxies.ChegouahoradecriarmosnossaConnectionFactory.
Vamos criar o arquivoclient/app/util/ConnectionFactory.js com o método
estático getConnection() . Este método nos devolverá uma
COLOCANDOACASAEMORDEM
15.1ACLASSECONNECTIONFACTORY
322 15COLOCANDOACASAEMORDEM
Promisepelofatodeaaberturadeumaconexãocomobancoserrealizadademaneiraassíncrona.Porfim,lançaremosumaexceçãonoconstructor(),poissóqueremosqueoacessosejafeitopelométodoestático:
//client/app/util/ConnectionFactory.js
classConnectionFactory{
constructor(){
thrownewError('Nãoépossívelcriarinstânciasdessaclasse');
}
staticgetConnection(){
returnnewPromise((resolve,reject)=>{
});}
}
Antes de prosseguirmos, vamos importar nossa classeclient/index.html:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script><scriptsrc="app/util/ProxyFactory.js"></script><scriptsrc="app/util/Bind.js"></script><scriptsrc="app/util/ApplicationException.js"></script><scriptsrc="app/ui/converters/DataInvalidaException.js"></script>
15.1ACLASSECONNECTIONFACTORY 323
<scriptsrc="app/domain/negociacao/NegociacaoService.js"></script>
<scriptsrc="app/util/HttpService.js"></script>
<!--importouaqui--><scriptsrc="app/util/ConnectionFactory.js"></script>
<scriptsrc="app/app.js"></script><!--códigoposterioromitido-->
Temos o esqueleto da nossa classe, mas ela precisa seguiralgumasregras.Sãoelas:
Teremosumaconexãoparaaaplicaçãointeira,sendoassim, não importa quantas vezes seja chamado ométodo getConnection() , a conexão retornadadeveseramesma.
Toda conexão possui o método close() , mas oprogramadornãopodechamá-lo,porqueaconexãoéa mesma para a aplicação inteira. SóConnectionFactorypodefecharaconexão.
Tendo essas restrições em mente, continuaremos aimplementaçãodonossométodo,preparandoumarespostaparaatríadedeeventosdeaberturadeumaconexão:
//client/app/util/ConnectionFactory.js
//variávelqueguardaaconexão
classConnectionFactory{
constructor(){
thrownewError('Nãoépossívelcriarinstânciasdessaclasse');
}
324 15.1ACLASSECONNECTIONFACTORY
staticgetConnection(){
returnnewPromise((resolve,reject)=>{
constopenRequest=indexedDB.open('jscangaceiro',2);
openRequest.onupgradeneeded=e=>{
};
openRequest.onsuccess=e=>{
};
openRequest.onerror=e=>{
};
});}
}
Observequeutilizamosovalor2,porquefoiaúltimaversãodobancousada.
Hoje nossa aplicação possui apenas uma store chamadanegociacoes.Masnadanosimpededetrabalharmoscommais
deumastore.Paratornarnossocódigoflexível,vamosdeclararumarray logo acima da classe que por enquanto guarda a storenegociacoes.Toda vezqueprecisarmosdenovas stores, basta
adicionarmosseunomenoarray:
//arraycomumastoreapenasconststores=['negociacoes'];
classConnectionFactory{
constructor(){
thrownewError('Nãoépossívelcriarinstânciasdessaclasse');
15.1ACLASSECONNECTIONFACTORY 325
}
staticgetConnection(){
returnnewPromise((resolve,reject)=>{
constopenRequest=indexedDB.open('jscangaceiro',2);
openRequest.onupgradeneeded=e=>{
//ITERANOARRAYPARACONSTRUIRASSTORESstores.forEach(store=>{
//lógicaquecriaasstores});
};
openRequest.onsuccess=e=>{
};
openRequest.onerror=e=>{
};
});}
}
A criação do array de stores foi necessária porque nossaclasse não permite que propriedades sejam declaradas segundonossa convenção. Vale lembrar que ConnectionFactory teráapenasummétodoestático.Agora,sónosrestaimplementarmosalógicadecriaçãodasstores.
Naseçãoanterior,jápreparamosoterrenoparaquepossamosescrever a lógica de criação das nossas stores. Mas em vez de
15.2CRIANDOSTORES
326 15.2CRIANDOSTORES
escrevermos a lógica diretamente na função passada paraonupgradeneeded, vamoscriaroutrométodoestático comesta
finalidade,deixandoclaranossaintenção:
//client/app/util/ConnectionFactory.js
conststores=['negociacoes'];
classConnectionFactory{
constructor(){
thrownewError('Nãoépossívelcriarinstânciasdessaclasse');
}
staticgetConnection(){
returnnewPromise((resolve,reject)=>{
constopenRequest=indexedDB.open('jscangaceiro',2);
openRequest.onupgradeneeded=e=>{
//PASSAACONEXÃOPARAOMÉTODOConnectionFactory._createStores(e.target.result);};
openRequest.onsuccess=e=>{
};
openRequest.onerror=e=>{
};
});}
//CONVENÇÃODEMÉTODOPRIVADO//SÓFAZSENTIDOSERCHAMADOPELA//PRÓPRIACLASSE
15.2CRIANDOSTORES 327
static_createStores(connection){
stores.forEach(store=>{
//ifsembloco,maissucinto!
if(connection.objectStoreNames.contains(store))connection.deleteObjectStore(store);
connection.createObjectStore(store,{autoIncrement:true});
});}
}
Terminamos a implementação do evento onupgradeneed ,agora implementaremos os eventos onsuccess e onerror .Neles, simplesmente passamos a conexão para resolve() e oerroparareject():
//client/app/util/ConnectionFactory.js//códigoanterioromitido
openRequest.onsuccess=e=>{//passaoresultado(conexão)paraapromise!resolve(e.target.result);
};
openRequest.onerror=e=>{
console.log(e.target.error)//passaoerropararejectdapromise!reject(e.target.error.name)};
//códigoposterioromitido
Já temos uma classe testável. Depois de recarregarmos nossapáginaindex.html,atravésdoconsole,vamosutilizarométodogetConnection().
//NOCONSOLE!
328 15.2CRIANDOSTORES
ConnectionFactory.getConnection().then(connection=>console.log(connection));
Ainstruçãoexibiránoconsole:
Figura15.1:Conexãoimpressanoconsole
Está tudofuncionandocorretamente.Porém,acadachamadadométodo getConnection(), criaremos uma nova conexão eissonãosecoadunacomaregranaqualprecisamosterumaúnicaconexãoparaaaplicação.Veremoscomosolucionaristoaseguir.
Paragarantirmosquereceberemosamesmaconexãotodavezque o método getConnection() for chamado, criaremos avariávelconnectionantesdadeclaraçãodaclasse.Elacomeçarácomvalornull.Emseguida,logonoiníciodoblocodaPromise,verificamos se ela possui valor; se possuir, passamos a conexãopara resolve() , evitando assim repetir todo o processo deobtenção de uma conexão. Caso connection seja null ,significa que estamos obtendo a conexão pela primeira vez efaremosissoemonsuccess,guardando-anavariávelepassando-atambémpararesolve():
//client/app/util/ConnectionFactory.js
15.3GARANTINDOUMACONEXÃOAPENASPORAPLICAÇÃO
15.3GARANTINDOUMACONEXÃOAPENASPORAPLICAÇÃO 329
conststores=['negociacoes'];
//COMEÇASEMCONEXÃOletconnection=null;
classConnectionFactory{
constructor(){
thrownewError('Nãoépossívelcriarinstânciasdessaclasse');
}
staticgetConnection(){
returnnewPromise((resolve,reject)=>{
//SEUMACONEXÃOJÁFOICRIADA,//JÁPASSAPARARESOLVEERETORNALOGO!
if(connection)returnresolve(connection);
constopenRequest=indexedDB.open('jscangaceiro',2);
openRequest.onupgradeneeded=e=>{
ConnectionFactory._createStores(e.target.result);};
openRequest.onsuccess=e=>{
//SÓSERÁEXECUTADONAPRIMEIRAVEZQUEACONEXÃOFORCRIADA
connection=e.target.result;resolve(e.target.result);
};
openRequest.onerror=e=>{
console.log(e.target.error)reject(e.target.error.name)
};
330 15.3GARANTINDOUMACONEXÃOAPENASPORAPLICAÇÃO
});}
//códigoanterioromitido
}
Agora, no console, não importa a quantidade de vezes que ométodogetConnection()sejachamado,elesempreretornaráamesma conexão! Excelente, mas nossa solução nos criou outroproblema.
Com a página recarregada, vamos até o console pararealizarmos um teste. Vamos digitar os nomes das variáveisstores e connection , que foram declaradas em
ConnectionFactory:
Figura15.2:Acessandoasvariáveis
Somoscapazesdeacessarosvaloresdasvariáveisporqueelasforamdeclaradasnoescopoglobal.Alémdisso,nadaimpedequeoprogramador acidentalmente, emoutra parte da nossa aplicação,
15.3GARANTINDOUMACONEXÃOAPENASPORAPLICAÇÃO 331
realizeatribuiçõesindevidasparaelas.Eagora?
Paraevitarquestoreseconnectionvazemparaoescopoglobal,podemosconfiná-las emummódulo.Variáveise funçõesdeclaradasemummódulosóserãoacessíveisdentrodomódulo,apenas as variáveis e funçõesqueoprogramador explicitar serãoacessíveispelorestantedaaplicação.Emoutraspalavras,podemosdizerqueummódulotemseupróprioescopo.
Podemos criar um escopo dentro de um arquivo scriptenvolvendo todo seu código dentro de uma function() .Variáveis(var,letouconst)efunçõesdeclaradasnoescopodeumafunçãosãoacessíveisapenaspeloblocodafunção.
Vamos envolver todo o código do scriptConnectionFactory.jsdentrodafunçãotmp():
//client/app/util/ConnectionFactory.js
functiontmp(){
conststores=['negociacoes'];letconnection=null;
classConnectionFactory{
constructor(){
thrownewError('Nãoépossívelcriarinstânciasdessaclasse');
}
staticgetConnection(){
15.4 O PADRÃO DE PROJETO MODULEPATTERN
332 15.4OPADRÃODEPROJETOMODULEPATTERN
//códigoomitido}
static_createStores(connection){
//códigoomitido}
}
}
tmp();//chamaafunção!
Reparem que a última instrução é a chamada da funçãotmp()paraqueseublocosejaexecutado.
Depoisderecarregarmosonavegador,setentarmosacessarasvariáveisstoreseconnection,receberemos"...isnotdefined",pois elas não vivem mais no escopo global. Resolvemos umproblema,mascriamosoutro,pois tambémnão seremoscapazesde acessar ConnectionFactory.getConnection() . Classestambémsãoencapsuladasnoescopodefunções:
Figura15.3:ProblemaaoacessarConnectionFactory
15.4OPADRÃODEPROJETOMODULEPATTERN 333
Aideiaéminimizarmosousodoescopoglobal,maseleaindaéummalnecessário,poiséatravésdelequetornamosumaclasseacessívelparaoutra.
Se todo o código do script está dentro da função tmp() etemos interesse em tornar global a definição da classe, podemosfazer com que a função retorne a classe. Ao armazenarmos oretorno em outra variável declarada fora do escopo da funçãotmp(), ela serámais uma vez acessível globalmente pela nossa
aplicação:
//client/app/util/ConnectionFactory.s
functiontmp(){
conststores=['negociacoes'];letconnection=null;
//RETORNAADEFINIÇÃODACLASSEreturnclassConnectionFactory{
constructor(){
thrownewError('Nãoépossívelcriarinstânciasdessaclasse');
}
staticgetConnection(){
//códigoomitido}
static_createStores(connection){
//códigoomitido}
}
}
//AVARIÁVELVIVENOESCOPOGLOBAL
334 15.4OPADRÃODEPROJETOMODULEPATTERN
//PORQUEFOIDECLARADAFORADAFUNÇÃOconstConnectionFactory=tmp();
Depoisderecarregarnossapágina,podemosrealizarumnovoteste. Conseguiremos acessar a classe ConnectionFactory ,mantendostoreseconnectioninacessíveis:
Figura15.4:Acessandoaclassepeloconsole
Muitobom,masnossocódigopodeficaraindamelhor.Comousamosafunçãotmp()paracriarnossomódulo,elaéacessívelpelo console, pois está no escopo global e nada nos impede dechamá-laquantasvezesquisermos.Outropontoéqueprecisamoslembrardechamá-lalogodepoisdesuadeclaração;casocontrário,alógicadeseublocojamaisseráprocessada.
Podemos resolver esse problema através de uma IIFE(Immediately-invoked function expression), ou funções imediatasno português. O primeiro passo é tornar a função tmp()anônima,istoé,removendoseunome.Comonãotemosmaisumnome,precisaremosretirarainstruçãodachamadadetmp():
//client/app/util/ConnectionFactory.s
function(){
conststores=['negociacoes'];
15.4OPADRÃODEPROJETOMODULEPATTERN 335
letconnection=null;
returnclassConnectionFactory{
//códigoomitido}
}
Dojeitoqueestá,ocódigonãofuncionará,poisnãopodemosdeclarar funções anônimas dessa forma. Contudo, podemosenvolverafunçãoentreparênteses:
//client/app/util/ConnectionFactory.s
(function(){
conststores=['negociacoes'];letconnection=null;
returnclassConnectionFactory{
//códigoomitido}
})
Esse passo evita erro de sintaxe, mas ainda não é capaz deinvocarnossafunção.Paraisso,bastaadicionarmosumachamada()logoapósosparênteses:
//client/app/util/ConnectionFactory.s
(function(){
conststores=['negociacoes'];letconnection=null;
returnclassConnectionFactory{
//códigoomitido}
336 15.4OPADRÃODEPROJETOMODULEPATTERN
})();
Assimquenossoscriptforcarregado,afunçãoanônimadentrodosparêntesesseráexecutada.Comonãotemosmaisumnomedefunção,ninguémpoderáchamá-laatravésdoterminal.Porfim,elaprecisaguardarseuretornoemumavariável:
constConnectionFactory=(function(){
conststores=['negociacoes'];letconnection=null;
returnclassConnectionFactory{
//códigoomitido}
})();
O conjunto das soluções que acabamos de implementarseguemopadrãodeprojetoModulePattern.Nocaso,criamosummóduloqueexportaaclasseConnectionFactory.Comofoiditonos capítulos anteriores, o ES2015 (ES6) trouxe uma soluçãonativa para a criação de módulos, mas ainda não é o momentocertodeabordá-la.
Porfim,podemosaindausarumaarrowfunctionnolugardefunction():
//client/app/util/ConnectionFactory.js
constConnectionFactory=(()=>{
conststores=['negociacoes'];letconnection=null;
returnclassConnectionFactory{
//códigoomitido
15.4OPADRÃODEPROJETOMODULEPATTERN 337
}
})();
Agora que garantimos apenas uma conexão para a aplicaçãointeira, precisamos verificar a segunda regra que consiste nofechamento de uma conexão apenas pela classeConnectionFactory:
Umadasregrasqueprecisamosseguirénãopermitirmosqueodesenvolvedor feche a conexão através da conexão criada paraConnectionFactory, pois se ele fizer isso, estará fechando a
conexão da aplicação inteira. Nada impede o desenvolvedor dechamarométodoclose()daconexãoparafechá-la,atéporque,éum"caminhonatural"parafechá-la:
//EXEMPLODEFECHAMENTODECONEXÃOPELODESENVOLVEDOR
ConnectionFactory.getConnection().then(conn=>conn.close());
A única forma aceitável de fechamento é através da própriaConnectionFactory,porexemplo,pormeiodeummétodoqueaindacriaremos,chamadocloseConnection():
//EXEMPLODECOMODEVEMOSFECHARACONEXÃO,MÉTODONÃOEXISTEAINDAConnectionFactory.closeConnection();
Se todaconexãopossuiométodoclose(), comopodemosevitarqueoprogramadoro chame?UmasoluçãoéaplicarmosoMonkey Patch, que consiste na modificação de uma API jáexistente.Nocaso,vamosalterarométodoclose()originalda
15.5 MONKEY PATCH: GRANDES PODERESTRAZEMGRANDESRESPONSABILIDADES
338 15.5MONKEYPATCH:GRANDESPODERESTRAZEMGRANDESRESPONSABILIDADES
conexãoparaquelanceumaexceçãoalertandoodesenvolvedordequenãopodefechá-lo:
//client/app/util/ConnectionFactory.js//códigoanterioromitido
openRequest.onsuccess=e=>{
connection=e.target.result;connection.close=()=>{
thrownewError('Vocênãopodefechardiretamenteaconexão');
};resolve(connection);
};
//códigoposterioromitido
A natureza dinâmica da linguagem JavaScript nos permitealterar funções em tempo de execução. No caso, estamosatribuindoumanovalógicaparaoclose()daconexão.Depoisde recarregarmos nossa página, vamos realizar um novo testefechandoaconexão:
Figura15.5:Conexãoimpressanoconsole
Agora, para concluirmos nossa classe, criaremos o métodoestático closeConnection() em ConnectionFactory . Estemétodoseráoencarregadodefecharaconexãoparanós:
//client/app/util/ConnectionFactory.js
15.5MONKEYPATCH:GRANDESPODERESTRAZEMGRANDESRESPONSABILIDADES 339
constConnectionFactory=(()=>{
conststores=['negociacoes'];letconnection=null;
returnclassConnectionFactory{
staticgetConnection(){
//códigoomitido}
static_createStores(connection){
//códigoomitido}
//novométodo!
staticcloseConnection(){
if(connection){connection.close();
}}
}})();
Para testarmos, basta recarregar a página e, no console,executar primeiro a instrução que cria uma conexão, para entãofechá-lacomnossonovométodo:
//NOCONSOLEConnectionFactory.getConnection().then(conn=>console.log(conn));ConnectionFactory.closeConnection();
Recebemosumamensagemdeerro,aquelaqueprogramamosanteriormenteaotentarmosfecharaconexão:
340 15.5MONKEYPATCH:GRANDESPODERESTRAZEMGRANDESRESPONSABILIDADES
Figura15.6:Nãofoipossívelfecharaconexão
É fácil descobrirmos a causa. Nós substituímos o métodoclose()daconexãoparaevitarqueoprogramadordesavisadoafeche. No entanto, precisamos do comportamento original paraqueConnectionFactorysejacapazdefecharaconexão.Eagora?
Uma solução é guardarmos uma referência para a funçãooriginal antes de substituí-la. Essa referência será utilizada pelométodo closeConnection() da nossa ConnectionFactory .Nossaclassenofinalficaráassim:
constConnectionFactory=(()=>{
conststores=['negociacoes'];letconnection=null;
//VARIÁVELQUEARMAZENARÁAFUNÇÃOORIGINALletclose=null;
returnclassConnectionFactory{
constructor(){
thrownewError('Nãoépossívelcriarinstânciasdessaclasse');
15.5MONKEYPATCH:GRANDESPODERESTRAZEMGRANDESRESPONSABILIDADES 341
}
staticgetConnection(){
returnnewPromise((resolve,reject)=>{
if(connection)returnresolve(connection);
constopenRequest=indexedDB.open('jscangaceiro',2);
openRequest.onupgradeneeded=e=>{
ConnectionFactory._createStores(e.target.result);
};
openRequest.onsuccess=e=>{
connection=e.target.result;
//GUARDANDOAFUNÇÃOORIGINAL!close=connection.close.bind(connection);
connection.close=()=>{thrownewError('Vocênãopodefechardir
etamenteaconexão');};resolve(connection);
};
openRequest.onerror=e=>{
console.log(e.target.error)reject(e.target.error.name)
};
});}
static_createStores(connection){
//códigoomitido}
342 15.5MONKEYPATCH:GRANDESPODERESTRAZEMGRANDESRESPONSABILIDADES
staticcloseConnection(){
if(connection){
//CHAMANDOOCLOSEORIGINAL!close();
}}
}})();
É na variávelclose que guardamos uma referência para afunção original antes de realizarmos o monkey patch. Aliás,tivemos de apelar mais uma vez para a função bind() comconnection.close.bind(connection), paraque a funçãonão
percaseucontexto(nocaso,ainstânciadeconnection).
Comtudoemordem,recarregaremosmaisumavezapáginaerealizaremosnovamentenossoteste:
//NOCONSOLEConnectionFactory.getConnection().then(conn=>console.log(conn));ConnectionFactory.closeConnection();
Figura15.7:FechandoaconexãoporConnectionFactory
Excelente, conseguimos fechar a conexão pela
15.5MONKEYPATCH:GRANDESPODERESTRAZEMGRANDESRESPONSABILIDADES 343
ConnectionFactorye,comisso,atendemos todososrequisitospara lidarcomaconexão.Ainda faltaorganizarmosasoperaçõesdepersistência,assuntodopróximocapítulo.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/15
344 15.5MONKEYPATCH:GRANDESPODERESTRAZEMGRANDESRESPONSABILIDADES
CAPÍTULO16
"Obedecer é mais fácil do que entender." - Grande Sertão:Veredas
No capítulo anterior, aplicamos mais uma vez o padrão deprojetoFactorycriandoumaclassecomaresponsabilidadedecriarconexões. No entanto, o código anterior deixa a desejar, poisprecisamoslidarcomosdetalhesdepersistênciadenegociações.Seprecisarmos incluir negociações em diversas partes da nossaaplicação,teremosderepetirmuitocódigo.
Vejamos um exemplo de uso da nossa conexão para incluirumaconexão:
//EXEMPLODEUSOAPENAS
constnegociacao=newNegociacao(newDate(),200,1);
ConnectionFactory.getConnection().then(connection=>{
constrequest=connection.transaction(['negociacoes'],'readwrite').objectStore('negociacoes')
ENTRANDONALINHA
16ENTRANDONALINHA 345
.add(negociacao);
request.onsuccess=e=>{
console.log('negociaçãosavlarcomsucesso');}
request.onerror=e=>{
console.log('Nãofoipossívelsalvaranegociação')}
});
Aboanotíciaéqueháoutropadrãodeprojetoquepodenosajudar a esconder a complexidade de persistência, o padrão deprojetoDAO.
O padrão de projeto DAO (Data Access Object) tem comoobjetivo esconder os detalhes de acesso aos dados, fornecendométodos de alto nível para o desenvolvedor. Em nosso caso,criaremos a classeNegociacaoDao que terá como dependênciaumaconexão.VejamosumexemplodecomoseráapersistênciadeumanegociaçãoatravésdoDAOquecriaremosaseguir:
//EXEMPLODOQUEQUEREMOSCONSEGUIRFAZERCOMNOSSODAO
constnegociacao=newNegociao(newDate(),1,100);
ConnectionFactory.getConnection().then(conn=>newNegociacaoDao(conn)).then(dao=>dao.adiciona(negociacao)).then(()=>console.log('Negociacaosalvacomsucesso').catch(err=>console.log('Nãofoipossívelsalvaranegociaç
ão'));});
16.1OPADRÃODEPROJETODAO
346 16.1OPADRÃODEPROJETODAO
Agora que já sabemos até onde queremos chegar, daremosinícioàconstruçãodonossoDAOnapróximaseção.
O primeiro passo para criarmos nosso DAO é adotarmos aseguinte convenção. O nome da classe será o nome do modelopersistido, seguido do sufixo Dao . Se em nossa aplicaçãorealizamos operações de persistência com Negociacao , entãoteremosaclasseNegociacaoDao.
Alémdaconvençãoquevimos,aclasseDAOprecisaráreceberem seu constructor() a conexão com o banco. Isso porque,casoqueiramosutilizaromesmoDAOcomooutrobanco,bastapassarmos a conexão correta, sem termos de alterar qualquercódigonaclasseDAO.
Porfim,todososmétodosdepersistênciadoDAOretornarãoPromises, pois operações de persistência com IndexedDB sãoassíncronas.
Combasenas convenções que acabamosde aprender, vamoscriar o arquivoclient/app/domain/negociacao/NegociacaoDao.jsedeclararo esqueletodonossoDAO:
//client/app/domain/negociacao/NegociacaoDao.js
classNegociacaoDao{
constructor(connection){
this._connection=connection;
16.2 CRIANDO NOSSO DAO DENEGOCIAÇÕES
16.2CRIANDONOSSODAODENEGOCIAÇÕES 347
this._store='negociacoes';}
adiciona(negociacao){
returnnewPromise((resolve,reject)=>{
/*lidaremoscomainclusãoaqui*/});
}
listaTodos(){
returnnewPromise((resolve,reject)=>{
/*lidaremoscomoscursosaqui*/});
}}
O esqueleto do nosso DAO possui também a propriedadethis._storage. Ela guarda o nome da storage que será usada
pelo DAO. Sem hiato, vamos importar nossa nova classe emclient/index.html:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/ui/converters/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script><scriptsrc="app/util/ProxyFactory.js"></script><scriptsrc="app/util/Bind.js"></script><scriptsrc="app/util/ApplicationException.js"></script><scriptsrc="app/ui/converters/DataInvalidaException.js"></script>
<scriptsrc="app/domain/negociacao/NegociacaoService.js"></script>
348 16.2CRIANDONOSSODAODENEGOCIAÇÕES
<scriptsrc="app/util/HttpService.js"></script><scriptsrc="app/util/ConnectionFactory.js"></script>
<!--importouaqui--><scriptsrc="app/domain/negociacao/NegociacaoDao.js"></script>
<scriptsrc="app/app.js"></script>
<!--códigoposterioromitido-->
Agora que já temos o esqueleto do nosso DAO pronto,podemos dar início à implementação da lógica do métodoadiciona() , aquele responsável pela inclusão de novas
negociações.
Vamos implementar o método adiciona() deNegociacaoDao. Não teremos nenhuma novidade, aplicaremos
tudooqueaprendemosatéagora:
//client/app/domain/negociacao/NegociacaoDao.js//códigoanterioromitido
adiciona(negociacao){
returnnewPromise((resolve,reject)=>{
constrequest=this._connection.transaction([this._store],'readwrite').objectStore(this._store).add(negociacao);
request.onsuccess=e=>resolve();request.onerror=e=>{
console.log(e.target.error);reject('Nãofoipossívelsalvaranegociação');
16.3 IMPLEMENTANDO A LÓGICA DEINCLUSÃO
16.3IMPLEMENTANDOALÓGICADEINCLUSÃO 349
}});
}
//códigoposterioromitido
Não há surpresas, simplesmente quando a operação deinclusão é bem-sucedida, chamamosresolve() passandoumamensagem de sucesso. E quando há algum erro, chamamosreject()passandoumamensagemdealtonível,indicandoqueaoperaçãonãofoirealizada.
Veja que temos console.log(e.target.error) , muitoimportante para que o desenvolvedor escrutinando o consoleidentifique a causa do erro. Recarregando a página, faremos umteste. No console, vamos executar a seguinte instrução que criaumaconexãoeonossoDAO,eadicionaumanovanegociação:
//EXECUTARNOCONSOLEDONAVEGADOR
constnegociacao=newNegociacao(newDate(),7,100);
ConnectionFactory.getConnection().then(conn=>newNegociacaoDao(conn)).then(dao=>dao.adiciona(negociacao)).then(()=>console.log('Negociaçãosalvacomsucesso!')).catch(err=>console.log(err));
Veremosqueelegravouperfeitamenteasnegociações.
350 16.3IMPLEMENTANDOALÓGICADEINCLUSÃO
Figura16.1:Sucessonasgravação
Agora, vamos entender como se deu o encadeamento dasfunções. Primeiro, através da Promise retornada porConnectionFactory.getConnection(),temosacessoàconexãoencadeando uma chamada à função then() . Ainda nestethen(),depossedaconexão,retornamosimplicitamenteatravésde uma arrow function uma instância de NegociacaoDao querecebeaconexãoretornadapelaPromise.
Lembre-se de que as funções then() podem retornarqualquer valor, seja este síncrono ou o resultado da chamada deoutra Promise. Dessa forma, na próxima chamada a then() ,temos acesso à instância de NegociacaoDao , cujo o métodoadiciona() chamamos ao passar a negociação que desejamos
gravar.Comoométodoadiciona()éumaPromise,apróximachamadaencadeadaàthen()sóseráexecutadaseaoperaçãoforrealizadacomsucesso.
Por fim, temos a chamada da função catch() que podecapturarqualquererroquepossaocorrernaobtençãodaconexão
16.3IMPLEMENTANDOALÓGICADEINCLUSÃO 351
ou da operação de persistência. Falta ainda implementarmos ométodo listaTodos(), aquele que interage com o cursor doIndexedDB.
Vamos implementar o método listaTodos() deNegociacaoDao.Ainda não teremos nenhumanovidade, então
aplicaremosnossoconhecimentojáadquirido:
//client/app/domain/negociacao/NegociacaoDao js//códigoanterioromitido
listaTodos(){
returnnewPromise((resolve,reject)=>{
constnegociacoes=[];
constcursor=this._connection.transaction([this._store],'readwrite').objectStore(this._store).openCursor();
cursor.onsuccess=e=>{
constatual=e.target.result;
if(atual){
constnegociacao=newNegociacao(atual.value._data,atual.value._quantidade,atual.value._valor);
negociacoes.push(negociacao);atual.continue();
}else{
16.4 IMPLEMENTANDO A LÓGICA DALISTAGEM
352 16.4IMPLEMENTANDOALÓGICADALISTAGEM
//resolvendoapromisecomnegociacoesresolve(negociacoes);
}};
cursor.onerror=e=>{console.log(e.target.error);reject('Nãofoipossívellistarnasnegociações');
}
});}//códigoposterioromitido
Precisamostestarnossométodo,e faremosissomaisumavezatravésdoconsolecomaseguinteinstrução:
//NOCONSOLE
ConnectionFactory.getConnection().then(conn=>newNegociacaoDao(conn)).then(dao=>dao.listaTodos()).then(negociacoes=>console.log(negociacoes)).catch(err=>console.log(err));
Será exibido no console um array com todas as negociaçõesquepersistimosatéomomento.TemososdoismétodosdoDAOque criamos. Porém, toda vez que precisarmos de um DAO, oprogramador terá de obter uma conexão através deConnectionFactory, para em seguida obter uma instância do
DAO.PodemossimplificaresseprocessoisolandoacomplexidadedecriaçãodoDAOemumanovaclasse.
CriaremosaclasseDaoFactory que isolará a criaçãodeum
16.5CRIANDOUMADAOFACTORY
16.5CRIANDOUMADAOFACTORY 353
DAO para nós. Vamos criar o arquivoclient/util/DaoFactory.js e declarar nossa classe que
possuiráapenasométodoestáticogetNegociacaoDao():
//client/app/util/DaoFactory.js
classDaoFactory{
staticgetNegociacaoDao(){
returnConnectionFactory.getConnection().then(conn=>newNegociacaoDao(conn));
}}
OmétodogetNegociacaoDao() retornaumaPromiseque,ao ser resolvida, nos dá acesso a uma instância deNegociacaoDao.
Vamosimportaraclasseemclient/index.html,para logoemseguidarealizarmosumteste:
<!--client/index.html--><!--códigoanterioromitido-->
<scriptsrc="app/domain/negociacao/Negociacao.js"></script><scriptsrc="app/controllers/NegociacaoController.js"></script><scriptsrc="app/utils/DateConverter.js"></script><scriptsrc="app/domain/negociacao/Negociacoes.js"></script><scriptsrc="app/ui/views/View.js"></script><scriptsrc="app/ui/views/NegociacoesView.js"></script><scriptsrc="app/ui/models/Mensagem.js"></script><scriptsrc="app/ui/views/MensagemView.js"></script><scriptsrc="app/util/ProxyFactory.js"></script><scriptsrc="app/util/Bind.js"></script><scriptsrc="app/util/ApplicationException.js"></script><scriptsrc="app/ui/converters/DataInvalidaException.js"></script>
<scriptsrc="app/domain/negociacao/NegociacaoService.js"></script>
354 16.5CRIANDOUMADAOFACTORY
<scriptsrc="app/util/HttpService.js"></script><scriptsrc="app/util/ConnectionFactory.js"></script><scriptsrc="app/domain/negociacao/NegociacaoDao.js"></script>
<!--importouaqui--><scriptsrc="app/util/DaoFactory.js"></script>
<scriptsrc="app/app.js"></script>
<!--códigoposterioromitido-->
Apóso recarregamentodapágina, vamos executar a seguinteinstruçãonoconsoledonavegador:
//NOCONSOLE
DaoFactory.getNegociacaoDao().then(dao=>console.log(dao))
Noconsole,seráexibidaainstânciadeNegociacaoDao:
Figura16.2:DaoFactoryemação
Agoraque játemostodainfraestruturapronta,chegouahoraderealizarmosapersistênciadasnegociaçõesquecadastrarmos.
Na classe NegociacaoController , precisamos alterar ométodo adiciona() para que ele persista a negociação doformulário. Apenas se a operação for bem-sucedida,
16.6COMBINANDOPADRÕESDEPROJETO
16.6COMBINANDOPADRÕESDEPROJETO 355
adicionaremosanegociaçãonatabela.
Vamos alterar o bloco try que fará uso da classeDaoFactory:
//client/app/controllers/NegociacaoController.js
//códigoanterioromitido
adiciona(event){
try{
event.preventDefault();
//negociaçãoqueprecisamosincluirnobancoenatabela
constnegociacao=this._criaNegociacao();
DaoFactory.getNegociacaoDao().then(dao=>dao.adiciona(negociacao)).then(()=>{
//sótentaráincluirnatabelaseconseguiuantesincluirnobanco
this._negociacoes.adiciona(negociacao);this._mensagem.texto='Negociaçãoadicionadacom
sucesso';this._limpaFormulario();
}).catch(err=>this._mensagem.texto=err);
}catch(err){
//códigoomitido}
}
//códigoposterioromitido
356 16.6COMBINANDOPADRÕESDEPROJETO
Vamosrecarregarapágina,paradepois incluirmosumanovanegociação. Será exibida a mensagem de sucesso, inclusive anegociaçãoseráinseridanatabela:
Figura16.3:Negociaçãoadicionadacomsucesso
Paranãotermossombradedúvidasdagravaçãodanegociação,vamos abrir o Console e acessar a aba Application, para emseguidavisualizarmosoconteúdodastorenegociacoes:
Figura16.4:Dadosdanegociação
Excelente!Agoraprecisamosexibirtodasasnegociaçõessalvasnobanco.
16.6COMBINANDOPADRÕESDEPROJETO 357
Em NegociacaoDao , já temos o método listaTodos()implementado, só precisamos chamá-lo assim queNegociacaoControllerforinstanciado.Faremosissoatravésdeseuconstructor():
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
constructor(){
//códigoanterioromitido
DaoFactory.getNegociacaoDao().then(dao=>dao.listaTodos()).then(negociacoes=>
negociacoes.forEach(negociacao=>this._negociacoes.adiciona(negociacao)))
.catch(err=>this._mensagem.texto=err);}
//códigoposterioromitido}
Adicionamos cada negociação trazida do banco na lista denegociações que alimenta nossa tabela. Dessa forma, a tabela jáseráexibidalogodeinícioparaousuáriocomasnegociaçõesqueelejácadastrou.
16.7EXIBINDOTODASASNEGOCIAÇÕES
358 16.7EXIBINDOTODASASNEGOCIAÇÕES
Figura16.5:Bancocomtodasasinformações
Apesar de funcionar, é uma boa prática isolarmos em ummétodotodoocódigodeinicializaçãodeumaclassequenãodigarespeitoà inicializaçãodaspropriedadesdaclasse.Vamos criar ométodo_init()paraentãomovermosocódigoquebuscatodasasnegociaçõesparadentrodele:
classNegociacaoController{
constructor(){
//códigoanterioromitido
//chamaométododeinicializaçãothis._init();
}
_init(){
DaoFactory.getNegociacaoDao().then(dao=>dao.listaTodos()).then(negociacoes=>
negociacoes.forEach(negociacao=>this._negociacoes.adiciona(negociacao)))
.catch(err=>this._mensagem.texto=err);}
//códigoposterioromitido
16.7EXIBINDOTODASASNEGOCIAÇÕES 359
}
Ainda faltaumaoperaçãodepersistência,aquelaquepermiteexcluirnegociações.
Nosso NegociacaoDao ainda não possui um método queapaguetodasasnegociaçõescadastradas,echegouahoradecriá-lo. Ele será praticamente idêntico ao método adiciona , adiferença é que não receberá nenhum parâmetro e quechamaremosafunçãoclear()emvezdeadd().Adicionandoo novométodoapagaTodos():
//client/app/domain/negociacao/NegociacaoDao.js//códigoanterioromitido
apagaTodos(){
returnnewPromise((resolve,reject)=>{
constrequest=this._connection.transaction([this._store],'readwrite').objectStore(this._store).clear();
request.onsuccess=e=>resolve();
request.onerror=e=>{console.log(e.target.error);reject('Nãofoipossívelapagarasnegociações');
};
});}//códigoposterioromitido
Por fim, precisamos alterar o método apaga() de
16.8REMOVENDOTODASASNEGOCIAÇÕES
360 16.8REMOVENDOTODASASNEGOCIAÇÕES
NegociacaoController para que apague os dados do banco.Aliás, a tabela só seráesvaziada seaoperaçãodepersistênciadercerto:
//client/app/controllers/NegociacaoController.js//códigoanterioromitido
apaga(){
DaoFactory.getNegociacaoDao().then(dao=>dao.apagaTodos()).then(()=>{
this._negociacoes.esvazia();this._mensagem.texto='Negociaçõesapagadascomsuce
sso';}).catch(err=>this._mensagem.texto=err);
}//códigoposterioromitido
Já podemos recarregar nossa página e testarmos a novafuncionalidadeclicandonobotão"Apagar".Oresultandoseráaexclusão total das negociações do nosso banco e a limpeza databeladenegociações:
16.8REMOVENDOTODASASNEGOCIAÇÕES 361
Figura16.6:Negociaçõesexibidascomsucesso
Agora,quandorecarregarmosapágina,atabeladenegociaçõesestarávazia.
Figura16.7:Negociaçõestotalmenteapagadas
Ocódigoqueescrevemosfuncionacomoesperado,criamosa
16.9FACTORYFUNCTION
362 16.9FACTORYFUNCTION
classe DaoFactory com o método estáticogetNegociacaoDao()queescondeacomplexidadedacriaçãodeinstâncias da classe NegociacaoDao. Porém, vale a pena umareflexão.
LinguagenspresasexclusivamenteaoparadigmadaOrientaçãoaObjetonãopermitemcriarmétodos semquepertençamaumaclasse.Nãosãorarasasclassescomapenasummétodo,namaioriadasvezesestáticopara implementaropadrãodeprojetoFactory.Noentanto,comoJavaScriptémultiparadigma,podemosescrevermenos se reimplementarmos o padrão Factory através doparadigmaFuncional.
Vamos alterar o arquivoclient/util/DaoFactory.js. EledeclararáapenasafunçãogetNegociacaoDao:
//client/app/util/DaoFactory.js
functiongetNegociacaoDao(){
returnConnectionFactory.getConnection().then(conn=>newNegociacaoDao(conn));
}
Não temosmais a declaração da classe e poupamos algumaslinhasdecódigo.
Agora, precisamos alterarclient/app/controllers/NegociacaoController.js para
usar a função getNegociacaoDao() no lugar deDaoFactory.getNegociacaoDao():
//client/app/controllers/NegociacaoController.js
classNegociacaoController{
16.9FACTORYFUNCTION 363
constructor(){
//códigoomitido}
_init(){
//MUDANÇAAQUI
getNegociacaoDao().then(dao=>dao.listaTodos()).then(negociacoes=>
negociacoes.forEach(negociacao=>this._negociacoes.adiciona(negociacao)))
.catch(err=>this._mensagem.texto=err);}
adiciona(event){
event.preventDefault();
try{
constnegociacao=this._criaNegociacao();
//MUDANÇAAQUI
getNegociacaoDao().then(dao=>dao.adiciona(negociacao)).then(()=>{
this._negociacoes.adiciona(negociacao);this._mensagem.texto='Negociaçãoadicionadacom
sucesso';this._limpaFormulario();
}).catch(err=>this._mensagem.texto=err);
}catch(err){
//códigoomitido}
}
//códigoomitido
364 16.9FACTORYFUNCTION
apaga(){
//MUDANÇAAQUI
getNegociacaoDao().then(dao=>dao.apagaTodos()).then(()=>{
this._negociacoes.esvazia();this._mensagem.texto='Negociaçõesapagadascomsuce
sso';}).catch(err=>this._mensagem.texto=err);
}}
Simplificamos ainda mais nosso código. Em suma, quandoprecisamos apenas de um método para realizar nosso trabalho,podemos evitar a declaração de classes e utilizar em seu lugar adeclaração de uma função. A aplicação prática de uma ou outraformadependerátambémdainclinaçãododesenvolvedorparaumououtroparadigma.
Excelente, terminamos nossa aplicação! Mas será que elafuncionaránosmaisdiversosnavegadoresdomercado?
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/16
16.9FACTORYFUNCTION 365
CAPÍTULO17
"Sertão:édentrodagente."-GrandeSertão:Veredas
Vimos que o JavaScript possui duas características queassombramdesenvolvedoresnessalinguagem:oescopoglobaleaordemdecarregamentodescripts.Oprimeiroproblematentamosminimizar através do module pattern, mas ele ainda continuaexistindo. Já com o segundo, nada pôde ser feito, jogando aresponsabilidade de importação nas costas do desenvolvedor.Contudo,apartirdoES2015(ES6),apróprialinguagemJavaScriptatacouessesproblemasatravésdeumsistemanativodemódulos.
O sistema demódulos do ES2015 (ES6) baseia-se em quatropilares:
1. Cada script é um módulo por padrão, que esconde suasvariáveisefunçõesdomundoexterno.
DIVIDIRPARACONQUISTAR
17.1MÓDULOSDOES2015(ES6)
366 17DIVIDIRPARACONQUISTAR
2. A instrução export permite que ummódulo exporte osartefatosquedesejacompartilhar.
3. A instrução import permite que um módulo importeartefatos de outros módulos. Apenas artefatos exportadospodemserimportados.
4. O uso de um loader (carregador), aquele que será oresponsávelemcarregaroprimeiromóduloeresolvertodasassuasdependênciasparanós,semtermosdenospreocuparemimportarcadascriptemumaordemespecífica.
Um ponto a destacar dos quatro pilares que vimos é o doloader.MesmonaversãoES2017doJavaScriptnãoháconsensonaespecificaçãodoloader,porissoprecisamosapelarparabibliotecasde terceiros, capazes de resolver o carregamento de módulos. AbibliotecaqueusaremosseráaSystem.js.
AbibliotecaSystem.js(https://github.com/systemjs/systemjs)éum loaderuniversalcomsuporteaos formatosdemóduloAMD,CommonJS, inclusive do ES2015 (ES6), aquele que temosinteresse. Como temos o Node.js instalado como requisito deinfraestrutura, podemosbaixar a biblioteca atravésdonpm, seugerenciadordemódulos.
Precisamos preparar o terreno antes de instalarmos oSystem.js.Oprimeiropassoéabrirmosnossoterminalfavoritoe,com a certeza de estarmos dentro da pastajscangaceiro/client,executarocomando:
17.2INSTALANDOOLOADER
17.2INSTALANDOOLOADER 367
npminit
PodemosteclarEnterparatodasasperguntassemproblemaalgum e, no final, será gerado o arquivo package.json . Empoucaspalavras,estearquivoguardaráonomeeaversãodetodasasdependênciasquebaixarmospelonpm.
O arquivo jscangaceiro/client/package.json criadopossuiaseguinteestrutura:
{"name":"client","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo\"Error:notestspecified\"&&exit1"},"author":"","license":"ISC"}
Agora,aindadentrodapastajscangaceiro/client, vamosinstalaroSystem.jsatravésdocomando:
Oparâmetro--save registra omódulobaixadono arquivopackage.json.Porfim,apósocomandoterminardeexecutar,oSystem.js e todas as suas dependências estarão dentro da pastajscangaceiro/client/node_modules criada automaticamente
pelonpm.
Agora, sem nos assustarmos, removeremos todos os scriptsimportados em client/index.html . O System.js será oresponsável pelo carregamento dos nossos scripts que serãoconvertidosemmódulos.
368 17.2INSTALANDOOLOADER
Vamosimportarabibliotecaeindicarqueoprimeiromóduloasercarregadoseráoclient/app/app.js:
<!--client/index.html--><!--códigoanterioromitido-->
<divid="negociacoes"></div>
<!--importouabibliotecadoSystem.js--><scriptsrc="node_modules/systemjs/dist/system.js"></script>
<!--indicandoqualseráoprimeiromóduloasercarregado-->
<script>System
.import('app/app.js') catch(err console error(err))
</script></body></html>
É muito importante que estejamos acessando nosso projetoatravés do servidor disponibilizado com o projeto, porque oSystem.jsbaixaosmódulosatravésderequisiçõesassíncronascomXMLHttpRequest.
Acessandonossa aplicação pormeio delocalhost:3000 everificando o console do navegador, recebemos a seguintemensagemdeerro:
17.2INSTALANDOOLOADER 369
Figura17.1:Problemanocarregamentodomódulo
Oerroeratotalmenteesperado,poisaindaprecisamosajustarcadascriptquecriamostornando-osmódulosdoES2015(ES6).
Quando adotamos o sistema de módulos do ES2015 (ES6),cada scriptporpadrão seráconsideradoummóduloqueconfinatudo o que declara. Precisaremos configurar cada móduloindicandooqueeleimportaeexporta.
Uma técnica que pode ajudar no processo de ajuste éperguntarmosparaomóduloquais são suasdependências.Essasdependências precisam ser importadas através da instruçãoimport.Depois,precisamosnosperguntaroqueomódulodeve
17.3 TRANSFORMANDO SCRIPTS EMMÓDULOS
370 17.3TRANSFORMANDOSCRIPTSEMMÓDULOS
exportarparaaquelesquedesejamutilizá-lo.Fazemosissoatravésdainstruçãoexport.
Analisando omódulo client/app/app.js, vemos que elepossuiapenasumadependência,aclasseNegociacaoControllerdefinida no módulo NegociacaoController.js . Alteraremosapp.jseimportaremossuadependência:
//client/app/app.js
import{NegociacaoController}from'./controllers/NegociacaoController.js';
constcontroller=newNegociacaoController();const$=document.querySelector.bind(document);
//códigoposterioromitido
Nesse código, estamos importando a classeNegociacaoController do módulo
./controller/NegociacaoController.js.Contudo,issoaindanão funcionará,poisNegociacaoController.js não exporta aclasseNegociacaoController.Precisamosalterá-lo:
exportclassNegociacaoController{
//códigoomitido}
A questão agora é que nosso NegociacaoController
dependedeoutrasclasses.Vamosimportartodaselas:
import{Negociacoes}from'../domain/negociacao/Negociacoes.js';import{NegociacoesView}from'../ui/views/NegociacoesView.js';import{Mensagem}from'../ui/models/Mensagem.js';import{MensagemView}from'../ui/views/MensagemView.js';import{NegociacaoService}from'../domain/negociacao/NegociacaoService.js';
17.3TRANSFORMANDOSCRIPTSEMMÓDULOS 371
import{getNegociacaoDao}from'../util/DaoFactory.js';import{DataInvalidaException}from'../ui/converters/DataInvalidaException.js';import{Negociacao}from'../domain/negociacao/Negociacao.js';import{Bind}from'../util/Bind.js';import{DateConverter}from'../ui/converters/DateConverter.js';
exportclassNegociacaoController{/*códigoomitido*/}
Nãopodemosdeixardeimportarnenhumadependência;casocontrário, ela não será visível para nosso módulo. Agora,precisamosajustarcadaumdosmódulosimportados:
Negociacoes.js:
exportclassNegociacoes{/*códigoomitido*/
}
NegociacoesView.js:
import{View}from'./View.js';import{DateConverter}from'../converters/DateConverter.js';
exportclassNegociacoesViewextendsView{/*códigoomitido*/
}
A classe NegociacoesView precisou importar View eDateConverter , pois são dependências da classe. Vamos
continuar:
Mensagem.js:
exportclassMensagem{/*códigoomitido*/
}
372 17.3TRANSFORMANDOSCRIPTSEMMÓDULOS
MensagemView.js:
import{View}from'./View.js';
exportclassMensagemViewextendsView{/*códigoomitido*/
}
NegociacaoService.js:
import{HttpService}from'../../util/HttpService.js';import{Negociacao}from'./Negociacao.js';
exportclassNegociacaoService{/*códigoomitido*/
}
DaoFactory.js:
import{ConnectionFactory}from'./ConnectionFactory.js';import{NegociacaoDao}from'../domain/negociacao/NegociacaoDao.js';
exportfunctiongetNegociacaoDao{/*códigoomitido*/
}
DataInvalidaException.js:
import{ApplicationException}from'../../util/ApplicationException.js';
exportclassDataInvalidaExceptionextendsApplicationException{
/*códigoomitido*/}
Negociacao.js:
exportclassNegociacao{/*códigoomitido*/
17.3TRANSFORMANDOSCRIPTSEMMÓDULOS 373
}
NegociacaoDao.js:
import{Negociacao}from'./Negociacao.js';
exportclassNegociacaoDao{/*códigoomitido*/
}
DateConverter.js:
import{DataInvalidaException}from'./DataInvalidaException.js';
exportclassDateConverter{/*códigoomitido*/
}
View.js:
exportclassView{/*códigoomitido*/
}
Bind.js:
import{ProxyFactory}from'./ProxyFactory.js';
exportclassBind{/*códigoomitido*/
}
ParaaclasseConnectionFactory,podemosremoveraIIFE,poiscomoestamosusandoosistemademódulonativodoES2015(ES6),variáveise funçõesdomóduloautomaticamentenãoserãoacessíveis fora dele. No caso, basta exportarmos a classeConnectionFactoryparaqueelasejaacessível.
ConnectionFactory.js:
374 17.3TRANSFORMANDOSCRIPTSEMMÓDULOS
conststores=['negociacoes'];letconnection=null;letclose=null;
exportclassConnectionFactory{/*códigoomitido*/
}
ApplicationException.js:
exportclassApplicationExceptionextendsError{/*códigoomitido*/
}
HttpService.js:
exportclassHttpService{/*códigoomitido*/
}
ProxyFactory.js:
exportclassProxyFactory{/*códigoomitido*/
}
Todos os módulos foram configurados. Entretanto, serecarregarmos a aplicação e olharmos no console do navegador,veremosaseguintemensagemdeerro:
Figura17.2:Sucessonagravação
O System.js não conseguiu importar nossosmódulos, apesar
17.3TRANSFORMANDOSCRIPTSEMMÓDULOS 375
deles estarem no formato ES2015 (ES6). É necessária umaconfiguraçãoextraparaque sejam importadospelonosso loader.Essa configuração extra pode ser realizada através de umtranspiler,comoindicadonamensagemdeerro.Masoqueéumtranscompilador?
Umtranscompilador(transpiler)éumcompiladorquepermiterealizar transformações em nosso código, adicionando códigoextraouatémesmotraduzindoocódigo-fontedeumalinguagemparaoutra.
Esseprocessopodeserfeitodiretamentenonavegador,maséaltamentedesencorajadoemprodução,devidoàquantidadeextradescriptsqueserãocarregadosduranteoprocesso.Assim,oneraotempodecarregamentodapáginae,porconseguinte, impactanoranking de pesquisa orgânica do Google. Páginas que sãocarregadasmaisrapidamentesãofavorecidasnesseranking.
Precisamos de um transcompilador que rode localmente, emtempo de desenvolvimento, e que gere os arquivos modificadosque serão carregados pelo navegador. Sem dúvidas, o Babel(https://babeljs.io) é o transcompilador mais utilizado pelacomunidadeopensource.
Sua estrutura baseada em plugins possibilitou a criação dediversas funcionalidades já prontas para uso. É ele queutilizaremos. Porém, antes de instalarmos e configurarmos oBabel,precisamosrealizarumpequenoajusteemnossoprojeto.
Vamos renomear a pastaclient/app para client/app-
17.4OPAPELDEUMTRANSCOMPILADOR
376 17.4OPAPELDEUMTRANSCOMPILADOR
src.Osufixosrc indicaque essapasta armazenaos arquivosoriginaisdoprojeto.
Com a pasta renomeada, podemos passar para a próximaseção,parainstalaroBabel.
QuandousamosBabel,estamosadicionandoemnossoprojetoum build step, ou seja, um passo de construção em nossaaplicação. Isso significa que ela não pode ser consumidadiretamente antes de passar por esse processo. Sendo assim, oprimeiro passo é instalar o Babel em nosso projeto, para entãoconfigurá-lo.
Dentro da pasta jscangaceiro/client , instalaremos obabel-cliatravésdonpm:
Alémdobabel-cli, precisamos instalar o plugin babel-plugin-transform-es2015-modules-systemjsqueadequaráosmódulosdoES2015aosistemadecarregamentosdoSystem.js:
npminstallbabel-plugin-transform-es2015-modules-systemjs@6.24.1--save-dev
Agora,precisamosexplicitarqueopluginquebaixamosdeveserusadopeloBabel.Paraisso,vamoscriaroarquivo.babelrcdentrodejscangaceiro/client,comaseguinteconfiguração:
{"plugins":["transform-es2015-modules-systemjs"]
}
17.5BABEL,INSTALAÇÃOEBUILD-STEP
17.5BABEL,INSTALAÇÃOEBUILD-STEP 377
Com tudo instalado, vamos adicionar um script no arquivopackage.jsonqueexecutaráoBabelparanós.Onomedoscriptserábuild:
{"name":"client","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo\"Error:notestspecified\"&&exit1",
"build":"babelapp-src-dapp--source-maps"},"author":"","license":"ISC","dependencies":{"systemjs":"^0.20.12"
},"devDependencies":{"babel-cli":"^6.24.1","babel-plugin-transform-es2015-modules-systemjs":"^6.24.1"
}}
Vamosanalisaroscriptqueadicionamos:
"build":"babelapp-src-dapp--source-maps"
Já estamos preparados para testar. Tenha certeza de terrenomeadoapastaclient/app paraclient/app-src, comofoipedidoantes.
Vamostestarocomandonpmrunbuild:
npmrunbuild
Após o comando ter sido processado, será criadaautomaticamenteapastaclient/appcomamesmaestruturadepastasearquivosdeclient/app-src.Adiferençaéqueocódigoestátranscompilado.
378 17.5BABEL,INSTALAÇÃOEBUILD-STEP
Vamos abrir o arquivoclient/app/domain/negociacao/Negociacao.js e ver seu
conteúdo:
System.register([],function(_export,_context){"usestrict";
return{setters:[],execute:function(){
classNegociacao{
/*códigoanterioromitido*/}
_export("Negociacao",Negociacao);}
};});
É uma sintaxe bem diferente do arquivo original, massuficiente para que o System.js seja capaz de carregar nossasdependências. Porém, se um erro acontecer em nosso arquivo,veremos a indicação da posição do erro pelo debugger donavegadornoarquivotranscompilado.Precisamossaberaposiçãonoarquivooriginal,poisénelequeprecisamosdarmanutenção.
Aboanotíciaéque,aogerarmosnossosarquivos,oBabelgeratambémumarquivo.map(sourcemap)paracadaumdeles.
Um arquivo sourcemap serve para ajudar no processo dedepuraçãonoprópriobrowser, indicandoo errona estruturadoarquivo original em vez do arquivo transcompilado. Faz todosentido,poisénooriginalquedaremosmanutenção.
17.6SOURCEMAP
17.6SOURCEMAP 379
O carregamento de arquivos .map pode variar entrenavegadores. No Chrome, por exemplo, eles serão carregadosapenas quando o console do navegador for acessado. Já outrosnavegadores necessitam que o carregamento seja explicitadoatravés de alguma opção de desenvolvedor. Agora nada nosimpedederealizarmosumteste.
Acessando mais uma vez http://localhost:3000 erecarregandonossapágina,elacontinuafuncionandocomoantes.Vejaqueagorasóprecisamosindicarqualseráoprimeiromóduloasercarregadopelonossoloaderqueeleseencarregaráderesolvertodas as suas dependências, removendo assim essaresponsabilidadedodesenvolvedor.
Além do que vimos, não conseguimos mais acessar asdefiniçõesdasclassesqueantesviviamnoescopoglobalatravésdoconsole. Ou seja, não há mais nenhuma informação da nossaaplicaçãonoescopoglobal.
Casoalgumerro tenhaacontecido, é importanteverificarmosse exportamos e importamos corretamente todos os artefatos daaplicação,paraemseguidaexecutarmosmaisumavezocomandonpmrunbuild.Aliás, terdeexecutartodavezestecomandoa
cada modificação é jogar muita responsabilidade no console dodesenvolvedor. Na próxima seção, aprenderemos a automatizaresseprocesso.
Podemosmelhoraraexperiênciadodesenvolvedorrealizando
17.7 COMPILANDO ARQUIVOS EM TEMPOREAL
380 17.7COMPILANDOARQUIVOSEMTEMPOREAL
o processodetranscompilaçãoautomaticamentetodavezqueumarquivoforalteradonoprojeto.Aaçãodeveserrealizadaquandofazemosodeploy,paragarantirqueestátudocompilado.
Mas será que o desenvolvedor vai querer ter estaresponsabilidade a todomomento? Por isso, o Babel vem comowatcher, umobservadorde arquivosque automaticamente faráoprocessodetranscompilaçãoquandoumarquivoforalterado.Parahabilitá-lo, vamos no arquivopackge.json e adicionaremos owatch:
{"name":"client","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo\"Error:notestspecified\"&&exit1","build":"babelapp-src-dapp--source-maps","watch":"babelapp-src-dapp--source-maps--watch"
},"author":"","license":"ISC","devDependencies":{"babel-cli":"^6.24.1",
"babel-plugin-transform-es2015-modules-systemjs":"^6.24.1"},"dependencies":{"systemjs":"^0.20.12"
}}
Noterminal,vamosexecutarowatch:
npmrunwatch
Elevaicompilarosarquivoseoterminalficarámonitorandoamodificaçãodetodoseles.Oprocessodecompilaçãocorrerábeme, ao recarregarmos a página de cadastro, tudo funcionará
17.7COMPILANDOARQUIVOSEMTEMPOREAL 381
corretamente.
Demosumpassoextra introduzindoumtranscompiladoremnossoprojeto,masseráquepodemosiralém?Comcerteza!
AprendemosautilizarosistemademódulosdoES2015(ES6),evitando assim o escopo global e a responsabilidade decarregarmos scripts em uma ordem correta. Todavia, podemosorganizaraindamelhornossasimportações.
Vejamos primeiro como fizemos as importações emNegociacaoController:
//client/app-src/controllers/NegociacaoController.js
//APENASREVISÃODECÓDIGO
import{Negociacoes}from'../domain/negociacao/Negociacoes.js';import{NegociacoesView}from'../ui/views/NegociacoesView.js';import{Mensagem}from'../ui/models/Mensagem.js';import{MensagemView}from'../ui/views/MensagemView.js';import{NegociacaoService}from'../domain/negociacao/NegociacaoService.js';import{getNegociacaoDao}from'../util/DaoFactory.js';import{DataInvalidaException}from'../ui/converters/DataInvalidaException.js';import{Negociacao}from'../domain/negociacao/Negociacao.js';import{Bind}from'../util/Bind.js';import{DateConverter}from'../ui/converters/DateConverter.js';
exportclassNegociacaoController{
//códigoposterioromitido
17.8 BARREL, SIMPLIFICANDO AIMPORTAÇÃODEMÓDULOS
382 17.8BARREL,SIMPLIFICANDOAIMPORTAÇÃODEMÓDULOS
Reparequeimportamosváriosartefatosdeumamesmapasta,aliás, repetindo a instrução import diversas vezes. Podemossimplificarbastanteessaimportaçãoatravésdebarrels.Umbarrelnadamaisédoqueummóduloqueimportaeexportaosmódulosque importou. Com isso, podemos importar em uma únicainstruçãováriosartefatosexportadospelobarrel.Umexemplo:
//EXEMPLO,AINDANÃOENTRANAAPLICAÇÃO
import{Negociacoes,NegociacaoService,Negociacao}from'../domain/index';
Vamos começar criando o barrel para a pasta app-
src/models,atravésdoarquivoapp-src/models/index.ts:
//client/app-src/domain/index.ts
export*from'./negociacao/Negociacao.js';export*from'./negociacao/NegociacaoDao.js';export*from'./negociacao/NegociacaoService.js';export*from'./negociacao/Negociacoes.js';
Agora,faremosamesmacoisaparaclient/app-src/ui:
//client/app-src/ui/index.js
export*from'./views/MensagemView.js';export*from'./views/NegociacoesView.js';export*from'./views/View.js';export*from'./models/Mensagem.js';export*from'./converters/DataInvalidaException.js';export*from'./converters/DateConverter.js';
Porfim,faremosemclient/app-src/utils:
//client/app-src/util/index.js
export*from'./Bind.js';export*from'./ConnectionFactory.js';export*from'./DaoFactory.js';export*from'./ApplicationException.js';
17.8BARREL,SIMPLIFICANDOAIMPORTAÇÃODEMÓDULOS 383
export*from'./HttpService.js';export*from'./ProxyFactory.js';
Eentão,alteraremosNegociacaoController:
import{Negociacoes,NegociacaoService,Negociacao}from'../domain/index.js';import{NegociacoesView,MensagemView,Mensagem,DataInvalidaException,DateConverter}from'../ui/index.js';import{getNegociacaoDao,Bind}from'../util/index.js';
exportclassNegociacaoController{
//códigoposterioromitido
Muitomenoslinhasdecódigo!
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/17
384 17.8BARREL,SIMPLIFICANDOAIMPORTAÇÃODEMÓDULOS
CAPÍTULO18
"Sertãoéisto:osenhorempurraparatrás,masderepenteelevolta a rodear o senhor dos lados. Sertão é quandomenos seespera."-GrandeSertão:Veredas
Nocapítuloanterior,comautilizaçãodemódulosdoES2015(ES6), resolvemos tanto o problema do escopo global quanto daordem de importação de scripts. Ainda há uma melhoria quepodemos realizar em nosso código, mas primeiro, vamosrelembrardaimportânciadasPromisesnele.
Utilizamos o padrão de projeto Promise para evitarmos ocallbackHELL, além de centralizarmos em um único lugar o
tratamentodeerrodasPromisesenvolvidasnaoperação.Vejamoso código que implementamos no método _init() deNegociacaoController:
//REVISANDOAPENAS!
//client/app-src/controllers/NegociacaoController.js//códigoanterioromitido
_init(){
INDOALÉM
18INDOALÉM 385
.getNegociacaoDao().then(dao=>dao.listaTodos()).then(negociacoes=>
negociacoes.forEach(negociacao=>this._negociacoes.adiciona(negociacao)))
.catch(err=>this._mensagem.texto=err);//TRATAOERROEMUMLUGARAPENAS}
//códigoposterioromitido
Contudo, mesmo a solução com Promises não torna nossocódigo tão legível quanto a escrita de um código síncrono ebloqueante.Vejamosumcódigohipotético:
//EXEMPLODECOMOSERIASEOCÓDIGO//FOSSEESCRITODEMANEIRASÍNCRONAEBLOQUEANTE
_init(){
try{
//bloquearáaexecuçãodaaplicaçãoenquantonãoterminarconstdao=getNegociacaoDao();
//bloquearáaexecuçãodaaplicaçãoenquantonãoterminarconstnegociacoes=dao.listaTodos();
negociacoes.forEach(negociacao=>this._negociacoes.adiciona(negociacao)))}catch(err){
this._mensagem.texto=err;}}
Neste exemplo, a funçãogetNegociacaoDao() e ométododao.listaTodos() são síncronos e bloqueantes. Por serem
síncronos,podemoscapturarexceçõesatravésdascláusulastryecatch, pois o erro acontece na mesma pilha de execução do
nossoprograma.
386 18INDOALÉM
Porém,paraqueocódigoanteriornãoafeteaperformancedaaplicação,cadachamadadométodo_init()deveserexecutadaemumathread em separado, paranãobloquear o restantedaaplicação. Linguagens como Java e C# dão suporte amultithreading,resolvendooproblemadobloqueiodaaplicação.
O JavaScript é single thread (apesar de hoje podermos criarthreads levescomWebWorkers,quenãosão tão flexíveisquantothreads em outras linguagens), por isso não podemos executaroperaçõesdeI/Odemaneirabloqueante.Issoporque,seotempodeexecuçãodométodolistaTodos()demorassetrêssegundos,a aplicação ficaria congelada durante esse tempo até voltar arespondereprocessaroutrasoperaçõesexecutadaspelousuário.
Contudo, pormais estranho que isso possa soar, a partir doES2017 (ES8) já podemos escrever um código assíncrono nãobloqueante, muito parecido com a maneira que escrevemos umcódigo síncrono bloqueante, impactando diretamente nalegibilidade do código da nossa aplicação. Vejamos como napróximaseção.
A versão ECMAScript 2017 introduziu a sintaxeasync/await que permite escrevermos códigos assíncronos
envolvendoPromisesdemaneiraelegante.Vejamosmaisumavezo método_init(),masdessavezescritocomanovasintaxe:
//APENASUMEXEMPLO
async_init(){
18.1 ES2017 (ES8) E O AÇÚCAR SINTÁTICOASYNC/AWAIT
18.1ES2017(ES8)EOAÇÚCARSINTÁTICOASYNC/AWAIT 387
try{constdao=awaitgetNegociacaoDao();constnegociacoes=awaitdao.listaTodos();negociacoes.forEach(negociacao=>this._negociacoes.adiciona(
negociacao)))}catch(err){this._mensagem.texto=err;
}}
Vejam que a estrutura do nosso código é extremamenteparecida com a estrutura hipotética síncrona que vimos. Adiferença está emdois pontos.Oprimeiro é a instruçãoasyncusadaantesdonomedométodo_init().Essainstruçãoindicaque a função estápreparadapara executar operações assíncronasquepodemseresperadas.
Osegundopontoéapresençadainstruçãoawaitantesdaschamadas de métodos que devolvem Promises . QuandogetNegociacaoDao() for chamado, o bloco do método
_init()serásuspenso,saindodapilhadeexecuçãoprincipaldaaplicação.
Seonossométodonãofazmaispartedapilhadeexecuçãodathread principal, o JavaScript continuará a executar outrasoperações.Maseagora?Nossométodo_init()caiunolimbo?Sim,masatéqueaPromiseretornadaporgetNegociacaoDao()sejaresolvida.
QuandoaPromise,quejáéumaoperaçãoassíncronaenãobloqueante, for resolvida, ela internamente fará um resume donosso método _init() . Isso o fará voltar para a pilha deexecução principal e ficará disponível para ser processado peloEventLoopdoJavaScript.
388 18.1ES2017(ES8)EOAÇÚCARSINTÁTICOASYNC/AWAIT
Quando nosso método _init() volta a ser executado,recebemos como retorno o valor já resolvido da Promise dainstruçãogetNegociacaoDao().Queloucura!
Esseprocesso se repetiránapróxima instruçãocoma sintaxeawait antesda chamadadométododao.listaTodos(), que
tambémdevolveumaPromise.Porfim,podemostratarqualquerrejeiçãodasPromisesdentrodoblocotry ecatch, algoquenãoerapossívelantes.Issoépossívelporque,quandohouverumarejeição de uma Promise, também haverá o resume do método_init() e a causada rejeição será lançada comoumaexceção,
permitindo identificar com clareza o ponto em que o erroaconteceuemnossocódigo.
Asintaxeasync/awaitnaverdadeéumaçúcarsintáticoparao uso concomitante de Promises com funções geradoras(generators).
Generatorssãofunçõesquepermitemsuspendereresumirsuaexecuçãoemqualquerpontodoseubloco,mantendoseucontextooriginal. Outra característica é que podem retornar um valormesmo sem o uso da instrução return. Vamos construir umsimplesexemplo:
//EXEMPLODEFUNÇÃOGERADORA
function*minhaFuncaoGeradora(){
}
//itéumITERATORletit=minhaFuncaoGeradora();
18.2PARASABERMAIS:GENERATORS
18.2PARASABERMAIS:GENERATORS 389
Generators possuem uma declaração especial, e possuem apalavra-chavefunction seguidadeum* (asterisco).QuandoexecutamosminhaFuncaoGeradora(), recebemos como retornonãooseuresultado,masumIterator,umobjetoespecialquenospermiteresumirogeneratore,eventualmente,extrairvalores.
A instrução yield é aquela responsável em suspender aexecução do bloco do gerador e também, quando especificado,retornar um valor que pode ser acessado através do Iterator. Énecessárioqueovalorvenhaàdireitadainstrução:
//EXEMPLODEFUNÇÃOGERADORA
function*minhaFuncaoGeradora(){
for(leti=0;i<3;i++){
//INSTRUÇÃOYIELD!//SUSPENDEAEXECUÇÃODOBLOCODAFUNÇÃO//ERETORNAOVALORDEI
yieldi;}}
letit=minhaFuncaoGeradora();
Quando executarmos uma função geradora, seu bloco aindanão será executado. É por isso que obrigatoriamente precisamoschamar pelomenos uma vez ométodoit.next() do Iteratorretornado. Com a chamada it.next() , o bloco da funçãogeradoraseráexecutadoatéquesedeparecomainstruçãoyieldquevaisuspendê-lo.
//EXEMPLODEFUNÇÃOGERADORA
function*minhaFuncaoGeradora(){
390 18.2PARASABERMAIS:GENERATORS
for(leti=0;i<3;i++){
yieldi;}}
letit=minhaFuncaoGeradora();
//EXECUTAOBLOCODOGENERATOR,QUESERÁSUSPENSO//ASSIMQUEENCONTRARAINSTRUÇÃOYIELD
letobjeto=it.next();
A funçãoit.next() nos devolve um objeto que possui aspropriedades value e done . O primeiro nos dá acesso aqualquer valor retornado pela instrução yield, já o segundoindica através de true ou false se o bloco da função dogeneratorchegouaofim.
//EXEMPLODEFUNÇÃOGERADORA
function*minhaFuncaoGeradora(){
for(leti=0;i<3;i++){
//aosuspender,forneceaoiteratorovalordeiyieldi;
}}
letit=minhaFuncaoGeradora();letobjeto=it.next();console.log(objeto.value)//0éovalordeiconsole.log(objeto.done)//false,aindanãoterminou
Podemos chamar várias vezesit.next() para resumirmosnossafunção:
//EXEMPLODEFUNÇÃOGERADORA
function*minhaFuncaoGeradora(){
18.2PARASABERMAIS:GENERATORS 391
for(leti=0;i<3;i++){
yieldi;}}
letit=minhaFuncaoGeradora();
//EXECUTAOBLOCODOGENERATOR,QUESERÁSUSPENSO//ASSIMQUEENCONTRARAINSTRUÇÃOYIELD
letobjeto=it.next();console.log(objeto.value)//0console.log(objeto.done)//falseobjeto=it.next();console.log(objeto.value)//1console.log(objeto.done)//falseobjeto=it.next();console.log(objeto.value)//2console.log(objeto.done)//falseobjeto=it.next();console.log(objeto.value)//undefinedconsole.log(objeto.done)//true
Porfim,podemossimplificarnossocódigodestaforma:
//EXEMPLODEFUNÇÃOGERADORA
function*minhaFuncaoGeradora(){
for(leti=0;i<3;i++){
yieldi;}}
letit=minhaFuncaoGeradora();
letobjeto=null;
while(!(objeto=it.next()).done){console.log(objeto.value);
}
392 18.2PARASABERMAIS:GENERATORS
O mais importante para nós aqui é entendermos o papelsimplificadordeasync/awaitnousodeGeneratorsePromises.
Chegou a hora de usarmos async/wait em nosso projeto,poisjávimoscomoelefuncionaesuasvantagens.Vamoscomeçarpelométodoqueusamoscomoexemplohipotético,masqueagoramodificaremosefetivamente.
Vamos modificar o método _init() deNegociacaoController para que faça uso da instrução
async/await:
//client/app-src/controllers/NegociacaoController.js//códigoanterioromitido
async_init(){
try{constdao=awaitgetNegociacaoDao();constnegociacoes=awaitdao.listaTodos();negociacoes.forEach(negociacao=>this._negociacoes.adici
ona(negociacao));}catch(err){
//err.messageextraiapenasamensagemdeerrodaexceçã
this._mensagem.texto=err.message;}
}
//códigoposterioromitido
Lembre-sedequesópodemosutilizara sintaxeawait parauma Promise dentro de uma função async ; caso contrário,
18.3 REFATORANDO O PROJETO COMASYNC/WAIT
18.3REFATORANDOOPROJETOCOMASYNC/WAIT 393
receberemosamensagemdeerro"awaitisareservedword".
Agora,vamosalterarométodoadiciona():
//client/app-src/controllers/NegociacaoController.js//códigoanterioromitido
asyncadiciona(event){
try{
event.preventDefault();constnegociacao=this._criaNegociacao();
constdao=awaitgetNegociacaoDao();awaitdao.adiciona(negociacao);this._negociacoes.adiciona(negociacao);this._mensagem.texto='Negociaçãoadicionadacomsucesso
;this._limpaFormulario();
}catch(err){
this._mensagem.texto=err.message;}
}
//códigoposterioromitido
Ométodoapaga()também:
//client/app-src/controllers/NegociacaoController.js//códigoanterioromitido
asyncapaga(){
try{constdao=awaitgetNegociacaoDao();awaitdao.apagaTodos();this._negociacoes.esvazia();this._mensagem.texto='Negociaçõesapagadascomsucesso'
;
}catch(err){
394 18.3REFATORANDOOPROJETOCOMASYNC/WAIT
this._mensagem.texto=err.message;}
}
//códigoposterioromitido
Antes de alterarmos o método importaNegociacoes() ,alteraremos obtemNegociacoesDoPeriodo deNegociacaoService para que faça uso do async/await. O
métodoficaráassim:
//client/app-src/domain/negociacao/NegociacaoService.js
//códigoanterioromitido
asyncobtemNegociacoesDoPeriodo(){
try{letperiodo=awaitPromise.all([
this.obtemNegociacoesDaSemana(),this.obtemNegociacoesDaSemanaAnterior(),this.obtemNegociacoesDaSemanaRetrasada()
]);returnperiodo
.reduce((novoArray,item)=>novoArray.concat(item),[])
.sort((a,b)=>b.data.getTime()-a.data.getTime());
}catch(err){console.log(err);
thrownewError('Nãofoipossívelobterasnegociaçõesdoperíodo')
};}
Porfim,vamosvoltarparaaclasseNegociacaoControllereadequarométodoimportaNegociacoes():
//client/app-src/controllers/NegociacaoController.js//códigoanterioromitido
18.3REFATORANDOOPROJETOCOMASYNC/WAIT 395
asyncimportaNegociacoes(){
try{constnegociacoes=awaitthis._service.obtemNegociacoesD
oPeriodo();console.log(negociacoes);negociacoes.filter(novaNegociacao=>
!this._negociacoes.paraArray().some(negociacaoExistente=>
novaNegociacao.equals(negociacaoExistente)))
.forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensagem.texto='Negociaçõesdoperíodoimportadascomsucesso';
}catch(err){
this._mensagem.texto=err.message;}
}
Eparacompletar,vamosalterarDaoFactory.js:
//client/app-src/util/DaoFactory.jsimport{ConnectionFactory}from'./ConnectionFactory.js';import{NegociacaoDao}from'../domain/negociacao/NegociacaoDao.js';
exportasyncfunctiongetNegociacaoDao(){
letconn=awaitConnectionFactory.getConnection();returnnewNegociacaoDao(conn);}
Casotenhamoscometidoalgumerro,oprópriocompiladordoBabelnosindicaráondeerramos.Bastaolharmosamensagemdeerroapresentadanoconsole.
A sintaxe async/await , como vimos, é um recurso doES2017 (ES8). É suportado a partir do Chrome 55, tanto para
396 18.3REFATORANDOOPROJETOCOMASYNC/WAIT
desktop quanto para Android, do Opera 42, do Firefox 52 e doEdge15.Todavia,comoauxíliodoBabel,podemostranscompilarnosso código para ES2015 (ES6), garantindo assim uma maiorfolgaemtermosdecompatibilidadeentrenavegadores.
Para que possamos transcompilar nosso código de ES2017(ES8)paraES2015 (ES6),precisamos instalarumpreset doBabelcomestafinalidade.
Dentro da pasta jscangaceiro/client, vamos executar ocomando:
Estamos quase lá! Precisamos agora incluir o preset queacabamosdebaixarnalistadepresetsusadospeloBabelduranteoprocesso de transcompilação. Alterandojscangaceiro/.babelrc:
{"presets":["es2017"],"plugins":["transform-es2015-modules-systemjs"]
}
Quando executarmoso comandonpmrunwatch ou npmrunbuildnossopreset entraráemaçãoe realizaráa conversãodeasync/awaitparaousodePromise.
Por exemplo, se pegarmos uma parte do código deDaoFactory.js antes e depois da compilação, vemos uma
mudançaclaraemsuaestrutura:
18.4 GARANTINDO A COMPATIBILIDADECOMES2015
18.4GARANTINDOACOMPATIBILIDADECOMES2015 397
Antes:
//client/app/util/DaoFactory.js//códigoanterioromitido
asyncfunctiongetNegociacaoDao(){
letconn=awaitConnectionFactory.getConnection();returnnewNegociacaoDao(conn);
}
//códigoposterioromitido
Depois,comopreset:
//client/app/util/DaoFactory.js//códigoanterioromitido
functiongetNegociacaoDao(){return_asyncToGenerator(function*(){
letconn=yieldConnectionFactory.getConnection();returnnewNegociacaoDao(conn);})();
}
//códigoposterioromitido
É bom prevenir de todos os lados, como um verdadeirocangaceirofaz!
Agoraque temosuma formaunificadade lidarcomexceçõesatravésdetry/catch,podemosconvencionarque todaexceçãodo tipo ApplicationException é de negócio e será exibidadiretamente para o usuário. As demais exceções serão logadas,paraqueodesenvolvedor investiguesuacausa,eumamensagem
18.5LIDANDOMELHORCOMEXCEÇÕES
398 18.5LIDANDOMELHORCOMEXCEÇÕES
genéricaseráexibidaemseulugar.
Vamos criarmos uma função que saiba identificar o tipo daexceção. Faremos isso no próprio móduloclient/app/util/ApplicationException.js:
//client/app-src/util/ApplicationException.js
exportclassApplicationExceptionextendsError{
constructor(msg=''){
super(msg);this.name=this.constructor.name;
}}
//hackdoSystem.jsparaqueafunçãotenhaacessoàdefiniçãodaclasse
constexception=ApplicationException;
exportfunctionisApplicationException(err){
returnerrinstanceofexception||Object.getPrototypeOf(err)instanceofexception
}
Object.getPrototypeOf() faz parte da especificaçãoECMAScriptqueveiosubstituirapropriedade__proto__.Elaretornaoprototypedeobjeto.
A função que acabamos de criar verifica se o tipo ou oprototypedaexceçãosãoinstânciasdeApplicationException.O teste do prototype é importante, pois, sem ele,DataInvalidaExceptionretornariafalse.
18.5LIDANDOMELHORCOMEXCEÇÕES 399
Agora,criaremosmaisumafunçãoqueretornaamensagemdaexceçãoouamensagemgenérica,casoaexceçãonãosejadotipoApplicationException:
//client/app-src/util/ApplicationException.js
//códigoanterioromitido
//novafunção
exportfunctiongetExceptionMessage(err){
if(isApplicationException(err)){returnerr.message;
}else{console.log(err);return'Nãofoipossívelrealizaraoperação.';
}}
Excelente. Agora, em client/app-
src/controllers/NegociacaoController.js, importaremos afunçãogetExceptionMessage:
//client/app-src/controllers/NegociacaoController.js
import{Negociacoes,NegociacaoService,Negociacao}from'../domain/index.js';
//removeuDataInvalidaException
import{NegociacoesView,MensagemView,Mensagem,DateConverter}from'../ui/index.js';
//importougetExceptionMessage
import{getNegociacaoDao,Bind,getExceptionMessage}from'../util/index.js';
Com a função importada, vamos alterar todos os blocoscatchpara:
400 18.5LIDANDOMELHORCOMEXCEÇÕES
//todososblocoscatchdevemficarassim
}catch(err){
this._mensagem.texto=getExceptionMessage(err);}
Queremos que nossa classe NegociacaoService tambémlance exceções de negócio em vez de Error , para que amensagemdealtonívelsejaexibidaparaousuário.Então,vamosalterar client/app-
src/domain/negociacao/NegociacaoService.js . Primeiro,importaremosApplicationException:
//client/app-src/domain/negociacao/NegociacaoService.js
import{HttpService}from'../../util/HttpService.js';import{Negociacao}from'./Negociacao.js';import{ApplicationException}from'../../util/ApplicationException.js';
Agora, vamos substituir todas as instruções new Error(/*mensagem*/) pornew ApplicationException(/* mensagem*/).
Comasalteraçõesque fizemos,qualquerexceção lançadaporNegociacaoService será capturada no bloco catch emNegociacaoController , e sua mensagem de alto nível será
exibidaparaousuário.
NossaaplicaçãoacessaumaAPIexternaatravésderequisiçãoAjax,assíncronapornatureza.Contudo,éumaboapráticaevitar
18.6 DEBOUNCE PATTERN: CONTROLANDOAANSIEDADE
18.6DEBOUNCEPATTERN:CONTROLANDOAANSIEDADE 401
que a API seja "metralhada" por uma sucessão de requisiçõesindevidas.Nuncasabemossenossosusuáriossãoansiosos,não?
Nocasodanossaaplicação,nadanosimpededeclicarmos100vezes no botão "Importar Negociações" que realizará 100requisições para a API, apesar de não permitirmos maisimportações duplicadas. Podemos solucionar o problema queacabamosdeverpostergandoaaçãoquedesejamosexecutar.
Se,duranteumajaneladetempo,amesmaaçãoforexecutada,aanteriorserácanceladaepassaráavaleraúltima.Dessaforma,seo usuário clicar 100 vezes no botão"Importar Negociações"em uma janela de tempo de 1 segundo, apenas o último cliqueefetivamenteexecutaráaação.
Asoluçãoqueacabamosdeveré tãocorriqueiranouniversoJavaScript que se tornou um padrão, inclusive ganhou o nomeDebounce.Vamosimplementá-la!
Criaremos omóduloclient/app-src/util/Debounce.ts.O módulo exportará a função debounce que receberá comoparâmetroafunção alvo e a janelade tempo emmilissegundos.Seu retorno será uma nova função, que encapsula a funçãooriginal que desejamos aplicar Debounce através de umtemporizador. É esta nova função que associaremos ao eventoclickdobotãoresponsávelpelaimportaçãodenegociações:
//client/app-src/util/Debounce.ts
exportfunctiondebounce(fn,milissegundos){
18.7 IMPLEMENTANDO O DEBOUNCEPATTERN
402 18.7IMPLEMENTANDOODEBOUNCEPATTERN
return()=>{
//usaumtemporizadorparachamarfn()//depoisdetantosmilissegundossetTimeout(()=>fn(),milissegundos);
}}
Não podemos nos esquecer de exportarmos Debounce.jsatravésdobarrelclient/app-src/util/index.js.Alterandooarquivo:
//client/app-src/util/index.js
export*from'./Bind.js';export*from'./ConnectionFactory.js';export*from'./DaoFactory.js';export*from'./ApplicationException.js';export*from'./HttpService.js';export*from'./ProxyFactory.js';
//adicionouaexportação!export*from'./Debounce.js';
Sabemos que nossa função não está pronta, mas vamosimportá-la em client/app-src/app.ts e utilizá-la para oeventoclickdobotãoqueimportaasnegociações:
//client/app-src/app.ts
import{NegociacaoController}from'./controllers/NegociacaoController.js';
//importandoafunção
import{debounce}from'./util/index.js';
constcontroller=newNegociacaoController();
//códigoanterioromitido
18.7IMPLEMENTANDOODEBOUNCEPATTERN 403
$('#botao-importa').addEventListener('click',debounce(()=>
controller.importaNegociacoes.bind(controller),1000));
Em vez de usarmoscontroller.importaNegociacoes.bind(controller) , vamos
passar uma função que chamarácontroller.importaNegociacoes().Issonospermitirácolocaralgumas informações no console para nos ajudar a entender ocomportamentodonossocódigo.
//client/app-src/app.ts
import{NegociacaoController}from'./controllers/NegociacaoController.js';import{debounce}from'./util/index.js';
constcontroller=newNegociacaoController();
//códigoanterioromitido
$('#botao-importa').addEventListener('click',debounce(()=>{
console.log('EXECUTOUAOPERAÇÃODODEBOUNCE');controller.importaNegociacoes();
},1000));
Do jeito que está, se clicarmos 100 vezes em menos de umsegundo, cada ação será executada após um segundo. Ocomportamentodesejadoéqueapenasumaaçãosejaexecutada.
Agora, sóprecisamos lidar como cancelamentodas açõesnajaneladeumsegundo.Todosasaçõesqueforemexecutadascomoclique do botão precisam referenciar o mesmo timer para quepossampararoanteriorecriarumnovo.Paraisso,vamosdeclararavariáveltimernoescopodedebounce:
//client/app-src/util/Debounce.ts
404 18.7IMPLEMENTANDOODEBOUNCEPATTERN
exportfunctiondebounce(fn,milissegundos){
//guardaoIDdeumtimer,0indicaquenãohánenhumlettimer=0;
return()=>{setTimeout(()=>fn(),milissegundos);
}}
Afunçãoretornadapordebounceéaquelaqueserádisparadapeloeventoclickdobotão.Sendoassim,aprimeiracoisaqueprecisamos fazer é cancelar qualquer timer que já esteja emexecução,paraemseguidadefinirmosumnovotimer:
//client/app-src/util/Debounce.ts
exportfunctiondebounce(fn,milissegundos){
lettimer=0;return()=>{
//paraoúltimotimerdefinidoclearTimeout(timer);
//avariáveltimerganhaoIDdeumnovotemporizador//afetaavariávelnoescopodafunçãodebouncetimer=setTimeout(()=>fn(),milissegundos);
}}
Vamos recapitular. Quando debounce for invocado,retornaráumanovafunção.Éessanovafunçãoqueseráassociadaao evento "click" do botão. Quando o evento for disparado, afunção parará qualquer timer que esteja em andamento e criaráumnovotimer.
Senenhumaoutraaçãoforempreendidanajaneladetempodeum segundo, a função passada como primeiro parâmetro de
18.7IMPLEMENTANDOODEBOUNCEPATTERN 405
debounce será executada. Porém, se outro clique for realizadoantesda janeladeumsegundo terminar,o temporizadorvigenteserácanceladoeumnovotomaráoseulugar.
Porfim,podemosutilizarafunçãodebounce paraqualquereventoemnossaaplicaçãoquepreciseexecutarapenasumaaçãodentrodeumajaneladetempo.
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/18
406 18.7IMPLEMENTANDOODEBOUNCEPATTERN
CAPÍTULO19
"Ser ninja não é mais suficiente, é preciso ser cangaceiro" -FlávioAlmeida
Nocapítuloanterior,implementamosopadrãoDebounceparagarantir que a chamada do método importaNegociacoes deNegociacaoController fosse feita apenas uma vez dentro da
janeladetempodeumsegundo:
//app-src/app.js//códigoanterioromitido
//RELEMBRANDOAPENAS!
$('#botao-importa').addEventListener('click',debounce(()=>{
console.log('EXECUTOUAOPERAÇÃODODEBOUNCE');controller.importaNegociacoes();
},1000));
IsolamoselegantementealógicadopatternDebounceemumafunçãoparaquefossepossívelreaproveitá-laemoutroseventosdanossa aplicação. Contudo, se quisermos utilizar nossoNegociacaoControlleremoutrosprojetos,teremosdelembrarde realizar o debounce para qualquer evento que chame o
CHEGANDOAOLIMITE
19CHEGANDOAOLIMITE 407
método.
Umapossívelsoluçãoseriaadicionarachamadadedebouncediretamente no método importaNegociacoes , para termoscertezadequeelasejaaplicadaindependentedoeventonoqualométodo é chamado. Entretanto, isso poluiria ométodo com umcódigo que não lhe diz respeito, dificultando sua legibilidade.Apesardenãotermosumasoluçãoaparente,podemosconsegui-laatravésdopadrãodeprojetoDecorator.
Decorator é um padrão de projeto de software que permiteadicionarumcomportamentoaumobjeto jáexistenteemtempode execução, ou seja, agrega dinamicamente responsabilidadesadicionaisaumobjeto.
Indo direto ao ponto, queremos o mesmo resultado que játemoscomapenasestaalteraçãoemnossaaplicação:
//CÓDIGONÃOCOMPILA,APENASEXEMPLO
//app-src/controllers/NegociacaoController.js
//códigoanterioromitido
//DECORANDOOMÉTODO
@debounce()asyncimportaNegociacoes(){
//códigoomitido
}//códigoposterioromitido
Comasintaxequeacabamosdever,aaplicaçãododebounce
19.1OPADRÃODEPROJETODECORATOR
408 19.1OPADRÃODEPROJETODECORATOR
através de um Decorator é muito menos invasiva, pois nãoprecisamosmisturara lógicadesuachamadadentrodoblocodométodo. Além disso, independente do lugar que o métodoimportaNegociacoesforchamado,oDecoratorseráaplicado.
Apesar de muito elegante, não há suporte oficial para esterecurso no ECMAScript. Há uma proposta em andamento paraque Decorators se tornem padrão na linguagem(https://github.com/tc39/proposal-decorators). Vamos desistir deusá-lo?Comcerteza,não.
Uma das grandes vantagens de utilizarmos umtranscompiladoréacapacidadedeusarmosrecursosmodernosdalinguagemantesmesmoquesetornemoficiais,ouantesmesmodesua implementação nos navegadores. Como já estamos usandoBabel, basta adicionarmos umplugin para termos suporte a essefantásticorecurso.
Nãofiquecomreceiodeutilizaresterecursoexperimental.Porexemplo,alinguagemTypeScript,umsupersetdoECMAScript2015 criado pelaMicrosoft, também faz uso experimental deDecorators, que são suportados através de um processo detranscompilação fornecido pelo próprio compilador dalinguagem.
Semdelongas, vamos ativar essapoderosa funcionalidade emnossoprojeto.
19.1OPADRÃODEPROJETODECORATOR 409
Noterminal,dentrodapastaclient,vamosinstalaropluginbabel-plugin-transform-decorators-legacy.
npminstallbabel-plugin-transform-decorators-legacy@1.3.4--save-dev
Emseguida,noarquivoclient/.babelrc,adicionaremosopluginnalistadeplugins:
{"presets":["es2017"],
"plugins":["transform-es2015-modules-systemjs","transform-decorators-legacy"]}
Vamosalterarapp-src/app.jspararemovermosachamadadanossafunçãodebounce,quesetornaráumDecorator.Nossocódigovoltaráparaoestágioanterior,antesdousodafunção:
//app-src/app.js
import{NegociacaoController}from'./controllers/NegociacaoController.js';
//NÃOIMPORTAMAISDEBOUNCE
//códigoanterioromitido
//voltouparaaformaanterior$('#botao-importa').addEventListener('click',controller.importaNegociacoes.bind(controller));
Antes de continuarmos, há uma pegadinha noVisual StudioCode,casovocêoestejautilizando.Quandoeleencontrarasintaxe@NomeDoDeCorator,exibiráaseguintemensagemdeerro:
19.2 SUPORTANDO DECORATOR ATRAVÉSDOBABEL
410 19.2SUPORTANDODECORATORATRAVÉSDOBABEL
[js]Experimentalsupportfordecoratorsisafeaturethatissubjecttochangeinafuturerelease.Setthe'experimentalDecorators'optiontoremovethiswarning.
Para silenciarmos o Visual Studio Code, basta criarmos oarquivoclient/jsconfig.jsoncomoseguinteconteúdo:
{"compilerOptions":{
"experimentalDecorators":true}
}
ÉmuitoimportantequeapastaclientsejaabertanoVisualStudioCodeparaqueeleencontreoarquivojsconfig.json.Seabrirmos diretamente client/app-src , o arquivo não seráencontradoeamensagemdeerrocontinuaráaserexibida.
Agoraquejátemostudonolugar,daremosinícioàcriaçãodonosso Decorator. Aliás, podemos apagar o arquivo app-
src/util/Debounce.js , inclusive remover sua exportação deapp-src/util/index.js,quenofinalficaráassim:
//app-src/util/index.js
export*from'./Bind.js';export*from'./ConnectionFactory.js';export*from'./DaoFactory.js';export*from'./ApplicationException.js';export*from'./HttpService.js';export*from'./ProxyFactory.js';
//REMOVEUOEXPORTDOARQUIVOQUEACABAMOSDEAPAGAR
Vamos criar o arquivo app-
src/util/decorators/Debounce.js.Seuesqueletoseráassim:
//app-src/util/decorators/Debounce.js
exportfunctiondebounce(milissegundos=500){
19.2SUPORTANDODECORATORATRAVÉSDOBABEL 411
returnfunction(target,key,descriptor){
returndescriptor;}
}
Temosafunçãodebouncequerecebecomoparâmetroumaquantidade em milissegundos, o tempo que levará emconsideração.Senãorecebervaloralgum,seuvalorpadrãoserádemeiosegundograçasaodefaultparametersuportadopeloES2015(ES6).
TodafunçãoDecoratordeveretornaroutrafunçãoquerecebetrês parâmetros. O primeiro é o target , o alvo do nossoDecorator; e o segundo é o nome da propriedade na qual oDecoratorfoiutilizado.Emnossocaso,comovamosusá-loparaométodoimportaNegociacoes, o parâmetro key guardará onomedométodo.Porfim,temosodescriptor,quemereceumdestaqueespecial.
O descriptor é um objeto especial que nos dá acesso àimplementação original do método ou função, por meio dedescriptor.value. É importante queo retornoda funçãoque
recebe os três parâmetros que vimos sempre devolva odescriptor,modificadoounão.
Oprimeiropassoseráguardarumareferênciaparaométodooriginal,antesdeelesermodificado:
exportfunctiondebounce(milissegundos=500){
returnfunction(target,key,descriptor){
constmetodoOriginal=descriptor.value;
412 19.2SUPORTANDODECORATORATRAVÉSDOBABEL
returndescriptor;}
}
É importante guardar o método original, porque, aomodificarmos descriptor.value para aplicar o debounce ,precisaremosqueométodooriginalsejachamado.
Agora, vamos sobrescrever odescriptor.value com umanovafunção.Comonãosabemosseométododecoradopossuiounãoparâmetros,usaremosoRESToperatorparaquenossonovométodo esteja preparado para receber nenhum ou maisparâmetros:
//app-src/util/decorators/Debounce.js
exportfunctiondebounce(milissegundos=500){
returnfunction(target,key,descriptor){
constmetodoOriginal=descriptor.value;
lettimer=0;descriptor.value=function(...args){
clearTimeout(timer);//aquientraaimplementaçãodonossométodo//quesubstituiráooriginal
}
returndescriptor;}
}
Através de metodoOriginal.apply(this, args) ,chamamos o método original passando seu contexto e aquantidadedeargumentosrecebidapelométodoqueosubstituiu:
//app-src/util/decorators/Debounce.js
19.2SUPORTANDODECORATORATRAVÉSDOBABEL 413
exportfunctiondebounce(milissegundos=500){
returnfunction(target,key,descriptor){
constmetodoOriginal=descriptor.value;
lettimer=0;
descriptor.value=function(...args){clearInterval(timer);
//chamametodoOriginaldepoisdeXmilissegundos!timer=setTimeout(()=>metodoOriginal.apply(this,a
rgs),milissegundos);}
returndescriptor;}
}
Excelente, isso já é suficiente para que nosso Decoratorfuncione.Vamosexportá-lonobarrelapp-src/util/index.js,para em seguida importá-lo e utilizá-lo emNegociacaoController.
//app-src/util/index.js
export*from'./Bind.js';export*from'./ConnectionFactory.js';export*from'./DaoFactory.js';export*from'./ApplicationException.js';export*from'./HttpService.js';export*from'./ProxyFactory.js';export*from'./decorators/Debounce.js';
Agora,alterandoNegociacaoController:
import{Negociacoes,NegociacaoService,Negociacao}from'../domain/index.js';import{NegociacoesView,MensagemView,Mensagem,DateConverter}from'../ui/index.js';
414 19.2SUPORTANDODECORATORATRAVÉSDOBABEL
/importouodecoratorimport{getNegociacaoDao,Bind,getMessageException,debounce}from'../util/index.js';
exportclassNegociacaoController{
//códigoanterioromitido
@debounce()asyncimportaNegociacoes(){
//códigoomitido}
}
Podemos testar a aplicação. Inclusive podemos passar aquantidadeemmilissegundosquedesejamosparao@debounce,queelaserárespeitada:
//EXEMPLO:esperaumsegundoemeio
@debounce(1500)asyncimportaNegociacoes(){
//códigoomitido}
}
Que tal aplicarmos a solução também para o métodoadiciona() deNegociacaoController? Tudo bemque não
temosumasituaçãonaqualodebouncefaçamuitosentido,masé uma maneira de podermos testar a reutilização do nossoDecorator:
//app-src/controllers/NegociacaoController.js//códigoanterioromitido
19.3 UM PROBLEMA NÃO ESPERADO COMNOSSODECORATOR
19.3UMPROBLEMANÃOESPERADOCOMNOSSODECORATOR 415
@debounce()asyncadiciona(event){
//códigoomitido}
//códigoposterioromitido
Assim que recarregamos nossa página com os arquivosatualizados durante o processo de transcompilação do Babel,quando tentamos adicionar novas negociações, a página érecarregada indevidamente. É como se oevent.preventDefault() dométodoadiciona() não fosse
chamado.Oqueestáacontecendo?
Comoométodooriginalésubstituídoporumnovo,estenovométodo não realiza o event.preventDefault() . Como ométododecorado só será chamadodepois deXmilissegundos, oeventosubmitnãoentendequedevecancelarseueventopadrão,o da submissão do formulário, então a página recarregará.Podemos resolver isso facilmente alterando nosso código daseguinteforma:
exportfunctiondebounce(milissegundos=500){
returnfunction(target,key,descriptor){
constmetodoOriginal=descriptor.value;
lettimer=0;
descriptor.value=function(...args){
//MUDANÇA!if(event)event.preventDefault();
clearInterval(timer);timer=setTimeout(()=>metodoOriginal.apply(this,a
rgs),milissegundos);
416 19.3UMPROBLEMANÃOESPERADOCOMNOSSODECORATOR
}
returndescriptor;}
}
Podemostestarseavariável implícitaevent estádisponíveldurante a chamada do método. Se estiver, chamamosevent.preventDefault().
Umnovo teste indicaquenossa aplicaçãovoltoua funcionarcomo esperado. Mas será que podemos abusar mais umpouquinhodesterecurso?Éoqueveremosnapróximaseção.
Emnossa classeNegociacaoController, buscamos as tagsinput do formulário através da funçãoquerySelector, que
recebeoselectorCSSdoselementos.Veja:
//REVISANDOAPENASOCÓDIGO
//app-src/controllers/NegociacaoController.js
//códigoanterioromitido
exportclassNegociacaoController{
constructor(){
const$=document.querySelector.bind(document);this._inputData=$('#data');this._inputQuantidade=$('#quantidade');this._inputValor=$('#valor');
//códigoposterioromitido
Mas que tal criarmos um Decorator que saiba injetar as
19.4ELABORANDOUMDOMINJECTOR
19.4ELABORANDOUMDOMINJECTOR 417
dependênciasdoselementosdoDOMnainstânciadenossaclasse?Queremosalgocomo:
//EXEMPLOAPENAS
@controller('#data','#quantidade','#valor')exportclassNegociacaoController{
constructor(inputData,inputQuantidade,inputValor){
this._inputData=inputData;this._inputQuantidade=inputQuantidade;this._inputValor=inputValor;
Neste exemplo, o Decorator @controller recebe comoparâmetro os seletores CSS dos elementos que desejamos passarparaonovoconstructordaclasse,namesmaordem.
Agora que já aprendemos a criar Decorators de métodos,chegouahoradeaprendermosacriarDecoratorsdeclasses.
Vamos criar nosso Decorator de classe em app-
src/util/decorators/Controller.js , com a seguinteestrutura:
//app-src/util/decorators/Controller.js
exportfunctioncontroller(...seletores){
returnfunction(constructor){
}}
Nosso Decorator recebe como parâmetro uma quantidade
19.5DECORATORDECLASSE
418 19.5DECORATORDECLASSE
indeterminadadeparâmetros,por issousamosoRESTOperator.Esses parâmetros serão os seletores CSS dos elementos quedesejamosbuscar.
Assim como um Decorator de método, ele devolve umafunção. Mas desta vez, ao ser chamada, ela nos dá acesso aoconstructordaclassedecorada.Épormeiodessareferênciadoconstructor da classequepodemos criar instânciasdentrodo
nosso Decorator, inclusive passar para essas instâncias osparâmetrosquedesejarmos.
Precisamos converter a lista de seletores em uma lista deelementosdoDOM.Paraisso,usaremosafunçãomapquetodoarraypossui:
//app-src/util/decorators/Controller.js
exportfunctioncontroller(...seletores){
//listacomoselementosdoDOM
constelements=seletores.map(seletor=>document.querySelector(seletor));
returnfunction(constructor){
}}
Já temos um array com todos os elementos do DOM queprecisamos passar para o constructor da classe decorada.Agora, vamos guardar uma referência para o constructororiginal da classe, para então começarmos a esboçar um novoconstructor:
//app-src/util/decorators/Controller.js
19.5DECORATORDECLASSE 419
exportfunctioncontroller(...seletores){
constelements=seletores.map(seletor=>document.querySelector(seletor));
returnfunction(constructor){
constconstructorOriginal=constructor;
constconstructorNovo=function(){
}
}}
OconstructorNovodeveráchamarconstructorOriginalpassando todos os parâmetros que ele necessita. Um ponto adestacar é que usamos function e não arrow function paradefinironovoconstructor.Issonãofoiporacaso,poisothisdoconstructordeveserdinâmicoenãoestático.Setivéssemosusadoarrowfunctionlánafrente,receberíamosumerro.
Vamos continuar a implementar o novo constructor. AinstânciacriadapelachamadadeconstructorOriginaldeveserretornada:
//app-src/util/decorators/Controller.js
exportfunctioncontroller(...seletores){
constelements=seletores.map(seletor=>document.querySelector(seletor));
returnfunction(constructor){
constconstructorOriginal=constructor;
constconstructorNovo=function(){
returnnewconstructorOriginal(...elements);
420 19.5DECORATORDECLASSE
}}
}
Isso aindanão é suficiente.OconstructorNovo deve ter omesmoprototypedeconstructorOriginal. Isso é importante,poisaclassequedecoramospodeterherdadodeoutraclasse:
//app-src/util/decorators/Controller.js
exportfunctioncontroller(...seletores){
constelements=seletores.map(seletor=>document.querySelector(seletor));
returnfunction(constructor){
constconstructorOriginal=constructor;
constconstructorNovo=function(){
returnnewconstructorOriginal(...elements);}
//ajustandooprototypeconstructorNovo.prototype=constructorOriginal.prototype
;
//retornandoonovoconstructorreturnconstructorNovo;
}}
Terminamos nosso Decorator. Só precisamos exportá-loatravésdobarrildapastaapp-src/util,paraentãoimportá-loeutilizá-loemNegociacaoController.
Vamosalterarobarril:
//app-src/util/index.js
export*from'./Bind.js';
19.5DECORATORDECLASSE 421
export*from'./ConnectionFactory.js';export*from'./DaoFactory.js';export*from'./ApplicationException.js';export*from'./HttpService.js';export*from'./ProxyFactory.js';export*from'./decorators/Debounce.js';
//exportou!export*from'./decorators/Controller.js';
Agoraocontroller:
//app-src/controller/NegociacaoController.js
//códigoanterioromitido
//IMPORTOUODECORATOR
import{getNegociacaoDao,Bind,getMessageException,debounce,controller}from'../util/index.js';
//UTILIZANDOODECORATOR
@controller('#data','#quantidade','#valor')exportclassNegociacaoController{
//MÉTODOAGORARECEBETRÊSPARÂMETROSconstructor(inputData,inputQuantidade,inputValor){
this._inputData=inputData;this._inputQuantidade=inputQuantidade;this._inputValor=inputValor;
//códigoposterioromitido}
//códigoposterioromitido
Basta recarregarmos nossa página para vermos nossoDecoratoremação.NossocódigoéapenasumaamostradoqueoDecorator de classes pode fazer. Todavia, um olhar atentoperceberáquepodemossimplificaraindamaisnossocódigo.
422 19.5DECORATORDECLASSE
Como o constructor de NegociacaoController agorarecebe três parâmetros, podemos usar a mesma estratégia comObject.assign que utilizamos na classe Negociacao para
reduziraquantidadedeinstruções:
//app-src/controller/NegociacaoController.js
//códigoanterioromitido
import{getNegociacaoDao,Bind,getMessageException,debounce,controller}from'../util/index.js';
@controller('#data','#quantidade','#valor')exportclassNegociacaoController{
//OSPARÂMETROSAGORACOMEÇAMCOMUNDERLINE
constructor(_inputData,_inputQuantidade,_inputValor){
//UMAÚNICAINSTRUÇÃO
Object.assign(this,{_inputData,_inputQuantidade,_inputValor})
this._negociacoes=newBind(newNegociacoes(),newNegociacoesView('#negociacoes'),'adiciona','esvazia'
);
this._mensagem=newBind(newMensagem(),newMensagemView('#mensagemView'),'texto'
);
this._service=newNegociacaoService();
this._init();}
//códigoposterioromitido
19.5DECORATORDECLASSE 423
Angular (https://angular.io/) é um framework que faz usointenso de decorators, inclusive possui um sistema bemavançado de injeção de dependências. O código queescrevemoséumacaricaturadoqueestepoderosoframeworkpodenosoferecer.
Excelente.Commaisestaetapaconcluída,podemoscontinuarcomnossassimplificaçõesnapróximaseção.
Estamos a um passo de concluirmos nossa aplicação, maspodemos realizar mais uma melhoria que facilitará em muito avidadodesenvolvedorquandooassuntoérequisiçõesassíncronas.Primeiro, vamos rever a classeHttpService que criamos paraisolarmosoobjetoXmlHttpRequest:
//app-src/util/HttpService.js
exportclassHttpService{
get(url){
returnnewPromise((resolve,reject)=>{
constxhr=newXMLHttpRequest();
xhr.open('GET',url);
xhr.onreadystatechange=()=>{
if(xhr.readyState==4){
19.6 SIMPLIFICANDO REQUISIÇÕES AJAXCOMAAPIFETCH
424 19.6SIMPLIFICANDOREQUISIÇÕESAJAXCOMAAPIFETCH
if(xhr.status==200){
resolve(JSON.parse(xhr.responseText));}else{
console.log(xhr.responseText);reject(xhr.responseText);
}}
};
xhr.send();
});}
}
UtilizamosopadrãodeprojetoPromiseparafacilitarousodométodoget()emtodososlugaresqueessetipoderequisiçãoénecessário. Contudo, podemos usar um recurso do próprioJavaScript para realizar requisições Ajax que já suporta Promiseporpadrão,aAPIFetch.
AAPIfetchéexperimental,massuportadaapartirdoChrome42,Firefox39,InternetExplorer11,Edge38,Opera29eSafari10.Elasimplificadrasticamenteocódigopararealizarmosrequisiçõesassíncronas,comumentechamadasderequisiçõesAjaxnomundoJavaScript.
AAPI Fetch émais um recurso experimental bastante usadono mundo JavaScript. Não se preocupe por enquanto comquestões de compatibilidade, mas tenha certeza de estarusandoumnavegadorqueasuportenestemomento.
19.6SIMPLIFICANDOREQUISIÇÕESAJAXCOMAAPIFETCH 425
Parapodervermosnapráticasuasimplicidade,vamosapagaroconteúdodométodoget()deHttpServicepara reescrevê-lonovamentecomaAPIFetch.
AcessamosaAPIatravésdafunçãoglobalfetchquerecebecomoparâmetroaURLquedesejamosrealizarumarequisição:
//app-src/util/HttpService.js
exportclassHttpService{
get(url){
returnfetch(url).then(res=>console.log(res));
}}
A função fetch retorna uma Promise, e é por isso queobtemos seu resultado através da chamada encadeada à funçãothen.Contudo,arespostanãoéparseadaautomaticamenteparanós,mas tambémnão será necessário usarmosJSON.parse(),comofizemoscomarespostadeXMLHttpRequest.
Noexemploanterior,resnadamaisédoqueumobjetoqueencapsula a resposta.Atravésdosmétodostext() ejson(),lidamos com a resposta no formato texto ou como objetos noformatoJSON,respectivamente.
Nocaso, temos interesseem lidarcoma respostano formatoJSON:
//app-src/util/HttpService.js
exportclassHttpService{
//quemchamaométodoobtémosdadosatravésde.then()get(url){
426 19.6SIMPLIFICANDOREQUISIÇÕESAJAXCOMAAPIFETCH
returnfetch(url).then(res=>res.json());
}}
Comoestamosusandoarrowfunctionsembloco,oresultadode res.json() é retornado automaticamente para a próximachamada,encadeadaàfunçãothen().
Apesar de muito mais simples do que a versão comPromise/XMLHttpRequest, ainda precisamos lidar com possíveiserros durante a operação. Isso pode ser feito por quem chama ométodo get() bastando encadear uma chamada à funçãocatch().Vejamosumexemplodeuso:
//EXEMPLODEUMAPENAS
letservice=newHttpService();service
.get('http://endereco-de-uma-api').then(dados=>console.log(dados)).catch(err=>console.log(err));
Porém,háumapegadinhacomaAPIFetch.Afunçãocatchsó será chamada apenas se rejeitarmos a Promise. Para quepossamosfazerisso,precisamossabersearequisiçãofoirealizadacomsucessoounão,combaseemres.ok.
AlterandoocódigodeHttpService:
exportclassHttpService{
_handleErrors(res){
//senãoestiverok,lançaaexceçãoif(!res.ok)thrownewError(res.statusText);
//senenhumexceçãofoilançada,retornaaprópriarespo
19.6SIMPLIFICANDOREQUISIÇÕESAJAXCOMAAPIFETCH 427
stareturnres;
}
get(url){
returnfetch(url).then(res=>this._handleErrors(res)).then(res=>res.json());
}}
Para manter a organização do código, criamos o métodoprivado_handleErrors().O.then nofetch devolverá aprópria requisição this._handleErrors que será acessível nopróximo .then e será convertido para json. Com o if ,identificamos se tudo funcionou bem com o res.ok ; casocontrário,cairemosnoelseelançaremosumaexceção.Quandoa exceção for lançada, a Promise não vai para o .then doget(),masseguiráparaocatch.
Se atualizarmos a página no navegador, em alguns instantesreceberemosamensagemdequeasnegociaçõesdoperíodoforamimportadascorretamente.
Por padrão, fetch executa uma requisição com o métodoGET.EsequisermosenviarumJSONatravésdométodoPOST
paraumaAPI?Nessecaso,precisaremosconfigurararequisição.
Aboanotíciaéquenossoservidorestápreparadoparareceberuma requisição POST que envia um JSON para o endereço/negociacoes. O servidor apenas exibirá no console os dados
19.7 CONFIGURANDO UMA REQUISIÇÃOCOMAPIFETCH
428 19.7CONFIGURANDOUMAREQUISIÇÃOCOMAPIFETCH
recebidos.
O código de teste que escreveremos ficará no arquivoclient/app-src/app.js . Assim que nossa aplicação for
carregada, uma requisição será feita para o servidor. Poderíamostercriadooutroarquivo,masissonospouparátempo.
O primeiro passo será importar Negociacao de./domain/index.js , e preparar o cabeçalho da requisição,
indicandoqueoContent-Typeseráapplication/json:
//client/app-src/app.js
import{NegociacaoController}from'./controllers/NegociacaoController.js';
//importouNegociacao!import{Negociacao}from'./domain/index.js';
//códigoanterioromitido
constnegociacao=newNegociacao(newDate(),1,200);constcabecalhos=newHeaders();cabecalhos.set('Content-Type','application/json');
ÉpormeiodeumainstânciadeHeadersqueconfiguramosnossocabeçalho.Oobjetopossuiométodoset. É atravésdelequeindicamosoContent-Typequeutilizaremos.
Opróximopassoserácriarmosumobjetoquechamaremosdeconfig.Elepossuiaspropriedadesmethod,headersebodyque recebem respectivamenteos valoresPOST,cabecalhos eJSON.stringify(negociacao).
//client/app-src/app.js
import{NegociacaoController}from'./controllers/NegociacaoController.js';
19.7CONFIGURANDOUMAREQUISIÇÃOCOMAPIFETCH 429
//importouNegociacao!import{Negociacao}from'./domain/index.js';
//códigoanterioromitido
constnegociacao=newNegociacao(newDate(),1,200);constcabecalhos=newHeaders();cabecalhos.set('Content-Type','application/json');
constconfig={method:'POST',headers:cabecalhos,body:JSON.stringify(negociacao)
};
Com a configuração necessária, executamos nossa requisiçãopormeiodefetch:
//client/app-src/app.js
import{NegociacaoController}from'./controllers/NegociacaoController.js';
//importouNegociacao!import{Negociacao}from'./domain/index.js';
//códigoanterioromitido
constnegociacao=newNegociacao(newDate(),1,200);constcabecalhos=newHeaders();cabecalhos.set('Content-Type','application/json');
constconfig={method:'POST',headers:cabecalhos,body:JSON.stringify(negociacao)
};
fetch('/negociacoes',config).then(()=>console.log('Dadoenviadocomsucesso'));
Reparem que fetch recebe como segundo parâmetro o
430 19.7CONFIGURANDOUMAREQUISIÇÃOCOMAPIFETCH
objeto com todas as configurações que fizemos. Recarregandonossa página, ao olharmos para o terminal que roda nossoservidor,vemososdadosdanegociaçãoqueenviamos.
Servidorescutandonaporta:3000DadorecebidoviaPOST:{_data:2017-06-06T20:13:02.799Z,_quantidade:1,_valor:200}
Podemossimplificarumpoucomaisocódigoqueescrevemosutilizandooatalhoparadeclaraçãodeobjetosliteraisquejávimosnestelivro.
Vamos mudar o nome da constante cabecalhos paraheaders , e também vamos guardar o resultado de
JSON.stringify(negociacao)naconstantebodyeométodona constante method. Agora, a declaração do objeto configficarámaissimples:
//client/app-src/app.js
import{NegociacaoController}from'./controllers/NegociacaoController.js';import{Negociacao}from'./domain/index.js';
//códigoanterioromitido
constnegociacao=newNegociacao(newDate(),1,200);//mudouparaheaders!constheaders=newHeaders();headers.set('Content-Type','application/json');
//novaconstanteconstbody=JSON.stringify(negociacao);
//novaconstanteconstmethod='POST';
constconfig={method,headers,
19.7CONFIGURANDOUMAREQUISIÇÃOCOMAPIFETCH 431
body};
fetch('/negociacoes',config).then(()=>console.log('Dadoenviadocomsucesso'));
Éumapequenamudança,mas que deixa nosso código aindamaisenxutoelegível.
Estamos prestes a terminar nossa aplicação, porém há maisumamelhoriaquepodemosrealizarantesde finalizá-la.VejamosnossaclasseNegociacaoumaúltimavez:
//app-src/domain/negociacao/Negociacao.js
exportclassNegociacao{
constructor(_data,_quantidade,_valor){
Object.assign(this,{_quantidade,_valor});this._data=newDate(_data.getTime());Object.freeze(this);
}
//códigoposterioromitido}
Sabemosqueosparâmetros recebidos emseuconstructorsão obrigatórios, mas em nenhum momento testamos se forampassados.
Umasoluçãopossíveléaplicarmosumasériedecondiçõesifpara cada parâmetro, e então lançarmos uma exceção para essascondições:
19.8 VALIDAÇÃO COM PARÂMETRODEFAULT
432 19.8VALIDAÇÃOCOMPARÂMETRODEFAULT
//app-src/domain/negociacao/Negociacao.js
exportclassNegociacao{
constructor(_data,_quantidade,_valor){
if(!_data){thrownewError('dataéumparâmetroobrigatório');
}
if(!_quantidade){thrownewError('quantidadeéumparâmetroobrigatóri
o');}
if(!_valor){thrownewError('valoréumparâmetroobrigatório');
}
Object.assign(this,{_quantidade,_valor});this._data=newDate(_data.getTime());Object.freeze(this);
}
//códigoposterioromitido}
Asoluçãoquevimos,apesardefuncional,éumtantoverbosa.Umasoluçãomaiseleganteéisolarmosaexceçãolançadaemumafunção, para em seguida chamá-la como parâmetro default .Com essa estratégia, a função será chamada caso nenhumparâmetrosejachamado.
Vamoscriaroarquivoapp-src/util/Obrigatorio.ts queexportaráafunçãoobrigatorio:
//app-src/util/Obrigatorio.js
exportfunctionobrigatorio(parametro){
thrownewError(`${parametro}éumparâmetroobrigatório`);}
19.8VALIDAÇÃOCOMPARÂMETRODEFAULT 433
Nãopodemosnosesquecerdeadicioná-lonobarril:
//app-src/util/index.js
export*from'./Bind.js';export*from'./ConnectionFactory.js';export*from'./DaoFactory.js';export*from'./ApplicationException.js';export*from'./HttpService.js';export*from'./ProxyFactory.js';export*from'./decorators/Debounce.js';export*from'./decorators/Controller.js';
//exportando!export*from'./Obrigatorio.js';
Agora,naclasseNegociacao,faremosassim:
//app-src/controllers/NegociacaoController.js
import{obrigatorio}from'../../util/index.js';
exportclassNegociacao{
constructor(_data=obrigatorio('data'),_quantidade=obrigatorio('quantidade'),_valor=obrigatorio('valor')){
Object.assign(this,{_quantidade,_valor});this._data=newDate(_data.getTime());Object.freeze(this);
}
Experimentealterarométodo_criaNegociacaoomitindoapassagem de um parâmetro para o constructor deNegociacao.Aexeçãoserálançadaemensagemseráexibidaparao usuário.Umasoluçãoelegantepara trataraobrigatoriedadedeparâmetros.
434 19.8VALIDAÇÃOCOMPARÂMETRODEFAULT
Nossa aplicação funciona com esperado, mas o arquivoclient/app-src/app.js,queinicializanossocontroller,realiza
um POST através da API Fetch e também uma associação deeventos de elementos do DOM com os métodos de nossocontroller. Realizar esse tipo de associação é algo corriqueiro nomundoJavaScript.Seráquepodemosfacilitaresseprocesso?
Umapossívelsoluçãoéisolarmosalógicadeassociaçãoemumúnico lugar, para então aproveitá-la toda vez que houver anecessidade de associarmos um método de um controller a umevento.Nessesentido,podemoscriaroDecoratorbindEventquereceberácomoparâmetrooevento(click,submitetc.),oseletordoelementodoDOM(noqualnossométodoseráassociado)eumaflag para indicarmos se queremos realizar ou nãoevent.preventDefault().
Todavia,umDecoratordemétodoéaplicadoantesdaclasseserinstanciada.Comoseremoscapazesdeassociarummétodoaumeventosemtermosumainstância?
Podemos solucionar o problema anterior utilizando oDecoratorbindEvent apenasparaadicionar informações extras(metadados) sobre ométodo no qual foi associado. Em seguida,com auxílio de um Decorator de classe, procuraremos nosmétodos da instância da classe os metadados adicionados porbindEvent. As informações contidas nesses metadados serão
usadas para fazer a associação do evento com o método dainstância.
19.9REFLECT-METADATA:AVANÇANDONAMETAPROGRAMAÇÃO
19.9REFLECT-METADATA:AVANÇANDONAMETAPROGRAMAÇÃO 435
Contudo, o ECMAScript ainda não possui uma maneiraespecificada para inclusão de metadados na definição de nossasclasses. Há até uma proposta (https://rbuckton.github.io/reflect-metadata/) em discussão que poderá entrar em uma futuraatualização.
A boa notícia é que não precisamos esperar o futuro parautilizarmos o recurso que precisamos. Existe o projeto reflect-metadata (https://github.com/rbuckton/reflect-metadata) quesegue a especificação proposta para a linguagem. Inclusive,frameworks comoAngular e linguagens como TypeScript fazemusodestemesmoprojeto.
Já existe uma API de reflexão no ECMAScript 2015 (ES6)acessível através do objeto global Reflect. Todavia, o reflect-metadata adiciona novas funções que permitem trabalhar commetadados.
Vamosinstalaroreflect-metadatapeloterminal,dentrodapastaclient,comocomando:
Precisamos importá-lo como primeiro script emclient/index.htm:
<divid="negociacoes"></div>
<scriptsrc="node_modules/reflect-metadata/Reflect.js"></script>
<scriptsrc="node_modules/systemjs/dist/system.js"></script><script>
System.import('app/app.js')
.catch(err=>console.error(err));</script>
436 19.9REFLECT-METADATA:AVANÇANDONAMETAPROGRAMAÇÃO
</body>
Excelente,agora jápodemosdar inícioàconstruçãodonossoDecoratorbindEvent.
Criaremos oDecorator que adicionará ometadado quemaistardeusaremosparacompletarnossasolução.
Vamos criar o arquivo client/app-
src/util/decorators/BindEvent.js . Ele terá o seguinteesqueleto:
//client/app-src/util/decorators/BindEvent.js
import{obrigatorio}from'../../util/index.js';
exportfunctionbindEvent(event=obrigatorio('event'),selector=obrigatorio('selector'),prevent=true){
returnfunction(target,propertyKey,descriptor){
//éaquiqueprecisamosadicionarosmetadados
returndescriptor;}
}
Atéagoranãohánenhumanovidade, inclusiveusamosnossafunção obrigatorio nos dois primeiros parâmetros doDecorator. O valor padrão do último parâmetro será true, poisadotaremos como padrão o cancelamento do evento com
19.10 ADICIONANDO METADADOS COMDECORATORDEMÉTODO
19.10ADICIONANDOMETADADOSCOMDECORATORDEMÉTODO 437
event.preventDefault().
Agora, através de Reflect.defineMetadata() , vamosadicionarnossometadadoquechamaremosdebindEvent:
//client/app-src/util/decorators/BindEvent.js
import{obrigatorio}from'../../util/index.js';
exportfunctionbindEvent(event=obrigatorio('event'),selector=obrigatorio('selector'),prevent=true){
returnfunction(target,propertyKey,descriptor){
Reflect.defineMetadata('bindEvent',{event,selector,prevent,propertyKey},Object.getPrototypeOf(target),propertyKey);
returndescriptor;}
}
A função Reflect.defineMetadata recebe quatroparâmetros.Oprimeiroéonomequeidentificanossometadado.O segundo é um objeto JavaScript, nosso metadado, com aspropriedadesevent,selector, prevent e propertyKeyatravésdoatalhoparadeclaraçãodeobjetosquejávimos.
Emseguida,precisamospassaroprototypedainstânciaquedesejamos adicionar o metadado, por isso usamosObject.getPrototypeOf(target) e por fim o nome da
propriedade do objeto que receberá o metadado. Como vamosutilizar@bindEvent apenasemmétodos,propertyKey será onomedométodo.
438 19.10ADICIONANDOMETADADOSCOMDECORATORDEMÉTODO
Vamos adicionar nosso novo Decorator ao barrelclient/util/index.js:
//client/util/index.js
export*from'./Bind.js';export*from'./ConnectionFactory.js';export*from'./DaoFactory.js';export*from'./ApplicationException.js';export*from'./HttpService.js';export*from'./ProxyFactory.js';export*from'./decorators/Debounce.js';export*from'./decorators/Controller.js';export*from'./Obrigatorio.js';
//importou!export*from'./decorators/BindEvent.js';
Sem hiato, vamos importá-lo e adicioná-lo aos métodosadiciona() , importaNegociacoes() e apaga() deNegociacaoController:
//client/app-src/controller/NegociacaoController.js
//importaçõesanterioresomitidas
//importoubindEventimport{getNegociacaoDao,Bind,getMessageException,debounce,controller,bindEvent}from'../util/index.js';
//associaaoeventosubmitdoelementocomseletor.form@bindEvent('submit','.form')@debounce()asyncadiciona(event){//códigoomitido}
//códigoomitido
//associaaoeventoclickdoelementocomseletor#botao-importa@bindEvent('click','#botao-importa')@debounce()asyncimportaNegociacoes(){//códigoomitido}
//associaaoeventoclickdoelementocomseletor#botao-importa
19.10ADICIONANDOMETADADOSCOMDECORATORDEMÉTODO 439
@bindEvent('click','#botao-apaga')asyncapaga(){//códigoomitido}
//códigposterioromitido
Duranteoprocessodetranscompilação,eleseráprocessadoe,duranteessaetapa,adicionaráosmetadadosnosmétodosemquefoiusado.Contudo,precisamosdeumDecoratorquesejacapazdeacessar a instância da classe, buscar os metadados e fazer asassociaçõesdeeventos.Játemosodeclassecontroller.Vamosmodificá-loparaatendernossanecessidade.
Nossoprimeiropassoseráacessara instânciacriadae,comoauxíliodeObject.getOwnPropertyNames(), percorrer todas aspropriedadesdaclasse.Paracadapropriedade,verificamosatravésdeReflect.hasMetadata() se o metadado bindEvent estápresente. Esta função recebe como parâmetro o identificador dometadado, a instância da classe e, por último, o nome dapropriedadequeestásendoverificada:
//client/app-src/util/decorators/Controller.js
exportfunctioncontroller(...seletores){
constelements=seletores.map(seletor=>document.querySelector(seletor));
returnfunction(constructor){
constconstructorOriginal=constructor;
constconstructorNovo=function(){
//guardandoumareferênciaparaainstância
19.11 EXTRAINDO METADADOS COM UMDECORATORDECLASSE
440 19.11EXTRAINDOMETADADOSCOMUMDECORATORDECLASSE
constinstance=newconstructorOriginal(...elements);
//varrecadapropriedadedaclasseObject
.getOwnPropertyNames(constructorOriginal.prototype)
.forEach(property=>{
if(Reflect.hasMetadata('bindEvent',instance,property)){
//precisafazeraassociaçãodoeventocomométodo
}});
}
constructorNovo.prototype=constructorOriginal.prototype;
returnconstructorNovo;}
}
Estamosquaselá.Agora,dentrodacondiçãoif,precisamosextrair o objeto que adicionamos como metadado, para entãotermos os dados necessários para podermos realizar a associaçãodo evento. Vamos criar uma função auxiliar chamadaassociaEvento,quereceberáainstânciadaclasseeometadado.
É através de Reflect.getMetadata() que extraímos ainformação, passando como parâmetros o identificador dometadado, a instância da classe e o nome da propriedade quepossuiometadado:
//client/app-src/util/decorators/Controller.js
exportfunctioncontroller(...seletores){
constelements=seletores.map(seletor=>
19.11EXTRAINDOMETADADOSCOMUMDECORATORDECLASSE 441
document.querySelector(seletor));
returnfunction(constructor){
constconstructorOriginal=constructor;
constconstructorNovo=function(){
constinstance=newconstructorOriginal(...elements);
Object.getOwnPropertyNames(constructorOriginal.prototyp
e).forEach(property=>{
if(Reflect.hasMetadata('bindEvent',instance,property)){
associaEvento(instance,Reflect.getMetadata('bindEvent',inst
ance,property));}
});}
constructorNovo.prototype=constructorOriginal.prototype;
returnconstructorNovo;}
}
functionassociaEvento(instance,metadado){
document.querySelector(metadado.selector).addEventListener(metadado.event,event=>{
if(metadado.prevent)event.preventDefault();instance[metadado.propertyKey](event);
});}
Reparem que não associamos diretamente o método dainstância ao evento em associaEvento . Adicionamos uma
442 19.11EXTRAINDOMETADADOSCOMUMDECORATORDECLASSE
funçãointermediáriaqueentãochamaométododainstância.Issofoinecessárioparalivrarmosdodesenvolvedoraresponsabilidadederealizarevent.preventDefaultnosmétodosdocontroller.
Agora, só nos resta alterar o arquivo client/app-
src/app.js . Removeremos todas as instruções que associameventos, inclusive aquela que cria o alias $. Nosso arquivoficaráassim:
//client/app-src/app.js
import{NegociacaoController}from'./controllers/NegociacaoController.js';import{Negociacao}from'./domain/index.js';
constcontroller=newNegociacaoController();
//REMOVEUOALIAS$EASASSOCIAÇÕESDOSEVENTOS
constnegociacao=newNegociacao(newDate(),1,200);constheaders=newHeaders();headers.set('Content-Type','application/json');constbody=JSON.stringify(negociacao);constmethod='POST';
constconfig={method,headers,body
};
fetch('/negociacoes',config).then(()=>console.log('Dadoenviadocomsucesso'));
Missãocumprida!Simplificamosbastanteamaneirapelaqualrealizávamosaassociaçãodeumeventocomosmétodosdenossocontroller.
19.11EXTRAINDOMETADADOSCOMUMDECORATORDECLASSE 443
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/19
444 19.11EXTRAINDOMETADADOSCOMUMDECORATORDECLASSE
CAPÍTULO20
“Como é que você chegou aqui com vida, cabra velho?" -Lampião
Temosnossaaplicaçãototalmentefuncional.Todavia,existempontosquedevemserlevadosemconsideraçãoantesdebatermoso marteloedizermosqueaaplicaçãoestáprontaparaprodução:
OSystem.jscarregaoprimeiromódulodaaplicaçãoe,a partir dele, identifica e busca suas dependências,realizando diversas requisições para o servidor.Minimizar o número de requisições para o servidormelhora a performance da aplicação. Temos comoevitaressasmúltiplasrequisições?
OCSSdoBootstrapjáexistianoprojetoquebaixamosno início do livro. Será que podemosusar o próprionpm para baixar e gerenciar as dependências do
front-end?
Não há distinção entre a versão do projeto deprodução para a de desenvolvimento. Como
ENFRENTAMENTOFINAL
20ENFRENTAMENTOFINAL 445
concatenar eminificar os scripts parao ambientedeprodução?
Estamosusandoumserverdisponibilizadopeloautorpara tornar nosso projeto acessível pelo navegador.Como conseguir o resultado parecido comferramentasconceituadasdomercado?
Podemos responder a todas as perguntas com auxílio doWebpack.
Webpack (https://webpack.github.io/) é um module bundler,um agrupador de módulos usado por ferramentas famosas domercado,comoAngularCLI(https://cli.angular.io/),CreateReactApp (https://github.com/facebookincubator/create-react-app) eVue CLI (https://github.com/vuejs/vue-cli). Mas seu uso não éexclusivodessasferramentas.
OWebpackpermitetratardiversosrecursosdaaplicaçãocomoummódulo,inclusivearquivosCSS.Tudoissoéfeitoduranteumprocessodebuildque,nofinal,geraumúnicoarquivoJavaScript,geralmente chamado de bundle.js . Este possui todos osmódulos necessários para a aplicação.Neste sentido, oWebpackdispensaousodeumloader,comooSystem.jsqueusamosemnossoprojeto.
20.1WEBPACK,AGRUPADORDEMÓDULOS
20.2 PREPARANDO O TERRENO PARA OWEBPACK
446 20.1WEBPACK,AGRUPADORDEMÓDULOS
VamosprepararoterrenoparautilizarmosWebpackemnossoprojeto. Primeiro, emclient/index.html, vamos substituir aimportação do script do System.js e sua configuração por umaúnica tag<script>, que importa o arquivodist/bundle.jsqueaindanãoexiste.
<!--client/index.html--><!--códigoanterioromitido-->
<divid="negociacoes"></div>
<scriptsrc="node_modules/reflect-metadata/Reflect.js"></script>
<scriptsrc="dist/bundle.js"></script>
</body>
</html>
Vamosaproveitareapagarapastaclient/app, aquela quecontém os arquivos transcompilados pelo Babel . Nossosarquivoscompiladosseguirãooutrotrâmite.
Nãoutilizaremosmaisbabel-cli,nemosystemjs.Vamosremovê-los do nosso projeto, inclusive do arquivoclient/package.json.Conseguimosissoatravésdoscomandos:
npmuninstallbabel-cli--save-devnpmuninstallsystemjs--save
Comovimos,nãousaremosmaisobabel-cli,masoBabelainda precisa estar presente, pois o Webpack o utilizará paratranscompilarnossosarquivos.Então,vamosinstalarobabel-core(https://www.npmjs.com/package/babel-core), uma versão doBabeldesprovidade linhadecomandopara serusadaporoutrasferramentas.Vamos aproveitar e também instalar oWebpack deumavez:
20.2PREPARANDOOTERRENOPARAOWEBPACK 447
[email protected]@3.1.0--save-dev
Éimportanteutilizarasversõeshomologadasporesteautoraolongo do capítulo. Se mais tarde houver a necessidade deatualizarasbibliotecas, issopode ser feitono final,depoisdoleitorteracertezadequetudoestáfuncionando.
Agora que temos as dependências fundamentais baixadas,podemosdarinícioàconfiguraçãodoWebpack.
Webpack centraliza suas configurações no arquivowebpack.config.js . Uma das primeiras configurações que
precisamos fazer é indicarqual seráoprimeiromódulo (entry) aser carregado. O Webpack é inteligente para carregar suasdependênciasapartirdomódulo.Porfim,precisamosdizerolocalde saída (output) do bundle criado, o arquivo com todos osmódulosresolvidosdaaplicação.
Vamoscriaroarquivoclient/webpack.config.js,queleráo arquivo ./app-src/app.js . E o resultado com todos osmódulosresolvidosserásalvoemdist/bundle.js.
//client/webpack.config.js
constpath=require('path');
module.exports={entry:'./app-src/app.js',output:{
filename:'bundle.js',
20.3OTEMÍVELWEBPACK.CONFIG.JS
448 20.3OTEMÍVELWEBPACK.CONFIG.JS
path:path.resolve(__dirname,'dist')}
}
Teremos um único arquivo para ser importado pelonavegador,minimizandoassimaquantidadederequisições feitaspara o servidor. Nosso próximo passo será adicionarmos emclient/package.jsonumnpmscriptquechameobináriodo
Webpack, indicandoparaeleoarquivodeconfiguraçãoquedevelevaremconsideração.
Aliás, vamos remover todos os scripts que configuramos atéagora,mantendoapenasotest,paraentãoadicionarmosonovoscript.Emclient/package.json,faremos:
/*códigoanterioromitido*/
"scripts":{"test":"echo\"Error:notestspecified\"&&exit1",
"build-dev":"webpack--configwebpack.config.js"},
/*códigoposterioromitido*/
Agora,atravésdoterminaledentrodapastaclient, vamosexecutarocomando:
npmrunbuild-dev
Durante o processo de build, receberemos a seguintemensagemdeerronoterminal:
ERRORin./app-src/controllers/NegociacaoController.jsModuleparsefailed:/Users/flavioalmeida/Desktop/20/client/app-src/controllers/NegociacaoController.jsUnexpectedcharacter'@'(5:0)Youmayneedanappropriateloadertohandlethisfiletype.|import{getNegociacaoDao,Bind,debounce,controller,bindEvent}from'../util/index.js';
20.3OTEMÍVELWEBPACK.CONFIG.JS 449
||@controller('#data','#quantidade','#valor')|exportclassNegociacaoController{|@./app-src/app.js1:0-77
Isso acontece porque oWebpacknão reconhece a sintaxe doDecorator. Faz sentido, porque este recurso só está disponívelatravésdoprocessodecompilaçãodoBabel.
Para resolvermos este problema, precisamos solicitar aobabel-corequetranscompilenossosarquivosantesdoWebpackresolver os módulos. Mas como? É o que veremos na próximaseção.
A ponte de ligação entre oWebpack e o babel-core é obabel-loader (https://github.com/babel/babel-loader), umcarregadorexclusivovoltadoparaoBabel.Esseloaderleránossasconfiguraçõesemclient/.babelrcquandoforexecutado.
Vamosinstalaroloaderatravésdoterminal:
Isso ainda não é suficiente, precisamos fazer com queclient/webpack.config.js utilize o loader que acabamos de
baixar.Fazemosissoadicionandoaconfiguraçãomodule.Dentrodemodule,podemosterváriasrules (regras),ecadaregrapodeusarummóduloespecíficoquandoaplicada.
Alterandonossoarquivoclient/webpack.config.js:
20.4 BABEL-LOADER, A PONTE ENTRE OWEBPACKEOBABEL
450 20.4BABEL-LOADER,APONTEENTREOWEBPACKEOBABEL
//client/webpack.config.js
constpath=require('path');module.exports={
entry:'./app-src/app.js',output:{
filename:'bundle.js',path:path.resolve(__dirname,'dist')
},module:{
rules:[{
test:/\.js$/,exclude:/node_modules/,use:{
loader:'babel-loader'}
}]
}}
Porenquanto,temosapenasumaregra.Apropriedadetestindica a condição na qual nosso loader será aplicado. Usamos aexpressãoregular/\.js$/paraconsiderartodososarquivosqueterminamcomaextensão.js.Duranteesteprocesso,excluímosapastanode_modules,poisnãofazsentidoprocessarosarquivosdela. Por fim, dentro de use , indicamos o loader que seráutilizado,emnossocasoobabel-loader.
Agora já podemos tentar executar mais uma vez nossoprocessodebuildatravésdoterminal:
npmrunbuild-dev
Emaisumavez,recebemosumavisonele:
Hash:b9e23a2ce60c22bf8d05Version:webpack3.0.0Time:286ms
AssetSizeChunks ChunkNames
20.4BABEL-LOADER,APONTEENTREOWEBPACKEOBABEL 451
bundle.js3.87kB0[emitted]main[0]./app-src/app.js1.38kB{0}[built][1warning]
WARNINGin./app-src/app.jsSystem.registerisnotsupportedbywebpack.
Oqueseráquefoidessavez?
Quando usamos System.js , configuramos oclient/.babelrc para transcompilarnossosmódulos comum
padrão compatível com o loader. Contudo, esse formato não ésuportado pelo Webpack. A boa notícia é que, a partir da suaversão2.0,Webpackjásuportaporpadrãoosistemademódulosdo ES2015 (ES6).Dessa forma, não precisamosmais domódulo
babel-plugin-transform-es2015-modules-systemjs epodemos removê-lo. Além disso, também podemos atualizarclient/.babelrcparanãofazermaisusodoplugin.
Removendoomódulovianpm:
npmuninstallbabel-plugin-transform-es2015-modules-systemjs--save-dev
Agora,precisamosalterarclient/.babelrc.Emplugins,deixaremosapenasotransform-decorators-legacy:
//client/.babelrc{
"presets":["es2017"],"plugins":["transform-decorators-legacy"]
}
Agorapodemosgerarobuilddonossoprojetoqueelevaiatéofinal,semproblemaalgum:
npmrunbuild-dev
Dentro da pasta client/dist , foi gerado o arquivo
452 20.4BABEL-LOADER,APONTEENTREOWEBPACKEOBABEL
bundle.js.Éumarquivoquecontémtodososmódulosusadospelaaplicaçãoconcatenados.
Mesmo ainda sem um servidor rodando, se abrirmosclient/index.htmldiretamentenonavegador,nossaaplicaçãocontinuaráfuncionando.
Aprendemos a realizar um build de desenvolvimento, masminificar scripts em ambiente de produção é uma boa prática.Aliás, ainda não temos uma separação clara entre esses doisambientes.Chegouahoradebotaracasaemordem.
Vamos criar um novo npm script chamado build-prod,praticamente idêntico ao build-dev , mas que receberá oparâmetro -p . Isso indicará para o Webpack que estamossolicitando um build de produção, fazendo-o minificar nossobundle.jsnofinal.
Alterandoclient/package.json:
//códigoanterioromitido
"scripts":{"test":"echo\"Error:notestspecified\"&&exit1","build-dev":"webpack--configwebpack.config.js","build-prod":"webpack-p--configwebpack.config.js"
},
//códigoposterioromitido
Porfim,atravésdoterminal,vamosexecutarocomando:
npmrunbuild-prod
20.5PREPARANDOOBUILDDEPRODUÇÃO
20.5PREPARANDOOBUILDDEPRODUÇÃO 453
Umnovoerroéexibidonoterminal:
ERRORinbundle.jsfromUglifyJsUnexpectedtoken:name(Negociacao)[bundle.js:75,4]
Eagora?Quandousamosoparâmetro-p,oWebpackusaráomódulo Uglify para minificar nossos arquivos. Todavia, estemódulo ainda não é compatível com a sintaxe do ES2015 (ES6),razãodoerroquerecebemos.
UmasoluçãoseriatranscompilarnossoprojetoparaES5,masisso tornaria o processo de build mais lento, pois teríamos detranscompilar de ES2017 para ES2016, para só depoistranscompilar para ES5. Vamos abdicar do parâmetro -p einstalar o plugin babili-webpack-plugin do Webpack, que sabeminificarcorretamentenossocódigo.
Atravésdoterminal,instalaremosoplugincomocomando:
Excelente, agora precisamos configurá-lo emclient/webpack.config.js. Porém, esse plugin só pode ser
ativadonobuilddeprodução,enãonodedesenvolvimento.Parafazermos essa distinção, consultaremos a variável de ambienteNODE_ENV através de process.env.NODE_ENV , dentro dewebpack.config.js.OpluginsóseráativadoquandoovalordavariáveldeambienteNODE_ENVforproduction:
//client/webpack.config.js
constpath=require('path');
//IMPORTANDOOPLUGINconstbabiliPlugin=require('babili-webpack-plugin');
//COMEÇACOMNENHUMPLUGIN
454 20.5PREPARANDOOBUILDDEPRODUÇÃO
letplugins=[]
if(process.env.NODE_ENV=='production'){
//SEFORPRODUCTION,ADICIONAOPLUGINNALISTA
plugins.push(newbabiliPlugin());}
module.exports={entry:'./app-src/app.js',output:{
filename:'bundle.js',path:path.resolve(__dirname,'dist')
},module:{
rules:[{
test:/\.js$/,exclude:/node_modules/,use:{
loader:'babel-loader'}
}]
},//mesmacoisaqueplugins:pluginsplugins
}
Agora, precisamos alterar nosso arquivoclient/package.json,quedeveatribuiràvariáveldeambienteNODE_ENV o valorproduction toda vezqueo scriptbuild-prodforchamado:
//ATENÇÃO,PORENQUANTOINCOMPATÍVELCOMWINDOWS
//códigoanterioromitido
"scripts":{"test":"echo\"Error:notestspecified\"&&exit1","build-dev":"webpack--configwebpack.config.js","build-prod":"NODE_ENV=productionwebpack--configwebpack.c
20.5PREPARANDOOBUILDDEPRODUÇÃO 455
onfig.js"},
//códigoposterioromitido
O problema é que esta solução só funciona para mudarvariáveisdeambientenoLinuxenoMAC,masnãofuncionaránoWindows.Veremosasoluçãoaseguir.
Para termos uma solução de atribuição de variáveis deambiente que funcione nas plataformas Linux,Mac eWindows,podemos usar o módulo cross-env(https://www.npmjs.com/package/cross-env).Vamosinstalá-lo:
Agora, vamos alterar o script build-prod emclient/package.jsonparafazerusodocross-env:
//client/package.json//códigoanterioromitido
"scripts":{"test":"echo\"Error:notestspecified\"&&exit1","build-dev":"webpack--configwebpack.config.js","build-prod":"cross-envNODE_ENV=productionwebpack--config
webpack.config.js"},
//códigoposterioromitido
Testandonossaalteração:
npmrunbuild-prod
20.6 MUDANDO O AMBIENTE COM CROSS-ENV
456 20.6MUDANDOOAMBIENTECOMCROSS-ENV
Agora, o script build-prod criará oclient/dis/bundle.js minificado, perfeito para ser utilizado
emprodução.
Estamos utilizando Webpack para agrupar nossos módulos,mas precisar executar o npm script build-dev toda vez quealterarmos um arquivo desanima qualquer cangaceiro. Podemoslançar mão do webpack-dev-server(https://www.npmjs.com/package/webpack-dev-server), umservidor para ambiente de desenvolvimento que se integra comWebpack, disparando o build do projeto sempre que algumarquivo for modificado, e ainda por cima suporta livereload!Porém, antes de instalá-lo, precisamos realizar uma pequenaalteraçãoemjscangaceiro/server/.
O servidordisponibilizadonodownloaddoprojetono iníciodolivro,alémdedisponibilizarumaAPIparaserconsumidapelanossa aplicação, também disponibiliza nosso projeto para onavegador.Precisamosdesabilitaresteúltimorecurso,poisquemvai disponibilizar nossa aplicação para o navegador será owebpack-de-server.
Vamos alterar o arquivo server/config/express.js ecomentarasseguinteslinhasdecódigo:
//server/config/express.js
//códigoanterioromitido
//comentaressaslinhasquecompartilhamosarquivosestáticos
20.7 WEBPACK DEV SERVER ECONFIGURAÇÃO
20.7WEBPACKDEVSERVERECONFIGURAÇÃO 457
/*app.set('clientPath',path.join(__dirname,'../..','client'));console.log(app.get('clientPath'));app.use(express.static(app.get('clientPath')));*/
//códigoposterioromitido
Essa alteração é suficiente para que nosso servidordisponibilizeapenasaAPIdenegociações.
Precisamos alterar agora client/app-
src/domain/negociacao/NegociacaoService.js . NósestávamosusandooendereçorelativodaAPI,porquenossapáginaficava no mesmo servidor da API. Porém, como realizamos adivisão, precisamos adicionar http://localhost:3000 aoendereço:
//client/app-src/domain/negociacao/NegociacaoController.js
import{HttpService}from'../../util/HttpService.js';import{Negociacao}from'./Negociacao.js';
exportclassNegociacaoService{
constructor(){
this._http=newHttpService();}
obtemNegociacoesDaSemana(){
returnthis._http.get('http://localhost:3000/negociacoes/semana')
//códigoomitido}
obtemNegociacoesDaSemanaAnterior(){
returnthis._http
458 20.7WEBPACKDEVSERVERECONFIGURAÇÃO
.get('http://localhost:3000/negociacoes/anterior')//códigoomitido
}
obtemNegociacoesDaSemanaRetrasada(){
returnthis._http.get('http://localhost:3000/negociacoes/retrasada')//códigoomitido
}
//códigoposterioromitido}
Precisamos alterar também client/app-src/app.js, poisacessamosaAPIpararealizarmosumPOST:
//client/app-src/app.js
//códigoanterioromitidofetch('http://localhost:3000/negociacoes',config)
.then(()=>console.log('Dadoenviadocomsucesso'));
Agora jápodemos instalarnossonovo servidor.No terminal,dentrodapastaclient,executaremosocomando:
Em seguida, vamos adicionar o npm script start emclient/package.json,quelevantaránossoservidor.
"scripts":{"test":"echo\"Error:notestspecified\"&&exit1","build-dev":"webpack--configwebpack.config.js",
"build-prod":"cross-envNODE_ENV=productionwebpack--configwebpack.config.js",
"start":"webpack-dev-server"},
Antesdecontinuarmos,vamosapagarapastaclient/dist.Ela só existirá quando executarmos os scripts build-dev ou
20.7WEBPACKDEVSERVERECONFIGURAÇÃO 459
build-prod . Quando estivermos usando o webpack-dev-server, o build será feito na memória do servidor, evitando aescrita em disco para acelerar o seu carregamento. É coisa decangaceiromesmo!
Fechetodososterminais.Emseguida,comumnovoabertoedentrodapastaclient,executeocomando:
npmstart
Receberemosaseguintemensagemnoconsole:
>[email protected]/Users/flavioalmeida/Desktop/20/client>webpack-dev-server
Projectisrunningathttp://localhost:8080/webpackoutputisservedfrom/
//outrasmensagensdecompilação
Acessamosagoranossaaplicaçãoemumanovaporta,a8080através do endereço http://localhost:8080 . Porém, algopareceestarerrado.
Nossa página carrega,mas o dist/bundle.js indicado natagscriptdeindex.htmlnãofoiencontrado.Arazãodissoéquenossoservidordisponibilizouoarquivoemmemória,masnoendereço localhost:8080/bundle.js , e nãolocalhost:8080/dist/bundle.js.Podemosajustarocaminho
do bundle com uma pequena alteração emclient/webpack.config.js.
Emclient/webpack.config.js,dentrodeoutput,vamosadicionarachavepublicPath. Ela indicaráo caminhopúblicoacessívelatravésdonavegadorparanossobundle:
460 20.7WEBPACKDEVSERVERECONFIGURAÇÃO
//client/webpack.config.js
constpath=require('path');constbabiliPlugin=require('babili-webpack-plugin');
letplugins=[]
if(process.env.NODE_ENV=='production'){
plugins.push(newbabiliPlugin());}
module.exports={entry:'./app-src/app.js',output:{
filename:'bundle.js',path:path.resolve(__dirname,'dist'),publicPath:"dist"
},//códigoposterioromitido
Agora,bastapararmososervidoreexecutá-lonovamentecomnpmstart, para que ele fique de pémais uma vez comnossa
aplicaçãoacessandocorretamentedist/bundle.js.
Experimenteagorarealizaralteraçõesnoprojeto,porexemplo,adicionando um alert no método adiciona() deNegociacaoController . Um novo bundle em memória será
gerado e o navegador automaticamente será recarregado.Fantástico!
Mas ainda não acabou, podemos avançar ainda mais pelosertãodoWebpack!
Umoutropontoemnossaaplicaçãoquepodesermelhoradoé
20.8 TRATANDO ARQUIVOS CSS COMOMÓDULOS
20.8TRATANDOARQUIVOSCSSCOMOMÓDULOS 461
a maneira como gerenciamos e utilizamos as dependências defront-end. Vamos começar primeiro atacando a questão dosarquivosCSS.
Dentrodeclient/css, temososarquivosdoBootstrapqueforam disponibilizados com o download do projeto usado noinício do livro. Contudo, se quisermos utilizar outras bibliotecasCSS,teremosdebaixá-lasmanualmente,paraentãocopiá-lasparadentrodapastaclient/css.
Comcertezanãoqueremosassumiressatarefa,aindamaisquetemosonpm,ogerenciadordepacotesdoNode.jsquepodenosajudaragerenciarasdependênciasdofront-end.Oprimeiropassoquefaremoséapagarapastaclient/css, comos arquivosdoBootstrapqueforamdisponibilizados.Nãosepreocupe,confie!
Agora, como verdadeiros cangaceiros, vamos baixar oBootstrapatravésdonpm:
Como se não bastasse, removeremos a importação dosarquivos CSS do Bootstrap em client/index.html . A tag<head>ficaráassim:
<!--client/index.html--><!DOCTYPEhtml><html>
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width"><title>Negociações</title>
</head>
Agora, em vez de importamos o Bootstrap diretamente em
462 20.8TRATANDOARQUIVOSCSSCOMOMÓDULOS
client/index.html , vamos importá-lo como se fosse ummóduloemclient/app-src/app.js,issomesmo!
//client/app-src/app.js
//ATÉUMCANGACEIROVIRARIAOSOLHOSPARAISSO!import'bootstrap/dist/css/bootstrap.css';import'bootstrap/dist/css/bootstrap-theme.css';
import{NegociacaoController}from'./controllers/NegociacaoController.js';import{Negociacao}from'./domain/index.js';
//códigoposterioromitido
Realizamos o import debootstrap/dist/css/bootstrap.css.Dealgumamaneiraque
aindanãosabemos,oWebpackentendeporpadrãoquedesejamosacessar na verdadeclient/node_modules/bootstrap/dist/css/bootstrap.css .
É uma maneira nada convencional de importar um CSS, aindamaisqueestamostratandooCSSdoBootstrapcomoummódulo!Seráquefunciona?
Olhandonoterminal,vemosaseguintemensagemdeerro:
ERRORin./node_modules/bootstrap/dist/css/bootstrap.cssModuleparsefailed:/Users/flavioalmeida/Desktop/20/client/node_modules/bootstrap/dist/css/bootstrap.cssUnexpectedtoken(7:5)Youmayneedanappropriateloadertohandlethisfiletype.
OWebpack não sabe tratar o Bootstrap nem qualquer outroCSS como um módulo. Para que isso seja possível, precisamosensiná-loatravésdeumloaderquesaibalidarcomessasituação.
Para conseguirmos carregar CSS como módulos, precisamosdoauxíliodocss-loader(https://github.com/webpack-contrib/css-
20.8TRATANDOARQUIVOSCSSCOMOMÓDULOS 463
loader) e do style-loader (https://github.com/webpack-contrib/style-loader). O primeiro plugin transforma os arquivosCSSimportadosemumJSON,eosegundoutilizaessainformaçãoparaadicionardemaneirainlineosestilosdiretamentenoHTML,atravésdatag<style>,quandoobundleforcarregado.
Vamos parar nosso servidor, para então instalar os loadersatravésdoterminal:
[email protected]@0.18.2--save-dev
Isso ainda não é suficiente, precisamos configurá-los emclient/webpack.config.js.Realizandoamodificação:
constpath=require('path');constbabiliPlugin=require('babili-webpack-plugin');
letplugins=[]
if(process.env.NODE_ENV=='production'){
pluginn.push(newbabiliPlugin());}
module.exports={entry:'./app-src/app.js',output:{
filename:'bundle.js',path:path.resolve(__dirname,'dist'),publicPath:"dist"
},module:{
rules:[{
test:/\.js$/,exclude:/node_modules/,use:{
loader:'babel-loader'}
},
464 20.8TRATANDOARQUIVOSCSSCOMOMÓDULOS
/*NOVAREGRAAQUI*/{
test:/\.css$/,loader:'style-loader!css-loader'
}]
},plugins
}
Adicionamosumanovaregraqueseaplicaatodososarquivosque terminam com a extensão .css. Para estes arquivos, osloadersstyle-loadereocss-loaderentrarãoemaçãoparacarregá-los.Seráquefunciona?
Quando subimos mais uma vez o servidor com o comandonpm start, recebemos uma sucessão de mensagem de erros,
porém, como estamos próximos a nos tornarmos cangaceirosJavaScript,elasnãodevemnosapavorar.
OproblemaestánadependênciadoBootstrapdearquivosdefontesusadosporele.Duranteobuild,oWebpacknãosabelidarcom esses arquivos, inclusive nem sabe que eles devem estardentrodapastaclient/distduranteoprocessodedistribuição.Precisamostreiná-loatravésdenovosplugins.
Os plugins url-loader (https://github.com/webpack-contrib/url-loader) e file-loader (https://github.com/webpack-contrib/file-loader) são aqueles que conseguirão lidar com ocarregamentodosarquivosdefontesutilizadospeloBootstrap.
Atravésdoterminale,comosempre,dentrodapastaclient,vamosinstalarosplugins:
[email protected]@0.11.2--save-dev
20.8TRATANDOARQUIVOSCSSCOMOMÓDULOS 465
Por fim, precisamos colocar uma regra para cada tipo dearquivode fonte acessadopeloBootstrap.Não se assuste comasexpressões regulares, elas são expressões clássicas do mundoWebpack:
//client/webpack.config.js
constpath=require('path');constbabiliPlugin=require("babili-webpack-plugin");
letplugins=[]
if(process.env.NODE_ENV=='production'){
plugins.push(newbabiliPlugin());}
module.exports={entry:'./app-src/app.js',output:{
filename:'bundle.js',path:path.resolve(__dirname,'dist'),publicPath:"dist"
},module:{
rules:[{
test:/\.js$/,exclude:/node_modules/,use:{
loader:'babel-loader'}
},{
test:/\.css$/,loader:'style-loader!css-loader'
},{
test:/\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,loader:'url-loader?limit=10000&mimetype=applicat
ion/font-woff'},{
466 20.8TRATANDOARQUIVOSCSSCOMOMÓDULOS
test:/\.ttf(\?v=\d+\.\d+\.\d+)?$/,loader:'url-loader?limit=10000&mimetype=applicat
ion/octet-stream'},{
test:/\.eot(\?v=\d+\.\d+\.\d+)?$/,loader:'file-loader'
},{
test:/\.svg(\?v=\d+\.\d+\.\d+)?$/,loader:'url-loader?limit=10000&mimetype=image/sv
g+xml'}
]},plugins
}
Os loaders são aplicados da direita para a esquerda. Logo, ocss-loaderseráaplicadoprimeiroparaentãoostyle-loaderentraremação.
Agora, parando e subindo mais uma vez nosso servidor, obuild do Webpack é realizado sem problema algum e nossaaplicação continua sendo acessível através dehttp://localhost:8080.
Autilizaçãodocss-loader e dostyle-loader permiteimportarmos qualquerCSS comoummódulo emnossos scripts.Como o client/app-src/app.js é o primeiro módulo a sercarregadopela aplicação,nadamais justodoque importarmosoBootstrap e qualquer outro CSS que desejarmos utilizar em
nossaaplicação.Podemosatéfazerumteste.
Vamos criar o arquivo client/css/meucss.css com oseguinte estilo que adiciona uma sombra na borda da tabela denegociações:
20.8TRATANDOARQUIVOSCSSCOMOMÓDULOS 467
/*client/css/meucss.css*/
table{box-shadow:5px5px5px;
}
Agora, em client/app-src/app.js , vamos importar oarquivo CSS que acabamos de criar.Mas atenção, não podemosfazersimplesmenteimport'css/meucss.css',poisoWebpackentenderá que desejamos acessarclient/node_modules/css/meucss.css , já que este é seudefault . Para importarmos corretamente meucss.css ,
precisamos descer uma pasta, por isso usamos import
'../css/meucss.css':
//client/app-src/app.js
import'bootstrap/dist/css/bootstrap.css';import'bootstrap/dist/css/bootstrap-theme.css';
//importou!import'../css/meucss.css';
import{NegociacaoController}from'./controllers/NegociacaoController.js';import{Negociacao}
//códigoposterioromitido
Essa alteração fará com que o nosso navegador sejaautomaticamenterecarregado, levandoemconsideraçãoumnovoarquivo bundle.js criado em memória com a alteração queacabamosdefazer.
468 20.8TRATANDOARQUIVOSCSSCOMOMÓDULOS
Apráticade importarumarquivoCSScomomódulobrilhaainda mais quando trabalhamos com componentes. Umabiblioteca que utiliza essa estratégia é a React(https://facebook.github.io/react/).
Tudofuncionacomoesperado.Masseráquefuncionamesmo?Éoqueveremosaseguir.
Umolharatentoperceberáqueaaplicaçãocarregaráe,duranteuma janelade tempobempequena, ficará semestilo algum. Issoaconteceporqueosestilosque importamosestãosendoaplicadosprogramativamente pelo bundle que foi carregado. Aliás, eleincorporatodooCSSdoBootstrap,umbaitaCSSgigante!
Podemos voltar com a abordagem tradicional de carregar osarquivos CSS através da tag link e, ao mesmo tempo,aproveitarmos o processo de concatenação e minificação paradiferentesambientes.
Primeiro, vamos alterar client/index.html e importar oseguinteCSSqueconstruiremosduranteoprocessodebuild:
<!--client/index.html-->
<!DOCTYPEhtml><html>
<head>
20.9 RESOLVENDO O FOUC (FLASH OFUNSTYLEDCONTENT)
20.9RESOLVENDOOFOUC(FLASHOFUNSTYLEDCONTENT) 469
<metacharset="UTF-8"><metaname="viewport"content="width=device-width"><title>Negociações</title><linkrel="stylesheet"href="dist/styles.css">
</head><!--códigoposterioromitido-->
Agora, vamos instalar o plugin extract-text-webpack-plugin(https://github.com/webpack-contrib/extract-text-webpack-plugin)atravésdoterminal,aindadentrodapastaclient:
Com o plugin instalado, precisamos importá-lo emwebpack.config.jseguardarsuainstânciadentrodanossalistade plugins. Sua instância recebe como parâmetro o nome doarquivo CSS que será gerado no final. Também será precisoalterarmosaregra(rule)dosarquivosterminadosem.css.
Usaremos a funçãoextract() presentenopluginque faráum fallback para style-loader , caso não consiga extrair oarquivoCSS.Nossoarquivowebpack.config.jsestaráassim:
//client/webpack.config.js
constpath=require('path');constbabiliPlugin=require('babili-webpack-plugin');constextractTextPlugin=require('extract-text-webpack-plugin');
letplugins=[]
//ADICIONANDOOPLUGIN,//MAISABAIXO,MODIFICAÇÃODOLOADER
plugins.push(newextractTextPlugin("styles.css")
);
if(process.env.NODE_ENV=='production'){
470 20.9RESOLVENDOOFOUC(FLASHOFUNSTYLEDCONTENT)
plugins.push(newbabiliPlugin());}
module.exports={entry:'./app-src/app.js',output:{
filename:'bundle.js',path:path.resolve(__dirname,'dist'),publicPath:"dist"
},module:{
rules:[{
test:/\.js$/,exclude:/node_modules/,use:{
loader:'babel-loader'}
},
//MODIFICANDOOLOADER
{test:/\.css$/,use:extractTextPlugin.extract({
fallback:"style-loader",use:"css-loader"
})},{
test:/\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,loader:'url-loader?limit=10000&mimetype=applicat
ion/font-woff'},{
test:/\.ttf(\?v=\d+\.\d+\.\d+)?$/,loader:'url-loader?limit=10000&mimetype=applicat
ion/octet-stream'},{
test:/\.eot(\?v=\d+\.\d+\.\d+)?$/,loader:'file-loader'
},{
test:/\.svg(\?v=\d+\.\d+\.\d+)?$/,
20.9RESOLVENDOOFOUC(FLASHOFUNSTYLEDCONTENT) 471
loader:'url-loader?limit=10000&mimetype=image/svg+xml'
}]
},plugins
}
Agora, podemos fazer o build da nossa aplicação dedesenvolvimento ou de produção, pois nos dois casos teremostodos os arquivos CSS concatenados em um único arquivo,separando-odobundle.js.
Masseráqueestátudonamaisperfeitaharmonia?Veremosnapróximaseção.
Um cangaceiro mais atento verificará que o arquivodist/style.css gerado não foiminificado. Podemos resolver
isso com o plugin optimize-css-assets-webpack-plugin(https://github.com/NMFR/optimize-css-assets-webpack-plugin).Todavia,eledependedeumminificadorCSS,nocaso,usaremosocssnano(http://cssnano.co).
Vamosinstalarambosatravésdoterminal:
npminstalloptimize-css-assets-webpack-plugin@[email protected]
//client/webpack.config.js
constpath=require('path');constbabiliPlugin=require('babili-webpack-plugin');constextractTextPlugin=require('extract-text-webpack-plugin');
20.10 RESOLVEMOS UM PROBLEMA ECRIAMOSOUTRO,MASTEMSOLUÇÃO!
472 20.10RESOLVEMOSUMPROBLEMAECRIAMOSOUTRO,MASTEMSOLUÇÃO!
//IMPORTOUOPLUGIN
constoptimizeCSSAssetsPlugin=require('optimize-css-assets-webpack-plugin');
letplugins=[]
plugins.push(newextractTextPlugin("styles.css")
);
if(process.env.NODE_ENV=='production'){
plugins.push(newbabiliPlugin());
//ADICIONOUOPLUGINPARAOBUILDDEPRODUÇÃO
plugins.push(newoptimizeCSSAssetsPlugin({cssProcessor:require('cssnano'),cssProcessorOptions:{
discardComments:{removeAll:true
}},
canPrint:true}));
}
//códigoposterioromitido
Agora,aoexecutarmosnoterminaloscriptnpmrunbuild-prod,oarquivodist/styles.cssestaráminificado.
Talvez o leitor esteja se perguntando qual a diferença entreloader e plugin. A grosso modo, o primeiro trabalha com cadaarquivoindividualmenteduranteouantesdeobundlesergerado.Já o segundo trabalha comobundleno final do seuprocessodegeração.
20.11IMPORTANDOSCRIPTS
20.11IMPORTANDOSCRIPTS 473
Somos capazes de importar bibliotecas diretamente declient/node_modules, inclusive aprendemos a tratar arquivosCSS comomódulos. Será que o procedimento de importação debibliotecas JavaScriptdentrodeclient/mode_modules segueomesmoprocesso?Veremos.
Não temos um uso prático em nossa aplicação para oscomponentes do Bootstrap que utilizam seu scriptbootstrap.js . Contudo, vamos importá-lo para verificar seconseguimoscarregá-locorretamente.
Alterandoclient/app-src/app.js:
//client/app-src/app.js
import'bootstrap/dist/css/bootstrap.css';import'bootstrap/dist/css/bootstrap-theme.css';
//carregouoscriptdobootstrap,apenasomodalimport'bootstrap/js/modal.js';import'../css/meucss.css';
//códigoposterioromitido
Bom, pelo menos, no terminal não temos nenhum erro decompilaçãonoterminal.Masseabrirmosoconsoledonavegador,veremosaseguintemensagemdeerro:
UncaughtError:Bootstrap'sJavaScriptrequiresjQuery
Faztodosentido,porqueobootstrap.jsdependedojQuery(https://jquery.com) para funcionar, a biblioteca front-end queainda vive no coletivo imaginário dos programadores front-end.Como estamos usando o próprio npm para gerenciar nossasbibliotecas front-end, nada mais justo do que baixar o jQueryatravésdele:
474 20.11IMPORTANDOSCRIPTS
Agora, em client/app-src/app.js , vamos importar ojQueryantesdoscriptdobootstrap.js:
//client/app-src/app.js
import'bootstrap/dist/css/bootstrap.css';import'bootstrap/dist/css/bootstrap-theme.css';
//importouantesimport'jquery/dist/jquery.js';
import'bootstrap/js/modal.js';import'../css/meucss.css';
//códigoposterioromitido
Sabemosqueessaalteraçãogeraráumnovobundle.jsefaráorecarregamentodonavegador.Paranossasurpresa,continuamoscomoproblemaanteriornoconsoledonavegador:
bundle.js:11106UncaughtError:Bootstrap'sJavaScriptrequiresjQuery
EsseWebpack é cheio de surpresas, não émesmo? Vejamoscomoprocederaseguir.
O jQuerynão é encontrado em tempode execuçãoporque éumabibliotecaquevivenoescopoglobal.QuandoelaéprocessadapeloWebpack,elaéimportadacomosefosseummóduloedeixade ser acessível pelo restante da aplicação. Precisamos de umcomportamentodiferente.
20.12 LIDANDO COM DEPENDÊNCIASGLOBAIS
20.12LIDANDOCOMDEPENDÊNCIASGLOBAIS 475
Para tornarmos o jQuery globalmente disponível, precisamosda ajuda do webpack.ProvidePlugin(https://webpack.js.org/plugins/provide-plugin/). Owebpack.ProvidePluginautomaticamentecarregamódulosemvez de termos de importá-los em qualquer lugar da nossaaplicação.
Aboanotíciaéquenãoprecisamosinstalá-lo,poiseleépadrãodoWebpack.Basta importarmosomódulowebpack atravésderequire('webpack')emclient/webpack.config.js:
//client/webpack.config.js
constpath=require('path');constbabiliPlugin=require('babili-webpack-plugin');constextractTextPlugin=require('extract-text-webpack-plugin');constoptimizeCSSAssetsPlugin=require('optimize-css-assets-webpack-plugin');
//IMPORTOUOMÓDULODOWEBPACK!constwebpack=require('webpack');
letplugins=[]
plugins.push(newextractTextPlugin("styles.css"));
//OPLUGINVALETANTOPARAPRODUÇÃO//QUANTOPARADESENVOLVIMENTO
plugins.push(newwebpack.ProvidePlugin({$:'jquery/dist/jquery.js',jQuery:'jquery/dist/jquery.js'}));
if(process.env.NODE_ENV=='production'){
plugins.push(newbabiliPlugin());
476 20.12LIDANDOCOMDEPENDÊNCIASGLOBAIS
plugins.push(newoptimizeCSSAssetsPlugin({cssProcessor:require('cssnano'),cssProcessorOptions:{discardComments:{removeAll:true}},canPrint:true}));}
//códigoposterioromitido
Na modificação realizada, estamos tornandojquery/dist/jquery.jsacessívelcom$ejQueryparatodososmódulosdaaplicação.Essesnomessãoimportantes,porquesãoosaliasesusadospelabiblioteca.
Podemos remover o import do jQuery que fizemos emclient/app-src/app.js, pois, comovimos, ele será carregadopelowebpack.ProvidePlugin:
//client/app-src/app.js
import'bootstrap/dist/css/bootstrap.css';import'bootstrap/dist/css/bootstrap-theme.css';
//removeuoscriptdojQuery
import'bootstrap/js/modal.js';import'../css/meucss.css';
//códigoposterioromitido
NãotentetestaradisponibilidadedojQueryatravésdoconsoledo navegador, pois ele foi encapsulado dentro de uma funçãoduranteoprocessodebuild.
20.12LIDANDOCOMDEPENDÊNCIASGLOBAIS 477
OconsoleChromepossui uma funçãoauxiliar chamada$,disponível quando o jQuery não é carregado. Isso podeconfundi-lo, achando que este é o jQuery que carregamosatravésdoProvidePlugin.
Parasaberseeleestádisponívelounão,podemosrealizarumtestenopróprioclient/app-src/app.js:
//client/app-src/app.js
import'bootstrap/dist/css/bootstrap.css';import'bootstrap/dist/css/bootstrap-theme.css';import'bootstrap/js/modal.js';import'../css/meucss.css';import{NegociacaoController}from'./controllers/NegociacaoController.js';import{Negociacao}from'./domain/index.js';
$('h1').on('click',()=>alert('Foiclicado!'));
//códigoposterioromitido
Um cangaceiro só acredita vendo. Dessa forma, nada maisjustodoqueverificarnonavegadorofuncionamentodaaplicação.
Aliás,podemosfazeramesmacoisacomoscriptdoBootstrapque carregamos em nossa aplicação. Assim como jQuery, ele éativadoglobalmente,poismodificaocoredojQueryparasuportarsuasnovasfunções.
Podemos testaratravésdeconsole.log($('h1').modal) aexistênciada funçãomodal que só estarádisponível se o scriptmodal.jsdoBootstrapforcarregado:
478 20.12LIDANDOCOMDEPENDÊNCIASGLOBAIS
//client/app-src/app.js
import'bootstrap/dist/css/bootstrap.css';import'bootstrap/dist/css/bootstrap-theme.css';import'bootstrap/js/modal.js';import'../css/meucss.css';import{NegociacaoController}from'./controllers/NegociacaoController.js';import{Negociacao}from'./domain/index.js';
$('h1').on('click',()=>alert('Foiclicado!'));
//provandoaexistênciadafunção!console.log('Funçãoadicionadapelobootstrap:');console.log($('h1').modal);
//códigoposterioromitido
Excelente!AgorajápodemosrealizarmaisumnovoajusteemnossaconfiguraçãodoWebpack.
Cadamódulo do nosso bundle é envolvido por umwrapper,quenadamaisédoqueumafunção.Contudo,aexistênciadesseswrappers tornam a execução do nosso script no navegador umpoucomaislenta.
Entretanto, a partir doWebpack 3, podemos ativar o ScopeHoisting. Ele consiste em concatenar o escopo de todos osmódulos em um único wrapper, permitindo assim que nossocódigosejaexecutadomaisrapidamentenonavegador.
Vamos ativar esse recurso apenas no build de produção,adicionandoumainstânciadeModuleConcatenationPluginemnossalistadeplugins:
20.13 OTIMIZANDO O BUILD COM SCOPEHOISTING
20.13OTIMIZANDOOBUILDCOMSCOPEHOISTING 479
//client/webpack.config.js
//códigoanterioromitido
if(process.env.NODE_ENV=='production'){
//ATIVANDOOSCOPEHOISTING
plugins.push(newwebpack.optimize.ModuleConcatenationPlugin());
plugins.push(newbabiliPlugin());
plugins.push(newoptimizeCSSAssetsPlugin({cssProcessor:require('cssnano'),cssProcessorOptions:{discardComments:{removeAll:true}},canPrint:true}));}//códigoposterioromitido
Isso já é suficiente.Contudo, devido à dimensão reduzida danossaaplicação,teremosdificuldadedevernapráticaasmudançasnotempodeexecuçãodanossaaplicação.
Obundle.js,criadopelonossoscriptdoWebpack,possuiocódigodosnossosmódulos,inclusiveocódigodasbibliotecasqueutilizamos.O problema dessa abordagem é o aproveitamento docachedonavegador.
Toda vez que alterarmos o código do nosso projeto, algo
20.14 SEPARANDO NOSSO CÓDIGO DASBIBLIOTECAS
480 20.14SEPARANDONOSSOCÓDIGODASBIBLIOTECAS
extremamente comum, geraremos um novo bundle.js quedeverá ser baixado e cacheado pelo navegador. Contudo, setivermosdoisbundles, um para nosso código e outro para asbibliotecas, o último ficará mais tempo no cache do navegador,poismudacommenosfrequência.
ConseguimosessadivisãoatravésdoCommonsChunkPlugin,que já vem com o próprio Webpack. Alterandoclient/webpack.config.js:
constpath=require('path');constbabiliPlugin=require('babili-webpack-plugin');constextractTextPlugin=require('extract-text-webpack-plugin');constoptimizeCSSAssetsPlugin=require('optimize-css-assets-webpack-plugin');constwebpack=require('webpack');
letplugins=[]
//osdoispluginsquejáconfiguramosforamomitidos
plugins.push(newwebpack.optimize.CommonsChunkPlugin({name:'vendor',filename:'vendor.bundle.js'}));//códigoposterioromitido
Comestaconfiguração,estamosdizendoque,paraapartedanossa aplicação chamada de vendor, todas as bibliotecas quevamosindicarequefazempartedenode_modulesfarãopartedevendor.bundle.js.
Agora,precisamosdividirnossaaplicaçãoemduaspartes:
//client/webpack.config.js
20.14SEPARANDONOSSOCÓDIGODASBIBLIOTECAS 481
//códigoanterioromitido
//MUDAMOSOENTRY!
module.exports={
entry:{app:'./app-src/app.js',vendor:['jquery','bootstrap','reflect-metadata']},//códigoposterioromitido
Nossaaplicaçãoagorapossuidoispontosdeentrada,app evendor. Este nome não é por acaso, ele deve coincidir com onamequedemosnaconfiguraçãodoCommonsChunkPlugin.Apropriedade recebe um array com o nome dos módulos em node_modules que desejamos adicionar no bundlevendor.bundle.js.
Há mais um detalhe antes de realizarmos o build do nossoprojeto. Precisamos alterar client/index.html para quecarregueovendor.bundle.js.Nãoprecisaremosmaisimportaroreflect-metadata,poiselefarápartedevendor.bundle.js.
<!--client/index.html--><!--códigoanterioromitido-->
<divid="negociacoes"></div>
<!--importouonovobundle--><scriptsrc="dist/vendor.bundle.js"></script><scriptsrc="dist/bundle.js"></script>
</body></html>
Agora, ao realizar o build do nosso projeto, nossa pastaclient/distteráumaestruturaparecidacom:
482 20.14SEPARANDONOSSOCÓDIGODASBIBLIOTECAS
├──448c34a56d699c29117adc64c43affeb.woff2├──89889688147bd7575d6327160d64e760.svg├──bundle.js├──e18bbf611f2a2e43afc071aa2f4e1512.ttf├──f4769f9bdb7466be65088239c12046d1.eot├──fa2772327f55d8198301fdb8bcfc8158.woff├──styles.css└──vendor.bundle.js
Tivemosquealteraroarquivoclient/index.htmlmais deuma vez ao longo da nossa aventura com o Webpack.Adicionamosaimportaçãodestyle.cssgeradopeloextract-text-webpack-plugin e também a importação devendor.bundle.js gerado pelo CommonsChunkPlugin . Esseprocessomanual não precisa ser assim. Com o auxílio dohtml-webpack-plugin (https://github.com/jantimon/html-webpack-plugin)podemosautomatizaresseprocesso.
Ohtml-webpack-pluginécapazdegerarumnovoarquivo html com todas as importações de artefatos gerados peloWebpack automaticamente. Inclusive, podemos indicar umtemplateinicialquesofreráatransformaçãonolugardecriarmosum novo. Perfeito, pois já temos um código HTML escrito emclient/index.html.
Vamosinstalaropluginatravésdoterminal:
Antes de continuarmos, vamos renomear o arquivo client/index.html para client/main.html . A ideia égerarmosoarquivoindex.htmldentrodeclient/dist com
20.15 GERANDO A PÁGINA PRINCIPALAUTOMATICAMENTE
20.15GERANDOAPÁGINAPRINCIPALAUTOMATICAMENTE 483
todososoutrosarquivosgeradospeloWebpack.
Alémderenomearoarquivo,precisamosremovertodasastags<link> e<script> que importamarquivosCSS e JavaScriptrespectivamente.
Tendoacertezadasalteraçõesquefizemosantes,sónosrestaalteraroarquivoclient/webpack.config.js.Primeiro,vamosimportareadicionaroplugincomoprimeirodanossalista:
//client/webpack.config.js
constpath=require('path');constbabiliPlugin require('babili webpack plugin')constextractTextPlugin=require('extract-text-webpack-plugin');constoptimizeCSSAssetsPlugin=require('optimize-css-assets-webpack-plugin');constwebpack=require('webpack');
//importouonovopluginconstHtmlWebpackPlugin=require('html-webpack-plugin');
letplugins=[]
//adicionoucomoprimeirodalistaplugins.push(newHtmlWebpackPlugin({hash:true,minify:{html5:true,collapseWhitespace:true,removeComments:true,},filename:'index.html',template:__dirname+'/main.html',}));
//códigoposterioromitido
O plugin recebe um objeto como parâmetro com suasconfigurações,sãoelas:
484 20.15GERANDOAPÁGINAPRINCIPALAUTOMATICAMENTE
hash:quandotrue, adicionaumhashno finaldaURLdosarquivos script e CSS importados no arquivo HTML gerado,importanteparaversionamentoecachenonavegador.Quandoumbundlediferenteforgerado,ohashserádiferenteeissoésuficienteparainvalidarocachedonavegador,fazendo-ocarregaroarquivomaisnovo.
minify: recebe um objeto como parâmetro com asconfigurações utilizadas para minificar o HTML. Podemosconsultar todas as configurações possíveis no endereçohttps://github.com/kangax/html-minifier#options-quick-reference
filename: o nome do arquivo HTML que será gerado.Respeitaráovalordopathdeoutputquejáconfiguramoslogonoiníciodacriaçãodoarquivowebpack.config.js.
template:caminhodoarquivoqueservirácomotemplateparageraçãodeindex.html.
Por fim, precisamos demais uma alteração.Comoo arquivo index.html gerado ficará em client/dist/index.html ,precisamosajustarocaminhodosimportsdosscriptsedosestilos.Para isso, vamos remover a propriedadepublicPath da chaveoutput:
//client/webpack.config.js
//códigoanterioromitido
module.exports={entry:{app:'./app-src/app.js',vendor:['jquery','bootstrap','reflect-metadata']},
20.15GERANDOAPÁGINAPRINCIPALAUTOMATICAMENTE 485
output:{filename:'bundle.js',path:path.resolve(__dirname,'dist')/*removeupublicPath*/},//códigoposterioromitido
Pronto. Agora podemos testar a criação de um build, porexemplo,odedesenvolvimento:
npmrunbuild-dev
Olhandoclient/dist vemos que o arquivo index.htmlfoigerado:
├──448c34a56d699c29117adc64c43affeb.woff2├──89889688147bd7575d6327160d64e760.svg├──bundle.js├──e18bbf611f2a2e43afc071aa2f4e1512.ttf├──f4769f9bdb7466be65088239c12046d1.eot├──fa2772327f55d8198301fdb8bcfc8158.woff├──index.html├──styles.css└──vendor.bundle.js
Nossa solução funciona para o build de produção e tambémquandoestamosusandooWebpackDevServer.
Quando usamos o System.js, somos obrigados a indicar aextensãodearquivo.jsemnossosimports.ComWebpack,issonãoémaisnecessário.Vejamosumexemplo:
//client/app-src/util/DaoFactory.js
//REMOVEU.jsdonomedoarquivoimport{ConnectionFactory}from'./ConnectionFactory';
20.16 SIMPLIFICANDO AINDA MAIS AIMPORTAÇÃODEMÓDULOS
486 20.16SIMPLIFICANDOAINDAMAISAIMPORTAÇÃODEMÓDULOS
//REMOVEU.jsdonomedoarquivoimport{NegociacaoDao}from'../domain/negociacao/NegociacaoDao;
exportasyncfunctiongetNegociacaoDao(){
letconn=awaitConnectionFactory.getConnection();returnnewNegociacaoDao(conn);}
Além disso, podemos importar diretamente um barrel semtermos que indicar o arquivo index.js, indicando apenas ocaminhodapastaquecontémindex.js.Vejamosumexemplo:
//client/app-src/controllers/NegociacaoController.js
//removeuOindex.js,importandoapenasapasta
import{Negociacoes,NegociacaoService,Negociacao}from'../domain';import{NegociacoesView,MensagemView,Mensagem,DateConverter}from'../ui';import{getNegociacaoDao,Bind,getExceptionMessage,debounce,controller,bindEvent}from'../util';
@controller('#data','#quantidade','#valor')exportclassNegociacaoController{//códigoposterioromitido
Escrevermenosésemprebom,aindamaisemalgocorriqueirocomoaimportaçãodemódulos.
Nossa aplicação é embrião de uma Single Page Application,aquela que não recarrega durante seu uso e que se pautafortemente em JavaScript. Aliás, esse tipo de aplicação carregatodososscriptsqueprecisanoprimeirocarregamentodapágina.
20.17CODESPLITTINGELAZYLOADING
20.17CODESPLITTINGELAZYLOADING 487
Todavia, quando o tamanho do bundle é muito grande, oprimeiro carregamento da página deixará a desejar, o que podeimpactarnaexperiênciadousuário.
O tempo de carregamento da aplicação pode ser reduzidoatravés do code splitting (separação de código) do bundleprincipal da aplicação em bundle menores carregando-os sobdemanda através da técnica de lazy loading (carregamentopreguiçoso).
Para que possamos ver esse poderoso recurso em ação,elegeremosomóduloNegociacaoServicecomoaquelequeserácarregadosobdemanda.
Oprimeiropassoéremovermostodasasimportaçõesestáticasdomódulorealizadasemnossaaplicação.Primeiro,vamosalterarclient/app-src/domain/index.jsremovendoaimportaçãodomódulo.Nossoarquivoficaráassim:
//client/app-src/domain/index.js
export*from'./negociacao/Negociacao';
export*from'./negociacao/NegociacaoDao';
//NÃOIMPORTAMAISOMÓDULONegociacaoService
export*from'./negociacao/Negociacoes';
Agora, em client/app-
src/controller/NegociacaoController.js vamos remover aimportaçãodeNegociacaoServicelogonoiníciodadeclaraçãodo nosso módulo, inclusive não teremos mais a propriedadethis._service,poisoserviçoserácarregadosobdemanda:
//client/app-src/controllers/NegociacaoController.js
488 20.17CODESPLITTINGELAZYLOADING
//REMOVEUAIMPORTAÇÃODENegociacaoServiceimport{Negociacoes,Negociacao}from'../domain';
import{NegociacoesView,MensagemView,Mensagem,DateConverter}from'../ui';import{getNegociacaoDao,Bind,getExceptionMessage,debounce,controller,bindEvent}from'../util';
@controller('#data','#quantidade','#valor')exportclassNegociacaoController{
constructor(_inputData,_inputQuantidade,_inputValor){
Object.assign(this,{_inputData,_inputQuantidade,_inputValor})
this._negociacoes=newBind(newNegociacoes(),newNegociacoesView('#negociacoes'),'adiciona','esvazia');
this._mensagem=newBind(newMensagem(),newMensagemView('#mensagemView'),'texto');
//REMOVEUthis._service=newNegociacaoService();
this._init();}
//códigoposterioromitido
Agora,comauxíliodafunçãoSystem.importimportaremoso módulo ../domain/negociacao/NegociacaoService . OretornodeSystem.importéumapromisequenosdaráacessoaumobjetoque representaomódulo importado. Sendoo retornoda funçãoumapromise, podemosusarawait, pois a chamadaSystem.import está dentro de um método async, no caso
20.17CODESPLITTINGELAZYLOADING 489
importaNegociacoes:
//client/app-src/controllers/NegociacaoController.js
//códigoanterioromitido
@bindEvent('click','#botao-importa')@debounce()asyncimportaNegociacoes(){
try{//Lazyloadingdomódulo.//Importaremos{NegociacaoService}domódulo.
const{NegociacaoService}=awaitSystem.import('../domain/negociacao/NegociacaoService');
//criandoumanovainstânciadaclasseconstservice=newNegociacaoService();
//MUDAMOSDEthis._serviceparaservice
constnegociacoes=awaitservice.obtemNegociacoesDoPeriodo();console.log(negociacoes);negociacoes.filter(novaNegociacao=>
!this._negociacoes.paraArray().some(negociacaoExistente=>novaNegociacao.equals(negociacaoExistente))).forEach(negociacao=>this._negociacoes.adiciona(negociacao));
this._mensagem.texto='Negociaçõesdoperíodoimportadascomsucesso';}catch(err){this._mensagem.texto=getExceptionMessage(err);}}
//códigoposterioromitido
Com as alterações realizadas, faremos um teste. Através do
490 20.17CODESPLITTINGELAZYLOADING
terminal vamos executar o build de desenvolvimento, o maisrápido:
npmrunbuild-dev
Dentro da pasta client/dist existirá um novo arquivobundle,oarquivo0.bundle.js:
├──0.bundle.js├──448c34a56d699c29117adc64c43affeb.woff2├──89889688147bd7575d6327160d64e760.svg├──bundle.js├──e18bbf611f2a2e43afc071aa2f4e1512.ttf├──f4769f9bdb7466be65088239c12046d1.eot├──fa2772327f55d8198301fdb8bcfc8158.woff├──index.html├──styles.css└──vendor.bundle.js
DuranteoprocessodebuildWebpackéinteligenteosuficienteparasaberqueháumcarregamentosobdemandadeummóduloquenão foi importado estaticamente por nenhumoutromóduloda aplicação. Detectado o carregamento sob demanda, um novobundleserácriadocomocódigodomóduloqueserácarregadoapenasquandoousuárioclicarnobotão "Importarnegociações",poisénaaçãodométododocontrollerchamadopelobotãoqueomóduloserácarregadopelaprimeiravez.Semaistardeclicarmosoutras vezes no botão, nossa aplicação será automaticamenteinteligente para saber que o módulo já foi carregado e não obaixaránovamente.
Podemos inclusive testarnossaaplicaçãoatravésdoWebpackDev Server. Pelo terminal, vamos subir nosso servidor com ocomando:
npmstart
20.17CODESPLITTINGELAZYLOADING 491
Ao acessarmos a aplicação através do endereçolocalhost:8080 ela continuará funcionando.Porém,podemos
ficar mais pertos da especificação ECMAScript do queimaginamos,assuntodapróximaseção.
OECMAScript2015(ES6)especificouumamaneiradedefinirmódulos resolvidos estaticamente em tempo de compilação,porém não definiu uma maneira padronizada de carregar omódulo assincronamente sob demanda. Há uma proposta paraintroduzir importações dinâmicas na linguagem que está bemavançada e que pode ser acompanhada emhttps://github.com/tc39/proposal-dynamic-import.Aboa
notíciaéqueoChromeeoutrosnavegadoresjáderaminícioasuaimplementação.
Webpack é compatível com a nova sintaxe import() ,substitutadeSystem.import(),porémoparserdoBabelnãoécapaz de reconhecer a sintaxe import() como válida sem oauxíliodeumplugin.Podemosresolverissofacilmenteinstalandoo plugin babel-plugin-syntax-dynamic-import
(https://www.npmjs.com/package/babel-plugin-syntax-dynamic-import).
No terminal e dentro da pasta client vamos executar ocomando:
Em seguida, vamos alterarclient/.babelrc e adicionar opluginnalistadeplugins:
20.18SYSTEM.IMPORTVSIMPORT
492 20.18SYSTEM.IMPORTVSIMPORT
{"presets":["es2017"],"plugins":["transform-decorators-legacy","babel-plugin-syntax-dynamic-import"]}
Porfim,podemosalterarachamadadeSystem.importparaimportemNegociacaoController:
//client/app-src/controllers/NegociacaoController.js
//códigoanterioromitido
@bindEvent('click','#botao-importa')@debounce()asyncimportaNegociacoes(){
//MUDOUDESystem.importparaimport
const{NegociacaoService}=awaitimport('../domain/negociacao/NegociacaoService');
//códigoposterioromitido
Podemos realizar o build do projeto, seja ele dedesenvolvimento ou de produção que tudo continuaráfuncionando,inclusiveoWebpackDevserver.
E viva o cangaço! Agora que completamos definitivamentenossaaplicação,podemosgerarobuilddeproduçãoparaquesejarealizadoodeploynoservidordenossapreferência:
npmrunbuild-prod
Agora,sóprecisamoslevarparaproduçãooconteúdodapasta
20.19 QUAIS SÃO OS ARQUIVOS DEDISTRIBUIÇÃO?
20.19QUAISSÃOOSARQUIVOSDEDISTRIBUIÇÃO? 493
dist.
NossaaplicaçãoéumprojetoembrionáriodeumaSinglePageApplication, aquele tipo de aplicação quenão recarrega a páginadurante seu uso e que depende única e exclusivamente deJavaScriptparafuncionar.Nãotemosumsistemaderoteamentoeoutros recursos de uma aplicação mais completa criada porframeworks comoAngular, React, Ember entre outros.Mas issonãonosimpededecolocá-lanoar.
Valelembrarquenossaaplicaçãoéconstituídadeduaspartes.Aprimeira, a aplicação cliente, aquela que rodanonavegador, asegunda, a API do backend. Focaremos apenas o processo dedeploy (colocar no ar) da aplicação cliente, foco desta obra.Todavia,nadaimpedequeoleitorimplementeaAPIutilizadapelocurso utilizando outras linguagens como PHP, Java, C#, etc.realizando o deploy de sua API utilizando o serviço dehospedagem que mais se familiarizar. Aliás, é muito comumaplicações Single Page Application realizarem essa divisãomarcanteentreodeploydoclienteedaAPI.
Uma maneira rápida e gratuita sem grande burocracia derealizarmosodeploydanossaaplicaçãoéatravésdoGitHubPages(https://pages.github.com/).Oautorpresumequeoleitorjátenhauma conta no GitHub, configurado a chave de acesso em suamáquina, saiba clonar um repositório já existente e já tenhainstaladooGitemsuamáquina.ParaoleitorquenuncatrabalhoucomGitHub, esta é uma boa oportunidade de criar sua conta e
20.20 DEPLOY DO PROJETO NO GITHUBPAGES
494 20.20DEPLOYDOPROJETONOGITHUBPAGES
seguirotutorialnoprópriositehttps://github.com/.
ParacriarmosumapáginanoGitHubPagesprecisamoscriarum novo repositório obrigatoriamente com a estruturausuario.github.io.Ousuariononomedorepositórioéonomedo nosso usuário cadastrado no GitHub. Por exemplo, orepositório flaviohenriquealmeida.github.io inicia comflaviohenriquealmeida,usuáriodoautordestelivro.Depoisdecriado,cloneorepositórioemsuamáquinaemqualquerdiretórioquenãosejaomesmodanossaaplicação:
//clonandoorepositório,useoendereçodoseurepositório.
[email protected]:flaviohenriquealmeida/flaviohenriquealmeida.github.io.git
Excelente! Agora, vamos voltar para nosso projeto e gerar obuilddeprodução:
npmrunbuild-prod
Dentro de instantes, será criada a pasta client/dist comtodososarquivosdonossoprojeto.Vamoscopiaroconteúdodapastaclient/dist paradentrododiretóriodo repositórioqueacabamosdeclonar.
No terminal, agora dentro da pasta do repositório, vamosexecutaroscomandos:
gitadd.gitcommit-am"Primeirocommit"gitpush
Pronto,nossoprojetojáestánoar.Paraacessá-loacessamosaURL https://nomedousuario.github.io/, no exemplo queacabamos de ver, a URL é
20.20DEPLOYDOPROJETONOGITHUBPAGES 495
https://flaviohenriquealmeida.github.io/ . Contudo, hádoisproblemasqueprecisamserdestacados.
Nossaaplicaçãoécarregadanonavegador,porémnãoécapazde acessar nossa API mesmo utilizando o endereçohttp://localhost:3000.IssoaconteceporqueoGitHubpagessópermitirárequisiçõesAjaxparadomíniosqueutilizemhttpsenãohttp.CasovocêqueiradesenvolversuaprópriaAPIparasuaaplicação não esqueça desse detalhe, inclusive precisará habilitar CORS (https://pt.wikipedia.org/wiki/Cross-origin_resource_sharing)casocontrárioseunavegadorserecusaráa realizar a requisição Consulte a documentação da suaplataforma/linguagem preferida para criação de APIs para sabercomoprocedernahabilitaçãodeCORS.
OutropontoéqueaURLdaAPIutilizadaemnossaaplicaçãoaponta para um servidor local, de desenvolvimento. Porém,quandonossaaplicaçãoentraemprodução,oendereçodeveráserodaAPIqueestánoarenãooendereçolocaldedesenvolvimento.Dessa forma,precisamos fazer comquenossobuilddeproduçãoseja capaz de levar em consideração outro endereço diferente dehttp://localhost:3000, isto é, que considere o endereço deumaAPIemprodução.Comofaremos isso?Éoqueveremosnapróximaseção.
O Webpack já vem por padrão com o pluginwebpack.DefinePlugin. Este plugin permite definir variáveisquesóexistirãoduranteoprocessodebuildequepodemreceber
20.21ALTERANDOOENDEREÇODAAPINOBUILDDEPRODUÇÃO
496 20.21ALTERANDOOENDEREÇODAAPINOBUILDDEPRODUÇÃO
diferentes valores de acordo com o tipo de build que estamosrealizando.
Primeiro, vamos declarar a variável SERVICE_URL quereceberá como valor padrão o endereçohttp://localhost:3000.Éumaexigênciaqueastringatribuídaà variável seja transformada em um JSON através deJSON.stringify().Emseguida,vamosatribuirumnovovaloràvariáveldentrodacondiçãoifdeprodução:
//client/webpack.config.js
//códigoanterioromitido
letSERVICE_URL=JSON.stringify('http://localhost:3000');
if(process.env.NODE_ENV=='production'){
SERVICE_URL=JSON.stringify("http://enderecodasuaapi.com");
//códigoposterioromitido
Agora,vamosadicionaropluginnalistadeplugins:
//client/webpack.config.js
//códigoanterioromitido
letSERVICE_URL=JSON.stringify('http://localhost:3000');
if(process.env.NODE_ENV=='production'){
SERVICE_URL=JSON.stringify("http://enderecodasuaapi.com");
plugins.push(newwebpack.optimize.ModuleConcatenationPlugin());plugins.push(newbabiliPlugin());plugins.push(newoptimizeCSSAssetsPlugin({cssProcessor:require('cssnano'),cssProcessorOptions:{discardComments:{
20.21ALTERANDOOENDEREÇODAAPINOBUILDDEPRODUÇÃO 497
removeAll:true}},canPrint:true}));}
//ADICIONANDOOPLUGINNALISTA
plugins.push(newwebpack.DefinePlugin({SERVICE_URL}));
//códigoposterioromitido
Excelente! Por fim, precisamos alterarNegociacaoService.js e app.js para que façam uso davariávelSERVICE_URL.
Alterando client/app-
src/domain/negociacao/NegociacaoService.js:
//client/app-src/domain/negociacao/NegociacaoService.js
//códigoanterioromitido
obtemNegociacoesDaSemana(){
//PERCEBAMAMUDANÇA,UTILIZANDOTEMPLATELITERAL!
returnthis._http.get(`${SERVICE_URL}/negociacoes/semana`)
//códigoomitido}
obtemNegociacoesDaSemanaAnterior(){
returnthis._http.get(`${SERVICE_URL}/negociacoes/anterior`)//códigoomitido}
498 20.21ALTERANDOOENDEREÇODAAPINOBUILDDEPRODUÇÃO
obtemNegociacoesDaSemanaRetrasada(){
returnthis._http.get(`${SERVICE_URL}/negociacoes/retrasada`)
//códigoomitido}
//posteriorcódigoomitido
Porfim,alterandoclient/app-src/app.js:
//client/app-src/app.js
//códigoanterioromitido
fetch(`${SERVICE_URL}/negociacoes`,config).then(()=>console.log('Dadoenviadocomsucesso'));
Com as alterações que fizemos, os projetos criados com osscriptsbuild-prodebuild-devterãoendereçosdiferentesdeAPI. Aliás, vale lembrar que o Webpack Dev Server levará emconsideração o mesmo endereço de API do build dedesenvolvimento, faz todo sentido pois este servidor é para serusadoapenaslocalmente.
Porfim,quandousamosoWebpackDevServernãofazmuitosentido exibirmos no terminal as milhares de mensagens decompilação e criação dos nossos bundles. Queremos apenasmensagens de erro durante o build. Podemos resolver issofacilmente adicionando a propriedade devServer no arquivoclient/webpack.config.js:
//client/webpack.config.js
//códigoanterioromitido
plugins,devServer:{
20.21ALTERANDOOENDEREÇODAAPINOBUILDDEPRODUÇÃO 499
noInfo:true}}
ApropriedadedevServer:{noInfo:true} indicaqueapenasasmensagensdeerroserãoexibidasnoconsoleenquantoservirmosnossaaplicaçãoatravésdoWebpackDevServer.
Aolongodestelivro,aplicamosdesdeoinícioomodeloMVCepadrõesdeprojetospararesolvermosproblemas.Otempotodotransitamosentreosparadigmasorientadoaobjetoseofuncional.
FomosintroduzidosgradativamenteaosrecursosdalinguagemJavaScriptdasversõesES5,ES2015(ES6),ES2016(ES7), inclusive2017(ES8),importantesparanossoprojeto.
Antecipamos o futuro usando recursos que ainda estão emprocesso de consolidação na linguagem, graças ao uso de umtranscompilador.AprendemosWebpackeatédeployrealizamos!
Caroleitor,aotrilharseucaminhoatéestaseção,vocêacabadese tornarumCangaceiro JavaScript.Cada linhade códigopodelheserútilnoseudiaadia,ouatémesmoservirdeinspiraçãoparaquevocêcriesuasprópriassoluções.Paratodosvocês,meusvotosdesucesso.
Por fim, é possível acompanhar todas as propostas demudanças na linguagem JavaScript emhttps://github.com/tc39/proposals, para se manter atualizado.Inclusive, a partir deste endereço, você saberá o estágio atual daimplementação de Decorators na linguagem. É importante ficar
20.22CONSIDERAÇÕESFINAIS
500 20.22CONSIDERAÇÕESFINAIS
atentoparaosestágios(stages)daspropostas.Sãoeles:
Stage0:qualquerdiscussão,ideia,mudançaouadiçãoque ainda não foi submetida formalmente comoproposta.
Stage 1: uma proposta é formalizada e espera-seabordar preocupações transversais, interações comoutras propostas e preocupações de implementação.As propostas nesta etapa identificam um problemadiscreto e oferecem uma solução concreta para esseproblema.
Stage2:apropostadeveoferecerumrascunhoinicialdaespecificação.
Stage 3: neste estágio avançado, o editor deespecificações e os revisores designados devem terassinadoaespecificação final.É improvávelqueumapropostadesta fasemudealémdascorreçõesparaosproblemasidentificados.
State4:apropostacheganesteestágioquandoexistempelomenosduas implementações independentes queaprovamostestesdeaceitação.
Olharofuturoéterumanovaperspectivadopresente.
20.22CONSIDERAÇÕESFINAIS 501
O código completo deste capítulo você encontra emhttps://github.com/flaviohenriquealmeida/cangaceiro-javascript/tree/master/20
502 20.22CONSIDERAÇÕESFINAIS