Usando Vue.js para criar um painel meteorológico interativo com APIs
Publicados: 2022-03-10(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:
- Como planejar a implementação de um bom painel
- Como desenvolver aplicativos com Vue.js
- Como criar aplicativos orientados a dados
- Como visualizar dados usando o FusionCharts
Em particular, cada uma das seções aproxima você dos objetivos de aprendizado:
- Uma introdução ao painel meteorológico
Este capítulo fornece uma visão geral dos diferentes aspectos da empresa. - Criar o projeto
Nesta seção, você aprenderá a criar um projeto do zero usando a ferramenta de linha de comando Vue. - 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. - 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. - 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:
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:
- 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
- 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
- Inicialize o andaime do projeto com
vue-cli
Supondo que o passo anterior tenha corrido bem, o próximo passo é usar ovue-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
N
para a última. Lembre-se de que, embora owebpack-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 ferramentavue-cli
para owebpack-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!
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 primopackage-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 arquivopackage.json
. - pacote.json
Este arquivo é criado pela ferramenta vue-cli com base nos requisitos do templatewebpack-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 sobrepackage.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 notemplate — 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).
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
evue-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.
- Dados
- 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.
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.svg
—vlocation.svg
—search.svg
—winddirection.svg
—windspeed.svg
-
src/components/
—Content.vue
—Highlights.vue
—TempVarChart.vue
—UVIndex.vue
—Visibility.vue
—WindStatus.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çãodata()
, que retorna um objeto array chamadochildComponents
, 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 umfor
-loop, não é? Fazemos isso usando a diretivav-for
fornecida pelo Vue.js. A diretivav-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).
- Atua como um atributo da tag
- Dentro da lista não ordenada, cada item da lista deve ser os nomes dos componentes filhos pretendidos, conforme definido no objeto array
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 chamadoweather_data
e o retornamos da funçãodata()
deApp.vue
. O objetoweather_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 componenteContent.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>
- Atribua o objeto
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>
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 casoContent.vue
, deve receber o objetoweather_data
enviado a ele pelo componente paiApp.vue
. O Vue.js fornece um mecanismo para isso — tudo que você precisa é de um objeto array chamadoprops
, definido no objeto padrão exportado peloContent.vue
. Cada elemento do arrayprops
é 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 arrayprops
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>
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:
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:
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.
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óprioApp.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 doContent.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.
- As
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 comoContent.vue
, cujo único trabalho é passar os dados para os filhos mantendo a hierarquia estrutural. Lembre-se de que componentes burros comoHighlights.vue
eContent.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>
, odiv id
foi alterado parauvIndex
, que é mais legível. - No
export default {}
, a funçãodata()
agora retorna um objeto stringuvIndex
, cujo valor é extraído do objeto highlight recebido pelo componente usandoprops
. EsteuvIndex
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 comowindStatus
, para promover a legibilidade e também para estar em sincronia com o objeto de destaques que oApp.vue
fornece com os dados reais. - Mudanças de nomenclatura semelhantes foram feitas para a velocidade e direção — as novas são
windSpeed
ewindDirection
. - Um novo objeto
derivedWindDirection
entrou em jogo (também fornecido peloApp.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
etempVar
— 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 diretivav-bind:
:, ou sua abreviação:
(como fizemos aqui), com vários atributos de um elemento HTML personalizado! - A função
data()
agora retorna o objetofilename
(que existia anteriormente), junto com dois novos objetos (em vez do antigoweather_data
):tempVar
ehighlights
. 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:
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étododata()
no objetoexport default()
deApp.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 arquivoApp.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 erroCORS
(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 !
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ódulovue-fusioncharts
Node que instalamos para este projeto. Internamente,vue-fusioncharts
conta com o módulofusioncharts
, 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 arquivosrc/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 diretivav-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 objetowatch { }
. 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 dohandler
para esses dados foi definida. Por exemplo, queremos que o Vue fique de olho noprop
recebido, ou seja,data_prop_received_by_the_component
no pseudocódigo. Aprop
torna-se uma chave no objetowatch { }
, e o valor da chave é outro objeto — um método manipulador que descreve o que precisa ser feito sempre que aprop
muda. Com mecanismos tão elegantes para lidar com as mudanças, o app mantém sua reatividade. Odeep: 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çãodata()
usando a diretivav-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çãodata()
, os objetos de dados e seus valores são o que faz os gráficos funcionarem, por causa da vinculação feita pelas diretivasv-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
ou1
são de natureza booleana, onde0
representa algo não disponível/desligado e1
representa disponibilidade/estado ligado. No entanto, tenha cuidado, pois objetos de dados não booleanos também podem ter0
ou1
como seus valores, além de outros valores possíveis — depende do contexto. Por exemplo,containerbackgroundopacity
com seu valor padrão como0
é booleano, enquantolowerLimit
com seu valor padrão como0
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.
- Os objetos de dados cujos valores são
- 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 oshighlights
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 oApp.vue
um novo valor para qualquer objeto dentro dehighlights
, 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
<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.
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:
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:
<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.
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.