Usando Vue.js para criar um painel meteorológico interativo com APIs

Publicados: 2022-03-10
Resumo rápido ↬ Criar um painel com dados de API geralmente é um assunto complexo. Escolher sua pilha de tecnologia, integrar APIs, selecionar os gráficos certos e embelezar com estilos CSS pode se tornar complicado. Este tutorial é um guia passo a passo sobre como ajudar você a criar um painel de clima no Vue.js usando dados de API.

(Este é um artigo patrocinado.) Neste tutorial, você construirá um painel meteorológico simples do zero. Será um aplicativo de cliente final que não é um exemplo de “Hello World”, nem muito intimidante em seu tamanho e complexidade.

Todo o projeto será desenvolvido utilizando ferramentas do ecossistema Node.js + npm. Em particular, dependeremos fortemente da API Dark Sky para os dados, Vue.js para todo o trabalho pesado e FusionCharts para visualização de dados.

Pré-requisitos

Esperamos que você esteja familiarizado com o seguinte:

  • HTML5 e CSS3 (também usaremos os recursos básicos fornecidos pelo Bootstrap;
  • JavaScript (especialmente a forma ES6 de usar a linguagem);
  • Node.js e npm (o básico do ambiente e gerenciamento de pacotes está ótimo).

Além dos mencionados acima, seria ótimo se você tivesse familiaridade com Vue.js ou qualquer outro framework JavaScript similar. Não esperamos que você conheça o FusionCharts — é tão fácil de usar que você aprenderá rapidamente!

Aprendizados Esperados

Seus principais aprendizados com este projeto serão:

  1. Como planejar a implementação de um bom painel
  2. Como desenvolver aplicativos com Vue.js
  3. Como criar aplicativos orientados a dados
  4. Como visualizar dados usando o FusionCharts

Em particular, cada uma das seções aproxima você dos objetivos de aprendizado:

  1. Uma introdução ao painel meteorológico
    Este capítulo fornece uma visão geral dos diferentes aspectos da empresa.
  2. Criar o projeto
    Nesta seção, você aprenderá a criar um projeto do zero usando a ferramenta de linha de comando Vue.
  3. Personalizar a estrutura padrão do projeto
    O andaime de projeto padrão que você obteve na seção anterior não é suficiente; aqui você aprende o material adicional necessário para o projeto do ponto de vista estrutural.
  4. Aquisição e processamento de dados
    Esta seção é a carne do projeto; todo o código crítico para adquirir e processar dados da API é mostrado aqui. Espere gastar o máximo de tempo nesta seção.
  5. Visualização de dados com FusionCharts
    Uma vez que todos os dados e outras partes móveis do projeto estejam estabilizados, esta seção é dedicada a visualizar os dados usando FusionCharts e um pouco de CSS.

1. O fluxo de trabalho do painel

Antes de mergulharmos na implementação, é importante ser claro sobre o nosso plano. Dividimos nosso plano em quatro aspectos distintos:

Requisitos

Quais são os nossos requisitos para este projeto? Em outras palavras, quais são as coisas que queremos mostrar por meio do nosso Painel Meteorológico? Tendo em mente que nosso público-alvo provavelmente são meros mortais com gostos simples, gostaríamos de mostrar a eles o seguinte:

  • Detalhes do local para o qual eles desejam ver o clima, juntamente com algumas informações primárias sobre o clima. Como não há requisitos rigorosos, descobriremos os detalhes chatos mais tarde. No entanto, nesta fase, é importante notar que teremos que fornecer ao público uma caixa de pesquisa, para que eles possam fornecer informações para o local de seu interesse.
  • Informações gráficas sobre o clima de sua localização de interesse, como:
    • Variação de temperatura para o dia da consulta
    • Destaques do clima de hoje:
      • Velocidade e direção do vento
      • Visibilidade
      • Índice UV

Nota : Os dados obtidos da API fornecem informações sobre muitos outros aspectos do clima. Optamos por não usar todos eles para manter o código no mínimo.

Estrutura

Com base nos requisitos, podemos estruturar nosso dashboard conforme mostrado abaixo:

Estrutura do painel
(Visualização grande)

Dados

Nosso painel é tão bom quanto os dados que obtemos, porque não haverá visualizações bonitas sem dados adequados. Existem muitas APIs públicas que fornecem dados meteorológicos – algumas delas são gratuitas e outras não. Para nosso projeto, coletaremos dados da API Dark Sky. No entanto, não poderemos pesquisar o endpoint da API diretamente do lado do cliente. Não se preocupe, temos uma solução que será revelada na hora certa! Assim que obtivermos os dados para o local pesquisado, faremos algum processamento e formatação de dados - você sabe, o tipo de tecnicismo que nos ajuda a pagar as contas.

Visualização

Depois de obter dados limpos e formatados, nós os conectamos ao FusionCharts. Existem muito poucas bibliotecas JavaScript no mundo tão capazes quanto FusionCharts. Do grande número de ofertas do FusionCharts, usaremos apenas algumas - todas escritas em JavaScript, mas funcionam perfeitamente quando integradas ao wrapper Vue para FusionCharts.

Armados com o quadro maior, vamos sujar as mãos - é hora de tornar as coisas concretas! Na próxima seção, você criará o projeto básico do Vue, sobre o qual construiremos mais.

2. Criando o Projeto

Para criar o projeto, execute as seguintes etapas:

  1. Instalar Node.js + npm
    ( Se você tiver o Node.js instalado em seu computador, pule esta etapa. )
    O Node.js vem com o npm empacotado, então você não precisa instalar o npm separadamente. Dependendo do sistema operacional, baixe e instale o Node.js de acordo com as instruções fornecidas aqui.

    Uma vez instalado, provavelmente é uma boa ideia verificar se o software está funcionando corretamente e quais são suas versões. Para testar isso, abra a linha de comando/terminal e execute os seguintes comandos:
     node --version npm --version
  2. Instalar pacotes com npm
    Assim que o npm estiver funcionando, execute o seguinte comando para instalar os pacotes básicos necessários para o nosso projeto.
     npm install -g vue@2 vue-cli@2
  3. Inicialize o andaime do projeto com vue-cli
    Supondo que o passo anterior tenha corrido bem, o próximo passo é usar o vue-cli — uma ferramenta de linha de comando do Vue.js, para inicializar o projeto. Para fazer isso, execute o seguinte:
    • Inicialize o scaffolding com o template webpack-simple.
       vue init webpack-simple vue_weather_dashboard
      Você receberá um monte de perguntas — aceitar os padrões para todas, exceto a última pergunta, será bom o suficiente para este projeto; responda N para a última.
      Uma captura de tela da linha de comando/terminal
      (Visualização grande)
      Lembre-se de que, embora o webpack-simple seja excelente para prototipagem rápida e aplicativos leves como o nosso, ele não é particularmente adequado para aplicativos sérios ou implantação de produção. Se você quiser usar qualquer outro modelo (embora desaconselhássemos se você for um novato), ou gostaria de nomear seu projeto de outra forma, a sintaxe é:
       vue init [template-name] [project-name]
    • Navegue até o diretório criado por vue-cli para o projeto.
       cd vue_weather_dashboard
    • Instale todos os pacotes mencionados no package.json , que foi criado pela ferramenta vue-cli para o webpack-simple .
       npm install
    • Inicie o servidor de desenvolvimento e veja seu projeto Vue padrão funcionando no navegador!
       npm run dev

Se você é novo no Vue.js, reserve um momento para saborear sua última conquista — você criou um pequeno aplicativo Vue e está sendo executado em localhost:8080!

Uma captura de tela do site Vue.js
(Visualização grande)

Breve explicação da estrutura padrão do projeto

É hora de dar uma olhada na estrutura dentro do diretório vue_weather_dashboard , para que você tenha uma compreensão do básico antes de começarmos a modificá-lo.

A estrutura fica mais ou menos assim:

 vue_weather_dashboard |--- README.md |--- node_modules/ | |--- ... | |--- ... | |--- [many npm packages we installed] | |--- ... | |--- ... |--- package.json |--- package-lock.json |--- webpack.config.js |--- index.html |--- src | |--- App.vue | |--- assets | | |--- logo.png | |--- main.js

Embora possa ser tentador deixar de se familiarizar com os arquivos e diretórios padrão, se você é novo no Vue, recomendamos pelo menos dar uma olhada no conteúdo dos arquivos. Pode ser uma boa sessão educacional e desencadear perguntas que você deve buscar por conta própria, especialmente os seguintes arquivos:

  • package.json e apenas uma olhada em seu primo package-lock.json
  • webpack.config.js
  • index.html
  • src/main.js
  • src/App.vue

Uma breve explicação de cada um dos arquivos e diretórios mostrados no diagrama de árvore é fornecida abaixo:

  • README.md
    Nenhum prêmio para adivinhar - cabe principalmente aos humanos ler e entender as etapas necessárias para criar o andaime do projeto.
  • node_modules/
    Este é o diretório onde o npm baixa os pacotes necessários para iniciar o projeto. As informações sobre os pacotes necessários estão disponíveis no arquivo package.json .
  • pacote.json
    Este arquivo é criado pela ferramenta vue-cli com base nos requisitos do template webpack-simple , e contém informações sobre os pacotes npm (inclusive com suas versões e outros detalhes) que devem ser instalados. Dê uma olhada no conteúdo deste arquivo - é aqui que você deve visitar e talvez editar para adicionar/excluir pacotes necessários para o projeto e, em seguida, execute npm install. Leia mais sobre package.json aqui.
  • pacote-lock.json
    Este arquivo é criado pelo próprio npm e destina-se principalmente a manter um log das coisas que o npm baixou e instalou.
  • webpack.config.js
    Este é um arquivo JavaScript que contém a configuração do webpack — uma ferramenta que agrupa diferentes aspectos do nosso projeto (código, ativos estáticos, configuração, ambientes, modo de uso, etc.) e reduz antes de servir ao usuário. O benefício é que todas as coisas são interligadas automaticamente, e a experiência do usuário melhora muito devido à melhoria no desempenho do aplicativo (as páginas são servidas rapidamente e carregam mais rapidamente no navegador). Como você pode encontrar mais tarde, este é o arquivo que precisa ser inspecionado quando algo no sistema de compilação não funciona da maneira que deveria ser. Além disso, quando você deseja implantar o aplicativo, este é um dos principais arquivos que precisam ser editados (leia mais aqui).
  • index.html
    Este arquivo HTML serve como a matriz (ou você pode dizer, modelo) onde os dados e o código devem ser incorporados dinamicamente (é isso que o Vue faz principalmente), e então servidos ao usuário.
  • src/main.js
    Este arquivo JavaScript contém código que gerencia principalmente dependências de nível superior/projeto e define o componente Vue de nível superior. Em resumo, ele orquestra o JavaScript para todo o projeto e serve como ponto de entrada do aplicativo. Edite este arquivo quando precisar declarar dependências em todo o projeto em determinados módulos de nó ou quiser que algo seja alterado no componente Vue mais alto do projeto.
  • src/App.vue
    No ponto anterior, quando estávamos falando sobre o “componente Vue mais alto”, estávamos falando essencialmente sobre este arquivo. Cada arquivo .vue no projeto é um componente e os componentes são relacionados hierarquicamente. No início, temos apenas um arquivo .vue , ou seja, App.vue , como nosso único componente. Mas em breve adicionaremos mais componentes ao nosso projeto (principalmente seguindo a estrutura do painel) e os vincularemos de acordo com nossa hierarquia desejada, sendo o App.vue o ancestral de todos. Esses arquivos .vue conterão código em um formato que o Vue deseja que escrevamos. Não se preocupe, eles são códigos JavaScript escritos mantendo uma estrutura que pode nos manter sãos e organizados. Você foi avisado — ao final deste projeto, se você é novo no Vue, você pode ficar viciado no template — script — style template — script — style template — script — style de organização de código!

Agora que criamos a base, é hora de:

  • Modifique os templates e ajuste um pouco os arquivos de configuração, para que o projeto se comporte do jeito que queremos.
  • Crie novos arquivos .vue e implemente a estrutura do painel com código Vue.

Vamos aprendê-los na próxima seção, que será um pouco longa e exige alguma atenção. Se você precisa de cafeína ou água, ou quer descarregar – agora é a hora!

3. Personalizando a estrutura padrão do projeto

É hora de mexer na base que o projeto andaime nos deu. Antes de começar, certifique-se de que o servidor de desenvolvimento fornecido pelo webpack esteja em execução. A vantagem de executar esse servidor continuamente é que qualquer alteração feita no código-fonte — uma que você salva e atualiza a página da Web — é refletida imediatamente no navegador.

Se você deseja iniciar o servidor de desenvolvimento, basta executar o seguinte comando no terminal (supondo que seu diretório atual seja o diretório do projeto):

 npm run dev

Nas seções a seguir, modificaremos alguns dos arquivos existentes e adicionaremos alguns novos arquivos. Ele será seguido por breves explicações sobre o conteúdo desses arquivos, para que você tenha uma ideia do que essas alterações pretendem fazer.

Modificar arquivos existentes

index.html

Nosso aplicativo é literalmente um aplicativo de página única, porque há apenas uma página da Web que é exibida no navegador. Falaremos sobre isso mais tarde, mas primeiro vamos fazer nossa primeira alteração — alterando o texto dentro da tag <title> .

Com esta pequena revisão, o arquivo HTML se parece com o seguinte:

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <!-- Modify the text of the title tag below --> <title>Vue Weather Dashboard</title> </head> <body> <div></div> <script src="/dist/build.js"></script> </body> </html>

Reserve um momento para atualizar a página da Web em localhost:8080 e veja a alteração refletida na barra de título da guia no navegador - ela deve dizer "Vue Weather Dashboard". No entanto, isso foi apenas para demonstrar o processo de fazer alterações e verificar se está funcionando. Temos mais coisas para fazer!

Esta página HTML simples carece de muitas coisas que queremos em nosso projeto, especialmente o seguinte:

  • Algumas metainformações
  • Links de CDN para Bootstrap (estrutura CSS)
  • link para folha de estilo personalizada (ainda a ser adicionado no projeto)
  • Ponteiros para a API de geolocalização do Google Maps da tag <script>

Depois de adicionar essas coisas, o index.html final tem o seguinte conteúdo:

 <!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="src/css/style.css"> <title>Weather Dashboard</title> <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC-lCjpg1xbw-nsCc11Si8Ldg2LKYizqI4&libraries=places"></script> </head> <body> <div></div> <script src="/dist/build.js"></script> </body> </html>

Salve o arquivo e atualize a página da web. Você deve ter notado um pequeno aumento enquanto a página estava sendo carregada - é principalmente devido ao fato de que o estilo da página agora está sendo controlado pelo Bootstrap, e os elementos de estilo como fontes, espaçamento, etc. anterior (se você não tiver certeza, volte para o padrão e veja a diferença).

Uma captura de tela ao atualizar a página da Web com localhost:8080
(Visualização grande)

Nota : Uma coisa importante antes de prosseguirmos - o URL da API do Google Maps contém uma chave que é uma propriedade de FusionCharts. Por enquanto, você pode usar essa chave para construir o projeto, pois não queremos que você fique atolado com esses detalhes minuciosos (que podem ser distrações enquanto você é novo). No entanto, recomendamos que você gere e use sua própria chave da API do Google Maps assim que tiver feito algum progresso e se sentir à vontade para prestar atenção a esses pequenos detalhes.

pacote.json

No momento em que escrevemos isso, usamos certas versões dos pacotes npm para nosso projeto e sabemos com certeza que essas coisas funcionam juntas. No entanto, no momento em que você estiver executando o projeto, é muito possível que as versões estáveis ​​mais recentes dos pacotes que o npm baixa para você não sejam as mesmas que usamos, e isso pode quebrar o código (ou fazer coisas que estão além do nosso controle). Assim, é muito importante ter exatamente o mesmo arquivo package.json que foi usado para construir este projeto, para que nosso código/explicações e os resultados obtidos sejam consistentes.

O conteúdo do arquivo package.json deve ser:

 { "name": "vue_weather_dashboard", "description": "A Vue.js project", "version": "1.0.0", "author": "FusionCharts", "license": "MIT", "private": true, "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" }, "dependencies": { "axios": "^0.18.0", "babel": "^6.23.0", "babel-cli": "^6.26.0", "babel-polyfill": "^6.26.0", "fusioncharts": "^3.13.3", "moment": "^2.22.2", "moment-timezone": "^0.5.21", "vue": "^2.5.11", "vue-fusioncharts": "^2.0.4" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ], "devDependencies": { "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-preset-env": "^1.6.0", "babel-preset-stage-3": "^6.24.1", "cross-env": "^5.0.5", "css-loader": "^0.28.7", "file-loader": "^1.1.4", "vue-loader": "^13.0.5", "vue-template-compiler": "^2.4.4", "webpack": "^3.6.0", "webpack-dev-server": "^2.9.1" } }

Incentivamos você a percorrer o novo package.json e descobrir quais são as funções de diferentes objetos no json. Você pode preferir alterar o valor da chave “ author ” para o seu nome. Além disso, os pacotes mencionados nas dependências se revelarão no momento certo no código. Por enquanto, é suficiente saber que:

  • os pacotes relacionados ao babel são para manipular adequadamente o código de estilo ES6 pelo navegador;
  • axios lida com solicitações HTTP baseadas em Promise;
  • moment e moment-timezone são para manipulação de data/hora;
  • fusioncharts e vue-fusioncharts são responsáveis ​​por renderizar gráficos:
  • vue , por razões óbvias.

webpack.config.js

Assim como package.json , sugerimos que você mantenha um arquivo webpack.config.js consistente com o que usamos para construir o projeto. No entanto, antes de fazer qualquer alteração, recomendamos que você compare cuidadosamente o código padrão no webpack.config.js e o código que fornecemos abaixo. Você notará algumas diferenças – pesquise no Google e tenha uma ideia básica do que elas significam. Como explicar detalhadamente as configurações do webpack está fora do escopo deste artigo, você está sozinho a esse respeito.

O arquivo webpack.config.js personalizado é o seguinte:

 var path = require('path') var webpack = require('webpack') module.exports = { entry: ['babel-polyfill', './src/main.js'], output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'build.js' }, module: { rules: [ { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ], }, { test: /\.vue$/, loader: 'vue-loader', options: { loaders: { } // other vue-loader options go here } }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, { test: /\.(png|jpg|gif|svg)$/, loader: 'file-loader', options: { name: '[name].[ext]?[hash]' } } ] }, resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' }, extensions: ['*', '.js', '.vue', '.json'] }, devServer: { historyApiFallback: true, noInfo: true, overlay: true, host: '0.0.0.0', port: 8080 }, performance: { hints: false }, devtool: '#eval-source-map' } if (process.env.NODE_ENV === 'production') { module.exports.devtool = '#source-map' // https://vue-loader.vuejs.org/en/workflow/production.html module.exports.plugins = (module.exports.plugins || []).concat([ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }), new webpack.LoaderOptionsPlugin({ minimize: true }) ]) }

Com as alterações feitas no webpack.config.js do projeto, é imperativo que você pare o servidor de desenvolvimento que está rodando ( Ctrl + C ), e reinicie-o com o seguinte comando executado a partir do diretório do projeto após instalar todos os pacotes mencionados no package.json arquivo package.json :

 npm install npm run dev

Com isso, termina a provação de ajustar as configurações e garantir que os pacotes certos estejam no lugar. No entanto, isso também marca a jornada de modificação e escrita de código, que é um pouco longa, mas também muito gratificante!

src/main.js

Este arquivo é a chave para a orquestração de alto nível do projeto — é aqui que definimos:

  • Quais são as dependências de nível superior (onde obter os pacotes npm mais importantes necessários);
  • Como resolver as dependências, juntamente com instruções ao Vue sobre o uso de plugins/wrappers, se houver;
  • Uma instância do Vue que gerencia o componente superior no projeto: src/App.vue (o arquivo nodal .vue ).

De acordo com nossos objetivos para o arquivo src/main.js , o código deve ser:

 // Import the dependencies and necessary modules import Vue from 'vue'; import App from './App.vue'; import FusionCharts from 'fusioncharts'; import Charts from 'fusioncharts/fusioncharts.charts'; import Widgets from 'fusioncharts/fusioncharts.widgets'; import PowerCharts from 'fusioncharts/fusioncharts.powercharts'; import FusionTheme from 'fusioncharts/themes/fusioncharts.theme.fusion'; import VueFusionCharts from 'vue-fusioncharts'; // Resolve the dependencies Charts(FusionCharts); PowerCharts(FusionCharts); Widgets(FusionCharts); FusionTheme(FusionCharts); // Globally register the components for project-wide use Vue.use(VueFusionCharts, FusionCharts); // Instantiate the Vue instance that controls the application new Vue({ el: '#app', render: h => h(App) })

src/App.vue

Este é um dos arquivos mais importantes de todo o projeto e representa o componente mais alto da hierarquia — o aplicativo inteiro em si, como um todo. Para nosso projeto, esse componente fará todo o trabalho pesado, que exploraremos mais tarde. Por enquanto, queremos nos livrar do clichê padrão e colocar algo nosso.

Se você é novo no modo Vue de organizar o código, seria melhor ter uma ideia da estrutura geral dentro dos arquivos .vue . Os arquivos .vue são compostos por três seções:

  • Modelo
    É aqui que o modelo HTML da página é definido. Além do HTML estático, esta seção também contém a maneira do Vue de incorporar conteúdo dinâmico, usando as chaves duplas {{ }} .
  • Roteiro
    JavaScript rege esta seção e é responsável por gerar conteúdo dinâmico que vai e fica dentro do modelo HTML em locais apropriados. Esta seção é principalmente um objeto que é exportado e consiste em:
    • Dados
      Esta é uma função em si, e normalmente ela retorna alguns dados desejados encapsulados dentro de uma boa estrutura de dados.
    • Métodos
      Um objeto que consiste em uma ou mais funções/métodos, cada um dos quais geralmente manipula os dados de uma forma ou de outra, e também controla o conteúdo dinâmico do modelo HTML.
    • Calculado
      Muito parecido com o objeto de método discutido acima com uma distinção importante - enquanto todas as funções dentro do objeto de método são executadas sempre que qualquer uma delas é chamada, as funções dentro do objeto computado se comportam de forma muito mais sensata e são executadas se e somente se tiver sido chamado.
  • Estilo
    Esta seção é para o estilo CSS que se aplica ao HTML da página (escrito dentro do template) — coloque o bom e velho CSS aqui para deixar suas páginas lindas!

Mantendo o paradigma acima em mente, vamos customizar minimamente o código em App.vue :

 <template> <div> <p>This component's code is in {{ filename }}</p> </div> </template> <script> export default { data() { return { filename: 'App.vue' } }, methods: { }, computed: { }, } </script> <style> </style>

Lembre-se de que o trecho de código acima é simplesmente para testar se o App.vue está trabalhando com nosso próprio código nele. Mais tarde, ele passará por muitas alterações, mas primeiro salve o arquivo e atualize a página no navegador.

Uma captura de tela do navegador com a mensagem "O código deste componente está no App.vue"
(Visualização grande)

Neste ponto, provavelmente é uma boa ideia obter ajuda com ferramentas. Confira o Vue devtools for Chrome, e se você não tiver muitos problemas em usar o Google Chrome como seu navegador padrão para desenvolvimento, instale a ferramenta e brinque um pouco com ela. Ele será extremamente útil para desenvolvimento e depuração adicionais, quando as coisas se tornarem mais complicadas.

Diretórios e arquivos adicionais

O próximo passo seria adicionar arquivos adicionais, para que a estrutura do nosso projeto fique completa. Nós adicionaríamos os seguintes diretórios e arquivos:

  • src/css/style.css
  • src/assets/calendar.svgvlocation.svgsearch.svgwinddirection.svgwindspeed.svg
  • src/components/Content.vueHighlights.vueTempVarChart.vueUVIndex.vueVisibility.vueWindStatus.vue

Nota : Salve os arquivos .svg com hiperlink em seu projeto.

Crie os diretórios e arquivos mencionados acima. A estrutura final do projeto deve ter uma aparência (lembre-se de excluir pastas e arquivos da estrutura padrão que agora são desnecessários):

 vue_weather_dashboard/ |--- README.md |--- node_modules/ | |--- ... | |--- ... | |--- [many npm packages we installed] | |--- ... | |--- ... |--- package.json |--- package-lock.json |--- webpack.config.js |--- index.html |--- src/ | |--- App.vue | |--- css/ | | |--- style.css | |--- assets/ | | |--- calendar.svg | | |--- location.svg | | |--- location.svg | | |--- winddirection.svg | | |--- windspeed.svg | |--- main.js | |--- components/ | | |--- Content.vue | | |--- Highlights.vue | | |--- TempVarChart.vue | | |--- UVIndex.vue | | |--- Visibility.vue | | |--- WindStatus.vue

Pode haver alguns outros arquivos, como .babelrc , .gitignore , .editorconfig , etc. na pasta raiz do projeto. Você pode ignorá-los com segurança por enquanto.

Na seção a seguir, adicionaremos conteúdo mínimo aos arquivos recém-adicionados e testaremos se eles estão funcionando corretamente.

src/css/style.css

Embora não seja de muita utilidade imediatamente, copie o seguinte código para o arquivo:

 @import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500"); :root { font-size: 62.5%; } body { font-family: Roboto; font-weight: 400; width: 100%; margin: 0; font-size: 1.6rem; } #sidebar { position: relative; display: flex; flex-direction: column; background-image: linear-gradient(-180deg, #80b6db 0%, #7da7e2 100%); } #search { text-align: center; height: 20vh; position: relative; } #location-input { height: 42px; width: 100%; opacity: 1; border: 0; border-radius: 2px; background-color: rgba(255, 255, 255, 0.2); margin-top: 16px; padding-left: 16px; color: #ffffff; font-size: 1.8rem; line-height: 21px; } #location-input:focus { outline: none; } ::placeholder { color: #FFFFFF; opacity: 0.6; } #current-weather { color: #ffffff; font-size: 8rem; line-height: 106px; position: relative; } #current-weather>span { color: #ffffff; font-size: 3.6rem; line-height: 42px; vertical-align: super; opacity: 0.8; top: 15px; position: absolute; } #weather-desc { font-size: 2.0rem; color: #ffffff; font-weight: 500; line-height: 24px; } #possibility { color: #ffffff; font-size: 16px; font-weight: 500; line-height: 19px; } #max-detail, #min-detail { color: #ffffff; font-size: 2.0rem; font-weight: 500; line-height: 24px; } #max-detail>i, #min-detail>i { font-style: normal; height: 13.27px; width: 16.5px; opacity: 0.4; } #max-detail>span, #min-detail>span { color: #ffffff; font-family: Roboto; font-size: 1.2rem; line-height: 10px; vertical-align: super; } #max-summary, #min-summary { opacity: 0.9; color: #ffffff; font-size: 1.4rem; line-height: 16px; margin-top: 2px; opacity: 0.7; } #search-btn { position: absolute; right: 0; top: 16px; padding: 2px; z-index: 999; height: 42px; width: 45px; background-color: rgba(255, 255, 255, 0.2); border: none; } #dashboard-content { text-align: center; height: 100vh; } #date-desc, #location-desc { color: #ffffff; font-size: 1.6rem; font-weight: 500; line-height: 19px; margin-bottom: 15px; } #date-desc>img { top: -3px; position: relative; margin-right: 10px; } #location-desc>img { top: -3px; position: relative; margin-left: 5px; margin-right: 15px; } #location-detail { opacity: 0.7; color: #ffffff; font-size: 1.4rem; line-height: 20px; margin-left: 35px; } .centered { position: fixed; top: 45%; left: 50%; transform: translate(-50%, -50%); } .max-desc { width: 80px; float: left; margin-right: 28px; } .temp-max-min { margin-top: 40px } #dashboard-content { background-color: #F7F7F7; } .custom-card { background-color: #FFFFFF !important; border: 0 !important; margin-top: 16px !important; margin-bottom: 20px !important; } .custom-content-card { background-color: #FFFFFF !important; border: 0 !important; margin-top: 16px !important; margin-bottom: 0px !important; } .header-card { height: 50vh; } .content-card { height: 43vh; } .card-divider { margin-top: 0; } .content-header { color: #8786A4; font-size: 1.4rem; line-height: 16px; font-weight: 500; padding: 15px 10px 5px 15px; } .highlights-item { min-height: 37vh; max-height: 38vh; background-color: #FFFFFF; } .card-heading { color: rgb(33, 34, 68); font-size: 1.8rem; font-weight: 500; line-height: 21px; text-align: center; } .card-sub-heading { color: #73748C; font-size: 1.6rem; line-height: 19px; } .card-value { color: #000000; font-size: 1.8rem; line-height: 21px; } span text { font-weight: 500 !important; } hr { padding-top: 1.5px; padding-bottom: 1px; margin-bottom: 0; margin-top: 0; line-height: 0.5px; } @media only screen and (min-width: 768px) { #sidebar { height: 100vh; } #info { position: fixed; bottom: 50px; width: 100%; padding-left: 15px; } .wrapper-right { margin-top: 80px; } } @media only screen and (min-width:1440px) { #sidebar { width: 350px; max-width: 350px; flex: auto; } #dashboard-content { width: calc(100% — 350px); max-width: calc(100% — 350px); flex: auto; } }

src/assets/

Neste diretório, baixe e salve os arquivos .svg mencionados abaixo:

  • calendar.svg
  • location.svg
  • search.svg
  • winddirection.svg
  • windspeed.svg

src/components/Content.vue

Isso é o que chamamos de “componente burro” (ou seja, um espaço reservado) que existe apenas para manter a hierarquia e essencialmente passa dados para seus componentes filhos.

Lembre-se de que não há uma barra técnica para escrever todo o nosso código no arquivo App.vue , mas adotamos a abordagem de dividir o código aninhando os componentes por dois motivos:

  • Para escrever código limpo, o que ajuda na legibilidade e manutenção;
  • Para replicar a mesma estrutura que veremos na tela, ou seja, a hierarquia.

Antes de aninharmos o componente definido em Content.vue dentro do componente raiz App.vue , vamos escrever um código de brinquedo (mas educacional) para Content.vue :

 <template> <div> <p>This child components of Content.vue are:</p> <ul> <li v-for="child in childComponents">{{ child }}</li> </ul> </div> </template> <script> export default { data () { return { childComponents: ['TempVarChart.vue', 'Highlights.vue'] } }, methods: { }, computed: { }, } </script> <style> </style>

No código, observe atentamente e entenda o seguinte:

  • Dentro da tag <script> (onde obviamente escrevemos algum código JavaScript), definimos um objeto que é exportado (disponibilizado para outros arquivos) por padrão. Este objeto contém uma função data() , que retorna um objeto array chamado childComponents , com seus elementos sendo nomes dos arquivos de componentes que devem ser aninhados ainda mais.
  • Dentro da tag <template> (onde escrevemos algum template HTML), o que interessa é o <ul> .
    • Dentro da lista não ordenada, cada item da lista deve ser os nomes dos componentes filhos pretendidos, conforme definido no objeto array childComponents . Além disso, a lista deve se estender automaticamente até o último elemento do array. Parece que devemos escrever um for -loop, não é? Fazemos isso usando a diretiva v-for fornecida pelo Vue.js. A diretiva v-for :
      • Atua como um atributo da tag <li> , itera pelo array, renderiza os nomes dos componentes filhos onde o iterador é mencionado dentro dos colchetes {{ }} (onde escrevemos o texto para os itens da lista).

O código e a explicação acima formam a base do seu entendimento subsequente de como o script e o modelo estão inter-relacionados e como podemos usar as diretivas fornecidas pelo Vue.js.

Aprendemos bastante, mas mesmo depois de tudo isso, ainda temos uma coisa a aprender sobre como conectar componentes perfeitamente na hierarquia — passar dados do componente pai para seus filhos. Por enquanto, precisamos aprender a passar alguns dados de src/App.vue para src/components/Content.vue , para que possamos usar as mesmas técnicas para o restante do aninhamento de componentes neste projeto.

Os dados que passam dos componentes pai para os filhos podem parecer simples, mas o diabo está nos detalhes! Conforme explicado brevemente abaixo, existem várias etapas envolvidas para fazê-lo funcionar:

  • Definindo e os dados
    Por enquanto, queremos alguns dados estáticos para brincar - um objeto contendo valores codificados sobre diferentes aspectos do clima ficará bem! Criamos um objeto chamado weather_data e o retornamos da função data() de App.vue . O objeto weather_data é fornecido no snippet abaixo:
 weather_data: { location: "California", temperature: { current: "35 C", }, highlights: { uvindex: "3", windstatus: { speed: "20 km/h", direction: "NE", }, visibility: "12 km", }, },
  • Passando os dados do pai
    Para passar os dados, precisamos de um destino para onde queremos enviar os dados! Nesse caso, o destino é o componente Content.vue , e a maneira de implementá-lo é:
    • Atribua o objeto weather_data a um atributo personalizado da tag <Content>
    • Vincule o atributo aos dados usando a diretiva v-bind : fornecida pelo Vue.js, que torna o valor do atributo dinâmico (responsivo às alterações feitas nos dados originais).
       <Content v-bind:weather_data=“weather_data”></Content>

Definir e passar os dados é tratado no lado da fonte do handshake, que no nosso caso é o arquivo App.vue .

O código para o arquivo App.vue , em seu status atual, é fornecido abaixo:

 <template> <div> <p>This component's code is in {{ filename }}</p> <Content v-bind:weather_data="weather_data"></Content> </div> </template> <script> import Content from './components/Content.vue' export default { name: 'app', components: { 'Content': Content }, data () { return { filename: 'App.vue', weather_data: { location: "California", temperature: { current: "35 C", }, highlights: { uvindex: "3", windstatus: { speed: "20 km/h", direction: "NE", }, visibility: "12 km", }, }, } }, methods: { }, computed: { }, } </script> <style> </style> 
Uma captura de tela do navegador com a mensagem “O código deste componente está no App.vue. Esses componentes filhos do Content.vue são: TempVarChart.vue, Highlights.vue”
(Visualização grande)

Com os dados definidos e passados ​​da fonte (componente pai), agora é responsabilidade do filho receber os dados e renderizá-los adequadamente, conforme explicado nas próximas duas etapas.

  • Recebendo os dados pela criança
    O componente filho, neste caso Content.vue , deve receber o objeto weather_data enviado a ele pelo componente pai App.vue . O Vue.js fornece um mecanismo para isso — tudo que você precisa é de um objeto array chamado props , definido no objeto padrão exportado pelo Content.vue . Cada elemento do array props é um nome dos objetos de dados que deseja receber de seu pai. Por enquanto, o único objeto de dados que deve receber é weather_data do App.vue. Assim, o array props se parece com:
 <template> // HTML template code here </template> <script> export default { props: ["weather_data"], data () { return { // data here } }, } </script> <style> // component specific CSS here </style>
  • Renderizando os dados na página
    Agora que garantimos o recebimento dos dados, a última tarefa que precisamos concluir é renderizar os dados. Para este exemplo, vamos despejar diretamente os dados recebidos na página da web, apenas para ilustrar a técnica. No entanto, em aplicativos reais (como o que estamos prestes a construir), os dados normalmente passam por muito processamento e apenas as partes relevantes deles são exibidas de maneira adequada ao propósito. Por exemplo, neste projeto iremos eventualmente obter dados brutos da API de clima, limpá-los e formatá-los, alimentar os dados nas estruturas de dados necessárias para os gráficos e depois visualizá-los. De qualquer forma, para exibir o dump de dados brutos, usaremos apenas os colchetes {{ }} que o Vue entende, conforme mostrado no trecho abaixo:
 <template> <div> // other template code here {{ weather_data }} </div> </template>

Agora é hora de assimilar todos os pedaços. O código para Content.vue — em seu status atual — é fornecido abaixo:

 <template> <div> <p>This child components of Content.vue are:</p> <ul> <li v-for="child in childComponents">{{ child }}</li> </ul> {{ weather_data }} </div> </template> <script> export default { props: ["weather_data"], data () { return { childComponents: ['TempVarChart.vue', 'Highlights.vue'] } }, methods: { }, computed: { }, } </script> <style> #pagecontent { border: 1px solid black; padding: 2px; } </style> 
Uma captura de tela do navegador com o resultado do código fornecido
(Visualização grande)

Depois de fazer as alterações discutidas acima, atualize a página da Web no navegador e veja como ela fica. Reserve um momento para apreciar a complexidade que o Vue lida - se você modificar o objeto weather_data em App.vue , ele será transmitido silenciosamente para Content.vue e, eventualmente, para o navegador que exibe a página da web! Tente alterando o valor para o local da chave.

Embora tenhamos aprendido sobre props e vinculação de dados usando dados estáticos, usaremos dados dinâmicos coletados usando APIs da Web no aplicativo e alteraremos o código de acordo .

Resumo

Antes de passarmos para o restante dos arquivos .vue , vamos resumir o que aprendemos enquanto escrevíamos o código para App.vue e components/Content.vue :

  • O arquivo App.vue é o que chamamos de componente raiz — aquele que fica no topo da hierarquia de componentes. O restante dos arquivos .vue representa componentes que são seus filhos diretos, netos e assim por diante.
  • O arquivo Content.vue é um componente fictício — sua responsabilidade é passar os dados para os níveis abaixo e manter a hierarquia estrutural, para que nosso código permaneça consistente com a filosofia “*o que vemos é o que implementamos*”.
  • A relação pai-filho do componente não acontece do nada — você deve registrar um componente (globalmente ou localmente, dependendo do uso pretendido do componente) e, em seguida, aninhá -lo usando tags HTML personalizadas (cujas grafias são as exatas igual ao dos nomes com os quais os componentes foram registrados).
  • Uma vez registrados e aninhados, os dados são passados ​​de componentes pai para filho, e o fluxo nunca é reverso (coisas ruins acontecerão se a arquitetura do projeto permitir refluxo). O componente pai é a fonte relativa dos dados e transmite dados relevantes para seus filhos usando a diretiva v-bind para os atributos dos elementos HTML personalizados. A criança recebe os dados destinados a ela usando adereços e decide por conta própria o que fazer com os dados.

Para o restante dos componentes, não vamos entrar em explicações detalhadas — vamos apenas escrever o código com base nos aprendizados do resumo acima. O código será auto-evidente e, se você ficar confuso sobre a hierarquia, consulte o diagrama abaixo:

Um diagrama explicando a hierarquia do código
(Visualização grande)

O diagrama diz que TempVarChart.vue e Highlights.vue são o filho direto de Content.vue . Assim, pode ser uma boa ideia preparar o Content.vue para enviar dados para esses componentes, o que fazemos usando o código abaixo:

 <template> <div> <p>This child components of Content.vue are:</p> <ul> <li v-for="child in childComponents">{{ child }}</li> </ul> {{ weather_data }} <temp-var-chart :tempVar="tempVar"></temp-var-chart> <today-highlights :highlights="highlights"></today-highlights> </div> </template> <script> import TempVarChart from './TempVarChart.vue' import Highlights from './Highlights.vue' export default { props: ["weather_data"], components: { 'temp-var-chart': TempVarChart, 'today-highlights': Highlights }, data () { return { childComponents: ['TempVarChart.vue', 'Highlights.vue'], tempVar: this.weather_data.temperature, highlights: this.weather_data.highlights, } }, methods: { }, computed: { }, } </script> <style> </style>

Depois de salvar esse código, você receberá erros — não se preocupe, isso é esperado. Ele será corrigido assim que você tiver o restante dos arquivos de componentes prontos. Se incomoda você não conseguir ver a saída, comente as linhas que contêm as tags de elemento personalizado <temp-var-chart> e <today-highlights> .

Para esta seção, este é o código final de Content.vue . No restante desta seção, faremos referência a este código e não aos anteriores que escrevemos para aprendizado.

src/components/TempVarChart.vue

Com seu componente pai Content.vue passando os dados, o TempVarChart.vue deve ser configurado para receber e renderizar os dados, conforme mostrado no código abaixo:

 <template> <div> <p>Temperature Information:</p> {{ tempVar }} </div> </template> <script> export default { props: ["tempVar"], data () { return { } }, methods: { }, computed: { }, } </script> <style> </style>

src/components/Highlights.vue

Este componente também receberá dados do App.vue — seu componente pai. Depois disso, ele deve ser vinculado a seus componentes filhos e os dados relevantes devem ser passados ​​para eles.

Vamos primeiro ver o código para receber dados do pai:

 <template> <div> <p>Weather Highlights:</p> {{ highlights }} </div> </template> <script> export default { props: ["highlights"], data () { return { } }, methods: { }, computed: { }, } </script> <style> </style>

Neste ponto, a página da web se parece com a imagem abaixo:

Resultado do código exibido no navegador
(Visualização grande)

Agora precisamos modificar o código do Highlights.vue para registrar e aninhar seus componentes filhos, seguido de passar os dados para os filhos. O código para isso é o seguinte:

 <template> <div> <p>Weather Highlights:</p> {{ highlights }} <uv-index :highlights="highlights"></uv-index> <visibility :highlights="highlights"></visibility> <wind-status :highlights="highlights"></wind-status> </div> </template> <script> import UVIndex from './UVIndex.vue'; import Visibility from './Visibility.vue'; import WindStatus from './WindStatus.vue'; export default { props: ["highlights"], components: { 'uv-index': UVIndex, 'visibility': Visibility, 'wind-status': WindStatus, }, data () { return { } }, methods: { }, computed: { }, } </script> <style> </style>

Depois de salvar o código e ver a página da Web, espera-se que você veja erros na ferramenta Developer Console fornecida pelo navegador. eles aparecem porque, embora Highlights.vue esteja enviando dados, ninguém os está recebendo. Ainda temos que escrever o código para os filhos do Highlights.vue .

Observe que não fizemos muito do processamento de dados, ou seja, não extraímos os fatores individuais dos dados climáticos que estão na seção Destaques do painel. Poderíamos ter feito isso na função data() , mas preferimos manter Highlights.vue um componente burro que apenas passa todo o dump de dados que recebe para cada um dos filhos, que então possuem seus próprios extratos o que é necessário para eles . No entanto, recomendamos que você experimente extrair dados no Highlights.vue e envie dados relevantes para cada componente filho - mesmo assim, é um exercício de boa prática!

src/components/UVIndex.vue

O código para este componente recebe o despejo de dados de destaques de Highlights.vue , extrai os dados para o Índice UV e os renderiza na página.

 <template> <div> <p>UV Index: {{ uvindex }}</p> </div> </template> <script> export default { props: ["highlights"], data () { return { uvindex: this.highlights.uvindex } }, methods: { }, computed: { }, } </script> <style> </style>

src/components/Visibility.vue

O código para este componente recebe o despejo de dados de destaques de Highlights.vue , extrai os dados para Visibilidade e os renderiza na página.

 <template> <div> <p>Visibility: {{ visibility }}</p> </div> </template> <script> export default { props: ["highlights"], data () { return { visibility: this.highlights.visibility, } }, methods: { }, computed: { }, } </script> <style> </style>

src/components/WindStatus.vue

O código para este componente recebe o dump de dados de Highlights de Highlights.vue , extrai os dados para Wind Status (velocidade e direção) e os renderiza na página.

 <template> <div> <p>Wind Status:</p> <p>Speed — {{ speed }}; Direction — {{ direction }}</p> </div> </template> <script> export default { props: ["highlights"], data () { return { speed: this.highlights.windstatus.speed, direction: this.highlights.windstatus.direction } }, methods: { }, computed: { }, } </script> <style> </style>

Depois de adicionar o código para todos os componentes, dê uma olhada na página da web no navegador.

Resultado do código exibido no navegador
(Visualização grande)

Para não desanimar, mas todo esse trabalho foi apenas para vincular os componentes na hierarquia e testar se o fluxo de dados está acontecendo entre eles ou não! Na próxima seção, descartaremos a maior parte do código que escrevemos até agora e adicionaremos muito mais referente ao projeto real. No entanto, certamente manteremos a estrutura e o aninhamento dos componentes; os aprendizados desta seção nos permitirão construir um painel decente com Vue.js.

4. Aquisição e processamento de dados

Lembre-se do objeto weather_data em App.vue ? Ele tinha alguns dados codificados que usamos para testar se todos os componentes estão funcionando corretamente e também para ajudá-lo a aprender alguns aspectos básicos do aplicativo Vue sem ficar atolado nos detalhes dos dados do mundo real. No entanto, agora é hora de abandonarmos nosso shell e sairmos para o mundo real, onde os dados da API dominarão a maior parte do nosso código.

Preparando Componentes Filhos para Receber e Processar Dados Reais

Nesta seção, você obterá o despejo de código para todos os componentes, exceto App.vue . O código lidará com o recebimento de dados reais do App.vue (diferente do código que escrevemos na seção anterior para receber e renderizar dados fictícios).

Recomendamos fortemente a leitura cuidadosa do código de cada componente, para que você tenha uma ideia de quais dados cada um desses componentes está esperando e, eventualmente, usará na visualização.

Parte do código e a estrutura geral serão semelhantes aos que você viu na estrutura anterior - portanto, você não enfrentará algo drasticamente diferente. No entanto, o diabo está nos detalhes! Portanto, examine o código com cuidado e, quando tiver entendido razoavelmente bem, copie o código para os respectivos arquivos de componentes em seu projeto.

Nota : Todos os componentes nesta seção estão no diretório src/components/ . Assim, a cada vez, o caminho não será mencionado — apenas o nome do arquivo .vue será mencionado para identificar o componente.

Content.vue

 <template> <div> <temp-var-chart :tempVar="tempVar"></temp-var-chart> <today-highlights :highlights="highlights"></today-highlights> </div> </template> <script> import TempVarChart from './TempVarChart.vue'; import Highlights from './Highlights.vue'; export default { props: ['highlights', 'tempVar'], components: { 'temp-var-chart': TempVarChart, 'today-highlights': Highlights }, } </script>

As seguintes alterações foram feitas a partir do código anterior:

  • No <template> , o texto e os dados dentro de {{ }} foram removidos, pois agora estamos apenas recebendo dados e passando para os filhos, sem renderização específica desse componente.
  • No export default {} :
    • As props foram alteradas para corresponder aos objetos de dados que serão enviados pelo pai: App.vue . A razão para alterar os adereços é que o próprio App.vue exibirá alguns dos dados que adquire da API de clima e outros recursos online, com base na consulta de pesquisa do usuário, e passará o restante dos dados. No código fictício que escrevemos anteriormente, App.vue estava passando todo o dump de dados fictício, sem qualquer discriminação, e os adereços do Content.vue foram configurados de acordo.
    • A função data() agora não retorna nada, pois não estamos fazendo nenhuma manipulação de dados neste componente.

TempVarChart.vue

Este componente deve receber projeções detalhadas de temperatura para o resto do dia atual e, eventualmente, exibi-las usando o FusionCharts. Mas, por enquanto, vamos exibi-los apenas como texto na página da web.

 <template> <div> {{ tempVar.tempToday }} </div> </template> <script> export default { props: ["tempVar"], components: {}, data() { return { }; }, methods: { }, }; </script> <style> </style>

Highlights.vue

 <template> <div> <uv-index :highlights="highlights"></uv-index> <visibility :highlights="highlights"></visibility> <wind-status :highlights="highlights"></wind-status> </div> </template> <script> import UVIndex from './UVIndex.vue'; import Visibility from './Visibility.vue'; import WindStatus from './WindStatus.vue'; export default { props: ["highlights"], components: { 'uv-index': UVIndex, 'visibility': Visibility, 'wind-status': WindStatus, }, data () { return { } }, methods: { }, computed: { }, } </script> <style> </style>

As alterações feitas no código anterior são:

  • No <template> , o texto e os dados dentro de {{ }} foram removidos, porque este é um componente burro, assim como Content.vue , cujo único trabalho é passar os dados para os filhos mantendo a hierarquia estrutural. Lembre-se de que componentes burros como Highlights.vue e Content.vue existem para manter a paridade entre a estrutura visual do painel e o código que escrevemos.

UVIndex.vue

As alterações feitas no código anterior são as seguintes:

  • No <template> e no <style> , o div id foi alterado para uvIndex , que é mais legível.
  • No export default {} , a função data() agora retorna um objeto string uvIndex , cujo valor é extraído do objeto highlight recebido pelo componente usando props . Este uvIndex agora é usado temporariamente para exibir o valor como texto dentro do <template> . Mais tarde, inseriremos esse valor na estrutura de dados adequada para renderizar um gráfico.

Visibility.vue

 <template> <div> <p>Visibility: {{ visibility }}</p> </div> </template> <script> export default { props: ["highlights"], data () { return { visibility: this.highlights.visibility.toString() } }, methods: { }, computed: { }, } </script> <style> </style>

A única alteração feita neste arquivo (em relação ao seu código anterior) é que a definição do objeto de visibility retornado pela função data() agora contém toString() em seu final, pois o valor recebido do pai será um flutuante número do ponto, que precisa ser convertido em string.

WindStatus.vue

 <template> <div> <p>Wind Speed — {{ windSpeed }}</p> <p>Wind Direction — {{ derivedWindDirection }}, or {{ windDirection }} degree clockwise with respect to true N as 0 degree.</p> </div> </template> <script> export default { props: ["highlights"], data () { return { windSpeed: this.highlights.windStatus.windSpeed, derivedWindDirection: this.highlights.windStatus.derivedWindDirection, windDirection: this.highlights.windStatus.windDirection } }, methods: { }, computed: { }, } </script> <style> </style>

As alterações feitas no código anterior são as seguintes:

  • Em todo o arquivo, windstatus foi renomeado como windStatus , para promover a legibilidade e também para estar em sincronia com o objeto de destaques que o App.vue fornece com os dados reais.
  • Mudanças de nomenclatura semelhantes foram feitas para a velocidade e direção — as novas são windSpeed ​​e windDirection .
  • Um novo objeto derivedWindDirection entrou em jogo (também fornecido pelo App.vue no pacote de destaques).

Por enquanto, os dados recebidos são renderizados como texto; posteriormente, ele será conectado à estrutura de dados necessária para visualização.

Teste com dados fictícios

Recorrer a dados fictícios repetidamente pode ser um pouco frustrante para você, mas há algumas boas razões por trás disso:

  • Fizemos muitas alterações no código de cada componente e é uma boa ideia testar se essas alterações estão quebrando o código. Em outras palavras, devemos verificar se o fluxo de dados está intacto, agora que estamos prestes a passar para partes mais complexas do projeto.
  • Os dados reais da API de clima on-line precisarão de muita massagem, e pode ser difícil para você fazer malabarismos entre o código para aquisição e processamento de dados e o código para fluxo de dados suave pelos componentes. A ideia é manter o quantum de complexidade sob controle, para que tenhamos uma melhor compreensão dos erros que podemos enfrentar.

Nesta seção, o que fazemos é essencialmente codificar alguns dados json no App.vue , que obviamente serão substituídos por dados ao vivo em um futuro próximo. Há muita semelhança entre a estrutura json fictícia e a estrutura json que usaremos para os dados reais. Portanto, também fornece uma ideia aproximada do que esperar dos dados reais, assim que os encontrarmos.

No entanto, admitimos que esta está longe de ser a abordagem ideal que se pode adotar ao construir um projeto do zero. No mundo real, você geralmente começa com a fonte de dados real, brinca um pouco com ela para entender o que pode e deve ser feito para domá-la e, em seguida, pensa na estrutura de dados json apropriada para capturar as informações relevantes. Nós o protegemos intencionalmente de todo esse trabalho sujo, pois leva você mais longe do objetivo – aprender a usar Vue.js e FusionCharts para criar um painel.

Vamos agora pular para o novo código para App.vue:

 <template> <div> <dashboard-content :highlights="highlights" :tempVar="tempVar"></dashboard-content> </div> </template> <script> import Content from './components/Content.vue' export default { name: 'app', components: { 'dashboard-content': Content }, data () { return { tempVar: { tempToday: [ {hour: '11.00 AM', temp: '35'}, {hour: '12.00 PM', temp: '36'}, {hour: '1.00 PM', temp: '37'}, {hour: '2.00 PM', temp: '38'}, {hour: '3.00 PM', temp: '36'}, {hour: '4.00 PM', temp: '35'}, ], }, highlights: { uvIndex: 4, visibility: 10, windStatus: { windSpeed: '30 km/h', windDirection: '30', derivedWindDirection: 'NNE', }, }, } }, methods: { }, computed: { }, } </script> <style> </style>

As alterações feitas no código em relação à sua versão anterior são as seguintes:

  • O nome do componente filho foi alterado para conteúdo do painel e, consequentemente, o elemento HTML personalizado no <template> foi revisado. Observe que agora temos dois atributos — highlights e tempVar — em vez de um único atributo que usamos anteriormente com o elemento personalizado. Assim, os dados associados a esses atributos também foram alterados. O interessante aqui é que podemos usar a diretiva v-bind: :, ou sua abreviação : (como fizemos aqui), com vários atributos de um elemento HTML personalizado!
  • A função data() agora retorna o objeto filename (que existia anteriormente), junto com dois novos objetos (em vez do antigo weather_data ): tempVar e highlights . A estrutura do json é apropriada para o código que escrevemos nos componentes filhos, para que eles possam extrair os dados necessários dos dumps. As estruturas são bastante autoexplicativas, e você pode esperar que elas sejam bastante semelhantes quando lidamos com dados ao vivo. No entanto, a mudança significativa que você encontrará é a ausência de hardcoding (óbvio, não é) - deixaremos os valores em branco como o estado padrão e escreveremos código para atualizá-los dinamicamente com base nos valores que receberemos do API meteorológica.

Você escreveu muito código nesta seção, sem ver a saída real. Antes de prosseguir, dê uma olhada no navegador (reinicie o servidor com npm run dev , se necessário) e aproveite a glória de sua conquista. A página da web que você deve ver neste momento se parece com a imagem abaixo:

Resultado do código exibido no navegador
(Visualização grande)

Código para aquisição e processamento de dados

Esta seção será a carne do projeto, com todo o código a ser escrito em App.vue para o seguinte:

  • Entrada de localização do usuário — uma caixa de entrada e um botão de chamada para ação são suficientes;
  • Funções utilitárias para várias tarefas; essas funções serão chamadas posteriormente em várias partes do código do componente;
  • Obtendo dados de geolocalização detalhados da API do Google Maps para JavaScript;
  • Obtendo dados meteorológicos detalhados da API Dark Sky;
  • Formatação e processamento dos dados de geolocalização e clima, que serão repassados ​​aos componentes filhos.

As subseções a seguir ilustram como podemos implementar as tarefas estabelecidas para nós nos pontos acima. Com algumas exceções, a maioria deles seguirá a sequência.

Entrada do usuário

É bastante óbvio que a ação começa quando o usuário fornece o nome do local para o qual os dados meteorológicos precisam ser exibidos. Para que isso aconteça, precisamos implementar o seguinte:

  • Uma caixa de entrada para inserir o local;
  • Um botão de envio que informa ao nosso aplicativo que o usuário inseriu o local e é hora de fazer o resto. Também implementaremos o comportamento quando o processamento começar ao pressionar Enter .

O código que mostramos abaixo será restrito à parte do modelo HTML do App.vue . Vamos apenas mencionar o nome do método associado aos eventos de clique e defini-los posteriormente no objeto de métodos do <script> em App.vue.

 <div> <input type="text" ref="input" placeholder="Location?" @keyup.enter="organizeAllDetails" > <button @click="organizeAllDetails"> <img src="./assets/Search.svg" width="24" height="24"> </button> </div>

Colocar o trecho acima no lugar certo é trivial - nós deixamos isso para você. No entanto, as partes interessantes do trecho são:

  • @keyup.enter="organizeAllDetails"
  • @click="organizeAllDetails"

Como você sabe das seções anteriores, @ é a abreviação do Vue para a diretiva v-on :, que está associada a algum evento. A novidade é “ organizeAllDetails ” — nada mais é do que o método que será acionado assim que os eventos (pressionar Enter ou clicar no botão) acontecerem. Ainda estamos para definir o método, e o quebra-cabeça estará completo até o final desta seção.

Exibição de informações de texto controladas pelo App.vue

Uma vez que a entrada do usuário aciona a ação e muitos dados são adquiridos das APIs, encontramos a pergunta inevitável – “O que fazer com todos esses dados?”. Obviamente, alguma massagem de dados é necessária, mas isso não responde totalmente à nossa pergunta! Precisamos decidir qual é o uso final dos dados, ou mais diretamente, quais são as entidades que recebem diferentes pedaços dos dados adquiridos e processados?

Os componentes filhos do App.vue , com base em sua hierarquia e finalidade, são os concorrentes da linha de frente para a maior parte dos dados. No entanto, também teremos alguns dados que não pertencem a nenhum desses componentes filhos, mas são bastante informativos e tornam o dashboard completo. Podemos fazer bom uso deles se os exibirmos como informações de texto controladas diretamente pelo App.vue , enquanto o restante dos dados é passado para a criança para ser exibido como gráficos bonitos.

Com esse contexto em mente, vamos nos concentrar no código para definir o estágio de uso de dados de texto. É um modelo HTML simples neste ponto, no qual os dados eventualmente virão e ficarão.

 <div> <div class="wrapper-left"> <div> {{ currentWeather.temp }} <span>°C</span> </div> <div>{{ currentWeather.summary }}</div> <div class="temp-max-min"> <div class="max-desc"> <div> <i>▲</i> {{ currentWeather.todayHighLow.todayTempHigh }} <span>°C</span> </div> <div>at {{ currentWeather.todayHighLow.todayTempHighTime }}</div> </div> <div class="min-desc"> <div> <i>▼</i> {{ currentWeather.todayHighLow.todayTempLow }} <span>°C</span> </div> <div>at {{ currentWeather.todayHighLow.todayTempLowTime }}</div> </div> </div> </div> <div class="wrapper-right"> <div class="date-time-info"> <div> <img src="./assets/calendar.svg" width="20" height="20"> {{ currentWeather.time }} </div> </div> <div class="location-info"> <div> <img src="./assets/location.svg" width="10.83" height="15.83" > {{ currentWeather.full_location }} <div class="mt-1"> Lat: {{ currentWeather.formatted_lat }} <br> Long: {{ currentWeather.formatted_long }} </div> </div> </div> </div> </div>

No trecho acima, você deve entender o seguinte:

  • O material dentro de {{ }} — eles são a maneira do Vue de inserir dados dinâmicos no template HTML, antes de renderizar no navegador. Você já os encontrou antes, e não há nada de novo ou surpreendente. Apenas tenha em mente que esses objetos de dados derivam do método data() no objeto export default() de App.vue . Eles têm valores padrão que definiremos de acordo com nossos requisitos e, em seguida, escreveremos certos métodos para preencher os objetos com dados reais da API.

Não se preocupe por não ver as mudanças no navegador — os dados ainda não estão definidos, e é natural que o Vue não renderize coisas que não conhece. No entanto, uma vez que os dados são definidos (e, por enquanto, você pode até verificar codificando os dados), os dados de texto serão controlados pelo App.vue .

O método data()

O método data() é uma construção especial nos arquivos .vue — ele contém e retorna objetos de dados que são tão cruciais para o aplicativo. Lembre-se da estrutura genérica da parte <script> em qualquer arquivo .vue — ela contém aproximadamente o seguinte:

 <script> // import statements here export default { // name, components, props, etc. data() { return { // the data that is so crucial for the application is defined here. // the data objects will have certain default values chosen by us. // The methods that we define below will manipulate the data. // Since the data is bounded to various attributes and directives, they // will update as and when the values of the data objects change. } }, methods: { // methods (objects whose values are functions) here. // bulk of dynamic stuff (the black magic part) is controlled from here. }, computed: { // computed properties here }, // other objects, as necessary } </script>

Até agora, você encontrou os nomes de alguns dos objetos de dados, mas são muito mais. A maioria deles é relevante para os componentes filho, cada um dos quais lida com um aspecto diferente do despejo de informações meteorológicas. Abaixo está todo o método data() que precisaremos para este projeto — você terá uma boa ideia sobre quais dados esperamos das APIs e como estamos divulgando os dados, com base na nomenclatura dos objetos.

 data() { return { weatherDetails: false, location: '', // raw location from input lat: '', // raw latitude from google maps api response long: '', // raw longitude from google maps api response completeWeatherApi: '', // weather api string with lat and long rawWeatherData: '', // raw response from weather api currentWeather: { full_location: '', // for full address formatted_lat: '', // for N/S formatted_long: '', // for E/W time: '', temp: '', todayHighLow: { todayTempHigh: '', todayTempHighTime: '', todayTempLow: '', todayTempLowTime: '' }, summary: '', possibility: '' }, tempVar: { tempToday: [ // gets added dynamically by this.getSetHourlyTempInfoToday() ], }, highlights: { uvIndex: '', visibility: '', windStatus: { windSpeed: '', windDirection: '', derivedWindDirection: '' }, } }; },

Como você pode ver, na maioria dos casos o valor padrão é vazio, porque isso será suficiente neste momento. Métodos serão escritos para manipular os dados e preenchê-los com valores apropriados, antes de serem renderizados ou passados ​​para os componentes filhos.

Métodos no App.vue

Para arquivos .vue , os métodos geralmente são escritos como valores de chaves aninhadas no objeto methods { } . Sua função principal é manipular os objetos de dados do componente. Vamos escrever os métodos no App.vue mantendo a mesma filosofia em mente. No entanto, com base em sua finalidade, podemos categorizar os métodos do App.vue no seguinte:

  • Métodos utilitários
  • Métodos orientados a ação/evento
  • Métodos de aquisição de dados
  • Métodos de processamento de dados
  • Métodos de colagem de alto nível

É importante que você entenda isso — estamos apresentando os métodos de bandeja porque já descobrimos como as APIs funcionam, quais dados elas fornecem e como devemos usar os dados em nosso projeto. Não é que tiramos os métodos do nada e escrevemos algum código misterioso para lidar com os dados. Para fins de aprendizado, é um bom exercício ler e entender diligentemente o código dos métodos e dados. No entanto, quando se depara com um novo projeto que você precisa construir do zero, você deve fazer todo o trabalho sujo sozinho, e isso significa experimentar muito as APIs - seu acesso programático e sua estrutura de dados, antes de colá-las perfeitamente com os dados estrutura que seu projeto exige. Você não terá nenhuma mão segurando, e haverá momentos frustrantes, mas tudo isso faz parte do amadurecimento como desenvolvedor.

Nas subseções a seguir, explicaremos cada um dos tipos de métodos e também mostraremos a implementação dos métodos pertencentes a essa categoria. Os nomes dos métodos são bastante autoexplicativos sobre seu propósito, assim como sua implementação, que acreditamos que você achará fácil de seguir. No entanto, antes disso, lembre-se do esquema geral de escrita de métodos em arquivos .vue :

 <script> // import statements here export default { // name, components, props, etc. data() { return { // the data that is so crucial for the application is defined here. } }, methods: { // methods (objects whose values are functions) here. // bulk of dynamic stuff (the black magic part) is controlled from here. method_1: function(arg_1) { }, method_2: function(arg_1, arg_2) { }, method_3: function(arg_1) { }, ……. }, computed: { // computed properties here }, // other objects, as necessary } </script>

Métodos de utilidade

Os métodos utilitários, como o nome sugere, são métodos escritos principalmente com a finalidade de modularização de código repetitivo usado para tarefas marginais. Eles são chamados por outros métodos quando necessário. Abaixo estão os métodos utilitários para App.vue :

 convertToTitleCase: function(str) { str = str.toLowerCase().split(' '); for (var i = 0; i < str.length; i++) { str[i] = str[i].charAt(0).toUpperCase() + str[i].slice(1); } return str.join(' '); },
 // To format the “possibility” (of weather) string obtained from the weather API formatPossibility: function(str) { str = str.toLowerCase().split('-'); for (var i = 0; i < str.length; i++) { str[i] = str[i].charAt(0).toUpperCase() + str[i].slice(1); } return str.join(' '); },
 // To convert Unix timestamps according to our convenience unixToHuman: function(timezone, timestamp) { /* READ THIS BEFORE JUDGING & DEBUGGING For any location beyond the arctic circle and the antarctic circle, the goddamn weather api does not return certain keys/values in each of this.rawWeatherData.daily.data[some_array_index]. Due to this, console throws up an error. The code is correct, the problem is with the API. May be later on I will add some padding to tackle missing values. */ var moment = require('moment-timezone'); // for handling date & time var decipher = new Date(timestamp * 1000); var human = moment(decipher) .tz(timezone) .format('llll'); var timeArray = human.split(' '); var timeNumeral = timeArray[4]; var timeSuffix = timeArray[5]; var justTime = timeNumeral + ' ' + timeSuffix; var monthDateArray = human.split(','); var monthDate = monthDateArray[1].trim(); return { fullTime: human, onlyTime: justTime, onlyMonthDate: monthDate }; },
 // To convert temperature from fahrenheit to celcius fahToCel: function(tempInFahrenheit) { var tempInCelcius = Math.round((5 / 9) * (tempInFahrenheit — 32)); return tempInCelcius; },
 // To convert the air pressure reading from millibar to kilopascal milibarToKiloPascal: function(pressureInMilibar) { var pressureInKPA = pressureInMilibar * 0.1; return Math.round(pressureInKPA); },
 // To convert distance readings from miles to kilometers mileToKilometer: function(miles) { var kilometer = miles * 1.60934; return Math.round(kilometer); },
 // To format the wind direction based on the angle deriveWindDir: function(windDir) { var wind_directions_array = [ { minVal: 0, maxVal: 30, direction: 'N' }, { minVal: 31, maxVal: 45, direction: 'NNE' }, { minVal: 46, maxVal: 75, direction: 'NE' }, { minVal: 76, maxVal: 90, direction: 'ENE' }, { minVal: 91, maxVal: 120, direction: 'E' }, { minVal: 121, maxVal: 135, direction: 'ESE' }, { minVal: 136, maxVal: 165, direction: 'SE' }, { minVal: 166, maxVal: 180, direction: 'SSE' }, { minVal: 181, maxVal: 210, direction: 'S' }, { minVal: 211, maxVal: 225, direction: 'SSW' }, { minVal: 226, maxVal: 255, direction: 'SW' }, { minVal: 256, maxVal: 270, direction: 'WSW' }, { minVal: 271, maxVal: 300, direction: 'W' }, { minVal: 301, maxVal: 315, direction: 'WNW' }, { minVal: 316, maxVal: 345, direction: 'NW' }, { minVal: 346, maxVal: 360, direction: 'NNW' } ]; var wind_direction = ''; for (var i = 0; i < wind_directions_array.length; i++) { if ( windDir >= wind_directions_array[i].minVal && windDir <= wind_directions_array[i].maxVal ) { wind_direction = wind_directions_array[i].direction; } } return wind_direction; },

Embora não o tenhamos implementado, você pode retirar os métodos utilitários do arquivo .vue e colocá-los em um arquivo JavaScript separado. Tudo o que você precisa fazer é importar o arquivo .js no início da parte do script no arquivo .vue , e pronto. Essa abordagem funciona muito bem e mantém o código limpo, especialmente em grandes aplicativos onde você pode usar muitos métodos que são melhor agrupados com base em sua finalidade. Você pode aplicar essa abordagem a todos os grupos de métodos listados neste artigo e ver o efeito em si. No entanto, sugerimos que você faça esse exercício depois de ter seguido o curso apresentado aqui, para que você tenha uma compreensão geral de todas as partes trabalhando em completa sincronia, e também tenha um software funcional ao qual você possa consultar, uma vez que algo pausas durante a experimentação.

Métodos Orientados a Ação/Evento

Esses métodos geralmente são executados quando precisamos realizar uma ação correspondente a um evento. Dependendo do caso, o evento pode ser acionado a partir de uma interação do usuário ou programaticamente. No arquivo App.vue , esses métodos ficam abaixo dos métodos utilitários.

 makeInputEmpty: function() { this.$refs.input.value = ''; },
 makeTempVarTodayEmpty: function() { this.tempVar.tempToday = []; },
 detectEnterKeyPress: function() { var input = this.$refs.input; input.addEventListener('keyup', function(event) { event.preventDefault(); var enterKeyCode = 13; if (event.keyCode === enterKeyCode) { this.setHitEnterKeyTrue(); } }); },
 locationEntered: function() { var input = this.$refs.input; if (input.value === '') { this.location = "New York"; } else { this.location = this.convertToTitleCase(input.value); } this.makeInputEmpty(); this.makeTempVarTodayEmpty(); },

Uma coisa interessante em alguns dos trechos de código acima é o uso de $ref . Em termos simples, é a maneira do Vue de associar a declaração de código que a contém, à construção HTML que ela deve afetar (para mais informações, leia o guia oficial). Por exemplo, os métodos makeInputEmpty() e detectEnterKeyPress() afetam a caixa de entrada, pois no HTML da caixa de entrada mencionamos o valor do atributo ref como input .

Métodos de aquisição de dados

Estamos usando as duas APIs a seguir em nosso projeto:

  • API do geocodificador do Google Maps
    Esta API serve para obter as coordenadas do local que o usuário pesquisa. Você precisará de uma chave de API para si mesmo, que pode ser obtida seguindo a documentação no link fornecido. Por enquanto, você pode usar a chave de API usada pelo FusionCharts, mas solicitamos que você não abuse dela e obtenha uma chave própria. Referimo-nos à API JavaScript do index.html deste projeto e usaremos os construtores fornecidos por ela para nosso código no arquivo App.vue .
  • A API de clima do céu escuro
    Esta API é para obter os dados meteorológicos correspondentes às coordenadas. No entanto, não o usaremos diretamente; vamos envolvê-lo em um URL que redireciona através de um dos servidores do FusionCharts. O motivo é que, se você enviar uma solicitação GET para a API de um aplicativo totalmente cliente, como o nosso, isso resultará no frustrante erro CORS (mais informações aqui e aqui).

Observação importante : como usamos as APIs do Google Maps e da Dark Sky, ambas as APIs têm suas próprias chaves de API que compartilhamos com você neste artigo. Isso ajudará você a se concentrar nos desenvolvimentos do lado do cliente, em vez da dor de cabeça da implementação de back-end. No entanto, recomendamos que você crie suas próprias chaves , pois nossas chaves de APIs virão com limites e, se esses limites excederem, você não poderá experimentar o aplicativo sozinho.

Para o Google Maps, acesse este artigo para obter sua chave de API. Para a API Dark Sky, visite https://darksky.net/dev para criar sua chave de API e respectivos endpoints.

Com o contexto em mente, vamos ver a implementação dos métodos de aquisição de dados para o nosso projeto.

 getCoordinates: function() { this.locationEntered(); var loc = this.location; var coords; var geocoder = new google.maps.Geocoder(); return new Promise(function(resolve, reject) { geocoder.geocode({ address: loc }, function(results, status) { if (status == google.maps.GeocoderStatus.OK) { this.lat = results[0].geometry.location.lat(); this.long = results[0].geometry.location.lng(); this.full_location = results[0].formatted_address; coords = { lat: this.lat, long: this.long, full_location: this.full_location }; resolve(coords); } else { alert("Oops! Couldn't get data for the location"); } }); }); },
 /* The coordinates that Google Maps Geocoder API returns are way too accurate for our requirements. We need to bring it into shape before passing the coordinates on to the weather API. Although this is a data processing method in its own right, we can't help mentioning it right now, because the data acquisition method for the weather API has dependency on the output of this method. */ setFormatCoordinates: async function() { var coordinates = await this.getCoordinates(); this.lat = coordinates.lat; this.long = coordinates.long; this.currentWeather.full_location = coordinates.full_location; // Remember to beautify lat for N/S if (coordinates.lat > 0) { this.currentWeather.formatted_lat = (Math.round(coordinates.lat * 10000) / 10000).toString() + '°N'; } else if (coordinates.lat < 0) { this.currentWeather.formatted_lat = (-1 * (Math.round(coordinates.lat * 10000) / 10000)).toString() + '°S'; } else { this.currentWeather.formatted_lat = ( Math.round(coordinates.lat * 10000) / 10000 ).toString(); } // Remember to beautify long for N/S if (coordinates.long > 0) { this.currentWeather.formatted_long = (Math.round(coordinates.long * 10000) / 10000).toString() + '°E'; } else if (coordinates.long < 0) { this.currentWeather.formatted_long = (-1 * (Math.round(coordinates.long * 10000) / 10000)).toString() + '°W'; } else { this.currentWeather.formatted_long = ( Math.round(coordinates.long * 10000) / 10000 ).toString(); } },
 /* This method dynamically creates the the correct weather API query URL, based on the formatted latitude and longitude. The complete URL is then fed to the method querying for weather data. Notice that the base URL used in this method (without the coordinates) points towards a FusionCharts server — we must redirect our GET request to the weather API through a server to avoid the CORS error. */ fixWeatherApi: async function() { await this.setFormatCoordinates(); var weatherApi = 'https://csm.fusioncharts.com/files/assets/wb/wb-data.php?src=darksky&lat=' + this.lat + '&long=' + this.long; this.completeWeatherApi = weatherApi; },
 fetchWeatherData: async function() { await this.fixWeatherApi(); var axios = require('axios'); // for handling weather api promise var weatherApiResponse = await axios.get(this.completeWeatherApi); if (weatherApiResponse.status === 200) { this.rawWeatherData = weatherApiResponse.data; } else { alert('Hmm... Seems like our weather experts are busy!'); } },

Through these methods, we have introduced the concept of async-await in our code. If you have been a JavaScript developer for some time now, you must be familiar with the callback hell, which is a direct consequence of the asynchronous way JavaScript is written. ES6 allows us to bypass the cumbersome nested callbacks, and our code becomes much cleaner if we write JavaScript in a synchronous way, using the async-await technique. However, there is a downside. It takes away the speed that asynchronous code gives us, especially for the portions of the code that deals with data being exchanged over the internet. Since this is not a mission-critical application with low latency requirements, and our primary aim is to learn stuff, the clean code is much more preferable over the slightly fast code.

Data Processing Methods

Now that we have the methods that will bring the data to us, we need to prepare the ground for properly receiving and processing the data. Safety nets must be cast, and there should be no spills — data is the new gold (OK, that might be an exaggeration in our context)! Enough with the fuss, let's get to the point.

Technically, the methods we implement in this section are aimed at getting the data out of the acquisition methods and the data objects in App.vue , and sometimes setting the data objects to certain values that suits the purpose.

getTimezone: function() { return this.rawWeatherData.timezone; },
 getSetCurrentTime: function() { var currentTime = this.rawWeatherData.currently.time; var timezone = this.getTimezone(); this.currentWeather.time = this.unixToHuman( timezone, currentTime ).fullTime; },
 getSetSummary: function() { var currentSummary = this.convertToTitleCase( this.rawWeatherData.currently.summary ); if (currentSummary.includes(' And')) { currentSummary = currentSummary.replace(' And', ','); } this.currentWeather.summary = currentSummary; },
 getSetPossibility: function() { var possible = this.formatPossibility(this.rawWeatherData.daily.icon); if (possible.includes(' And')) { possible = possible.replace(' And', ','); } this.currentWeather.possibility = possible; },
 getSetCurrentTemp: function() { var currentTemp = this.rawWeatherData.currently.temperature; this.currentWeather.temp = this.fahToCel(currentTemp); },
 getTodayDetails: function() { return this.rawWeatherData.daily.data[0]; },
 getSetTodayTempHighLowWithTime: function() { var timezone = this.getTimezone(); var todayDetails = this.getTodayDetails(); this.currentWeather.todayHighLow.todayTempHigh = this.fahToCel( todayDetails.temperatureMax ); this.currentWeather.todayHighLow.todayTempHighTime = this.unixToHuman( timezone, todayDetails.temperatureMaxTime ).onlyTime; this.currentWeather.todayHighLow.todayTempLow = this.fahToCel( todayDetails.temperatureMin ); this.currentWeather.todayHighLow.todayTempLowTime = this.unixToHuman( timezone, todayDetails.temperatureMinTime ).onlyTime; },
 getHourlyInfoToday: function() { return this.rawWeatherData.hourly.data; },
 getSetHourlyTempInfoToday: function() { var unixTime = this.rawWeatherData.currently.time; var timezone = this.getTimezone(); var todayMonthDate = this.unixToHuman(timezone, unixTime).onlyMonthDate; var hourlyData = this.getHourlyInfoToday(); for (var i = 0; i < hourlyData.length; i++) { var hourlyTimeAllTypes = this.unixToHuman(timezone, hourlyData[i].time); var hourlyOnlyTime = hourlyTimeAllTypes.onlyTime; var hourlyMonthDate = hourlyTimeAllTypes.onlyMonthDate; if (todayMonthDate === hourlyMonthDate) { var hourlyObject = { hour: '', temp: '' }; hourlyObject.hour = hourlyOnlyTime; hourlyObject.temp = this.fahToCel(hourlyData[i].temperature).toString(); this.tempVar.tempToday.push(hourlyObject); /* Since we are using array.push(), we are just adding elements at the end of the array. Thus, the array is not getting emptied first when a new location is entered. to solve this problem, a method this.makeTempVarTodayEmpty() has been created, and called from this.locationEntered(). */ } } /* To cover the edge case where the local time is between 10 — 12 PM, and therefore there are only two elements in the array this.tempVar.tempToday. We need to add the points for minimum temperature and maximum temperature so that the chart gets generated with atleast four points. */ if (this.tempVar.tempToday.length <= 2) { var minTempObject = { hour: this.currentWeather.todayHighLow.todayTempHighTime, temp: this.currentWeather.todayHighLow.todayTempHigh }; var maxTempObject = { hour: this.currentWeather.todayHighLow.todayTempLowTime, temp: this.currentWeather.todayHighLow.todayTempLow }; /* Typically, lowest temp are at dawn, highest temp is around mid day. Thus we can safely arrange like min, max, temp after 10 PM. */ // array.unshift() adds stuff at the beginning of the array. // the order will be: min, max, 10 PM, 11 PM. this.tempVar.tempToday.unshift(maxTempObject, minTempObject); } },
 getSetUVIndex: function() { var uvIndex = this.rawWeatherData.currently.uvIndex; this.highlights.uvIndex = uvIndex; },
 getSetVisibility: function() { var visibilityInMiles = this.rawWeatherData.currently.visibility; this.highlights.visibility = this.mileToKilometer(visibilityInMiles); },
 getSetWindStatus: function() { var windSpeedInMiles = this.rawWeatherData.currently.windSpeed; this.highlights.windStatus.windSpeed = this.mileToKilometer( windSpeedInMiles ); var absoluteWindDir = this.rawWeatherData.currently.windBearing; this.highlights.windStatus.windDirection = absoluteWindDir; this.highlights.windStatus.derivedWindDirection = this.deriveWindDir( absoluteWindDir ); },

Métodos de cola de alto nível

Com os métodos de utilidade, aquisição e processamento fora do nosso caminho, agora nos resta a tarefa de orquestrar tudo. Fazemos isso criando métodos de cola de alto nível, que essencialmente chamam os métodos escritos acima em uma sequência específica, para que toda a operação seja executada sem problemas.

 // Top level for info section // Data in this.currentWeather organizeCurrentWeatherInfo: function() { // data in this.currentWeather /* Coordinates and location is covered (get & set) in: — this.getCoordinates() — this.setFormatCoordinates() There are lots of async-await involved there. So it's better to keep them there. */ this.getSetCurrentTime(); this.getSetCurrentTemp(); this.getSetTodayTempHighLowWithTime(); this.getSetSummary(); this.getSetPossibility(); },
 // Top level for highlights organizeTodayHighlights: function() { // top level for highlights this.getSetUVIndex(); this.getSetVisibility(); this.getSetWindStatus(); },
 // Top level organization and rendering organizeAllDetails: async function() { // top level organization await this.fetchWeatherData(); this.organizeCurrentWeatherInfo(); this.organizeTodayHighlights(); this.getSetHourlyTempInfoToday(); },

montado

O Vue fornece ganchos de ciclo de vida da instância — propriedades que são essencialmente métodos e são acionados quando o ciclo de vida da instância atinge esse estágio. Por exemplo, criado, montado, antes da atualização, etc., são todos ganchos de ciclo de vida muito úteis que permitem ao programador controlar a instância em um nível muito mais granular do que seria possível de outra forma.

No código de um componente Vue, esses ganchos de ciclo de vida são implementados exatamente como você faria para qualquer outro prop . Por exemplo:

 <template> </template> <script> // import statements export default { data() { return { // data objects here } }, methods: { // methods here }, mounted: function(){ // function body here }, } </script> <style> </style>

Armado com esse novo entendimento, dê uma olhada no código abaixo para o prop mounted de App.vue :

 mounted: async function() { this.location = "New York"; await this.organizeAllDetails(); }

Código completo para App.vue

Cobrimos muito terreno nesta seção, e as últimas seções deram a você coisas em pedaços. No entanto, é importante que você tenha o código completo e montado para App.vue (sujeito a modificações adicionais nas seções subsequentes). Aqui vai:

 <template> <div> <div class="container-fluid"> <div class="row"> <div class="col-md-3 col-sm-4 col-xs-12 sidebar"> <div> <input type="text" ref="input" placeholder="Location?" @keyup.enter="organizeAllDetails" > <button @click="organizeAllDetails"> <img src="./assets/Search.svg" width="24" height="24"> </button> </div> <div> <div class="wrapper-left"> <div> {{ currentWeather.temp }} <span>°C</span> </div> <div>{{ currentWeather.summary }}</div> <div class="temp-max-min"> <div class="max-desc"> <div> <i>▲</i> {{ currentWeather.todayHighLow.todayTempHigh }} <span>°C</span> </div> <div>at {{ currentWeather.todayHighLow.todayTempHighTime }}</div> </div> <div class="min-desc"> <div> <i>▼</i> {{ currentWeather.todayHighLow.todayTempLow }} <span>°C</span> </div> <div>at {{ currentWeather.todayHighLow.todayTempLowTime }}</div> </div> </div> </div> <div class="wrapper-right"> <div class="date-time-info"> <div> <img src="./assets/calendar.svg" width="20" height="20"> {{ currentWeather.time }} </div> </div> <div class="location-info"> <div> <img src="./assets/location.svg" width="10.83" height="15.83" > {{ currentWeather.full_location }} <div class="mt-1"> Lat: {{ currentWeather.formatted_lat }} <br> Long: {{ currentWeather.formatted_long }} </div> </div> </div> </div> </div> </div> <dashboard-content class="col-md-9 col-sm-8 col-xs-12 content" :highlights="highlights" :tempVar="tempVar" ></dashboard-content> </div> </div> </div> </template> <script> import Content from './components/Content.vue'; export default { name: 'app', props: [], components: { 'dashboard-content': Content }, data() { return { weatherDetails: false, location: '', // raw location from input lat: '', // raw latitude from google maps api response long: '', // raw longitude from google maps api response completeWeatherApi: '', // weather api string with lat and long rawWeatherData: '', // raw response from weather api currentWeather: { full_location: '', // for full address formatted_lat: '', // for N/S formatted_long: '', // for E/W time: '', temp: '', todayHighLow: { todayTempHigh: '', todayTempHighTime: '', todayTempLow: '', todayTempLowTime: '' }, summary: '', possibility: '' }, tempVar: { tempToday: [ // gets added dynamically by this.getSetHourlyTempInfoToday() ], }, highlights: { uvIndex: '', visibility: '', windStatus: { windSpeed: '', windDirection: '', derivedWindDirection: '' }, } }; }, methods: { // Some utility functions convertToTitleCase: function(str) { str = str.toLowerCase().split(' '); for (var i = 0; i < str.length; i++) { str[i] = str[i].charAt(0).toUpperCase() + str[i].slice(1); } return str.join(' '); }, formatPossibility: function(str) { str = str.toLowerCase().split('-'); for (var i = 0; i < str.length; i++) { str[i] = str[i].charAt(0).toUpperCase() + str[i].slice(1); } return str.join(' '); }, unixToHuman: function(timezone, timestamp) { /* READ THIS BEFORE JUDGING & DEBUGGING For any location beyond the arctic circle and the antarctic circle, the goddamn weather api does not return certain keys/values in each of this.rawWeatherData.daily.data[some_array_index]. Due to this, console throws up an error. The code is correct, the problem is with the API. May be later on I will add some padding to tackle missing values. */ var moment = require('moment-timezone'); // for handling date & time var decipher = new Date(timestamp * 1000); var human = moment(decipher) .tz(timezone) .format('llll'); var timeArray = human.split(' '); var timeNumeral = timeArray[4]; var timeSuffix = timeArray[5]; var justTime = timeNumeral + ' ' + timeSuffix; var monthDateArray = human.split(','); var monthDate = monthDateArray[1].trim(); return { fullTime: human, onlyTime: justTime, onlyMonthDate: monthDate }; }, fahToCel: function(tempInFahrenheit) { var tempInCelcius = Math.round((5 / 9) * (tempInFahrenheit — 32)); return tempInCelcius; }, milibarToKiloPascal: function(pressureInMilibar) { var pressureInKPA = pressureInMilibar * 0.1; return Math.round(pressureInKPA); }, mileToKilometer: function(miles) { var kilometer = miles * 1.60934; return Math.round(kilometer); }, deriveWindDir: function(windDir) { var wind_directions_array = [ { minVal: 0, maxVal: 30, direction: 'N' }, { minVal: 31, maxVal: 45, direction: 'NNE' }, { minVal: 46, maxVal: 75, direction: 'NE' }, { minVal: 76, maxVal: 90, direction: 'ENE' }, { minVal: 91, maxVal: 120, direction: 'E' }, { minVal: 121, maxVal: 135, direction: 'ESE' }, { minVal: 136, maxVal: 165, direction: 'SE' }, { minVal: 166, maxVal: 180, direction: 'SSE' }, { minVal: 181, maxVal: 210, direction: 'S' }, { minVal: 211, maxVal: 225, direction: 'SSW' }, { minVal: 226, maxVal: 255, direction: 'SW' }, { minVal: 256, maxVal: 270, direction: 'WSW' }, { minVal: 271, maxVal: 300, direction: 'W' }, { minVal: 301, maxVal: 315, direction: 'WNW' }, { minVal: 316, maxVal: 345, direction: 'NW' }, { minVal: 346, maxVal: 360, direction: 'NNW' } ]; var wind_direction = ''; for (var i = 0; i < wind_directions_array.length; i++) { if ( windDir >= wind_directions_array[i].minVal && windDir <= wind_directions_array[i].maxVal ) { wind_direction = wind_directions_array[i].direction; } } return wind_direction; }, // Some basic action oriented functions makeInputEmpty: function() { this.$refs.input.value = ''; }, makeTempVarTodayEmpty: function() { this.tempVar.tempToday = []; }, detectEnterKeyPress: function() { var input = this.$refs.input; input.addEventListener('keyup', function(event) { event.preventDefault(); var enterKeyCode = 13; if (event.keyCode === enterKeyCode) { this.setHitEnterKeyTrue(); } }); }, locationEntered: function() { var input = this.$refs.input; if (input.value === '') { this.location = "New York"; } else { this.location = this.convertToTitleCase(input.value); } this.makeInputEmpty(); this.makeTempVarTodayEmpty(); }, getCoordinates: function() { this.locationEntered(); var loc = this.location; var coords; var geocoder = new google.maps.Geocoder(); return new Promise(function(resolve, reject) { geocoder.geocode({ address: loc }, function(results, status) { if (status == google.maps.GeocoderStatus.OK) { this.lat = results[0].geometry.location.lat(); this.long = results[0].geometry.location.lng(); this.full_location = results[0].formatted_address; coords = { lat: this.lat, long: this.long, full_location: this.full_location }; resolve(coords); } else { alert("Oops! Couldn't get data for the location"); } }); }); }, // Some basic asynchronous functions setFormatCoordinates: async function() { var coordinates = await this.getCoordinates(); this.lat = coordinates.lat; this.long = coordinates.long; this.currentWeather.full_location = coordinates.full_location; // Remember to beautify lat for N/S if (coordinates.lat > 0) { this.currentWeather.formatted_lat = (Math.round(coordinates.lat * 10000) / 10000).toString() + '°N'; } else if (coordinates.lat < 0) { this.currentWeather.formatted_lat = (-1 * (Math.round(coordinates.lat * 10000) / 10000)).toString() + '°S'; } else { this.currentWeather.formatted_lat = ( Math.round(coordinates.lat * 10000) / 10000 ).toString(); } // Remember to beautify long for N/S if (coordinates.long > 0) { this.currentWeather.formatted_long = (Math.round(coordinates.long * 10000) / 10000).toString() + '°E'; } else if (coordinates.long < 0) { this.currentWeather.formatted_long = (-1 * (Math.round(coordinates.long * 10000) / 10000)).toString() + '°W'; } else { this.currentWeather.formatted_long = ( Math.round(coordinates.long * 10000) / 10000 ).toString(); } }, fixWeatherApi: async function() { await this.setFormatCoordinates(); var weatherApi = 'https://csm.fusioncharts.com/files/assets/wb/wb-data.php?src=darksky&lat=' + this.lat + '&long=' + this.long; this.completeWeatherApi = weatherApi; }, fetchWeatherData: async function() { await this.fixWeatherApi(); var axios = require('axios'); // for handling weather api promise var weatherApiResponse = await axios.get(this.completeWeatherApi); if (weatherApiResponse.status === 200) { this.rawWeatherData = weatherApiResponse.data; } else { alert('Hmm... Seems like our weather experts are busy!'); } }, // Get and set functions; often combined, because they are short // For basic info — left panel/sidebar getTimezone: function() { return this.rawWeatherData.timezone; }, getSetCurrentTime: function() { var currentTime = this.rawWeatherData.currently.time; var timezone = this.getTimezone(); this.currentWeather.time = this.unixToHuman( timezone, currentTime ).fullTime; }, getSetSummary: function() { var currentSummary = this.convertToTitleCase( this.rawWeatherData.currently.summary ); if (currentSummary.includes(' And')) { currentSummary = currentSummary.replace(' And', ','); } this.currentWeather.summary = currentSummary; }, getSetPossibility: function() { var possible = this.formatPossibility(this.rawWeatherData.daily.icon); if (possible.includes(' And')) { possible = possible.replace(' And', ','); } this.currentWeather.possibility = possible; }, getSetCurrentTemp: function() { var currentTemp = this.rawWeatherData.currently.temperature; this.currentWeather.temp = this.fahToCel(currentTemp); }, getTodayDetails: function() { return this.rawWeatherData.daily.data[0]; }, getSetTodayTempHighLowWithTime: function() { var timezone = this.getTimezone(); var todayDetails = this.getTodayDetails(); this.currentWeather.todayHighLow.todayTempHigh = this.fahToCel( todayDetails.temperatureMax ); this.currentWeather.todayHighLow.todayTempHighTime = this.unixToHuman( timezone, todayDetails.temperatureMaxTime ).onlyTime; this.currentWeather.todayHighLow.todayTempLow = this.fahToCel( todayDetails.temperatureMin ); this.currentWeather.todayHighLow.todayTempLowTime = this.unixToHuman( timezone, todayDetails.temperatureMinTime ).onlyTime; }, getHourlyInfoToday: function() { return this.rawWeatherData.hourly.data; }, getSetHourlyTempInfoToday: function() { var unixTime = this.rawWeatherData.currently.time; var timezone = this.getTimezone(); var todayMonthDate = this.unixToHuman(timezone, unixTime).onlyMonthDate; var hourlyData = this.getHourlyInfoToday(); for (var i = 0; i < hourlyData.length; i++) { var hourlyTimeAllTypes = this.unixToHuman(timezone, hourlyData[i].time); var hourlyOnlyTime = hourlyTimeAllTypes.onlyTime; var hourlyMonthDate = hourlyTimeAllTypes.onlyMonthDate; if (todayMonthDate === hourlyMonthDate) { var hourlyObject = { hour: '', temp: '' }; hourlyObject.hour = hourlyOnlyTime; hourlyObject.temp = this.fahToCel(hourlyData[i].temperature).toString(); this.tempVar.tempToday.push(hourlyObject); /* Since we are using array.push(), we are just adding elements at the end of the array. Thus, the array is not getting emptied first when a new location is entered. to solve this problem, a method this.makeTempVarTodayEmpty() has been created, and called from this.locationEntered(). */ } } /* To cover the edge case where the local time is between 10 — 12 PM, and therefore there are only two elements in the array this.tempVar.tempToday. We need to add the points for minimum temperature and maximum temperature so that the chart gets generated with atleast four points. */ if (this.tempVar.tempToday.length <= 2) { var minTempObject = { hour: this.currentWeather.todayHighLow.todayTempHighTime, temp: this.currentWeather.todayHighLow.todayTempHigh }; var maxTempObject = { hour: this.currentWeather.todayHighLow.todayTempLowTime, temp: this.currentWeather.todayHighLow.todayTempLow }; /* Typically, lowest temp are at dawn, highest temp is around mid day. Thus we can safely arrange like min, max, temp after 10 PM. */ // array.unshift() adds stuff at the beginning of the array. // the order will be: min, max, 10 PM, 11 PM. this.tempVar.tempToday.unshift(maxTempObject, minTempObject); } }, // For Today Highlights getSetUVIndex: function() { var uvIndex = this.rawWeatherData.currently.uvIndex; this.highlights.uvIndex = uvIndex; }, getSetVisibility: function() { var visibilityInMiles = this.rawWeatherData.currently.visibility; this.highlights.visibility = this.mileToKilometer(visibilityInMiles); }, getSetWindStatus: function() { var windSpeedInMiles = this.rawWeatherData.currently.windSpeed; this.highlights.windStatus.windSpeed = this.mileToKilometer( windSpeedInMiles ); var absoluteWindDir = this.rawWeatherData.currently.windBearing; this.highlights.windStatus.windDirection = absoluteWindDir; this.highlights.windStatus.derivedWindDirection = this.deriveWindDir( absoluteWindDir ); }, // top level for info section organizeCurrentWeatherInfo: function() { // data in this.currentWeather /* Coordinates and location is covered (get & set) in: — this.getCoordinates() — this.setFormatCoordinates() There are lots of async-await involved there. So it's better to keep them there. */ this.getSetCurrentTime(); this.getSetCurrentTemp(); this.getSetTodayTempHighLowWithTime(); this.getSetSummary(); this.getSetPossibility(); }, organizeTodayHighlights: function() { // top level for highlights this.getSetUVIndex(); this.getSetVisibility(); this.getSetWindStatus(); }, // topmost level orchestration organizeAllDetails: async function() { // top level organization await this.fetchWeatherData(); this.organizeCurrentWeatherInfo(); this.organizeTodayHighlights(); this.getSetHourlyTempInfoToday(); }, }, mounted: async function() { this.location = "New York"; await this.organizeAllDetails(); } }; </script>

E, finalmente, depois de tanta paciência e trabalho duro, você pode ver o fluxo de dados com sua força bruta! Visite o aplicativo no navegador, atualize a página, procure um local na caixa de pesquisa do aplicativo e pressione Enter !

O aplicativo como mostrado no navegador
(Visualização grande)

Agora que terminamos todo o trabalho pesado, faça uma pausa. As seções subsequentes se concentram no uso dos dados para criar gráficos bonitos e informativos, seguidos de dar ao nosso aplicativo de aparência feia uma sessão de preparação muito merecida usando CSS.

5. Visualização de dados com FusionCharts

Considerações fundamentais para gráficos

Para o usuário final, a essência de um dashboard é essencialmente esta: uma coleção de informações filtradas e cuidadosamente selecionadas sobre um determinado tópico, transmitidas por meio de instrumentos visuais/gráficos para ingestão rápida. Eles não se importam com as sutilezas de sua engenharia de pipeline de dados ou com a estética do seu código - tudo o que eles querem é uma visualização de alto nível em 3 segundos. Portanto, nosso aplicativo bruto exibindo dados de texto não significa nada para eles, e é hora de implementarmos mecanismos para agrupar os dados com gráficos.

No entanto, antes de nos aprofundarmos na implementação dos gráficos, vamos considerar algumas questões pertinentes e as possíveis respostas do nosso ponto de vista:

  • Que tipo de gráficos são apropriados para o tipo de dados com os quais estamos lidando?
    Bem, a resposta tem dois aspectos – o contexto e o propósito. Por contexto, queremos dizer o tipo de dados e, em geral, se encaixam no esquema de coisas maiores, delimitadas pelo escopo e público do projeto. E por propósito, queremos dizer essencialmente “o que queremos enfatizar?”. Por exemplo, podemos representar a temperatura de hoje em diferentes momentos do dia usando um gráfico de colunas (colunas verticais de largura igual, com altura proporcional ao valor que a coluna representa). No entanto, raramente estamos interessados ​​nos valores individuais, mas sim na variação geral e na tendência ao longo dos dados. Para atender à finalidade, é de nosso interesse usar um gráfico de linhas, e faremos isso em breve.
  • O que deve ser mantido em mente antes de selecionar uma biblioteca de gráficos?
    Como estamos fazendo um projeto predominantemente usando tecnologias baseadas em JavaScript, é óbvio que qualquer biblioteca de gráficos que escolhermos para nosso projeto seja nativa do mundo JavaScript. Com essa premissa básica em mente, devemos considerar o seguinte antes de zerar qualquer biblioteca específica:
    • Suporte para os frameworks de nossa escolha , que neste caso, é o Vue.js. Um projeto pode ser desenvolvido em outros frameworks JavaScript populares como React ou Angular — verifique o suporte da biblioteca de gráficos para seu framework favorito. Além disso, o suporte para outras linguagens de programação populares como Python, Java, C++, .Net (AS e VB), especialmente quando o projeto envolve algumas coisas sérias de back-end, deve ser considerado.
    • Disponibilidade de tipos e funcionalidades de gráficos , pois é quase impossível saber de antemão qual será a forma final e a finalidade dos dados no projeto (especialmente se os requisitos forem regulamentados por seus clientes em um ambiente profissional). Nesse caso, você deve lançar sua rede em toda a rede e escolher uma biblioteca de gráficos que tenha a coleção mais ampla de gráficos. Mais importante, para diferenciar seu projeto de outros, a biblioteca deve ter recursos suficientes na forma de atributos de gráfico configuráveis, para que você possa ajustar e personalizar a maioria dos aspectos dos gráficos e o nível certo de granularidade. Além disso, as configurações padrão do gráfico devem ser sensatas e a documentação da biblioteca deve ser de primeira, por razões óbvias para desenvolvedores profissionais.
    • Curva de aprendizado, comunidade de suporte e equilíbrio também devem ser levados em consideração, especialmente quando você é novo na visualização de dados. Em uma ponta do espectro, você tem ferramentas proprietárias como Tableau e Qlickview que custam uma bomba, têm curva de aprendizado suave, mas também vêm com muitas limitações em termos de personalização, integração e implantação. Por outro lado, há o d3.js — vasto, gratuito (código aberto) e personalizável em sua essência, mas você precisa pagar o preço de uma curva de aprendizado muito íngreme para poder fazer qualquer coisa produtiva com a biblioteca.

O que você precisa é o ponto ideal - o equilíbrio certo entre produtividade, cobertura, personalização, curva de aprendizado e, claro, custo. Convidamos você a dar uma olhada no FusionCharts — a biblioteca de gráficos JavaScript mais abrangente e pronta para empresas do mundo para a web e dispositivos móveis, que usaremos neste projeto para criar gráficos.

Introdução ao FusionCharts

O FusionCharts é usado mundialmente como a biblioteca de gráficos JavaScript por milhões de desenvolvedores espalhados por centenas de países ao redor do mundo. Tecnicamente, é tão carregado e configurável quanto possível, com suporte para integrá-lo a quase qualquer pilha de tecnologia popular usada para projetos baseados na web. O uso comercial do FusionCharts requer uma licença, e você deve pagar pela licença dependendo do seu caso de uso (entre em contato com o departamento de vendas se estiver curioso). No entanto, estamos usando o FusionCharts nestes projetos apenas para experimentar algumas coisas e, portanto, a versão não licenciada (vem com uma pequena marca d'água em seus gráficos e algumas outras restrições). Usar a versão não licenciada é perfeitamente adequado quando você está testando os gráficos e usando-os em seus projetos pessoais ou não comerciais. Se você planeja implantar o aplicativo comercialmente, certifique-se de ter uma licença do FusionCharts.

Como este é um projeto envolvendo Vue.js, precisaremos de dois módulos que precisam ser instalados, caso não seja feito antes:

  • O módulo fusioncharts , pois contém tudo o que você precisa para criar os gráficos
  • O módulo vue-fusioncharts , que é essencialmente um wrapper para fusioncharts, para que possa ser usado em um projeto Vue.js

Se você não os instalou anteriormente (conforme as instruções da terceira seção), instale-os executando o seguinte comando no diretório raiz do projeto:

 npm install fusioncharts vue-fusioncharts --save

Em seguida, certifique-se de que o arquivo src/main.js do projeto tenha o seguinte código (também mencionado na seção 3):

 import Vue from 'vue'; import App from './App.vue'; import FusionCharts from 'fusioncharts'; import Charts from 'fusioncharts/fusioncharts.charts'; import Widgets from 'fusioncharts/fusioncharts.widgets'; import PowerCharts from 'fusioncharts/fusioncharts.powercharts'; import FusionTheme from 'fusioncharts/themes/fusioncharts.theme.fusion'; import VueFusionCharts from 'vue-fusioncharts'; Charts(FusionCharts); PowerCharts(FusionCharts); Widgets(FusionCharts); FusionTheme(FusionCharts); Vue.use(VueFusionCharts, FusionCharts); new Vue({ el: '#app', render: h => h(App) })

Talvez a linha mais crítica no snippet acima seja a seguinte:

 Vue.use(VueFusionCharts, FusionCharts)

Ele instrui o Vue a usar o módulo vue-fusioncharts para dar sentido a muitas coisas no projeto que aparentemente não são explicitamente definidas por nós, mas são definidas no próprio módulo. Além disso, esse tipo de declaração implica declaração global , o que significa que em qualquer lugar que o Vue encontre algo estranho no código do nosso projeto (coisas que não definimos explicitamente sobre o uso de FusionCharts), ele aparecerá pelo menos uma vez nos vue-fusioncharts e módulos de nó fusioncharts para suas definições, antes de gerar erros. Se tivéssemos usado FusionCharts em uma parte isolada de nosso projeto (não usando em quase todos os arquivos componentes), talvez a declaração local fizesse mais sentido.

Com isso, você está pronto para usar o FusionCharts no projeto. Estaremos usando uma variedade de gráficos, a escolha depende do aspecto dos dados meteorológicos que queremos visualizar. Além disso, veremos a interação de vinculação de dados, componentes personalizados e observadores em ação.

Esquema geral para usar o Fusioncharts em arquivos .vue

Nesta seção, explicaremos a ideia geral de usar o FusionCharts para criar vários gráficos nos arquivos .vue . Mas primeiro, vamos ver o pseudocódigo que ilustra esquematicamente a ideia central.

 <template> <div> <fusioncharts :attribute_1="data_object_1" :attribute_2="data_object_2" … … ... > </fusioncharts> </div> </template> <script> export default { props: ["data_prop_received_by_the_component"], components: {}, data() { return { data_object_1: "value_1", data_object_2: "value_2", … … }; }, methods: {}, computed: {}, watch: { data_prop_received_by_the_component: { handler: function() { // some code/logic, mainly data manipulation based }, deep: true } } }; </script> <style> // component specific special CSS code here </style>

Vamos entender diferentes partes do pseudocódigo acima:

  • No <template> , no nível superior <div> (que é praticamente obrigatório para o código HTML do modelo de cada componente), temos o componente personalizado <fusioncharts> . Temos a definição do componente contido no módulo vue-fusioncharts Node que instalamos para este projeto. Internamente, vue-fusioncharts conta com o módulo fusioncharts , que também foi instalado. Importamos os módulos necessários e resolvemos suas dependências, instruímos o Vue a usar o wrapper globalmente (em todo o projeto) no arquivo src/main.js e, portanto, não há falta de definição para o componente personalizado <fusioncharts> que usamos aqui. Além disso, o componente personalizado tem atributos personalizados, e cada atributo personalizado é vinculado a um objeto de dados (e, por sua vez, seus valores), pela diretiva v-bind , para a qual a abreviação é o símbolo de dois pontos ( : ). Aprenderemos sobre os atributos e seus objetos de dados associados com mais detalhes, quando discutirmos alguns dos gráficos específicos usados ​​neste projeto.
  • No <script> , primeiro você declara as props que o componente deve receber, e então segue definindo os objetos de dados que estão vinculados aos atributos de <fusioncharts> . Os valores atribuídos aos objetos de dados são os valores que os atributos de <fusioncharts> , e os gráficos são criados com base nesses valores puxados. Além destes, a parte mais interessante do código é o objeto watch { } . Este é um objeto muito especial no esquema de coisas do Vue — ele essencialmente instrui o Vue a observar quaisquer mudanças que ocorram em certos dados, e então tomar ações baseadas em como a função do handler para esses dados foi definida. Por exemplo, queremos que o Vue fique de olho no prop recebido, ou seja, data_prop_received_by_the_component no pseudocódigo. A prop torna-se uma chave no objeto watch { } , e o valor da chave é outro objeto — um método manipulador que descreve o que precisa ser feito sempre que a prop muda. Com mecanismos tão elegantes para lidar com as mudanças, o app mantém sua reatividade. O deep: true representa um sinalizador booleano que você pode associar aos observadores, para que o objeto que está sendo observado seja observado com bastante profundidade, ou seja, até as alterações feitas nos níveis aninhados do objeto são rastreadas.
    ( Para mais informações sobre observadores, consulte a documentação oficial ).

Agora que você está equipado com uma compreensão do esquema geral das coisas, vamos mergulhar nas implementações específicas dos gráficos nos arquivos do componente .vue . O código será bastante autoexplicativo, e você deve tentar entender como as especificidades se encaixam no esquema geral das coisas descritas acima.

Implementação de gráficos em arquivos .vue

Embora as especificidades da implementação variem de um gráfico para outro, a seguinte explicação é aplicável a todos eles:

  • <template>
    Como explicado anteriormente, o componente personalizado <fusioncharts> tem vários atributos, cada um deles sendo vinculado ao objeto de dados correspondente definido na função data() usando a diretiva v-bind : . Os nomes dos atributos são bastante autoexplicativos para o que eles significam, e descobrir os objetos de dados correspondentes também é trivial.
  • <script>
    Na função data() , os objetos de dados e seus valores são o que faz os gráficos funcionarem, por causa da vinculação feita pelas diretivas v-bind ( : ) usadas nos atributos de <fusioncharts> . Antes de nos aprofundarmos nos objetos de dados individuais, vale a pena mencionar algumas características gerais:
    • Os objetos de dados cujos valores são 0 ou 1 são de natureza booleana, onde 0 representa algo não disponível/desligado e 1 representa disponibilidade/estado ligado. No entanto, tenha cuidado, pois objetos de dados não booleanos também podem ter 0 ou 1 como seus valores, além de outros valores possíveis — depende do contexto. Por exemplo, containerbackgroundopacity com seu valor padrão como 0 é booleano, enquanto lowerLimit com seu valor padrão como 0 significa simplesmente que o número zero é seu valor literal.
    • Alguns objetos de dados lidam com propriedades CSS como margem, preenchimento, tamanho da fonte, etc. — o valor tem uma unidade implícita de “px” ou pixel. Da mesma forma, outros objetos de dados podem ter unidades implícitas associadas a seus valores. Para obter informações detalhadas, consulte a respectiva página de atributos do gráfico do FusionCharts Dev Center.
  • Na função data() , talvez o objeto mais interessante e não óbvio seja o dataSource. Este objeto tem três objetos principais aninhados nele:
    • chart : este objeto encapsula muitos atributos de gráfico relacionados à configuração e aparência do gráfico. É quase uma construção obrigatória que você encontrará em todos os gráficos que criará para este projeto.
    • colorrange : Este objeto é um tanto específico para o gráfico em questão, e está presente principalmente em gráficos que lidam com várias cores/tons para demarcar diferentes subfaixas da escala utilizada no gráfico.
    • valor: Este objeto, novamente, está presente em gráficos que possuem um valor específico que precisa ser destacado no intervalo da escala.
  • O objeto watch { } é talvez a coisa mais importante que faz este gráfico, e os outros gráficos usados ​​neste projeto, ganharem vida. A reatividade dos gráficos, ou seja, os gráficos se atualizando com base nos novos valores resultantes de uma nova consulta do usuário, é controlada pelos observadores definidos neste objeto. Por exemplo, nós definimos um watcher para os highlights de prop recebidos pelo componente, e então definimos uma função handler para instruir o Vue sobre as ações necessárias que ele deve tomar, quando algo mudar sobre o objeto que está sendo observado em todo o projeto. Isso significa que sempre que o App.vue um novo valor para qualquer objeto dentro de highlights , as informações chegarão até esse componente e o novo valor será atualizado nos objetos de dados desse componente. O gráfico vinculado aos valores também é atualizado como resultado desse mecanismo.

As explicações acima são bastante amplas para nos ajudar a desenvolver uma compreensão intuitiva do quadro maior. Depois de entender os conceitos intuitivamente, você sempre pode consultar a documentação do Vue.js e do FusionCharts, quando algo não estiver claro para você no próprio código. Deixamos o exercício para você e, a partir da próxima subseção, não explicaremos as coisas que abordamos nesta subseção.

src/components/TempVarChart.vue

Um diagrama mostrando a temperatura horária
(Visualização grande)
 <template> <div class="custom-card header-card card"> <div class="card-body pt-0"> <fusioncharts type="spline" width="100%" height="100%" dataformat="json" dataEmptyMessage="i-https://i.postimg.cc/R0QCk9vV/Rolling-0-9s-99px.gif" dataEmptyMessageImageScale=39 :datasource="tempChartData" > </fusioncharts> </div> </div> </template> <script> export default { props: ["tempVar"], components: {}, data() { return { tempChartData: { chart: { caption: "Hourly Temperature", captionFontBold: "0", captionFontColor: "#000000", captionPadding: "30", baseFont: "Roboto", chartTopMargin: "30", showHoverEffect: "1", theme: "fusion", showaxislines: "1", numberSuffix: "°C", anchorBgColor: "#6297d9", paletteColors: "#6297d9", drawCrossLine: "1", plotToolText: "$label<br><hr><b>$dataValue</b>", showAxisLines: "0", showYAxisValues: "0", anchorRadius: "4", divLineAlpha: "0", labelFontSize: "13", labelAlpha: "65", labelFontBold: "0", rotateLabels: "1", slantLabels: "1", canvasPadding: "20" }, data: [], }, }; }, methods: { setChartData: function() { var data = []; for (var i = 0; i < this.tempVar.tempToday.length; i++) { var dataObject = { label: this.tempVar.tempToday[i].hour, value: this.tempVar.tempToday[i].temp }; data.push(dataObject); } this.tempChartData.data = data; }, }, mounted: function() { this.setChartData(); }, watch: { tempVar: { handler: function() { this.setChartData(); }, deep: true }, }, }; </script> <style> </style>

src/components/UVIndex.vue

Este componente contém um gráfico extremamente útil — o Angular Gauge.

Índice UV
(Visualização grande)

O código para o componente é dado abaixo. Para obter informações detalhadas sobre os atributos de gráfico do Angular Gauge, consulte a página FusionCharts Dev Center para Angular Gauge.

 <template> <div class="highlights-item col-md-4 col-sm-6 col-xs-12 border-top"> <div> <fusioncharts :type="type" :width="width" :height="height" :containerbackgroundopacity="containerbackgroundopacity" :dataformat="dataformat" :datasource="datasource" ></fusioncharts> </div> </div> </template> <script> export default { props: ["highlights"], components: {}, data() { return { type: "angulargauge", width: "100%", height: "100%", containerbackgroundopacity: 0, dataformat: "json", datasource: { chart: { caption: "UV Index", captionFontBold: "0", captionFontColor: "#000000", captionPadding: "30", lowerLimit: "0", upperLimit: "15", lowerLimitDisplay: "1", upperLimitDisplay: "1", showValue: "0", theme: "fusion", baseFont: "Roboto", bgAlpha: "0", canvasbgAlpha: "0", gaugeInnerRadius: "75", gaugeOuterRadius: "110", pivotRadius: "0", pivotFillAlpha: "0", valueFontSize: "20", valueFontColor: "#000000", valueFontBold: "1", tickValueDistance: "3", autoAlignTickValues: "1", majorTMAlpha: "20", chartTopMargin: "30", chartBottomMargin: "40" }, colorrange: { color: [ { minvalue: "0", maxvalue: this.highlights.uvIndex.toString(), code: "#7DA9E0" }, { minvalue: this.highlights.uvIndex.toString(), maxvalue: "15", code: "#D8EDFF" } ] }, annotations: { groups: [ { items: [ { id: "val-label", type: "text", text: this.highlights.uvIndex.toString(), fontSize: "20", font: "Source Sans Pro", fontBold: "1", fillcolor: "#212529", x: "$gaugeCenterX", y: "$gaugeCenterY" } ] } ] }, dials: { dial: [ { value: this.highlights.uvIndex.toString(), baseWidth: "0", radius: "0", borderThickness: "0", baseRadius: "0" } ] } } }; }, methods: {}, computed: {}, watch: { highlights: { handler: function() { this.datasource.colorrange.color[0].maxvalue = this.highlights.uvIndex.toString(); this.datasource.colorrange.color[1].minvalue = this.highlights.uvIndex.toString(); this.datasource.annotations.groups[0].items[0].text = this.highlights.uvIndex.toString(); }, deep: true } } }; </script>

src/components/Visibility.vue

Neste componente, utilizamos um Medidor Linear Horizontal para representar a visibilidade, conforme mostra a imagem abaixo:

Uma captura de tela do medidor linear horizontal representando a visibilidade do ar (16 km)
(Visualização grande)

O código para o componente é dado abaixo. Para uma compreensão aprofundada dos diferentes atributos deste tipo de gráfico, consulte a página do FusionCharts Dev Center para o medidor linear horizontal.

 <template> <div class="highlights-item col-md-4 col-sm-6 col-xs-12 border-left border-right border-top"> <div> <fusioncharts :type="type" :width="width" :height="height" :containerbackgroundopacity="containerbackgroundopacity" :dataformat="dataformat" :datasource="datasource" > </fusioncharts> </div> </div> </template> <script> export default { props: ["highlights"], components: {}, methods: {}, computed: {}, data() { return { type: "hlineargauge", width: "100%", height: "100%", containerbackgroundopacity: 0, dataformat: "json", creditLabel: false, datasource: { chart: { caption: "Air Visibility", captionFontBold: "0", captionFontColor: "#000000", baseFont: "Roboto", numberSuffix: " km", lowerLimit: "0", upperLimit: "40", showPointerShadow: "1", animation: "1", transposeAnimation: "1", theme: "fusion", bgAlpha: "0", canvasBgAlpha: "0", valueFontSize: "20", valueFontColor: "#000000", valueFontBold: "1", pointerBorderAlpha: "0", chartBottomMargin: "40", captionPadding: "30", chartTopMargin: "30" }, colorRange: { color: [ { minValue: "0", maxValue: "4", label: "Fog", code: "#6297d9" }, { minValue: "4", maxValue: "10", label: "Haze", code: "#7DA9E0" }, { minValue: "10", maxValue: "40", label: "Clear", code: "#D8EDFF" } ] }, pointers: { pointer: [ { value: this.highlights.visibility.toString() } ] } } }; }, watch: { highlights: { handler: function() { this.datasource.pointers.pointer[0].value = this.highlights.visibility.toString(); }, deep: true } } }; </script>

src/components/WindStatus.vue

Este componente exibe a velocidade e a direção do vento (velocidade do vento, se você for experiente em física), e é muito difícil representar um vetor usando um gráfico. Para esses casos, sugerimos representá-los com o auxílio de algumas belas imagens e valores de texto. Como a representação que pensamos é totalmente dependente do CSS, vamos implementá-la na próxima seção que trata do CSS. No entanto, dê uma olhada no que pretendemos criar:

Status do Vento: Direção do Vento (esquerda) e Velocidade do Vento (direita)
(Visualização grande)
 <template> <div class="highlights-item col-md-4 col-sm-6 col-xs-12 border-top"> <div> <div class="card-heading pt-5">Wind Status</div> <div class="row pt-4 mt-4"> <div class="col-sm-6 col-md-6 mt-2 text-center align-middle"> <p class="card-sub-heading mt-3">Wind Direction</p> <p class="mt-4"><img src="../assets/winddirection.svg" height="40" width="40"></p> <p class="card-value mt-4">{{ highlights.windStatus.derivedWindDirection }}</p> </div> <div class="col-sm-6 col-md-6 mt-2"> <p class="card-sub-heading mt-3">Wind Speed</p> <p class="mt-4"><img src="../assets/windspeed.svg" height="40" width="40"></p> <p class="card-value mt-4">{{ highlights.windStatus.windSpeed }} km/h</p> </div> </div> </div> </div> </template> <script> export default { props: ["highlights"], components: {}, data() { return {}; }, methods: {}, computed: {} }; </script>

Finalizando com Highlights.vue

Lembre-se de que já implementamos código com CSS para todos os componentes — exceto Content.vue e Highlights.vue . Como o Content.vue é um componente burro que apenas transmite dados, o estilo mínimo necessário já foi abordado. Além disso, já escrevemos o código apropriado para estilizar a barra lateral e os cartões que contêm os gráficos. Portanto, tudo o que nos resta fazer é adicionar alguns bits estilísticos ao Highlights.vue , que envolve principalmente o uso das classes CSS:

 <template> <div class="custom-content-card content-card card"> <div class="card-body pb-0"> <div class="content-header h4 text-center pt-2 pb-3">Highlights</div> <div class="row"> <uv-index :highlights="highlights"></uv-index> <visibility :highlights="highlights"></visibility> <wind-status :highlights="highlights"></wind-status> </div> </div> </div> </template> <script> import UVIndex from "./UVIndex.vue"; import Visibility from "./Visibility.vue"; import WindStatus from "./WindStatus.vue"; export default { props: ["highlights"], components: { "uv-index": UVIndex, "visibility": Visibility, "wind-status": WindStatus, }, }; </script>

Implantação e código-fonte

Com os gráficos e o estilo em ordem, estamos prontos! Reserve um momento para apreciar a beleza de sua criação.

O resultado
(Visualização grande)

Agora é hora de você implantar seu aplicativo e compartilhá-lo com seus colegas. Se você não tem muita ideia sobre implantação e espera que nós o ajudemos, veja aqui nossa opinião sobre as ideias de implantação. O artigo vinculado também contém sugestões sobre como remover a marca d'água FusionCharts na parte inferior esquerda de cada gráfico.

Se você errar em algum lugar e quiser um ponto de referência, o código-fonte está disponível no Github.