Uso de Vue.js para crear un panel meteorológico interactivo con API

Publicado: 2022-03-10
Resumen rápido ↬ Crear un tablero con datos de API suele ser un asunto complejo. Elegir su pila tecnológica, integrar API, seleccionar los gráficos correctos y embellecer con estilos CSS puede ser complicado. Este tutorial es una guía paso a paso sobre cómo ayudarlo a crear un panel meteorológico en Vue.js usando datos API.

(Este es un artículo patrocinado). En este tutorial, creará un panel meteorológico simple desde cero. Será una aplicación para el cliente final que no es un ejemplo de "Hello World", ni demasiado intimidante en su tamaño y complejidad.

Todo el proyecto se desarrollará utilizando herramientas del ecosistema Node.js + npm. En particular, dependeremos en gran medida de la API Dark Sky para los datos, Vue.js para todo el trabajo pesado y FusionCharts para la visualización de datos.

requisitos previos

Esperamos que esté familiarizado con lo siguiente:

  • HTML5 y CSS3 (también usaremos las funciones básicas proporcionadas por Bootstrap;
  • JavaScript (especialmente la forma ES6 de usar el lenguaje);
  • Node.js y npm (los conceptos básicos del entorno y la gestión de paquetes están bien).

Además de los mencionados anteriormente, sería genial si está familiarizado con Vue.js o cualquier otro marco JavaScript similar. No esperamos que conozca FusionCharts : ¡es tan fácil de usar que lo aprenderá sobre la marcha!

Aprendizajes Esperados

Sus aprendizajes clave de este proyecto serán:

  1. Cómo planificar la implementación de un buen tablero
  2. Cómo desarrollar aplicaciones con Vue.js
  3. Cómo crear aplicaciones basadas en datos
  4. Cómo visualizar datos usando FusionCharts

En particular, cada una de las secciones lo lleva un paso más cerca de los objetivos de aprendizaje:

  1. Una introducción al panel meteorológico
    Este capítulo le ofrece una descripción general de los diferentes aspectos de la empresa.
  2. Crear el proyecto
    En esta sección, aprenderá a crear un proyecto desde cero utilizando la herramienta de línea de comandos de Vue.
  3. Personalizar la estructura del proyecto por defecto
    El andamiaje de proyecto predeterminado que obtiene en la sección anterior no es suficiente; aquí aprende las cosas adicionales necesarias para el proyecto desde un punto de vista estructural.
  4. Adquisición y procesamiento de datos
    Esta sección es la carne del proyecto; todo el código crítico para adquirir y procesar datos de la API se muestra aquí. Espere pasar el máximo tiempo en esta sección.
  5. Visualización de datos con FusionCharts
    Una vez que tenemos todos los datos y otras partes móviles del proyecto estabilizadas, esta sección está dedicada a visualizar los datos usando FusionCharts y un poco de CSS.

1. El flujo de trabajo del tablero

Antes de sumergirnos en la implementación, es importante tener claro nuestro plan. Dividimos nuestro plan en cuatro aspectos distintos:

Requisitos

¿Cuáles son nuestros requisitos para este proyecto? En otras palabras, ¿cuáles son las cosas que queremos mostrar a través de nuestro Weather Dashboard? Teniendo en cuenta que nuestro público objetivo son probablemente meros mortales con gustos sencillos, nos gustaría mostrarles lo siguiente:

  • Detalles de la ubicación para la que quieren ver el clima, junto con información primaria sobre el clima. Dado que no hay requisitos estrictos, descubriremos los detalles aburridos más adelante. Sin embargo, en esta etapa, es importante tener en cuenta que tendremos que proporcionar a la audiencia un cuadro de búsqueda, para que puedan proporcionar información sobre la ubicación de su interés.
  • Información gráfica sobre el clima de su ubicación de interés, como:
    • Variación de temperatura para el día de la consulta
    • Aspectos destacados del clima de hoy:
      • Velocidad y dirección del viento
      • Visibilidad
      • Índice UV

Nota : Los datos obtenidos de la API brindan información sobre muchos otros aspectos del clima. Elegimos no usarlos todos para mantener el código al mínimo.

Estructura

Según los requisitos, podemos estructurar nuestro tablero como se muestra a continuación:

Estructura del tablero
(Vista previa grande)

Datos

Nuestro tablero es tan bueno como los datos que obtenemos, porque no habrá visualizaciones bonitas sin los datos adecuados. Hay muchas API públicas que proporcionan datos meteorológicos, algunas de ellas son gratuitas y otras no. Para nuestro proyecto, recopilaremos datos de la API Dark Sky. Sin embargo, no podremos sondear el extremo de la API directamente desde el extremo del cliente. ¡No se preocupe, tenemos una solución alternativa que se revelará en el momento adecuado! Una vez que obtengamos los datos de la ubicación buscada, procesaremos y formatearemos algunos datos, ya sabes, el tipo de tecnicismos que nos ayudan a pagar las facturas.

Visualización

Una vez que obtengamos datos limpios y formateados, los conectaremos a FusionCharts. Hay muy pocas bibliotecas de JavaScript en el mundo tan capaces como FusionCharts. De la gran cantidad de ofertas de FusionCharts, usaremos solo algunas, todas escritas en JavaScript, pero funcionan a la perfección cuando se integran con el envoltorio Vue para FusionCharts.

Armados con el panorama general, ensuciémonos las manos: ¡es hora de hacer las cosas concretas! En la siguiente sección, creará el proyecto básico de Vue, sobre el cual construiremos más.

2. Creación del proyecto

Para crear el proyecto, ejecute los siguientes pasos:

  1. Instalar Node.js + npm
    ( Si tiene Node.js instalado en su computadora, omita este paso).
    Node.js viene con npm incluido, por lo que no necesita instalar npm por separado. Según el sistema operativo, descargue e instale Node.js de acuerdo con las instrucciones proporcionadas aquí.

    Una vez instalado, probablemente sea una buena idea verificar si el software funciona correctamente y cuáles son sus versiones. Para probar eso, abra la línea de comandos/terminal y ejecute los siguientes comandos:
     node --version npm --version
  2. Instalar paquetes con npm
    Una vez que tenga npm en funcionamiento, ejecute el siguiente comando para instalar los paquetes básicos necesarios para nuestro proyecto.
     npm install -g vue@2 vue-cli@2
  3. Inicialice el andamiaje del proyecto con vue-cli
    Suponiendo que el paso anterior haya ido bien, el siguiente paso es usar vue-cli , una herramienta de línea de comandos de Vue.js, para inicializar el proyecto. Para hacer eso, ejecute lo siguiente:
    • Inicialice el scaffolding con la plantilla webpack-simple.
       vue init webpack-simple vue_weather_dashboard
      Se le harán un montón de preguntas: aceptar los valores predeterminados para todos menos la última pregunta será lo suficientemente bueno para este proyecto; responda N para la última.
      Una captura de pantalla de la línea de comandos/terminal
      (Vista previa grande)
      Tenga en cuenta que, aunque webpack-simple es excelente para la creación rápida de prototipos y aplicaciones livianas como la nuestra, no es particularmente adecuado para aplicaciones serias o implementación de producción. Si desea utilizar cualquier otra plantilla (aunque le desaconsejamos si es un novato), o le gustaría nombrar su proyecto de otra manera, la sintaxis es:
       vue init [template-name] [project-name]
    • Navegue al directorio creado por vue-cli para el proyecto.
       cd vue_weather_dashboard
    • Instale todos los paquetes mencionados en package.json , que ha sido creado por la herramienta vue-cli para la plantilla webpack-simple .
       npm install
    • ¡Inicie el servidor de desarrollo y vea su proyecto Vue predeterminado funcionando en el navegador!
       npm run dev

Si es nuevo en Vue.js, tómese un momento para saborear su último logro: ¡ha creado una pequeña aplicación Vue y se está ejecutando en localhost: 8080!

Una captura de pantalla del sitio web de Vue.js
(Vista previa grande)

Breve explicación de la estructura del proyecto por defecto

Es hora de echar un vistazo a la estructura dentro del directorio vue_weather_dashboard , para que comprenda los conceptos básicos antes de que comencemos a modificarlo.

La estructura se parece a esto:

 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

Aunque puede ser tentador omitir familiarizarse con los archivos y directorios predeterminados, si es nuevo en Vue, le recomendamos que al menos eche un vistazo al contenido de los archivos. Puede ser una buena sesión educativa y desencadenar preguntas que debe realizar por su cuenta, especialmente los siguientes archivos:

  • package.json , y solo un vistazo a su primo package-lock.json
  • webpack.config.js
  • index.html
  • src/main.js
  • src/App.vue

A continuación se proporciona una breve explicación de cada uno de los archivos y directorios que se muestran en el diagrama de árbol:

  • LÉAME.md
    No hay premio por adivinar: es principalmente para que los humanos lean y comprendan los pasos necesarios para crear el andamiaje del proyecto.
  • node_modules/
    Este es el directorio donde npm descarga los paquetes necesarios para iniciar el proyecto. La información sobre los paquetes necesarios está disponible en el archivo package.json .
  • paquete.json
    Este archivo lo crea la herramienta vue-cli en función de los requisitos de la plantilla webpack-simple y contiene información sobre los paquetes npm (incluidas sus versiones y otros detalles) que deben instalarse. Examine detenidamente el contenido de este archivo: aquí es donde debe visitar y quizás editar para agregar o eliminar los paquetes necesarios para el proyecto y luego ejecutar npm install. Lea más sobre package.json aquí.
  • paquete-bloqueo.json
    Este archivo lo crea el propio npm y está destinado principalmente a mantener un registro de las cosas que npm descargó e instaló.
  • webpack.config.js
    Este es un archivo JavaScript que contiene la configuración de webpack, una herramienta que agrupa diferentes aspectos de nuestro proyecto (código, activos estáticos, configuración, entornos, modo de uso, etc.) y lo minimiza antes de entregarlo al usuario. El beneficio es que todas las cosas se unen automáticamente y la experiencia del usuario mejora enormemente debido a la mejora en el rendimiento de la aplicación (las páginas se sirven rápidamente y se cargan más rápido en el navegador). Como puede encontrar más adelante, este es el archivo que debe inspeccionarse cuando algo en el sistema de compilación no funciona de la manera prevista. Además, cuando desee implementar la aplicación, este es uno de los archivos clave que debe editarse (lea más aquí).
  • índice.html
    Este archivo HTML sirve como matriz (o puede decir, plantilla) donde los datos y el código se incrustarán dinámicamente (eso es lo que hace principalmente Vue) y luego se entregarán al usuario.
  • src/principal.js
    Este archivo JavaScript contiene código que administra principalmente las dependencias de nivel superior/de proyecto y define el componente Vue de nivel superior. En resumen, orquesta el JavaScript para todo el proyecto y sirve como punto de entrada de la aplicación. Edite este archivo cuando necesite declarar dependencias de todo el proyecto en ciertos módulos de nodo, o si desea que se cambie algo sobre el componente superior de Vue en el proyecto.
  • src/App.vue
    En el punto anterior, cuando hablábamos del "componente superior de Vue", básicamente hablábamos de este archivo. Cada archivo .vue del proyecto es un componente y los componentes están relacionados jerárquicamente. Al principio, solo tenemos un archivo .vue , es decir, App.vue , como nuestro único componente. Pero en breve agregaremos más componentes a nuestro proyecto (principalmente siguiendo la estructura del tablero) y los vincularemos de acuerdo con nuestra jerarquía deseada, siendo App.vue el ancestro de todos. Estos archivos .vue contendrán código en un formato que Vue quiere que escribamos. No te preocupes, son código JavaScript escrito manteniendo una estructura que puede mantenernos cuerdos y organizados. Se le advirtió: al final de este proyecto, si es nuevo en Vue, puede volverse adicto a la template — script — style template — script — style template — script — style forma de organizar el código!

Ahora que hemos creado la base, es hora de:

  • Modifique las plantillas y modifique un poco los archivos de configuración, para que el proyecto se comporte de la manera que queremos.
  • Cree nuevos archivos .vue e implemente la estructura del tablero con código Vue.

Los aprenderemos en la siguiente sección, que va a ser un poco larga y requiere algo de atención. Si necesita cafeína o agua, o quiere descargar, ¡ahora es el momento!

3. Personalización de la estructura del proyecto por defecto

Es hora de jugar con los cimientos que nos ha dado el proyecto andamiado. Antes de comenzar, asegúrese de que el servidor de desarrollo proporcionado por webpack se esté ejecutando. La ventaja de ejecutar este servidor continuamente es que cualquier cambio que realice en el código fuente, uno que guarde y actualice la página web, se refleja inmediatamente en el navegador.

Si desea iniciar el servidor de desarrollo, simplemente ejecute el siguiente comando desde la terminal (asumiendo que su directorio actual es el directorio del proyecto):

 npm run dev

En las siguientes secciones, modificaremos algunos de los archivos existentes y agregaremos algunos archivos nuevos. Le seguirán breves explicaciones del contenido de esos archivos, para que tenga una idea de lo que significan esos cambios.

Modificar archivos existentes

índice.html

Nuestra aplicación es literalmente una aplicación de una sola página, porque solo hay una página web que se muestra en el navegador. Hablaremos de esto más adelante, pero primero hagamos nuestro primer cambio: alterar el texto dentro de la etiqueta <title> .

Con esta pequeña revisión, el archivo HTML se parece a lo siguiente:

 <!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>

Tómese un momento para actualizar la página web en localhost:8080 y vea el cambio reflejado en la barra de título de la pestaña en el navegador; debería decir "Panel meteorológico Vue". Sin embargo, esto fue solo para demostrarle el proceso de realizar cambios y verificar si funciona. ¡Tenemos más cosas que hacer!

Esta simple página HTML carece de muchas cosas que queremos en nuestro proyecto, especialmente las siguientes:

  • Algo de metainformación
  • Enlaces CDN a Bootstrap (marco CSS)
  • enlace a la hoja de estilo personalizada (aún por agregar en el proyecto)
  • Punteros a la API de geolocalización de Google Maps desde la etiqueta <script>

Después de agregar esas cosas, el index.html final tiene el siguiente contenido:

 <!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>

Guarde el archivo y actualice la página web. Es posible que haya notado un pequeño bache mientras se cargaba la página; se debe principalmente al hecho de que Bootstrap ahora controla el estilo de la página, y los elementos de estilo como fuentes, espaciado, etc. son diferentes del valor predeterminado que teníamos. antes (si no está seguro, vuelva al valor predeterminado y vea la diferencia).

Una captura de pantalla cuando actualiza la página web con localhost: 8080
(Vista previa grande)

Nota : una cosa importante antes de continuar: la URL de la API de Google Maps contiene una clave que es propiedad de FusionCharts. Por ahora, puede usar esta clave para construir el proyecto, ya que no queremos que se atasque con este tipo de detalles minuciosos (que pueden ser distracciones mientras es nuevo). Sin embargo, le recomendamos encarecidamente que genere y utilice su propia clave API de Google Maps una vez que haya realizado algún progreso y se sienta cómodo para prestar atención a estos pequeños detalles.

paquete.json

Al momento de escribir esto, usamos ciertas versiones de los paquetes npm para nuestro proyecto, y sabemos con certeza que esas cosas funcionan juntas. Sin embargo, en el momento en que esté ejecutando el proyecto, es muy posible que las últimas versiones estables de los paquetes que npm descarga para usted no sean las mismas que usamos, y esto podría romper el código (o hacer cosas que están más allá nuestro mando). Por lo tanto, es muy importante tener exactamente el mismo archivo package.json que se usó para compilar este proyecto, de modo que nuestro código/explicaciones y los resultados que obtenga sean coherentes.

El contenido del archivo package.json debe 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" } }

Lo alentamos a que revise el nuevo package.json y descubra cuáles son las funciones de los diferentes objetos en el json. Es posible que prefiera cambiar el valor de la clave " author " a su nombre. Además, los paquetes mencionados en las dependencias se revelarán en el momento adecuado en el código. Por el momento, es suficiente saber que:

  • Los paquetes relacionados con babel son para manejar correctamente el código de estilo ES6 por parte del navegador;
  • axios se ocupa de las solicitudes HTTP basadas en promesas;
  • moment y moment-timezone son para manipulación de fecha/hora;
  • fusioncharts y vue-fusioncharts son responsables de representar gráficos:
  • vue , por razones obvias.

webpack.config.js

Al igual que con package.json , le sugerimos que mantenga un archivo webpack.config.js que sea consistente con el que usamos para construir el proyecto. Sin embargo, antes de realizar cualquier cambio, le recomendamos que compare cuidadosamente el código predeterminado en webpack.config.js y el código que proporcionamos a continuación. Notará bastantes diferencias: búsquelas en Google y tenga una idea básica de lo que significan. Dado que explicar las configuraciones de paquetes web en profundidad está fuera del alcance de este artículo, usted está solo en este sentido.

El archivo webpack.config.js personalizado es el siguiente:

 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 }) ]) }

Con los cambios realizados en el webpack.config.js del proyecto, es imperativo que detenga el servidor de desarrollo que se está ejecutando ( Ctrl + C ) y lo reinicie con el siguiente comando ejecutado desde el directorio del proyecto después de instalar todos los paquetes mencionados en el package.json Archivo package.json :

 npm install npm run dev

Con esto, termina la terrible experiencia de ajustar las configuraciones y garantizar que los paquetes correctos estén en su lugar. Sin embargo, esto también marca el viaje de modificar y escribir código, que es un poco largo pero también muy gratificante.

src/principal.js

Este archivo es la clave para la orquestación de nivel superior del proyecto; es aquí donde definimos:

  • Cuáles son las dependencias de nivel superior (dónde obtener los paquetes npm más importantes necesarios);
  • Cómo resolver las dependencias, junto con instrucciones para Vue sobre el uso de complementos/envolturas, si corresponde;
  • Una instancia de Vue que administra el componente superior del proyecto: src/App.vue (el archivo nodal .vue ).

De acuerdo con nuestros objetivos para el archivo src/main.js , el código debería 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 es uno de los archivos más importantes de todo el proyecto y representa el componente superior en la jerarquía: la aplicación en sí misma, como un todo. Para nuestro proyecto, este componente hará todo el trabajo pesado, que exploraremos más adelante. Por ahora, queremos deshacernos del modelo predeterminado y poner algo propio.

Si es nuevo en la forma en que Vue organiza el código, sería mejor tener una idea de la estructura general dentro de los archivos .vue . Los archivos .vue componen de tres secciones:

  • Plantilla
    Aquí es donde se define la plantilla HTML para la página. Además del HTML estático, esta sección también contiene la forma en que Vue incrusta contenido dinámico, usando las llaves dobles {{ }} .
  • Texto
    JavaScript gobierna esta sección y es responsable de generar contenido dinámico que va y se ubica dentro de la plantilla HTML en los lugares apropiados. Esta sección es principalmente un objeto que se exporta y consta de:
    • Datos
      Esta es una función en sí misma y, por lo general, devuelve algunos datos deseados encapsulados dentro de una estructura de datos agradable.
    • Métodos
      Un objeto que consta de una o más funciones/métodos, cada uno de los cuales normalmente manipula datos de una forma u otra, y también controla el contenido dinámico de la plantilla HTML.
    • calculado
      Al igual que el objeto de método discutido anteriormente con una distinción importante: mientras que todas las funciones dentro del objeto de método se ejecutan cada vez que se llama a cualquiera de ellas, las funciones dentro del objeto calculado se comportan de manera mucho más sensata y se ejecutan si y solo si ha sido llamado.
  • Estilo
    Esta sección es para el estilo CSS que se aplica al HTML de la página (escrito dentro de la plantilla): ¡ponga aquí el buen CSS antiguo para hacer que sus páginas sean hermosas!

Teniendo en cuenta el paradigma anterior, personalicemos mínimamente el código en 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>

Recuerde que el fragmento de código anterior es simplemente para probar que App.vue funciona con nuestro propio código. Más tarde pasará por muchos cambios, pero primero guarde el archivo y actualice la página en el navegador.

Una captura de pantalla del navegador con el mensaje "El código de este componente está en App.vue"
(Vista previa grande)

En este punto, probablemente sea una buena idea obtener ayuda con las herramientas. Consulte las herramientas de desarrollo de Vue para Chrome, y si no tiene muchos problemas para usar Google Chrome como su navegador predeterminado para el desarrollo, instale la herramienta y juegue un poco con ella. Será extremadamente útil para un mayor desarrollo y depuración, cuando las cosas se vuelvan más complicadas.

Directorios y archivos adicionales

El siguiente paso sería agregar archivos adicionales, para que la estructura de nuestro proyecto quede completa. Agregaríamos los siguientes directorios y archivos:

  • src/css/style.css
  • src/assets/calendar.svgvlocation.svgwinddirection.svg search.svg velocidad del windspeed.svg
  • src/components/Content.vueHighlights.vueTempVarChart.vueUVIndex.vueVisibility.vueWindStatus.vue

Nota : Guarde los archivos .svg con hipervínculos en su proyecto.

Cree los directorios y archivos mencionados anteriormente. La estructura final del proyecto debería verse como (recuerde eliminar carpetas y archivos de la estructura predeterminada que ahora son innecesarios):

 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

Puede haber algunos otros archivos, como .babelrc , .gitignore , .editorconfig , etc. en la carpeta raíz del proyecto. Puede ignorarlos de forma segura por ahora.

En la siguiente sección, agregaremos contenido mínimo a los archivos recién agregados y probaremos si funcionan correctamente.

src/css/estilo.css

Aunque no será de mucha utilidad de inmediato, copie el siguiente código en el archivo:

 @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/activos/

En este directorio, descargue y guarde los archivos .svg que se mencionan a continuación:

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

src/components/Content.vue

Esto es lo que llamamos un "componente tonto" (es decir, un marcador de posición) que está ahí solo para mantener la jerarquía y, esencialmente, transmite datos a sus componentes secundarios.

Recuerde que no existe una barra técnica para escribir todo nuestro código en el archivo App.vue , pero adoptamos el enfoque de dividir el código anidando los componentes por dos razones:

  • Para escribir código limpio, que ayuda a la legibilidad y mantenimiento;
  • Para replicar la misma estructura que veremos en pantalla, es decir, la jerarquía.

Antes de anidar el componente definido en Content.vue dentro del componente raíz App.vue , escribamos un código de juguete (pero educativo) 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>

En el código, observe cuidadosamente y comprenda lo siguiente:

  • Dentro de la etiqueta <script> (donde obviamente escribimos código JavaScript), definimos un objeto que se exporta (que está disponible para otros archivos) de forma predeterminada. Este objeto contiene una función data() , que devuelve un objeto de matriz llamado childComponents , cuyos elementos son nombres de los archivos de componentes que deben anidarse aún más.
  • Dentro de la etiqueta <template> (donde escribimos alguna plantilla HTML), lo que interesa es <ul> .
    • Dentro de la lista desordenada, cada elemento de la lista debe ser el nombre de los componentes secundarios deseados, tal como se define en el objeto de matriz childComponents . Además, la lista debería extenderse automáticamente hasta el último elemento de la matriz. Parece que deberíamos escribir un bucle for , ¿no? Lo hacemos usando la directiva v-for proporcionada por Vue.js. La directiva v-for :
      • Actúa como un atributo de la etiqueta <li> , itera a través de la matriz, representa los nombres de los componentes secundarios donde se menciona el iterador dentro de los corchetes {{ }} (donde escribimos el texto para los elementos de la lista).

El código y la explicación anterior forman la base de su comprensión posterior de cómo se interrelacionan el script y la plantilla, y cómo podemos usar las directivas proporcionadas por Vue.js.

Hemos aprendido mucho, pero incluso después de todo esto, nos queda una cosa por aprender sobre cómo conectar componentes en jerarquía sin problemas: pasar datos del componente principal a sus hijos. Por ahora, necesitamos aprender a pasar algunos datos de src/App.vue a src/components/Content.vue , de modo que podamos usar las mismas técnicas para el resto del anidamiento de componentes en este proyecto.

Los datos que se filtran desde los componentes principales a los secundarios pueden parecer simples, ¡pero el problema está en los detalles! Como se explica brevemente a continuación, hay varios pasos necesarios para que funcione:

  • Definición y los datos
    Por ahora, queremos jugar con algunos datos estáticos: ¡un objeto que contenga valores codificados sobre diferentes aspectos del clima estará bien! Creamos un objeto llamado weather_data y lo devolvemos desde la función data() de App.vue . El objeto weather_data se proporciona en el siguiente fragmento:
 weather_data: { location: "California", temperature: { current: "35 C", }, highlights: { uvindex: "3", windstatus: { speed: "20 km/h", direction: "NE", }, visibility: "12 km", }, },
  • Pasar los datos del padre
    ¡Para pasar los datos, necesitamos un destino donde queremos enviar los datos! En este caso, el destino es el componente Content.vue , y la forma de implementarlo es:
    • Asigne el objeto weather_data a un atributo personalizado de la etiqueta <Content>
    • Vincule el atributo con los datos mediante la directiva v-bind : proporcionada por Vue.js, que hace que el valor del atributo sea dinámico (en respuesta a los cambios realizados en los datos originales).
       <Content v-bind:weather_data=“weather_data”></Content>

La definición y el paso de los datos se manejan en el lado de la fuente del protocolo de enlace, que en nuestro caso es el archivo App.vue .

El código para el archivo App.vue , en su estado actual, se proporciona a continuación:

 <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> 
Una captura de pantalla del navegador con el mensaje “El código de este componente está en App.vue. Estos componentes secundarios de Content.vue son: TempVarChart.vue, Highlights.vue”
(Vista previa grande)

Con los datos definidos y pasados ​​desde la fuente (componente principal), ahora es responsabilidad del niño recibir los datos y representarlos de manera adecuada, como se explica en los siguientes dos pasos.

  • Recibir los datos por parte del niño
    El componente secundario, en este caso Content.vue , debe recibir el objeto weather_data que le envía el componente principal App.vue . Vue.js proporciona un mecanismo para hacerlo; todo lo que necesita es un objeto de matriz llamado props , definido en el objeto predeterminado exportado por Content.vue . Cada elemento de los props de la matriz es un nombre de los objetos de datos que desea recibir de su padre. Por ahora, el único objeto de datos que se supone que debe recibir es weather_data de App.vue. Por lo tanto, la matriz de props se ve así:
 <template> // HTML template code here </template> <script> export default { props: ["weather_data"], data () { return { // data here } }, } </script> <style> // component specific CSS here </style>
  • Representación de los datos en la página.
    Ahora que nos hemos asegurado de recibir los datos, la última tarea que debemos completar es renderizar los datos. Para este ejemplo, volcaremos directamente los datos recibidos en la página web, solo para ilustrar la técnica. Sin embargo, en aplicaciones reales (como la que estamos a punto de construir), los datos normalmente pasan por mucho procesamiento, y solo las partes relevantes se muestran de la manera que se adapta al propósito. Por ejemplo, en este proyecto eventualmente obtendremos datos sin procesar de la API meteorológica, los limpiaremos y formatearemos, alimentaremos los datos a las estructuras de datos necesarias para los gráficos y luego los visualizaremos. De todos modos, para mostrar el volcado de datos sin procesar, solo usaremos los corchetes {{ }} que entiende Vue, como se muestra en el fragmento a continuación:
 <template> <div> // other template code here {{ weather_data }} </div> </template>

Ahora es el momento de asimilar todas las partes y piezas. El código de Content.vue , en su estado actual, se proporciona a continuación:

 <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> 
Una captura de pantalla del navegador con el resultado del código proporcionado
(Vista previa grande)

Después de realizar los cambios mencionados anteriormente, actualice la página web en el navegador y vea cómo se ve. Tómese un momento para apreciar la complejidad que maneja Vue: si modifica el objeto weather_data en App.vue , se transmite silenciosamente a Content.vue y, finalmente, al navegador que muestra la página web. Intente cambiar el valor de la ubicación clave.

Aunque hemos aprendido acerca de los accesorios y el enlace de datos usando datos estáticos, usaremos datos dinámicos recopilados usando API web en la aplicación y cambiaremos el código en consecuencia .

Resumen

Antes de pasar al resto de los archivos .vue , resumamos lo que aprendimos mientras escribíamos el código para App.vue y components/Content.vue :

  • El archivo App.vue es lo que llamamos el componente raíz, el que se encuentra en la parte superior de la jerarquía de componentes. El resto de los archivos .vue representan componentes que son su hijo directo, nieto, etc.
  • El archivo Content.vue es un componente ficticio: su responsabilidad es pasar los datos a los niveles inferiores y mantener la jerarquía estructural, de modo que nuestro código siga siendo coherente con la filosofía "*lo que vemos es lo que implementamos*".
  • La relación padre-hijo del componente no surge de la nada: debe registrar un componente (ya sea global o localmente, según el uso previsto del componente) y luego anidarlo usando etiquetas HTML personalizadas (cuya ortografía es la misma). mismo que el de los nombres con los que se han registrado los componentes).
  • Una vez registrados y anidados, los datos pasan de los componentes principales a los secundarios y el flujo nunca se invierte (sucederán cosas malas si la arquitectura del proyecto permite el reflujo). El componente principal es la fuente relativa de los datos y transmite datos relevantes a sus elementos secundarios utilizando la directiva v-bind para los atributos de los elementos HTML personalizados. El niño recibe los datos destinados a él utilizando accesorios y luego decide por sí mismo qué hacer con los datos.

Para el resto de los componentes, no nos entregaremos a una explicación detallada, solo escribiremos el código en función de los aprendizajes del resumen anterior. El código será evidente, y si se confunde acerca de la jerarquía, consulte el diagrama a continuación:

Un diagrama que explica la jerarquía del código.
(Vista previa grande)

El diagrama dice que TempVarChart.vue y Highlights.vue son hijos directos de Content.vue . Por lo tanto, podría ser una buena idea preparar Content.vue para enviar datos a esos componentes, lo que hacemos usando el siguiente código:

 <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>

Una vez que guarde este código, obtendrá errores; no se preocupe, se espera. Se solucionará una vez que tenga listos el resto de los archivos de componentes. Si le molesta no poder ver el resultado, comente las líneas que contienen las etiquetas de elementos personalizados <temp-var-chart> y <today-highlights> .

Para esta sección, este es el código final de Content.vue . Para el resto de esta sección, haremos referencia a este código , y no a los anteriores que escribimos para aprender.

src/components/TempVarChart.vue

Con su componente principal Content.vue transmitiendo los datos, TempVarChart.vue debe configurarse para recibir y representar los datos, como se muestra en el siguiente código:

 <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 también recibirá datos de App.vue , su componente principal. Después de eso, debe vincularse con sus componentes secundarios y se les deben transmitir los datos relevantes.

Primero veamos el código para recibir datos del padre:

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

En este punto, la página web se parece a la siguiente imagen:

Resultado del código mostrado en el navegador
(Vista previa grande)

Ahora necesitamos modificar el código de Highlights.vue para registrar y anidar sus componentes secundarios, y luego pasar los datos a los elementos secundarios. El código para ello es el siguiente:

 <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>

Una vez que guarde el código y vea la página web, se espera que vea errores en la herramienta Developer Console proporcionada por el navegador; aparecen porque aunque Highlights.vue envía datos, nadie los recibe. Todavía tenemos que escribir el código para los elementos secundarios de Highlights.vue .

Observe que no hemos realizado gran parte del procesamiento de datos, es decir, no hemos extraído los factores individuales de los datos meteorológicos que se encuentran en la sección Destacados del tablero. Podríamos haber hecho eso en la función data() , pero preferimos mantener Highlights.vue como un componente tonto que simplemente pasa el volcado de datos completo que recibe a cada uno de los niños, quienes luego poseen sus propios extractos lo que es necesario para ellos. . Sin embargo, lo alentamos a que intente extraer datos en Highlights.vue y envíe datos relevantes a cada componente secundario; ¡no obstante, es un ejercicio de buena práctica!

src/components/UVIndex.vue

El código de este componente recibe el volcado de datos de los aspectos más destacados de Highlights.vue , extrae los datos para el índice UV y los representa en la 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/Visibilidad.vue

El código de este componente recibe el volcado de datos de los aspectos más destacados de Highlights.vue , extrae los datos de visibilidad y los muestra en la 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

El código de este componente recibe el volcado de datos de los momentos destacados de Highlights.vue , extrae los datos del estado del viento (velocidad y dirección) y los muestra en la 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>

Después de agregar el código para todos los componentes, eche un vistazo a la página web en el navegador.

Resultado del código mostrado en el navegador
(Vista previa grande)

No es por desanimarnos, pero todo este esfuerzo fue solo para vincular los componentes en jerarquía y probar si el flujo de datos está ocurriendo entre ellos o no. En la siguiente sección, descartaremos la mayor parte del código que hemos escrito hasta ahora y agregaremos mucho más relacionado con el proyecto real. Sin embargo, ciertamente mantendremos la estructura y el anidamiento de los componentes; los aprendizajes de esta sección nos permitirán construir un panel de control decente con Vue.js.

4. Adquisición y procesamiento de datos

¿Recuerdas el objeto weather_data en App.vue ? Tenía algunos datos codificados que usamos para probar si todos los componentes funcionan correctamente y también para ayudarlo a aprender algunos aspectos básicos de la aplicación Vue sin atascarse en los detalles de los datos del mundo real. Sin embargo, ahora es el momento de que nos deshagamos de nuestro caparazón y salgamos al mundo real, donde los datos de la API dominarán la mayor parte de nuestro código.

Preparación de componentes secundarios para recibir y procesar datos reales

En esta sección, obtendrá un volcado de código para todos los componentes, excepto App.vue . El código se encargará de recibir datos reales de App.vue (a diferencia del código que escribimos en la sección anterior para recibir y generar datos ficticios).

Recomendamos encarecidamente que lea el código de cada componente detenidamente, para que se haga una idea de qué datos espera cada uno de esos componentes y, finalmente, utilizará en la visualización.

Parte del código y la estructura general serán similares a los que ha visto en la estructura anterior, por lo que no enfrentará algo drásticamente diferente. Sin embargo, ¡el diablo está en los detalles! Por lo tanto, examine el código detenidamente y, cuando lo haya entendido razonablemente bien, cópielo en los archivos de componentes respectivos de su proyecto.

Nota : Todos los componentes de esta sección están en el directorio src/components/ . Entonces, cada vez, no se mencionará la ruta; solo se mencionará el nombre del archivo .vue para identificar el componente.

Contenido.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>

Se han realizado los siguientes cambios con respecto al código anterior:

  • En <template> , el texto y los datos dentro de {{ }} se han eliminado, ya que ahora solo estamos recibiendo datos y pasándolos a los elementos secundarios, sin representación específica de este componente.
  • En el export default {} :
    • Los props se han cambiado para que coincidan con los objetos de datos que enviará el padre: App.vue . La razón para cambiar los accesorios es que App.vue mostrará algunos de los datos que adquiere de la API meteorológica y otros recursos en línea, según la consulta de búsqueda del usuario, y transmitirá el resto de los datos. En el código ficticio que escribimos anteriormente, App.vue pasaba todo el volcado de datos ficticios, sin ninguna discriminación, y los accesorios de Content.vue se configuraron en consecuencia.
    • La función data() ahora no devuelve nada, ya que no estamos manipulando datos en este componente.

TempVarChart.vue

Se supone que este componente recibe proyecciones de temperatura detalladas para el resto del día actual y, finalmente, las muestra mediante FusionCharts. Pero por el momento, los mostraremos solo como texto en la página web.

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

Destacados.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>

Los cambios realizados con respecto al código anterior son:

  • En <template> , se eliminaron el texto y los datos dentro de {{ }} , porque este es un componente tonto, al igual que Content.vue , cuyo único trabajo es pasar los datos a los niños mientras se mantiene la jerarquía estructural. Recuerde que existen componentes tontos como Highlights.vue y Content.vue para mantener la paridad entre la estructura visual del tablero y el código que escribimos.

UVIndex.vue

Los cambios realizados al código anterior son los siguientes:

  • En <template> y <style> , el div id se ha cambiado a uvIndex , que es más legible.
  • En el export default {} , la función data() ahora devuelve un objeto de cadena uvIndex , cuyo valor se extrae del objeto destacado recibido por el componente mediante props . Este uvIndex ahora se usa temporalmente para mostrar el valor como texto dentro de <template> . Más adelante, conectaremos este valor a la estructura de datos adecuada para representar un gráfico.

Visibilidad.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>

El único cambio realizado en este archivo (con respecto a su código anterior) es que la definición del objeto de visibility devuelto por la función data() ahora contiene toString() al final, ya que el valor recibido del padre será un flotante número de punto, que debe convertirse en cadena.

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>

Los cambios realizados al código anterior son los siguientes:

  • A lo largo del archivo, windstatus se ha renombrado como windStatus , para promover la legibilidad y también para estar sincronizado con el objeto destacado que App.vue proporciona con datos reales.
  • Se han realizado cambios de nombre similares para la velocidad y la dirección; los nuevos son windSpeed ​​y windDirection .
  • Ha entrado en juego un nuevo objeto derivadoWindDirection (también proporcionado por derivedWindDirection en el App.vue destacado).

Por ahora, los datos recibidos se representan como texto; más tarde, se conectará a la estructura de datos necesaria para la visualización.

Pruebas con datos ficticios

Recurrir a datos ficticios repetidamente puede ser un poco frustrante para usted, pero hay algunas buenas razones detrás de esto:

  • Hemos realizado muchos cambios en el código de cada componente y es una buena idea probar si esos cambios están rompiendo el código. En otras palabras, debemos verificar si el flujo de datos está intacto, ahora que estamos a punto de pasar a partes más complejas del proyecto.
  • Los datos reales de la API del clima en línea necesitarán mucho masaje, y puede ser abrumador para usted hacer malabarismos entre el código para la adquisición y el procesamiento de datos y el código para el flujo fluido de datos a lo largo de los componentes. La idea es mantener bajo control la cantidad de complejidad, para que tengamos una mejor comprensión de los errores que podemos enfrentar.

En esta sección, lo que hacemos es esencialmente codificar algunos datos json en App.vue , que obviamente serán reemplazados con datos en vivo en un futuro cercano. Hay mucha similitud entre la estructura json ficticia y la estructura json que usaremos para los datos reales. Por lo tanto, también le brinda una idea aproximada de qué esperar de los datos reales, una vez que los encontramos.

Sin embargo, admitimos que esto está lejos del enfoque ideal que uno podría adoptar al construir un proyecto de este tipo desde cero. En el mundo real, a menudo comenzará con la fuente de datos real, jugará un poco con ella para comprender qué se puede y se debe hacer para controlarla, y luego pensará en la estructura de datos json adecuada para capturar la información relevante. Lo protegemos intencionalmente de todo ese trabajo sucio, ya que lo lleva más lejos del objetivo: aprender a usar Vue.js y FusionCharts para crear un tablero.

Pasemos ahora al nuevo código de 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>

Los cambios realizados en el código con respecto a su versión anterior son los siguientes:

  • El nombre del componente secundario se cambió a dashboard-content y, en consecuencia, se revisó el elemento HTML personalizado en la <template> . Tenga en cuenta que ahora tenemos dos atributos ( highlights y tempVar ) en lugar de un solo atributo que usamos anteriormente con el elemento personalizado. En consecuencia, los datos asociados con esos atributos también han cambiado. Lo que es interesante aquí es que podemos usar la directiva v-bind: o su forma abreviada : (como lo hemos hecho aquí), ¡con múltiples atributos de un elemento HTML personalizado!
  • La función data() ahora devuelve el objeto de nombre de filename (que existía antes), junto con dos objetos nuevos (en lugar del antiguo weather_data ): tempVar y highlights . La estructura del json es apropiada para el código que hemos escrito en los componentes secundarios, para que puedan extraer las piezas de datos que necesitan de los volcados. Las estructuras se explican por sí mismas y puede esperar que sean bastante similares cuando tratamos con datos en vivo. Sin embargo, el cambio significativo que encontrará es la ausencia de codificación (obvio, ¿no es así): dejaremos los valores en blanco como estado predeterminado y escribiremos código para actualizarlos dinámicamente en función de los valores que recibiremos del API meteorológica.

Ha escrito mucho código en esta sección, sin ver el resultado real. Antes de continuar, eche un vistazo al navegador (reinicie el servidor con npm run dev , si es necesario) y disfrute de la gloria de su logro. La página web que debería ver en este punto se parece a la siguiente imagen:

Resultado del código mostrado en el navegador
(Vista previa grande)

Código para adquisición y procesamiento de datos

Esta sección será la esencia del proyecto, con todo el código que se escribirá en App.vue para lo siguiente:

  • Entrada de ubicación del usuario: un cuadro de entrada y un botón de llamada a la acción es suficiente;
  • Funciones de utilidad para diversas tareas; estas funciones se llamarán más adelante en varias partes del código del componente;
  • Obtener datos detallados de geolocalización de la API de Google Maps para JavaScript;
  • Obtener datos meteorológicos detallados de la API Dark Sky;
  • Formateo y procesamiento de los datos meteorológicos y de geolocalización, que se transmitirán a los componentes secundarios.

Las subsecciones que siguen ilustran cómo podemos implementar las tareas establecidas para nosotros en los puntos anteriores. Con algunas excepciones, la mayoría seguirá la secuencia.

Entrada del usuario

Es bastante obvio que la acción comienza cuando el usuario proporciona el nombre del lugar para el cual se deben mostrar los datos meteorológicos. Para que esto suceda, necesitamos implementar lo siguiente:

  • Un cuadro de entrada para ingresar la ubicación;
  • Un botón de envío que le dice a nuestra aplicación que el usuario ingresó la ubicación y que es hora de hacer el resto. También implementaremos el comportamiento cuando el procesamiento comience al presionar Enter .

El código que mostramos a continuación estará restringido a la parte de la plantilla HTML de App.vue . Solo mencionaremos el nombre del método asociado con los eventos de clic y los definiremos más adelante en el objeto de métodos del <script> en 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 el fragmento anterior en el lugar correcto es trivial: se lo dejamos a usted. Sin embargo, las partes interesantes del fragmento son:

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

Como sabe por las secciones anteriores, @ es la forma abreviada de Vue para la directiva v-on :, que está asociada con algún evento. Lo nuevo es " organizeAllDetails ": no es más que el método que se activará una vez que sucedan los eventos (presionando Enter o haciendo clic en el botón). Todavía tenemos que definir el método, y el rompecabezas estará completo al final de esta sección.

Visualización de información de texto controlada por App.vue

Una vez que la entrada del usuario desencadena la acción y se adquieren muchos datos de las API, nos encontramos con la pregunta inevitable: "¿Qué hacer con todos estos datos?". Obviamente, se requiere un poco de masaje de datos, ¡pero eso no responde completamente a nuestra pregunta! Necesitamos decidir cuál es el uso final de los datos, o más directamente, ¿cuáles son las entidades que reciben diferentes fragmentos de los datos adquiridos y procesados?

Los componentes secundarios de App.vue , según su jerarquía y propósito, son los contendientes de primera línea para la mayor parte de los datos. Sin embargo, también tendremos algunos datos que no pertenecen a ninguno de esos componentes secundarios, pero que son bastante informativos y completan el tablero. Podemos hacer un buen uso de ellos si los mostramos como información de texto directamente controlada por App.vue , mientras que el resto de los datos se transmiten al niño para que, en última instancia, se muestren como gráficos bonitos.

Con este contexto en mente, concentrémonos en el código para establecer el escenario del uso de datos de texto. Es una plantilla HTML simple en este punto, en la que los datos eventualmente vendrán y se asentarán.

 <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>

En el fragmento anterior, debe comprender lo siguiente:

  • Las cosas dentro de {{ }} : son la forma en que Vue inserta datos dinámicos en la plantilla HTML, antes de que se represente en el navegador. Los ha encontrado antes, y no hay nada nuevo o sorprendente. Solo tenga en cuenta que estos objetos de datos provienen del método data() en el objeto export default() de App.vue . Tienen valores predeterminados que estableceremos de acuerdo con nuestros requisitos y luego escribiremos ciertos métodos para completar los objetos con datos API reales.

No se preocupe por no ver los cambios en el navegador: los datos aún no están definidos y es natural que Vue no represente cosas que no conoce. Sin embargo, una vez que se configuran los datos (y por ahora, incluso puede verificar codificando los datos), App.vue controlará los datos de texto.

El método data()

El método data() es una construcción especial en los archivos .vue : contiene y devuelve objetos de datos que son cruciales para la aplicación. Recuerde la estructura genérica de la parte <script> en cualquier archivo .vue ; contiene aproximadamente lo siguiente:

 <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>

Hasta ahora, ha encontrado los nombres de algunos de los objetos de datos, pero hay muchos más. La mayoría de ellos son relevantes para los componentes secundarios, cada uno de los cuales maneja un aspecto diferente del volcado de información meteorológica. A continuación se muestra el método data() completo que necesitaremos para este proyecto: tendrá una idea clara de qué datos esperamos de las API y cómo estamos diseminando los datos, según la nomenclatura de los 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 puede ver, en la mayoría de los casos, el valor predeterminado está vacío, porque eso será suficiente en este punto. Se escribirán métodos para manipular los datos y llenarlos con valores apropiados, antes de que se representen o pasen a los componentes secundarios.

Métodos en App.vue

Para archivos .vue , los métodos generalmente se escriben como valores de claves anidadas en el objeto de methods { } . Su función principal es manipular los objetos de datos del componente. Escribiremos los métodos en App.vue teniendo en cuenta la misma filosofía. Sin embargo, según su propósito, podemos clasificar los métodos de App.vue en los siguientes:

  • Métodos de utilidad
  • Métodos orientados a acciones/eventos
  • Métodos de adquisición de datos
  • Métodos de procesamiento de datos
  • Métodos de pegado de alto nivel

Es importante que comprenda esto: le presentamos los métodos en un plato porque ya hemos descubierto cómo funcionan las API, qué datos brindan y cómo debemos usar los datos en nuestro proyecto. No es que sacamos los métodos de la nada y escribimos un código arcano para manejar los datos. Con el fin de aprender, es un buen ejercicio leer diligentemente y comprender el código de los métodos y datos. Sin embargo, cuando se enfrenta a un nuevo proyecto que tiene que construir desde cero, debe hacer todo el trabajo sucio usted mismo, y eso significa experimentar mucho con las API, su acceso programático y su estructura de datos, antes de unirlos a la perfección con los datos. estructura que demanda su proyecto. No tendrás que tomarte de la mano y habrá momentos frustrantes, pero todo eso es parte de la madurez como desarrollador.

En las siguientes subsecciones, explicaremos cada uno de los tipos de métodos y también mostraremos la implementación de los métodos que pertenecen a esa categoría. Los nombres de los métodos se explican por sí mismos acerca de su propósito, al igual que su implementación, que creemos que encontrará bastante fácil de seguir. Sin embargo, antes de eso, recuerde el esquema general de métodos de escritura en archivos .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 utilidad

Los métodos de utilidad, como sugiere el nombre, son métodos escritos principalmente con el propósito de modularizar el código repetitivo utilizado para tareas marginales. Son llamados por otros métodos cuando es necesario. A continuación se muestran los métodos de utilidad 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; },

Aunque no lo hemos implementado, puede extraer los métodos de utilidad del archivo .vue y colocarlos en un archivo JavaScript separado. Todo lo que necesita hacer es importar el archivo .js al comienzo de la parte del script en el archivo .vue , y debería estar listo para comenzar. Este enfoque funciona muy bien y mantiene el código limpio, especialmente en aplicaciones grandes donde puede usar muchos métodos que se agrupan mejor según su propósito. Puede aplicar este enfoque a todos los grupos de métodos enumerados en este artículo y ver el efecto en sí mismo. Sin embargo, le sugerimos que haga ese ejercicio una vez que haya seguido el curso presentado aquí, de modo que tenga una comprensión general de todas las partes que funcionan en completa sincronización, y también tenga una pieza de software funcional a la que pueda consultar, una vez algo. se rompe mientras experimenta.

Métodos orientados a acciones/eventos

Estos métodos generalmente se ejecutan cuando necesitamos realizar una acción correspondiente a un evento. Según el caso, el evento puede desencadenarse a partir de una interacción del usuario o mediante programación. En el archivo App.vue , estos métodos se encuentran debajo de los métodos de utilidad.

 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(); },

Una cosa interesante en algunos de los fragmentos de código anteriores es el uso de $ref . En términos simples, es la forma en que Vue asocia la declaración de código que lo contiene a la construcción HTML que se supone que afecta (para obtener más información, lea la guía oficial). Por ejemplo, los métodos makeInputEmpty() y detectEnterKeyPress() afectan el cuadro de entrada, porque en el HTML del cuadro de entrada hemos mencionado el valor del atributo ref como input .

Métodos de adquisición de datos

Estamos utilizando las siguientes dos API en nuestro proyecto:

  • API de geocodificador de Google Maps
    Esta API es para obtener las coordenadas de la ubicación que busca el usuario. Necesitará una clave API para usted, que puede obtener siguiendo la documentación en el enlace dado. Por ahora, puede usar la clave API utilizada por FusionCharts, pero le pedimos que no abuse de ella y obtenga una clave propia. Nos referimos a la API de JavaScript del index.html de este proyecto, y usaremos los constructores que proporciona para nuestro código en el archivo App.vue .
  • La API de tiempo de cielo oscuro
    Esta API es para obtener los datos meteorológicos correspondientes a las coordenadas. Sin embargo, no lo usaremos directamente; lo envolveremos dentro de una URL que redirige a través de uno de los servidores de FusionCharts. La razón es que si envía una solicitud GET a la API desde una aplicación totalmente cliente como la nuestra, se produce el frustrante error CORS (más información aquí y aquí).

Nota importante : dado que hemos utilizado las API de Google Maps y Dark Sky, ambas API tienen sus propias claves de API que hemos compartido con usted en este artículo. Esto lo ayudará a concentrarse en los desarrollos del lado del cliente en lugar del dolor de cabeza de la implementación de back-end. Sin embargo, le recomendamos que cree sus propias claves , ya que nuestras claves de API tendrán límites y, si estos límites se superan, no podrá probar la aplicación usted mismo.

Para Google Maps, vaya a este artículo para obtener su clave API. Para la API de Dark Sky, visite https://darksky.net/dev para crear su clave de API y los puntos finales respectivos.

Con el contexto en mente, veamos la implementación de los métodos de adquisición de datos para nuestro proyecto.

 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 pegamento de alto nivel

Con los métodos de utilidad, adquisición y procesamiento fuera de nuestro camino, ahora nos queda la tarea de orquestar todo. Lo hacemos mediante la creación de métodos de unión de alto nivel, que esencialmente llama a los métodos escritos anteriormente en una secuencia particular, para que toda la operación se ejecute sin 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

Vue proporciona ganchos del ciclo de vida de la instancia: propiedades que son esencialmente métodos y se activan cuando el ciclo de vida de la instancia alcanza esa etapa. Por ejemplo, created,mounted, beforeUpdate, etc., son enlaces de ciclo de vida muy útiles que permiten al programador controlar la instancia a un nivel mucho más granular de lo que hubiera sido posible de otro modo.

En el código de un componente de Vue, estos ganchos de ciclo de vida se implementan como lo haría con cualquier otra prop . Por ejemplo:

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

Armado con esta nueva comprensión, eche un vistazo al código a continuación para el accesorio mounted de App.vue :

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

Código completo para App.vue

Hemos cubierto mucho terreno en esta sección, y las últimas secciones le han dado cosas en fragmentos. Sin embargo, es importante que tenga el código ensamblado completo para App.vue (sujeto a modificaciones adicionales en secciones posteriores). Aquí va:

 <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>

Y finalmente, después de mucha paciencia y arduo trabajo, ¡puedes ver el flujo de datos con toda su potencia! Visite la aplicación en el navegador, actualice la página, busque una ubicación en el cuadro de búsqueda de la aplicación y presione Entrar .

La aplicación como se muestra en el navegador
(Vista previa grande)

Ahora que hemos terminado con todo el trabajo pesado, tómese un descanso. Las secciones subsiguientes se centran en el uso de los datos para crear gráficos atractivos e informativos, seguidos de darle a nuestra aplicación de apariencia fea una merecida sesión de preparación usando CSS.

5. Visualización de datos con FusionCharts

Consideraciones fundamentales para gráficos

Para el usuario final, la esencia de un tablero es esencialmente esta: una colección de información filtrada y cuidadosamente seleccionada sobre un tema en particular, transmitida a través de instrumentos visuales/gráficos para una ingesta rápida. No les importan las sutilezas de la ingeniería de canalización de datos ni la estética de su código; todo lo que quieren es una vista de alto nivel en 3 segundos. Por lo tanto, nuestra aplicación cruda que muestra datos de texto no significa nada para ellos, y ya es hora de que implementemos mecanismos para envolver los datos con gráficos.

Sin embargo, antes de profundizar en la implementación de gráficos, consideremos algunas preguntas pertinentes y las posibles respuestas desde nuestra perspectiva:

  • ¿Qué tipo de gráficos son apropiados para el tipo de datos que estamos tratando?
    Bueno, la respuesta tiene dos aspectos: el contexto y el propósito. Por contexto, nos referimos al tipo de datos y su ajuste general en el esquema de cosas más grandes, limitado por el alcance y la audiencia del proyecto. Y por propósito, esencialmente queremos decir "¿en qué queremos enfatizar?". Por ejemplo, podemos representar la temperatura de hoy en diferentes momentos del día usando un Gráfico de columnas (columnas verticales de igual ancho, con altura proporcional al valor que representa la columna). Sin embargo, rara vez estamos interesados ​​en los valores individuales, sino más bien en la variación general y la tendencia a lo largo de los datos. Para cumplir con el propósito, nos conviene utilizar un gráfico de líneas, y lo haremos en breve.
  • ¿Qué se debe tener en cuenta antes de seleccionar una biblioteca de gráficos?
    Dado que estamos haciendo un proyecto que utiliza predominantemente tecnologías basadas en JavaScript, es obvio que cualquier biblioteca de gráficos que elijamos para nuestro proyecto debe ser nativa del mundo de JavaScript. Con esa premisa básica en mente, debemos considerar lo siguiente antes de ponernos a cero en cualquier biblioteca en particular:
    • Soporte para los frameworks de nuestra elección , que en este caso es Vue.js. Un proyecto se puede desarrollar en otros marcos JavaScript populares como React o Angular; consulte el soporte de la biblioteca de gráficos para su marco favorito. Además, se debe considerar la compatibilidad con otros lenguajes de programación populares como Python, Java, C ++, .Net (AS y VB), especialmente cuando el proyecto involucra algunas cosas serias de back-end.
    • Disponibilidad de tipos de gráficos y características , ya que es casi imposible saber de antemano cuál será la forma final y el propósito de los datos en el proyecto (especialmente si los requisitos están regulados por sus clientes en un entorno profesional). En este caso, debe ampliar su red y elegir una biblioteca de gráficos que tenga la colección más amplia de gráficos. Más importante aún, para diferenciar su proyecto de los demás, la biblioteca debe tener suficientes funciones en forma de atributos de gráficos configurables, de modo que pueda ajustar y personalizar la mayoría de los aspectos de los gráficos y el nivel correcto de granularidad. Además, las configuraciones de gráficos predeterminadas deben ser sensatas, y la documentación de la biblioteca debe ser de primera categoría, por razones que son obvias para los desarrolladores profesionales.
    • También se debe tener en cuenta la curva de aprendizaje, la comunidad de apoyo y el equilibrio , especialmente cuando es nuevo en la visualización de datos. En un extremo del espectro, tiene herramientas patentadas como Tableau y Qlickview que cuestan una bomba, tienen una curva de aprendizaje suave, pero también vienen con muchas limitaciones en términos de personalización, integración e implementación. En el otro extremo está d3.js: amplio, gratuito (código abierto) y personalizable en su esencia, pero debe pagar el precio de una curva de aprendizaje muy pronunciada para poder hacer algo productivo con la biblioteca.

Lo que necesita es el punto óptimo: el equilibrio adecuado entre productividad, cobertura, personalización, curva de aprendizaje y, por supuesto, costo. Lo animamos a que eche un vistazo a FusionCharts, la biblioteca de creación de gráficos de JavaScript más completa y lista para empresas del mundo para la web y los dispositivos móviles, que usaremos en este proyecto para crear gráficos.

Introducción a FusionCharts

FusionCharts se utiliza en todo el mundo como la biblioteca de gráficos de JavaScript de referencia por millones de desarrolladores repartidos en cientos de países de todo el mundo. Técnicamente, es tan cargado y configurable como puede ser, con soporte para integrarlo con casi cualquier pila de tecnología popular utilizada para proyectos basados ​​en la web. El uso comercial de FusionCharts requiere una licencia, y debe pagar por la licencia según su caso de uso (comuníquese con ventas si tiene curiosidad). Sin embargo, estamos usando FusionCharts en estos proyectos solo para probar algunas cosas y, por lo tanto, la versión sin licencia (viene con una pequeña marca de agua en sus gráficos y algunas otras restricciones). El uso de la versión sin licencia está perfectamente bien cuando está probando los gráficos y usándolo en sus proyectos personales o no comerciales. Si tiene planes de implementar la aplicación comercialmente, asegúrese de tener una licencia de FusionCharts.

Dado que este es un proyecto que involucra a Vue.js, necesitaremos dos módulos que deben instalarse, si no se hizo antes:

  • El módulo fusioncharts , porque contiene todo lo necesario para crear los gráficos.
  • El módulo vue-fusioncharts , que es esencialmente un contenedor para fusioncharts, para que pueda usarse en un proyecto Vue.js

Si no los ha instalado antes (como se indica en la tercera sección), instálelos ejecutando el siguiente comando desde el directorio raíz del proyecto:

 npm install fusioncharts vue-fusioncharts --save

A continuación, asegúrese de que el archivo src/main.js del proyecto tenga el siguiente código (también mencionado en la sección 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) })

Quizás la línea más crítica en el fragmento anterior es la siguiente:

 Vue.use(VueFusionCharts, FusionCharts)

Le indica a Vue que use el módulo vue-fusioncharts para dar sentido a muchas cosas en el proyecto que aparentemente no están definidas explícitamente por nosotros, pero están definidas en el propio módulo. Además, este tipo de declaración implica una declaración global , por lo que queremos decir que en cualquier lugar que Vue encuentre algo extraño en el código de nuestro proyecto (cosas que no hemos definido explícitamente sobre el uso de FusionCharts), al menos se verá una vez en vue-fusioncharts. y módulos de nodo fusioncharts para sus definiciones, antes de arrojar errores. Si hubiéramos usado FusionCharts en una parte aislada de nuestro proyecto (sin usarlo en casi todos los archivos de componentes), entonces quizás la declaración local hubiera tenido más sentido.

Con eso, ya está todo listo para usar FusionCharts en el proyecto. Utilizaremos una gran variedad de gráficos, y la elección dependerá del aspecto de los datos meteorológicos que queramos visualizar. Además, veremos la interacción del enlace de datos, los componentes personalizados y los observadores en acción.

Esquema general para usar Fusioncharts en archivos .vue

En esta sección, explicaremos la idea general de usar FusionCharts para crear varios gráficos en los archivos .vue . Pero primero, veamos el pseudocódigo que ilustra esquemáticamente la idea 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>

Comprendamos las diferentes partes del pseudocódigo anterior:

  • En <template> , dentro del nivel superior <div> (que es prácticamente obligatorio para el código HTML de la plantilla de cada componente), tenemos el componente personalizado <fusioncharts> . Tenemos la definición del componente contenida en el módulo Node vue-fusioncharts que hemos instalado para este proyecto. Internamente, vue-fusioncharts se basa en el módulo fusioncharts , que también se ha instalado. Importamos los módulos necesarios y resolvimos sus dependencias, le indicamos a Vue que use el contenedor globalmente (a lo largo del proyecto) en el archivo src/main.js y, por lo tanto, no hay falta de definición para el componente <fusioncharts> personalizado que hemos usado. aquí. Además, el componente personalizado tiene atributos personalizados, y cada uno de los atributos personalizados está vinculado a un objeto de datos (y, a su vez, a sus valores), mediante la directiva v-bind , cuya abreviatura es el símbolo de dos puntos ( : . Aprenderemos sobre los atributos y sus objetos de datos asociados con mayor detalle, cuando discutamos algunos de los gráficos específicos utilizados en este proyecto.
  • En el <script> , primero declara los accesorios que se supone que debe recibir el componente y luego continúa definiendo los objetos de datos que están limitados a los atributos de <fusioncharts> . Los valores asignados a los objetos de datos son los valores que extraen los atributos de <fusioncharts> , y los gráficos se crean sobre la base de esos valores extraídos. Aparte de estos, la parte más interesante del código es el objeto watch { } . Este es un objeto muy especial en el esquema de cosas de Vue: esencialmente le indica a Vue que vigile cualquier cambio que ocurra en ciertos datos y luego tome medidas basadas en cómo se ha definido la función del handler para esos datos. Por ejemplo, queremos que Vue vigile la prop recibida, es decir, data_prop_received_by_the_component en el pseudocódigo. La prop se convierte en una clave en el objeto watch { } , y el valor de la clave es otro objeto: un método de controlador que describe lo que se debe hacer cada vez que cambia la prop . Con mecanismos tan elegantes para manejar los cambios, la aplicación mantiene su reactividad. El deep: true representa un indicador booleano que puede asociar con los observadores, de modo que el objeto que se está observando se observa con bastante profundidad, es decir, incluso se rastrean los cambios realizados en los niveles anidados del objeto.
    ( Para más información sobre los vigilantes consultar la documentación oficial ).

Ahora que está equipado con una comprensión del esquema general de las cosas, profundicemos en las implementaciones específicas de los gráficos en los archivos de componentes .vue . El código se explicará bastante por sí mismo, y debe tratar de comprender cómo encajan los detalles en el esquema general de las cosas descritas anteriormente.

Implementación de gráficos en archivos .vue

Si bien los detalles específicos de la implementación varían de un gráfico a otro, la siguiente explicación se aplica a todos ellos:

  • <template>
    Como se explicó anteriormente, el componente personalizado <fusioncharts> tiene varios atributos, cada uno de ellos vinculado al objeto de datos correspondiente definido en la función data() mediante el uso de la directiva v-bind :. Los nombres de los atributos se explican por sí mismos por lo que significan, y descubrir los objetos de datos correspondientes también es trivial.
  • <script>
    En la función data() , los objetos de datos y sus valores son los que hacen que los gráficos funcionen, debido al enlace realizado por las directivas v-bind ( : utilizadas en los atributos de <fusioncharts> . Antes de profundizar en los objetos de datos individuales, vale la pena mencionar algunas características generales:
    • Los objetos de datos cuyos valores son 0 o 1 son de naturaleza booleana, donde 0 representa algo que no está disponible/desactivado y 1 representa el estado de disponibilidad/activado. Sin embargo, tenga cuidado de que los objetos de datos no booleanos también pueden tener 0 o 1 como valores, además de otros valores posibles; depende del contexto. Por ejemplo, containerbackgroundopacity con su valor predeterminado 0 es booleano, mientras que lowerLimit con su valor predeterminado 0 simplemente significa que el número cero es su valor literal.
    • Algunos objetos de datos se ocupan de las propiedades de CSS, como el margen, el relleno, el tamaño de fuente, etc.; el valor tiene una unidad implícita de "px" o píxel. De manera similar, otros objetos de datos pueden tener unidades implícitas asociadas con sus valores. Para obtener información detallada, consulte la página de atributos del gráfico respectivo de FusionCharts Dev Center.
  • En la función data() , quizás el objeto más interesante y no obvio es el origen de datos. Este objeto tiene tres objetos principales anidados dentro de él:
    • chart : este objeto encapsula muchos atributos de gráfico relacionados con la configuración y la estética del gráfico. Es casi una construcción obligatoria que encontrarás en todos los gráficos que crearás para este proyecto.
    • colorrange : este objeto es algo específico del gráfico que se está considerando y está presente principalmente en los gráficos que tratan con múltiples colores/tonos para demarcar diferentes subrangos de la escala utilizada en el gráfico.
    • valor: este objeto, nuevamente, está presente en los gráficos que tienen un valor específico que debe resaltarse en el rango de la escala.
  • El objeto watch { } es quizás lo más importante que hace que este gráfico y los demás gráficos utilizados en este proyecto cobren vida. La reactividad de los gráficos, es decir, los gráficos que se actualizan a sí mismos en función de los nuevos valores resultantes de una nueva consulta del usuario, está controlada por los observadores definidos en este objeto. Por ejemplo, definimos un observador para los highlights de la propiedad recibidos por el componente, y luego definimos una función de controlador para instruir a Vue sobre las acciones necesarias que debe tomar, cuando algo cambia sobre el objeto que se observa en todo el proyecto. Esto significa que cada vez que App.vue produce un valor nuevo para cualquiera de los objetos dentro de las funciones highlights , la información se filtra hasta este componente y el valor nuevo se actualiza en los objetos de datos de este componente. El gráfico que está vinculado a los valores también se actualiza como resultado de este mecanismo.

Las explicaciones anteriores son pinceladas bastante amplias para ayudarnos a desarrollar una comprensión intuitiva del panorama general. Una vez que entiendas los conceptos de forma intuitiva, siempre puedes consultar la documentación de Vue.js y FusionCharts, cuando algo no te quede claro del propio código. Le dejamos el ejercicio a usted y, a partir de la siguiente subsección, no explicaremos las cosas que cubrimos en esta subsección.

src/components/TempVarChart.vue

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

src/components/UVIndex.vue

Este componente contiene un gráfico extremadamente útil: el indicador angular.

Índice UV
(Vista previa grande)

El código para el componente se proporciona a continuación. Para obtener información detallada sobre los atributos del gráfico de Angular Gauge, consulte la página del Centro de desarrollo de FusionCharts 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/Visibilidad.vue

En este componente, usamos un indicador lineal horizontal para representar la visibilidad, como se muestra en la imagen a continuación:

Captura de pantalla del indicador lineal horizontal que representa la visibilidad del aire (16 km)
(Vista previa grande)

El código para el componente se proporciona a continuación. Para una comprensión profunda de los diferentes atributos de este tipo de gráfico, consulte la página del centro de desarrollo de FusionCharts para el indicador lineal 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 muestra la velocidad y la dirección del viento (velocidad del viento, si tiene conocimientos de física), y es muy difícil representar un vector usando un gráfico. Para tales casos, sugerimos representarlos con la ayuda de algunas bonitas imágenes y valores de texto. Dado que la representación en la que hemos pensado depende completamente de CSS, la implementaremos en la siguiente sección que trata sobre CSS. Sin embargo, eche un vistazo a lo que pretendemos crear:

Estado del viento: dirección del viento (izquierda) y velocidad del viento (derecha)
(Vista previa grande)
 <template> <div class="highlights-item col-md-4 col-sm-6 col-xs-12 border-top"> <div> <div class="card-heading pt-5">Wind Status</div> <div class="row pt-4 mt-4"> <div class="col-sm-6 col-md-6 mt-2 text-center align-middle"> <p class="card-sub-heading mt-3">Wind Direction</p> <p class="mt-4"><img src="../assets/winddirection.svg" height="40" width="40"></p> <p class="card-value mt-4">{{ highlights.windStatus.derivedWindDirection }}</p> </div> <div class="col-sm-6 col-md-6 mt-2"> <p class="card-sub-heading mt-3">Wind Speed</p> <p class="mt-4"><img src="../assets/windspeed.svg" height="40" width="40"></p> <p class="card-value mt-4">{{ highlights.windStatus.windSpeed }} km/h</p> </div> </div> </div> </div> </template> <script> export default { props: ["highlights"], components: {}, data() { return {}; }, methods: {}, computed: {} }; </script>

Terminando con Highlights.vue

Recuerde que ya implementamos código con CSS para todos los componentes, excepto Content.vue y Highlights.vue . Dado que Content.vue es un componente tonto que solo transmite datos, ya se cubrió el estilo mínimo que necesita. Además, ya hemos escrito el código apropiado para diseñar la barra lateral y las tarjetas que contienen los gráficos. Por lo tanto, todo lo que nos queda por hacer es agregar algunos bits de estilo a Highlights.vue , lo que implica principalmente el uso de las clases 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>

Implementación y código fuente

Con los gráficos y el estilo en orden, ¡hemos terminado! Tómese un momento para apreciar la belleza de su creación.

El resultado
(Vista previa grande)

Ha llegado el momento de que implemente su aplicación y la comparta con sus compañeros. Si no tiene mucha idea sobre la implementación y espera que lo ayudemos, consulte aquí nuestras ideas sobre implementación. El artículo vinculado también contiene sugerencias sobre cómo eliminar la marca de agua de FusionCharts en la parte inferior izquierda de cada gráfico.

Si te equivocas en alguna parte y quieres un punto de referencia, el código fuente está disponible en Github.