使用 Vue.js 创建带有 API 的交互式天气仪表板
已发表: 2022-03-10(这是一篇赞助文章。)在本教程中,您将从头开始构建一个简单的天气仪表板。 它将是一个客户端应用程序,既不是“Hello World”示例,也不是其规模和复杂性太吓人。
整个项目将使用来自 Node.js + npm 生态系统的工具进行开发。 特别是,我们将严重依赖 Dark Sky API 来处理数据,Vue.js 来处理所有繁重的工作,而 FusionCharts 来处理数据可视化。
先决条件
我们希望您熟悉以下内容:
- HTML5 和 CSS3 (我们还将使用 Bootstrap 提供的基本功能;
- JavaScript (尤其是 ES6 语言的使用方式);
- Node.js 和 npm (环境和包管理的基础知识就好了)。
除了上面提到的之外,如果您熟悉Vue.js或任何其他类似的 JavaScript 框架,那就太好了。 我们不希望您了解FusionCharts — 它非常易于使用,您可以随时学习!
预期学习
您从该项目中学到的主要知识将是:
- 如何计划实施一个好的仪表板
- 如何使用 Vue.js 开发应用程序
- 如何创建数据驱动的应用程序
- 如何使用 FusionCharts 可视化数据
特别是,每个部分都让您更接近学习目标:
- 天气仪表板简介
本章概述了工作的不同方面。 - 创建项目
在本节中,您将了解如何使用 Vue 命令行工具从头开始创建项目。 - 自定义默认项目结构
您在上一节中获得的默认项目脚手架是不够的; 在这里,您从结构的角度了解项目所需的其他内容。 - 数据采集和处理
这部分是项目的核心; 此处展示了从 API 获取和处理数据的所有关键代码。 期望在这部分花费最多的时间。 - 使用 FusionCharts 进行数据可视化
一旦我们稳定了项目的所有数据和其他移动部分,本节将致力于使用 FusionCharts 和一些 CSS 可视化数据。
1.仪表板工作流程
在我们深入实施之前,重要的是要清楚我们的计划。 我们将计划分为四个不同的方面:
要求
我们对这个项目有什么要求? 换句话说,我们想通过 Weather Dashboard 展示哪些内容? 请记住,我们的目标受众可能只是口味简单的凡人,我们想向他们展示以下内容:
- 他们想要查看天气的位置的详细信息,以及有关天气的一些主要信息。 由于没有严格的要求,我们稍后会弄清楚无聊的细节。 但是,在这个阶段,需要注意的是,我们必须为观众提供一个搜索框,以便他们可以提供他们感兴趣的位置的输入。
- 有关其感兴趣位置的天气的图形信息,例如:
- 查询当日温度变化
- 今日天气亮点:
- 风速和风向
- 能见度
- 紫外线指数
注意:从 API 获得的数据提供了有关天气的许多其他方面的信息。 为了将代码保持在最低限度,我们选择不使用所有这些。
结构
根据需求,我们可以构建我们的仪表板,如下所示:
数据
我们的仪表板与我们获得的数据一样好,因为如果没有适当的数据,就不会有漂亮的可视化。 有很多公共 API 可以提供天气数据——其中一些是免费的,而另一些则不是。 对于我们的项目,我们将从 Dark Sky API 收集数据。 但是,我们将无法直接从客户端轮询 API 端点。 别担心,我们有一个解决方法,会在适当的时候公布! 一旦我们获得搜索位置的数据,我们将进行一些数据处理和格式化——你知道,帮助我们支付账单的技术类型。
可视化
一旦我们获得干净且格式化的数据,我们将其插入 FusionCharts。 世界上很少有 JavaScript 库能像 FusionCharts 一样强大。 在 FusionCharts 提供的大量产品中,我们将只使用其中的一小部分——全部用 JavaScript 编写,但在与 FusionCharts 的 Vue 包装器集成时可以无缝工作。
有了更大的图景,让我们动手吧——是时候把事情具体化了! 在下一节中,您将创建基本的 Vue 项目,我们将在此基础上进一步构建。
2. 创建项目
要创建项目,请执行以下步骤:
- 安装 Node.js + npm
(如果您的计算机上安装了 Node.js,请跳过此步骤。 )
Node.js 与 npm 捆绑在一起,因此您无需单独安装 npm。 根据操作系统,按照此处给出的说明下载并安装 Node.js。
安装后,最好验证软件是否正常工作,以及它们的版本是什么。 要测试它,请打开命令行/终端并执行以下命令:node --version npm --version
- 使用 npm 安装包
启动并运行 npm 后,执行以下命令来安装我们项目所需的基本包。npm install -g vue@2 vue-cli@2
- 使用
vue-cli
初始化项目脚手架
假设上一步一切顺利,下一步就是使用vue-cli
(来自 Vue.js 的命令行工具)来初始化项目。 为此,请执行以下操作: - 使用 webpack-simple 模板初始化脚手架。
vue init webpack-simple vue_weather_dashboard
N
请记住,尽管webpack-simple
非常适合像我们这样的快速原型设计和轻量级应用程序,但它并不是特别适合严肃的应用程序或生产部署。 如果您想使用任何其他模板(尽管如果您是新手,我们会建议您不要这样做),或者想将您的项目命名为其他名称,语法为:vue init [template-name] [project-name]
- 导航到 vue-cli 为项目创建的目录。
cd vue_weather_dashboard
- 安装
package.json
中提到的所有包,该包是由vue-cli
工具为webpack-simple
模板创建的。npm install
- 启动开发服务器并在浏览器中查看您的默认 Vue 项目!
npm run dev
如果您是 Vue.js 的新手,请花点时间细细品味您的最新成就——您已经创建了一个小型 Vue 应用程序,它在 localhost:8080 上运行!
默认项目结构的简要说明
是时候看一下vue_weather_dashboard
目录里面的结构了,这样在我们开始修改之前你就可以了解基础知识了。
结构看起来像这样:
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
尽管跳过熟悉默认文件和目录可能很诱人,但如果您是 Vue 新手,我们强烈建议您至少查看一下文件的内容。 这可能是一个很好的教育课程,并引发您应该自行解决的问题,尤其是以下文件:
-
package.json
,看看它的表亲package-lock.json
-
webpack.config.js
-
index.html
-
src/main.js
-
src/App.vue
树形图中显示的每个文件和目录的简要说明如下:
- 自述文件.md
猜测没有奖品——主要是让人类阅读和理解创建项目脚手架所需的步骤。 - 节点模块/
这是 npm 下载启动项目所需的包的目录。package.json
文件中提供了有关所需软件包的信息。 - 包.json
该文件由 vue-cli 工具根据webpack-simple
模板的要求创建,包含有关必须安装的 npm 包的信息(包括其版本和其他详细信息)。 仔细看看这个文件的内容——这是你应该访问的地方,也许可以编辑添加/删除项目所需的包,然后运行 npm install。 在此处阅读有关package.json
的更多信息。 - 包-lock.json
该文件由 npm 自己创建,主要用于记录 npm 下载和安装的内容。 - webpack.config.js
这是一个包含 webpack 配置的 JavaScript 文件——一个将我们项目的不同方面(代码、静态资产、配置、环境、使用模式等)捆绑在一起的工具,并在将其提供给用户之前进行压缩。 好处是所有东西都自动捆绑在一起,并且由于应用程序性能的提高(页面服务迅速并且在浏览器上加载速度更快),用户体验大大增强。 正如您稍后可能会遇到的那样,当构建系统中的某些内容无法按预期方式运行时,需要检查该文件。 此外,当您要部署应用程序时,这是需要编辑的关键文件之一(在此处阅读更多内容)。 - 索引.html
这个 HTML 文件充当矩阵(或者你可以说,模板),数据和代码将在其中动态嵌入(这是 Vue 主要做的),然后提供给用户。 - src/main.js
这个 JavaScript 文件包含主要管理顶级/项目级依赖关系的代码,并定义了最顶级的 Vue 组件。 简而言之,它为整个项目编排了 JavaScript,并作为应用程序的入口点。 当您需要在某些节点模块上声明项目范围的依赖项时,请编辑此文件,或者您希望更改项目中最顶层的 Vue 组件的某些内容。 - src/App.vue
在前一点中,当我们谈论“最顶层的 Vue 组件”时,我们实质上是在谈论这个文件。 项目中的每个 .vue 文件都是一个组件,组件是层次相关的。 一开始,我们只有一个.vue
文件,即App.vue
,作为我们唯一的组件。 但很快我们将向我们的项目添加更多组件(主要遵循仪表板的结构),并根据我们想要的层次结构链接它们,App.vue 是所有组件的祖先。 这些.vue
文件将包含 Vue 希望我们编写的格式的代码。 别担心,它们是编写的 JavaScript 代码,维护一个可以让我们保持理智和有条理的结构。 你已经被警告了——在这个项目结束时,如果你是 Vue 的新手,你可能会沉迷于template — script — style
template — script — style
template — script — style
组织代码的方式!
现在我们已经创建了基础,是时候:
- 修改模板并稍微调整配置文件,以便项目按照我们想要的方式运行。
- 创建新的
.vue
文件,并使用 Vue 代码实现仪表板结构。
我们将在下一节中学习它们,这会有点长,需要注意。 如果您需要咖啡因或水,或者想要排便——现在正是时候!
3.自定义默认项目结构
是时候修补脚手架项目给我们的基础了。 在开始之前,请确保webpack
提供的开发服务器正在运行。 连续运行此服务器的优势在于,您对源代码所做的任何更改(保存并刷新网页)都会立即反映在浏览器上。
如果要启动开发服务器,只需从终端执行以下命令(假设您的当前目录是项目目录):
npm run dev
在以下部分中,我们将修改一些现有文件,并添加一些新文件。 随后将简要说明这些文件的内容,以便您了解这些更改的含义。
修改现有文件
索引.html
我们的应用程序实际上是一个单页应用程序,因为只有一个网页会显示在浏览器上。 我们稍后会讨论这个,但首先让我们进行第一个更改——更改<title>
标记中的文本。
通过这个小修订,HTML 文件如下所示:
<!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>
花点时间在localhost:8080
刷新网页,然后在浏览器选项卡的标题栏上看到变化——它应该显示“Vue Weather Dashboard”。 但是,这只是为了向您展示进行更改并验证其是否正常工作的过程。 我们还有更多事情要做!
这个简单的 HTML 页面缺少我们项目中需要的许多东西,尤其是以下内容:
- 一些元信息
- CDN 链接到 Bootstrap(CSS 框架)
- 链接到自定义样式表(尚未添加到项目中)
-
<script>
标签中指向 Google Maps Geolocation API 的指针
添加这些东西之后,最终的index.html
有以下内容:
<!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>
保存文件,然后刷新网页。 您可能已经注意到页面加载时有轻微的颠簸——这主要是因为页面样式现在由 Bootstrap 控制,并且字体、间距等样式元素与我们的默认值不同较早(如果您不确定,请回滚到默认值并查看差异)。
注意:在我们继续之前有一件重要的事情——Google Maps API 的 URL 包含一个密钥,它是 FusionCharts 的一个属性。 现在,您可以使用此密钥来构建项目,因为我们不希望您被这些类型的微小细节所困扰(当您是新手时,这可能会分散您的注意力)。 但是,我们强烈建议您在取得一些进展并愿意关注这些微小细节后,生成并使用自己的 Google Maps API 密钥。
包.json
在撰写本文时,我们为我们的项目使用了某些版本的 npm 包,我们确信这些东西可以协同工作。 但是,当您执行项目时,npm 为您下载的最新稳定版本的软件包很可能与我们使用的不同,这可能会破坏代码(或做超出我们的控制)。 因此,拥有用于构建此项目的完全相同的package.json
文件非常重要,这样我们的代码/解释和您得到的结果是一致的。
package.json
文件的内容应该是:
{ "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" } }
我们鼓励您浏览新的package.json
,并找出 json 中不同对象的功能。 您可能更喜欢将“ author
”键的值更改为您的姓名。 此外,依赖项中提到的包将在代码中的正确时间显示出来。 暂时,知道以下就足够了:
-
babel
相关的包用于正确处理浏览器的 ES6 样式代码; -
axios
处理基于 Promise 的 HTTP 请求; -
moment
和 moment-timezone 用于日期/时间操作; -
fusioncharts
和vue-fusioncharts
负责渲染图表: -
vue
,原因很明显。
webpack.config.js
与package.json
一样,我们建议您维护一个与我们用于构建项目的文件一致的webpack.config.js
文件。 但是,在进行任何更改之前,我们建议您仔细比较webpack.config.js
中的默认代码和我们在下面提供的代码。 你会注意到很多不同之处——用谷歌搜索它们并对它们的含义有一个基本的了解。 由于深入解释 webpack 配置超出了本文的范围,因此在这方面您只能靠自己。
自定义的webpack.config.js
文件如下:
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 }) ]) }
对项目的webpack.config.js
进行更改后,必须停止正在运行的开发服务器( Ctrl + C ),并在安装包中提到的所有package.json
后使用从项目目录执行的以下命令重新启动它package.json
文件:
npm install npm run dev
有了这个,调整配置和确保正确的包就位的折磨就结束了。 不过,这也标志着修改和编写代码的旅程,有点长,但也很有收获!
src/main.js
这个文件是项目顶层编排的关键——我们在这里定义:
- 什么是顶级依赖项(从哪里获得最重要的 npm 包);
- 如何解决依赖关系,以及 Vue 使用插件/包装器的说明(如果有);
- 管理项目中最顶层组件的 Vue 实例:
src/App.vue
(节点.vue
文件)。
根据我们对src/main.js
文件的目标,代码应该是:
// 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
这是整个项目中最重要的文件之一,它代表了层次结构中最顶层的组件——整个应用程序本身。 对于我们的项目,这个组件将完成所有繁重的工作,我们将在后面进行探讨。 现在,我们想摆脱默认的样板,并加入我们自己的东西。
如果您不熟悉 Vue 组织代码的方式,最好了解.vue
文件中的一般结构。 .vue
文件包含三个部分:
- 模板
这是定义页面的 HTML 模板的地方。 除了静态 HTML,本节还包含 Vue 使用双花括号{{ }}
嵌入动态内容的方式。 - 脚本
JavaScript 统治这一部分,并负责生成动态内容,这些内容在 HTML 模板中的适当位置出现和放置。 此部分主要是一个导出的对象,包括:- 数据
这本身就是一个函数,通常它会返回一些封装在一个不错的数据结构中的所需数据。 - 方法
由一个或多个函数/方法组成的对象,每个函数/方法通常以某种方式操作数据,并且还控制 HTML 模板的动态内容。 - 计算
很像上面讨论的方法对象,但有一个重要区别——虽然方法对象中的所有函数在任何一个被调用时都会执行,但计算对象中的函数表现得更加明智,并且仅当它被调用时才会执行。叫。
- 数据
- 风格
本节介绍适用于页面 HTML 的 CSS 样式(在模板中编写)——将好的旧 CSS 放在这里以使您的页面更漂亮!
牢记上述范式,让我们对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>
请记住,上面的代码片段只是为了测试App.vue
是否正在使用我们自己的代码。 它稍后会进行很多更改,但首先保存文件并在浏览器上刷新页面。
此时,在工具方面获得一些帮助可能是个好主意。 查看适用于 Chrome 的 Vue devtools,如果您在使用 Google Chrome 作为默认浏览器进行开发时没有太多问题,请安装该工具并尝试一下。 当事情变得更加复杂时,它将非常方便地进行进一步的开发和调试。
其他目录和文件
下一步是添加其他文件,以便我们的项目结构变得完整。 我们将添加以下目录和文件:
-
src/css/
—style.css
-
src/assets/
——calendar.svg
——vlocation.svg
——search.svg
——winddirection.svg
——windspeed.svg
-
src/components/
——Content.vue
——Highlights.vue
——TempVarChart.vue
——UVIndex.vue
——Visibility.vue
——WindStatus.vue
注意:将超链接的.svg
文件保存在您的项目中。
创建上面提到的目录和文件。 最终的项目结构应该看起来像(记住从默认结构中删除现在不需要的文件夹和文件):
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
项目的根文件夹中可能还有一些其他文件,例如.babelrc
、 .gitignore
、 .editorconfig
等。 您现在可以安全地忽略它们。
在下一节中,我们将对新添加的文件添加最少的内容,并测试它们是否正常工作。
src/css/style.css
虽然它不会立即有多大用处,但将以下代码复制到文件中:
@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; } }
源/资产/
在此目录中,下载并保存下面提到的.svg
文件:
-
calendar.svg
-
location.svg
-
search.svg
-
winddirection.svg
-
windspeed.svg
src/components/Content.vue
这就是我们所说的“哑组件”(即占位符),它只是为了维护层次结构,本质上将数据传递给它的子组件。
请记住,在App.vue
文件中编写我们所有的代码没有技术门槛,但我们采用通过嵌套组件来拆分代码的方法有两个原因:
- 编写干净的代码,这有助于可读性和可维护性;
- 复制我们将在屏幕上看到的相同结构,即层次结构。
在我们将Content.vue
中定义的组件嵌套在根组件App.vue
中之前,让我们为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>
在代码中,仔细观察并理解以下内容:
- 在
<script>
标记中(我们显然在其中编写了一些 JavaScript 代码),我们定义了一个默认导出的对象(使其可用于其他文件)。 该对象包含一个函数data()
,它返回一个名为childComponents
的数组对象,其元素是应该进一步嵌套的组件文件的名称。 - 在
<template>
标记(我们在其中编写一些 HTML 模板)中,感兴趣的是<ul>
。- 在无序列表中,每个列表项都应该是预期子组件的名称,如数组对象
childComponents
中所定义。 此外,列表应该自动扩展到数组的最后一个元素。 似乎我们应该写一个for
循环,不是吗? 我们通过使用 Vue.js 提供的v-for
指令来做到这一点。v-for
指令:- 作为
<li>
标记的一个属性,遍历数组,呈现子组件的名称,其中在{{ }}
括号中提到了迭代器(我们在其中编写列表项的文本)。
- 作为
- 在无序列表中,每个列表项都应该是预期子组件的名称,如数组对象
上面的代码和解释构成了你后续理解脚本和模板是如何相互关联的,以及我们如何使用 Vue.js 提供的指令的基础。
我们已经学到了很多东西,但即使在所有这些之后,我们还有一件事要学习如何在层次结构中无缝连接组件——将数据从父组件向下传递给它的子组件。 现在,我们需要学习如何将一些数据从src/App.vue
到src/components/Content.vue
,以便我们可以在这个项目中的其余组件嵌套中使用相同的技术。
从父组件流向子组件的数据可能听起来很简单,但魔鬼在细节中! 正如下面简要说明的那样,使其工作涉及多个步骤:
- 定义和数据
现在,我们希望使用一些静态数据——一个包含有关天气不同方面的硬编码值的对象就可以了! 我们创建了一个名为weather_data
的对象,并从App.vue
的data()
函数中返回它。weather_data
对象在下面的代码段中给出:
weather_data: { location: "California", temperature: { current: "35 C", }, highlights: { uvindex: "3", windstatus: { speed: "20 km/h", direction: "NE", }, visibility: "12 km", }, },
- 从父级传递数据
要传递数据,我们需要一个要发送数据的目的地! 在这种情况下,目的地是Content.vue
组件,实现它的方式是:- 将
weather_data
对象分配给<Content>
标记的自定义属性 - 使用 Vue.js 提供的
v-bind
指令将属性与数据绑定,该指令使属性值动态化(响应原始数据中所做的更改)。<Content v-bind:weather_data=“weather_data”></Content>
- 将
定义和传递数据是在握手的源端处理的,在我们的例子中是App.vue
文件。
App.vue
文件的代码,在其当前状态下,如下所示:
<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>
从源(父组件)定义和传递数据后,现在孩子有责任接收数据并适当地呈现它,如以下两个步骤所述。
- 孩子接收数据
子组件,在本例中为Content.vue
,必须接收父组件App.vue
发送给它的weather_data
对象。 Vue.js 提供了一种机制来做到这一点——你只需要一个名为props
的数组对象,定义在Content.vue
导出的默认对象中。 数组props
的每个元素都是它想要从其父级接收的数据对象的名称。 目前,它应该接收的唯一数据对象是来自 App.vue 的weather_data
。 因此,props
数组如下所示:
<template> // HTML template code here </template> <script> export default { props: ["weather_data"], data () { return { // data here } }, } </script> <style> // component specific CSS here </style>
- 渲染页面中的数据
现在我们已经确保接收到数据,我们需要完成的最后一个任务是渲染数据。 对于这个例子,我们将直接将接收到的数据转储到网页上,只是为了说明该技术。 然而,在实际应用程序中(比如我们即将构建的应用程序),数据通常会经过大量处理,并且只有数据的相关部分以适合目的的方式显示。 例如,在这个项目中,我们最终将从天气 API 获取原始数据,对其进行清理和格式化,将数据提供给图表所需的数据结构,然后将其可视化。 无论如何,为了显示原始数据转储,我们将只使用 Vue 可以理解的{{ }}
括号,如下面的代码片段所示:
<template> <div> // other template code here {{ weather_data }} </div> </template>
现在是时候吸收所有的点点滴滴了。 Content.vue
的代码——在它的当前状态下——如下所示:
<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>
进行上述更改后,在浏览器上刷新网页并查看其外观。 花点时间欣赏一下 Vue 处理的复杂性——如果你修改App.vue
中的weather_data
对象,它会默默地传送到Content.vue
,并最终传送到显示网页的浏览器! 尝试更改密钥位置的值。
虽然我们已经了解了使用静态数据的道具和数据绑定,但我们将在应用程序中使用通过 Web API 收集的动态数据,并将相应地更改代码。
概括
在我们继续讨论其余的.vue
文件之前,让我们总结一下我们在为App.vue
和components/Content.vue
编写代码时所学到的知识:
-
App.vue
文件就是我们所说的根组件——它位于组件层次结构的顶部。.vue
文件的其余部分表示作为其直接子代、孙代等的组件。 -
Content.vue
文件是一个虚拟组件——它的职责是将数据传递到下面的级别并维护结构层次结构,以便我们的代码与“*所见即所得*”的理念保持一致。 - 组件的父子关系不是凭空发生的——你必须注册一个组件(全局或本地,取决于组件的预期用途),然后使用自定义 HTML 标签嵌套它(其拼写是准确的与注册组件的名称相同)。
- 一旦注册和嵌套,数据就会从父组件传递到子组件,并且流程永远不会反向(如果项目架构允许回流,就会发生不好的事情)。 父组件是数据的相对来源,它使用自定义 HTML 元素属性的
v-bind
指令将相关数据传递给其子组件。 孩子使用道具接收预期的数据,然后自行决定如何处理数据。
对于其余的组件,我们将不做详细解释——我们将根据上述总结中的经验编写代码。 代码是不言而喻的,如果您对层次结构感到困惑,请参阅下图:
该图表明TempVarChart.vue
和Highlights.vue
是Content.vue
的直接子级。 因此,准备Content.vue
以将数据发送到这些组件可能是一个好主意,我们使用以下代码执行此操作:
<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>
保存此代码后,您将收到错误 - 不用担心,这是意料之中的。 一旦您准备好其余的组件文件,它将被修复。 如果您无法看到输出,请注释掉包含自定义元素标签<temp-var-chart>
和<today-highlights>
。
对于本节,这是Content.vue
的最终代码。 对于本节的其余部分,我们将参考此代码,而不是我们为学习而编写的之前的代码。
src/components/TempVarChart.vue
其父组件Content.vue
传递数据,必须设置TempVarChart.vue
来接收和渲染数据,如下代码所示:
<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
该组件还将从App.vue
(其父组件)接收数据。 之后,它应该与其子组件链接,并将相关数据传递给它们。
我们先来看从父节点接收数据的代码:
<template> <div> <p>Weather Highlights:</p> {{ highlights }} </div> </template> <script> export default { props: ["highlights"], data () { return { } }, methods: { }, computed: { }, } </script> <style> </style>
此时,网页如下图所示:
现在我们需要修改Highlights.vue
的代码来注册和嵌套它的子组件,然后将数据传递给子组件。 它的代码如下:
<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>
保存代码并查看网页后,您应该会在浏览器提供的开发者控制台工具中看到错误; 它们出现是因为尽管Highlights.vue
正在发送数据,但没有人接收它们。 我们还没有为Highlights.vue
的孩子编写代码。
请注意,我们没有做太多的数据处理,即,我们没有提取仪表板“亮点”部分下的天气数据的各个因素。 我们可以在data()
函数中做到这一点,但我们更喜欢让Highlights.vue
成为一个愚蠢的组件,它只是将它接收到的整个数据转储传递给每个孩子,然后孩子拥有自己的提取物对他们来说是必要的. 但是,我们鼓励您尝试在Highlights.vue
中提取数据,并将相关数据发送到每个子组件——尽管如此,这是一个很好的练习练习!
src/components/UVIndex.vue
该组件的代码从Highlights.vue
接收高光的数据转储,提取 UV Index 的数据,并将其呈现在页面上。
<template> <div> <p>UV Index: {{ uvindex }}</p> </div> </template> <script> export default { props: ["highlights"], data () { return { uvindex: this.highlights.uvindex } }, methods: { }, computed: { }, } </script> <style> </style>
src/components/Visibility.vue
该组件的代码从Highlights.vue
接收突出显示的数据转储,提取 Visibility 的数据,并将其呈现在页面上。
<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
该组件的代码从Highlights.vue
接收亮点的数据转储,提取风状态(速度和方向)的数据,并将其呈现在页面上。
<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>
添加所有组件的代码后,在浏览器上查看网页。
不要灰心,但所有这些辛勤工作只是为了将组件连接到层次结构中,并测试它们之间是否发生数据流! 在下一节中,我们将扔掉迄今为止编写的大部分代码,并添加更多与实际项目相关的代码。 但是,我们肯定会保留组件的结构和嵌套; 本节的学习将使我们能够使用 Vue.js 构建一个体面的仪表板。
4. 数据采集与处理
还记得App.vue
中的weather_data
对象吗? 它有一些硬编码数据,我们用来测试所有组件是否正常工作,也可以帮助您了解 Vue 应用程序的一些基本方面,而不会陷入现实世界数据的细节中。 然而,现在是我们摆脱外壳,进入现实世界的时候了,来自 API 的数据将主导我们的大部分代码。
准备子组件以接收和处理真实数据
在本节中,您将获得除App.vue
之外的所有组件的代码转储。 该代码将处理从App.vue
接收真实数据(与我们在上一节中编写的用于接收和渲染虚拟数据的代码不同)。
我们强烈建议您仔细阅读每个组件的代码,以便您了解每个组件所期望的数据,并最终在可视化中使用。
一些代码和整体结构将与您在之前的结构中看到的相似——因此您不会面临完全不同的情况。 然而,魔鬼在细节中! 因此,请仔细检查代码,当您对它们有相当好的理解时,将代码复制到项目中的相应组件文件中。
注意:本节中的所有组件都在src/components/
目录中。 所以每次都不会提及路径——只会提及.vue
文件名来标识组件。
内容.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>
对之前的代码进行了以下更改:
- 在
<template>
中,{{ }}
中的文本和数据已被删除,因为我们现在只是接收数据并将其传递给子组件,而没有呈现特定于该组件的内容。 - 在
export default {}
中:-
props
已更改以匹配父级将发送的数据对象:App.vue
。 之所以更改 props,是因为App.vue
本身会根据用户的搜索查询,展示它从天气 API 和其他在线资源获取的部分数据,并将其余数据传递出去。 在我们之前写的 dummy 代码中,App.vue
是在传递整个 dummy 数据转储,没有任何区别,Content.vue
的 props 也相应设置。 - data() 函数现在什么都不返回,因为我们没有在这个组件中进行任何数据操作。
-
TempVarChart.vue
该组件应该接收当天剩余时间的详细温度预测,并最终使用 FusionCharts 显示它们。 但目前,我们只会将它们显示为网页上的文本。
<template> <div> {{ tempVar.tempToday }} </div> </template> <script> export default { props: ["tempVar"], components: {}, data() { return { }; }, methods: { }, }; </script> <style> </style>
亮点.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>
对前面代码所做的更改是:
- 在
<template>
中,{{ }}
中的文本和数据已被删除,因为这是一个愚蠢的组件,就像Content.vue
一样,它的唯一工作是将数据传递给子级,同时保持结构层次结构。 请记住,诸如Highlights.vue
和Content.vue
之类的愚蠢组件的存在是为了保持仪表板的视觉结构与我们编写的代码之间的一致性。
UVIndex.vue
对之前代码所做的更改如下:
- 在
<template>
和<style>
中,div id
已更改为uvIndex
,更具可读性。 - 在
export default {}
中,data()
函数现在返回一个字符串对象uvIndex
,其值是从组件使用props
接收的 highlight 对象中提取的。 此uvIndex
现在临时用于将值显示为<template>
中的文本。 稍后,我们会将这个值插入到适合渲染图表的数据结构中。
可见性.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>
这个文件中唯一的变化(相对于它之前的代码)是data()
函数返回的visibility
对象的定义现在在其末尾包含toString()
,因为从父级接收到的值将是一个浮动的点数,需要转成字符串。
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>
对之前代码所做的更改如下:
- 在整个文件中,
windstatus
已重命名为windStatus
,以提高可读性并与App.vue
提供的带有实际数据的 highlight 对象同步。 - 对速度和方向进行了类似的命名更改——新的命名为
windSpeed
和windDirection
。 - 一个新的对象
derivedWindDirection
开始发挥作用(同样由App.vue
在 Highlights 包中提供)。
目前,接收到的数据呈现为文本; 稍后,它将被插入到可视化所需的数据结构中。
使用虚拟数据进行测试
反复使用虚拟数据可能会让您有些沮丧,但背后有一些很好的理由:
- 我们对每个组件的代码做了很多改动,最好测试一下这些改动是否破坏了代码。 换句话说,我们应该检查数据流是否完整,现在我们即将进入项目的更复杂部分。
- 来自在线天气 API 的真实数据需要大量处理,您可能会在数据采集和处理代码与平滑数据流向组件的代码之间折腾。 这个想法是控制复杂性的数量,以便我们更好地了解我们可能面临的错误。
在本节中,我们所做的基本上是在App.vue
中硬编码一些 json 数据,这些数据显然会在不久的将来被实时数据取代。 虚拟 json 结构与我们将用于实际数据的 json 结构之间有很多相似之处。 因此,它还为您提供了一个粗略的概念,即一旦我们遇到真实数据,可以期待什么。
然而,我们承认这远非从头开始构建这样一个项目时可能采用的理想方法。 在现实世界中,您通常会从真实的数据源开始,玩弄一下以了解可以和应该做些什么来驯服它,然后考虑使用适当的 json 数据结构来捕获相关信息。 我们故意让你远离所有那些肮脏的工作,因为它会让你离目标更远——学习如何使用 Vue.js 和 FusionCharts 来构建仪表板。
现在让我们进入 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>
代码相对于其先前版本所做的更改如下:
- 子组件的名称已更改为dashboard-content,并且相应地修改了
<template>
中的自定义 HTML 元素。 请注意,现在我们有两个属性 -highlights
和tempVar
- 而不是我们之前在自定义元素中使用的单个属性。 因此,与这些属性相关的数据也发生了变化。 有趣的是,我们可以使用v-bind:
指令,或者它的简写:
就像我们在这里所做的那样),以及自定义 HTML 元素的多个属性! -
data()
函数现在返回filename
对象(之前存在的),以及两个新对象(而不是旧的weather_data
):tempVar
和highlights
。 json 的结构适合我们在子组件中编写的代码,以便它们可以从转储中提取所需的数据片段。 这些结构是不言自明的,当我们处理实时数据时,您可以期望它们非常相似。 但是,您将遇到的重大变化是没有硬编码(很明显,不是吗)——我们将值留空作为默认状态,并编写代码以根据我们将从接收到的值动态更新它们天气 API。
您在本节中编写了很多代码,但没有看到实际输出。 在继续之前,请查看浏览器(如有必要,使用npm run dev
重新启动服务器),并享受您的成就的荣耀。 此时您应该看到的网页如下图所示:
数据采集和处理代码
本节将成为项目的核心,所有代码都将在App.vue
中编写,用于以下内容:
- 来自用户的位置输入——一个输入框和一个号召性用语按钮就足够了;
- 各种任务的实用功能; 稍后将在组件代码的各个部分调用这些函数;
- 从 Google Maps API for JavaScript 获取详细的地理位置数据;
- 从 Dark Sky API 获取详细的天气数据;
- 格式化和处理地理定位和天气数据,这些数据将传递给子组件。
以下小节说明了我们如何实现上述各点中为我们布置的任务。 除了一些例外,他们中的大多数人都会遵循这个顺序。
用户输入
很明显,当用户提供需要显示天气数据的地点名称时,该操作就开始了。 为此,我们需要实现以下内容:
- 输入位置的输入框;
- 一个提交按钮,告诉我们的应用程序用户已经输入了该位置,是时候完成剩下的工作了。 我们还将在点击Enter开始处理时实现该行为。
我们在下面展示的代码将被限制在App.vue
的 HTML 模板部分。 我们只会提到与点击事件关联的方法的名称,稍后在 App.vue 的 <script> 的方法对象中定义它们。
<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>
将上面的代码片段放在正确的位置是微不足道的——我们把它留给你。 然而,片段中有趣的部分是:
-
@keyup.enter="organizeAllDetails"
-
@click="organizeAllDetails"
正如您从前面的部分中知道的那样, @
是 Vue 对指令v-on
: 的简写,它与某些事件相关联。 新事物是“ organizeAllDetails
”——它只不过是在事件(按Enter或单击按钮)发生时触发的方法。 我们还没有定义方法,这个谜题将在本节结束时完成。
App.vue 控制的文本信息显示
一旦用户输入触发操作并从 API 获取大量数据,我们就会遇到不可避免的问题——“如何处理所有这些数据?”。 显然需要一些数据按摩,但这并不能完全回答我们的问题! 我们需要决定数据的最终用途是什么,或者更直接地说,哪些实体接收不同的采集和处理数据块?
App.vue
的子组件,基于它们的层次结构和用途,是大量数据的前线竞争者。 但是,我们也会有一些不属于任何这些子组件的数据,但它们的信息量很大,并使仪表板变得完整。 如果我们将它们显示为由App.vue
直接控制的文本信息,我们可以很好地利用它们,而其余的数据则传递给孩子最终显示为漂亮的图表。
考虑到这一点,让我们专注于设置使用文本数据的阶段的代码。 在这一点上,它是一个简单的 HTML 模板,数据最终会出现在它上面。
<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>
在上面的代码片段中,您应该了解以下内容:
-
{{ }}
里面的东西——它们是 Vue 在 HTML 模板中插入动态数据的方式,然后在浏览器中呈现。 你以前遇到过它们,没有什么新鲜的或令人惊讶的。 请记住,这些数据对象源自App.vue
的export default()
对象中的data()
方法。 它们有默认值,我们将根据我们的要求设置,然后编写某些方法来用真实的 API 数据填充对象。
不用担心在浏览器上看不到变化——数据还没有定义,Vue 不渲染它不知道的东西是很自然的。 但是,一旦设置了数据(现在,您甚至可以通过硬编码数据来检查),文本数据将由App.vue
控制。
data()
方法
data()
方法是.vue
文件中的一个特殊结构——它包含并返回对应用程序至关重要的数据对象。 回忆一下任何.vue
文件中<script>
部分的通用结构——它大致包含以下内容:
<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>
到目前为止,您已经遇到了一些数据对象的名称,但还有很多。 它们中的大多数都与子组件相关,每个子组件处理天气信息转储的不同方面。 下面给出了我们在这个项目中需要的整个data()
方法——根据对象的命名,您将对我们期望从 API 获得哪些数据以及我们如何传播数据有一个清晰的认识。
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: '' }, } }; },
如您所见,在大多数情况下,默认值为空,因为此时就足够了。 在渲染或传递给子组件之前,将编写用于操作数据并用适当的值填充数据的方法。
App.vue 中的方法
对于.vue
文件,方法通常写为嵌套在methods { }
对象中的键值。 它们的主要作用是操作组件的数据对象。 我们将在App.vue
中编写方法,牢记相同的理念。 但是,根据它们的用途,我们可以将App.vue
的方法分为以下几类:
- 实用方法
- 面向动作/事件的方法
- 数据采集方法
- 数据处理方法
- 高级胶水方法
理解这一点很重要——我们将这些方法呈现给您,因为我们已经弄清楚了 API 的工作原理、它们提供的数据以及我们应该如何在项目中使用这些数据。 并不是说我们凭空抽出这些方法,并编写了一些晦涩难懂的代码来处理数据。 出于学习的目的,认真阅读和理解方法和数据的代码是一个很好的练习。 但是,当面对一个必须从头开始构建的新项目时,您必须自己完成所有繁琐的工作,这意味着要对 API 进行大量试验——它们的编程访问和数据结构,然后才能将它们与数据无缝粘合您的项目所需的结构。 你不会有任何牵手,也会有令人沮丧的时刻,但这都是成熟的开发人员的一部分。
在下面的小节中,我们将解释每种方法类型,并展示属于该类别的方法的实现。 方法名称对于它们的目的是不言自明的,它们的实现也是如此,我们相信你会发现它们很容易理解。 不过,在此之前,回忆一下.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>
实用方法
顾名思义,实用程序方法是主要为模块化用于边缘任务的重复代码而编写的方法。 必要时通过其他方法调用它们。 下面给出了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; },
虽然我们还没有实现它,但你可以从.vue
文件中取出实用方法,并将其放在一个单独的 JavaScript 文件中。 您需要做的就是在.vue
文件中脚本部分的开头导入.js
文件,然后就可以开始了。 这种方法非常有效,并且可以保持代码干净,尤其是在大型应用程序中,您可能会使用许多根据用途更好地组合在一起的方法。 您可以将此方法应用于本文中列出的所有方法组,并查看效果本身。 但是,我们建议您在完成此处介绍的课程后进行该练习,以便您全面了解所有同步工作的部分,并拥有一个可以参考的工作软件试验时休息。
面向动作/事件的方法
这些方法一般在我们需要对某个事件采取对应的动作时执行。 根据具体情况,事件可能由用户交互触发,也可能以编程方式触发。 在App.vue
文件中,这些方法位于实用程序方法的下方。
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(); },
在上面的一些代码片段中,一件有趣的事情是$ref
的使用。 简单来说,这是 Vue 将包含它的代码语句与它应该影响的 HTML 结构相关联的方式(有关更多信息,请阅读官方指南)。 例如,方法makeInputEmpty()
和detectEnterKeyPress()
会影响输入框,因为在输入框的 HTML 中我们提到了属性ref
的值作为input
。
数据采集方法
我们在项目中使用了以下两个 API:
- 谷歌地图地理编码器 API
此 API 用于获取用户搜索位置的坐标。 您将需要自己的 API 密钥,您可以按照给定链接中的文档获取该密钥。 目前,您可以使用 FusionCharts 使用的 API 密钥,但我们要求您不要滥用它并获取自己的密钥。 我们从这个项目的 index.html 中引用了 JavaScript API,我们将使用它提供的构造函数来处理我们在App.vue
文件中的代码。 - 黑暗天空天气 API
该接口用于获取坐标对应的天气数据。 但是,我们不会直接使用它; 我们将把它包装在一个通过 FusionCharts 的服务器之一重定向的 URL 中。 原因是,如果您从像我们这样的完全客户端应用程序向 API 发送 GET 请求,则会导致令人沮丧的CORS
错误(此处和此处的更多信息)。
重要提示:由于我们使用了 Google Maps 和 Dark Sky API,这两个 API 都有自己的 API 密钥,我们在本文中与您分享了这些密钥。 这将帮助您专注于客户端开发,而不是令人头疼的后端实现。 但是,我们建议您创建自己的密钥,因为我们的 API 密钥会附带限制,如果超过这些限制,您将无法自行尝试应用程序。
对于 Google 地图,请参阅本文以获取您的 API 密钥。 对于 Dark Sky API,请访问 https://darksky.net/dev 以创建您的 API 密钥和相应的端点。
考虑到上下文,让我们看看我们项目的数据采集方法的实现。
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 ); },
高级胶水方法
使用实用程序、获取和处理方法,我们现在剩下的任务是编排整个事情。 我们通过创建高级粘合方法来做到这一点,它基本上以特定顺序调用上面编写的方法,以便整个操作无缝执行。
// 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(); },
安装
Vue 提供了实例生命周期钩子——本质上是方法的属性,并在实例生命周期到达该阶段时被触发。 例如,created、mounted、beforeUpdate 等都是非常有用的生命周期钩子,允许程序员在比其他方式更精细的级别控制实例。
在 Vue 组件的代码中,这些生命周期钩子的实现就像您对任何其他prop
一样。 例如:
<template> </template> <script> // import statements export default { data() { return { // data objects here } }, methods: { // methods here }, mounted: function(){ // function body here }, } </script> <style> </style>
有了这个新的理解,看看下面mounted
的App.vue
的代码:
mounted: async function() { this.location = "New York"; await this.organizeAllDetails(); }
App.vue 的完整代码
我们在本节中介绍了很多内容,最后几节为您提供了一些零碎的东西。 但是,拥有完整的App.vue
组装代码很重要(在后续部分中可能会进一步修改)。 它是这样的:
<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>
最后,经过如此多的耐心和辛勤工作,您可以看到具有原始力量的数据流! 在浏览器上访问应用程序,刷新页面,在应用程序的搜索框中搜索位置,然后按 Enter !
现在我们已经完成了所有繁重的工作,休息一下。 随后的部分重点介绍使用数据创建美观且信息丰富的图表,然后使用 CSS 为我们丑陋的应用程序提供一个当之无愧的修饰会话。
5. FusionCharts 数据可视化
图表的基本考虑
对于最终用户而言,仪表板的本质本质上是这样的:关于特定主题的过滤和精心策划的信息的集合,通过视觉/图形工具进行传达,以便快速获取。 他们不关心您的数据管道工程的微妙之处,也不关心您的代码是否美观——他们想要的只是 3 秒内的高级视图。 因此,我们显示文本数据的粗略应用程序对他们来说毫无意义,现在是我们实施用图表包装数据的机制的时候了。
然而,在我们深入研究图表的实现之前,让我们从我们的角度考虑一些相关的问题和可能的答案:
- 什么类型的图表适合我们正在处理的数据类型?
嗯,答案有两个方面——背景和目的。 通过上下文,我们指的是数据的类型,它总体上适合更大事物的方案,受项目范围和受众的限制。 出于目的,我们本质上是指“我们想要强调什么?”。 例如,我们可以使用柱形图(等宽的垂直柱,高度与柱所代表的值成正比)来表示一天中不同时间的今天温度。 但是,我们很少对单个值感兴趣,而是对整个数据的整体变化和趋势感兴趣。 为了达到目的,使用折线图符合我们的最大利益,我们很快就会这样做。 - 在选择图表库之前应该记住什么?
由于我们正在做一个主要使用基于 JavaScript 的技术的项目,因此我们为项目选择的任何图表库都应该是 JavaScript 世界的原生库。 考虑到这个基本前提,在对任何特定库进行归零之前,我们应该考虑以下几点:- 支持我们选择的框架,在本例中是 Vue.js。 可以在其他流行的 JavaScript 框架(如 React 或 Angular)中开发项目 - 检查图表库对您最喜欢的框架的支持。 此外,必须考虑支持其他流行的编程语言,如 Python、Java、C++、.Net(AS 和 VB),特别是当项目涉及一些严重的后端内容时。
- 图表类型和功能的可用性,因为几乎不可能事先知道项目中数据的最终形状和用途(特别是如果您的客户在专业环境中规定了要求)。 在这种情况下,您应该扩大您的网络,并选择一个拥有最广泛图表集合的图表库。 更重要的是,为了将您的项目与其他项目区分开来,该库应具有足够的可配置图表属性形式的功能,以便您可以微调和自定义图表的大部分方面以及正确的粒度级别。 此外,默认图表配置应该是合理的,并且库文档必须是一流的,原因对专业开发人员来说是显而易见的。
- 还必须考虑学习曲线、支持社区和平衡,尤其是当您不熟悉数据可视化时。 一方面,您拥有像 Tableau 和 Qlickview 这样的专有工具,这些工具成本高昂,学习曲线流畅,但在可定制性、集成和部署方面也有很多限制。 另一方面是 d3.js — 庞大、免费(开源),并且可以对其核心进行定制,但是您必须付出非常陡峭的学习曲线的代价才能使用该库做任何有成效的事情。
您需要的是最佳点——生产力、覆盖范围、可定制性、学习曲线和课程外成本之间的正确平衡。 我们建议您查看 FusionCharts——世界上最全面的企业级 Web 和移动 JavaScript 图表库,我们将在本项目中使用它来创建图表。
FusionCharts简介
FusionCharts 在全球范围内被遍布全球数百个国家的数百万开发人员用作首选 JavaScript 图表库。 从技术上讲,它尽可能地加载和配置,支持将它与几乎所有用于基于 Web 的项目的流行技术堆栈集成。 在商业上使用 FusionCharts 需要许可证,您必须根据您的用例为许可证付费(如果您有兴趣,请联系销售人员)。 然而,我们在这个项目中使用 FusionCharts 只是为了尝试一些东西,因此是非许可版本(在您的图表中带有一个小水印,以及一些其他限制)。 当您尝试图表并将其用于非商业或个人项目时,使用非许可版本非常好。 如果您有商业部署应用程序的计划,请确保您拥有 FusionCharts 的许可。
由于这是一个涉及 Vue.js 的项目,如果之前没有安装,我们将需要安装两个模块:
-
fusioncharts
模块,因为它包含创建图表所需的一切 vue-fusioncharts
模块,本质上是对 fusioncharts 的封装,因此可以在 Vue.js 项目中使用
如果您之前没有安装它们(如第三部分所述),请通过从项目的根目录执行以下命令来安装它们:
npm install fusioncharts vue-fusioncharts --save
接下来,确保项目的src/main.js
文件有以下代码(在第 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) })
上述代码段中最关键的行可能如下:
Vue.use(VueFusionCharts, FusionCharts)
它指示 Vue 使用 vue-fusioncharts 模块来理解项目中的许多东西,这些东西显然不是我们明确定义的,而是在模块本身中定义的。 此外,这种类型的声明意味着全局声明,我们的意思是 Vue 在我们项目的代码中遇到任何奇怪的事情(我们没有明确定义使用 FusionCharts 的事情),它至少会在 vue-fusioncharts 中查找一次和 fusioncharts 节点模块的定义,然后再抛出错误。 如果我们在项目的一个孤立部分中使用 FusionCharts(而不是在几乎所有组件文件中使用它),那么本地声明可能会更有意义。
这样,您就可以在项目中使用 FusionCharts。 我们将使用多种图表,选择取决于我们想要可视化的天气数据的方面。 此外,我们将看到数据绑定、自定义组件和观察者在行动中的相互作用。
在.vue
文件中使用 Fusioncharts 的一般方案
在本节中,我们将解释使用 FusionCharts 在.vue
文件中创建各种图表的总体思路。 但首先,让我们看一下示意性说明核心思想的伪代码。
<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>
让我们了解一下上述伪代码的不同部分:
- 在
<template>
中,在顶级<div>
(这对于每个组件的模板 HTML 代码几乎是强制性的),我们有自定义组件<fusioncharts>
。 我们已经为这个项目安装了vue-fusioncharts
节点模块中包含的组件的定义。 在内部,vue-fusioncharts
依赖于fusioncharts
模块,该模块也已安装。 我们导入了必要的模块并解决了它们的依赖关系,在src/main.js
文件中指示 Vue 全局(整个项目)使用包装器,因此我们使用的自定义<fusioncharts>
组件不缺少定义这里。 此外,自定义组件具有自定义属性,并且每个自定义属性都通过v-bind
指令绑定到一个数据对象(以及它们的值),该指令的简写是冒号 (:
) 符号。 当我们讨论本项目中使用的一些特定图表时,我们将更详细地了解属性及其关联的数据对象。 - 在
<script>
中,首先声明组件应该接收的 props,然后继续定义绑定到<fusioncharts>
属性的数据对象。 分配给数据对象的值是<fusioncharts>
的属性拉入的值,图表是根据拉入的值创建的。 除此之外,代码中最有趣的部分是watch { }
对象。 这是 Vue 方案中的一个非常特殊的对象——它本质上指示 Vue 监视某些数据发生的任何变化,然后根据该数据的handler
函数的定义方式采取行动。 例如,我们希望 Vue 监视接收到的prop
,即伪代码中的data_prop_received_by_the_component
。prop
成为watch { }
对象中的一个键,而该键的值是另一个对象——一个处理方法,描述了当prop
发生变化时需要做什么。 通过这种优雅的机制来处理更改,应用程序保持其反应性。deep: true
表示一个布尔标志,您可以将其与观察者相关联,以便对正在观察的对象进行相当深入的观察,即甚至跟踪对象嵌套级别中所做的更改。
(更多关于watchers的信息,请查阅官方文档)。
现在您已经了解了事物的一般方案,让我们深入了解.vue
组件文件中图表的具体实现。 代码将是不言自明的,您应该尝试了解细节如何适合上述一般方案。
.vue 文件中图表的实现
虽然实现的细节因图表而异,但以下解释适用于所有图表:
-
<template>
如前所述,<fusioncharts>
自定义组件有几个属性,每个属性都通过使用v-bind
: 指令绑定到data()
函数中定义的相应数据对象。 属性名称的含义是不言自明的,找出相应的数据对象也很简单。 -
<script>
在data()
函数中,数据对象及其值是图表工作的原因,因为<fusioncharts>
的属性上使用的v-bind
(:
) 指令完成了绑定。 在我们深入研究单个数据对象之前,值得一提的是一些一般特征:- 值为
0
或1
的数据对象本质上是布尔值,其中0
表示某些东西不可用/关闭,1
表示可用/打开状态。 但是,请注意非布尔数据对象也可以有0
或1
作为它们的值,除了其他可能的值——这取决于上下文。 例如,默认值为0
的containerbackgroundopacity
是布尔值,而默认值为0
的lowerLimit
仅表示数字零是其字面值。 - 一些数据对象处理 CSS 属性,如边距、填充、字体大小等——该值具有“px”或像素的隐含单位。 类似地,其他数据对象可以具有与其值关联的隐式单位。 详细信息请参考FusionCharts开发中心各自的图表属性页面。
- 值为
- 在
data()
函数中,也许最有趣和最不明显的对象是 dataSource。 该对象具有嵌套在其中的三个主要对象:- chart :该对象封装了很多与图表的配置和修饰相关的图表属性。 它几乎是一个强制性的结构,您会在您为此项目创建的所有图表中找到它。
- colorrange :此对象在某种程度上特定于所考虑的图表,主要出现在处理多种颜色/阴影的图表中,以划分图表中使用的比例的不同子范围。
- 值:此对象同样存在于图表中,该图表具有需要在比例范围内突出显示的特定值。
-
watch { }
对象可能是使该图表以及该项目中使用的其他图表栩栩如生的最关键的东西。 图表的反应性,即图表根据新用户查询产生的新值更新自身,由该对象中定义的观察者控制。 例如,我们为组件接收到的 prophighlights
定义了一个观察者,然后定义了一个处理函数来指示 Vue 当整个项目中正在观察的对象发生任何变化时它应该采取的必要行动。 这意味着每当App.vue
为highlights
中的任何对象生成新值时,信息都会一直向下传递到该组件,并且新值会在该组件的数据对象中更新。 绑定到值的图表也会由于这种机制而更新。
上面的解释是相当宽泛的笔触,可以帮助我们对更大的图景有一个直观的理解。 一旦你直观地理解了这些概念,你可以随时查阅 Vue.js 和 FusionCharts 的文档,当你从代码本身不清楚的时候。 我们将练习留给您,从下一小节开始,我们将不再解释我们在本小节中介绍的内容。
src/components/TempVarChart.vue
<template> <div class="custom-card header-card card"> <div class="card-body pt-0"> <fusioncharts type="spline" width="100%" height="100%" dataformat="json" dataEmptyMessage="i-https://i.postimg.cc/R0QCk9vV/Rolling-0-9s-99px.gif" dataEmptyMessageImageScale=39 :datasource="tempChartData" > </fusioncharts> </div> </div> </template> <script> export default { props: ["tempVar"], components: {}, data() { return { tempChartData: { chart: { caption: "Hourly Temperature", captionFontBold: "0", captionFontColor: "#000000", captionPadding: "30", baseFont: "Roboto", chartTopMargin: "30", showHoverEffect: "1", theme: "fusion", showaxislines: "1", numberSuffix: "°C", anchorBgColor: "#6297d9", paletteColors: "#6297d9", drawCrossLine: "1", plotToolText: "$label<br><hr><b>$dataValue</b>", showAxisLines: "0", showYAxisValues: "0", anchorRadius: "4", divLineAlpha: "0", labelFontSize: "13", labelAlpha: "65", labelFontBold: "0", rotateLabels: "1", slantLabels: "1", canvasPadding: "20" }, data: [], }, }; }, methods: { setChartData: function() { var data = []; for (var i = 0; i < this.tempVar.tempToday.length; i++) { var dataObject = { label: this.tempVar.tempToday[i].hour, value: this.tempVar.tempToday[i].temp }; data.push(dataObject); } this.tempChartData.data = data; }, }, mounted: function() { this.setChartData(); }, watch: { tempVar: { handler: function() { this.setChartData(); }, deep: true }, }, }; </script> <style> </style>
src/components/UVIndex.vue
这个组件包含一个非常有用的图表——角度计。
该组件的代码如下所示。 有关 Angular Gauge 图表属性的详细信息,请参阅 Angular Gauge 的 FusionCharts 开发中心页面。
<template> <div class="highlights-item col-md-4 col-sm-6 col-xs-12 border-top"> <div> <fusioncharts :type="type" :width="width" :height="height" :containerbackgroundopacity="containerbackgroundopacity" :dataformat="dataformat" :datasource="datasource" ></fusioncharts> </div> </div> </template> <script> export default { props: ["highlights"], components: {}, data() { return { type: "angulargauge", width: "100%", height: "100%", containerbackgroundopacity: 0, dataformat: "json", datasource: { chart: { caption: "UV Index", captionFontBold: "0", captionFontColor: "#000000", captionPadding: "30", lowerLimit: "0", upperLimit: "15", lowerLimitDisplay: "1", upperLimitDisplay: "1", showValue: "0", theme: "fusion", baseFont: "Roboto", bgAlpha: "0", canvasbgAlpha: "0", gaugeInnerRadius: "75", gaugeOuterRadius: "110", pivotRadius: "0", pivotFillAlpha: "0", valueFontSize: "20", valueFontColor: "#000000", valueFontBold: "1", tickValueDistance: "3", autoAlignTickValues: "1", majorTMAlpha: "20", chartTopMargin: "30", chartBottomMargin: "40" }, colorrange: { color: [ { minvalue: "0", maxvalue: this.highlights.uvIndex.toString(), code: "#7DA9E0" }, { minvalue: this.highlights.uvIndex.toString(), maxvalue: "15", code: "#D8EDFF" } ] }, annotations: { groups: [ { items: [ { id: "val-label", type: "text", text: this.highlights.uvIndex.toString(), fontSize: "20", font: "Source Sans Pro", fontBold: "1", fillcolor: "#212529", x: "$gaugeCenterX", y: "$gaugeCenterY" } ] } ] }, dials: { dial: [ { value: this.highlights.uvIndex.toString(), baseWidth: "0", radius: "0", borderThickness: "0", baseRadius: "0" } ] } } }; }, methods: {}, computed: {}, watch: { highlights: { handler: function() { this.datasource.colorrange.color[0].maxvalue = this.highlights.uvIndex.toString(); this.datasource.colorrange.color[1].minvalue = this.highlights.uvIndex.toString(); this.datasource.annotations.groups[0].items[0].text = this.highlights.uvIndex.toString(); }, deep: true } } }; </script>
src/components/Visibility.vue
在这个组件中,我们使用一个水平线性仪表来表示可见性,如下图所示:
该组件的代码如下所示。 要深入了解该图表类型的不同属性,请参阅 FusionCharts 开发中心页面的水平线性仪表。
<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
该组件显示风速和风向(风速,如果您精通物理学的话),并且很难使用图表来表示矢量。 对于这种情况,我们建议使用一些漂亮的图像和文本值来表示它们。 由于我们考虑的表示完全依赖于 CSS,我们将在下一节处理 CSS 中实现它。 但是,看看我们的目标是创造什么:
<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>
总结 Highlights.vue
回想一下,我们已经为所有组件实现了 CSS 代码——除了Content.vue
和Highlights.vue
。 由于Content.vue
是一个只中继数据的哑组件,它所需的最小样式已经涵盖。 此外,我们已经编写了适当的代码来设置侧边栏和包含图表的卡片的样式。 因此,我们剩下要做的就是在Highlights.vue
中添加一些风格位,这主要涉及使用 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>
部署和源代码
按顺序排列图表和样式,我们就完成了! 花点时间欣赏您的创作之美。
现在是您部署应用程序并与同行共享的时候了。 如果您对部署不太了解并希望我们为您提供帮助,请在此处查看我们对部署想法的看法。 链接的文章还包含有关如何删除每个图表左下角的 FusionCharts 水印的建议。
如果你在某个地方搞砸了并想要一个参考点,可以在 Github 上找到源代码。