• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

使用TypeScript和NativeScript创建天气应用程序

在本教程中,我将向您展示如何使用打字稿语言在 nativescript 中构建天气应用程序。 

在本系列的上一篇文章中,我们使用纯javascript 创建了一个笔记应用程序。这次我们将使用 typescript。首先让我们找出为什么 TypeScript 是构建 NativeScript 应用程序的好选择。 

1.为什么选择 TypeScript?

TypeScript 是 NativeScript 中的首选。核心 NativeScript 团队使用它来构建 NativeScript框架本身。以下是您希望使用 TypeScript 开发 NativeScript 应用程序的几个原因:

  • TypeScript 编译为 JavaScript。当编译器运行时,它会捕获您代码中可能存在的任何错误,以便您可以立即对它们采取行动,而无需等待 NativeScript 编译器完成。这意味着您作为开发人员的工作效率更高。  

  • TypeScript 允许您使用 ES6 功能,例如类、模块、箭头函数、模板文字等等。这意味着您可以使用更多工具来组织和编写更好的代码。

如果我在说服您方面做得不好,或者您想了解更多关于 TypeScript 为何适合使用 NativeScript 开发的信息,您可以查看使用 TypeScript 构建更好的 NativeScript 应用程序。

2.工装

为了充分利用 TypeScript 提供的功能,我建议您使用Visual Studio Code 文本编辑器。它具有 IntelliSense 功能,可在您编写 TypeScript 代码时提供智能自动完成功能,它与 Git 集成,并且还具有调试功能。 

最重要的是,还有一个NativeScript 插件 ,可以让您在开发 NativeScript 应用程序时更有效率。我发现一个有用的功能是模拟器集成。这允许您直接从文本编辑器运行模拟器并从文本编辑器本身调试应用程序。Visual Studio Code 是免费的,可在所有主要平台(Windows、Linux、OS X)上使用。 

但是,如果您不想离开舒适的文本编辑器,您还可以安装扩展程序,以更好地使用 TypeScript 进行编码。对于 Atom,有atom-typescript 插件。对于 Sublime Text,有TypeScript Sublime 插件。 

3.应用概述

我们要创建的应用程序是一个天气应用程序。它将具有以下页面: 

  • 一个显示当前天气以及温度、气压和湿度等相关信息的主页。

  • 一个预测页面,显示未来五天天气的五天预测。

以下是主页的外观:

使用TypeScript和NativeScript创建天气应用程序  第1张

这是预测页面:

使用TypeScript和NativeScript创建天气应用程序  第2张

您可以在其GitHub 存储库中找到此应用程序的完整源代码。

4.打开天气地图

天气数据将来自OpenWeatherMap api,就像任何其他api一样,您需要注册一个 API 密钥才能使用它。去注册个账号吧,我等着。登录后,转到API 密钥页面,复制密钥 字段的值,并将其保存在安全的地方。稍后,一旦您开始创建应用程序,您将需要它。

使用TypeScript和NativeScript创建天气应用程序  第3张

5.创建应用程序

现在您知道应用程序的外观,是时候开始实际创建它了。首先创建一个使用 TypeScript 模板的新 NativeScript 项目:

tns create weatherApp --template typescript

完成后,导航到 app 文件夹并创建以下文件夹和文件。为方便起见,您还可以下载或克隆GitHub存储库并从app文件夹中复制文件。

- common
    + constants.ts
    + navigation.ts
    + requestor.ts
    + utilities.ts
- fonts
- pages
    + forecast
        * forecast.css
        * forecast.ts
        * forecast.xml
        * forecast-view-model.ts
    + main
        * main.css
        * main.ts
        * main.xml
        * main-view-model.ts
- stores
    + location.ts
- app.css
- app.ts

我们只会在app 目录中工作,所以每次我引用文件路径或文件夹时,都假设该 app 目录是根目录。

安装依赖项

该应用程序需要几个依赖项:NativeScript 地理定位模块 和Moment。您可以使用以下命令安装 Geolocation 模块:

tns plugin add nativescript-geolocation

并安装 Moment:

npm install moment

地理定位模块用于确定用户的当前位置。Moment 允许轻松格式化 Unix 时间戳,稍后我们将从 API 中获取。

通用模块

在我们查看应用程序每个页面的代码之前,让我们先看看将在整个应用程序中使用的自定义模块。

常数

常量模块 ( common/constants.ts) 包含整个应用程序中使用的所有常量值:例如 OpenWeatherMap API 的基本 URL、您之前获得的 API 密钥、我们将使用的端点的路径、天气的字符代码图标和风向。

export const WEATHER_URL = 'http://api.openweathermap.org/data/2.5/';
export const WEATHER_APIKEY = 'YOUR OPENWEATHERMAP API KEY';
export const CURRENT_WEATHER_PATH = 'weather/';
export const WEATHER_FORECAST_PATH = 'forecast/daily/';
 
export const WEATHER_ICONS = {
  day: {
    'clear': 0xf00d,
    'clouds': 0xf002,
    'drizzle': 0xf009,
    'rain': 0xf008,
    'thunderstorm': 0x010,
    'snow': 0xf00a,
    'mist': 0xf0b6
  },
  night: {
    'clear': 0xf02e,
    'clouds': 0xf086,
    'drizzle': 0xf029,
    'rain': 0xf028,
    'thunderstorm': 0xf02d,
    'snow': 0xf02a,
    'mist': 0xf04a
  },
  neutral: {
    'temperature': 0xf055,
    'wind': 0xf050,
    'cloud': 0xf041,
    'pressure': 0xf079,
    'humidity': 0xf07a,
    'rain': 0xf019,
    'sunrise': 0xf046,
    'sunset': 0xf052
  }
};
 
export const WIND_DIRECTIONS = [
  "North", "North-northeast", "Northeast",
  "East-northeast", "East", "East-southeast", "Southeast",
  "South-southeast", "South", "South-southwest", "Southwest",
  "West-southwest", "West", "West-northwest", "Northwest", "North-northwest"
];

实用程序

实用程序模块包括各种实用功能:例如将度数转换为方向、确定风速的描述性文本、将开尔文转换为摄氏度以及将字符代码转换为字符。您将在后面的页面中看到如何使用所有这些功能。 

import constants = require('./constants');
 
export function degreeToDirection(num) {
  var val= Math.floor((num / 22.5) + .5);
  return constants.WIND_DIRECTIONS[(val % 16)];
}
 
export function describeWindSpeed(speed) {
  if(speed < 0.3) {
    return 'calm';
  } else if(speed >= 0.3 && speed < 1.6) {
    return 'light air';
  } else if (speed >= 1.6 && speed < 3.4) {
    return 'light breeze';
  } else if (speed >= 3.4 && speed < 5.5) {
    return 'gentle breeze';
  } else if (speed >= 5.5 && speed < 8) {
    return 'moderate breeze';
  } else if(speed >= 8 && speed < 10.8) {
    return 'fresh breeze';
  } else if(speed >= 10.8 && speed < 13.9) {
    return 'strong breeze';
  } else if(speed >= 13.9 && speed < 17.2) {
    return 'moderate gale';
  } else if (speed >= 17.2 && speed < 20.8) {
    return 'gale';
  } else if (speed >= 20.8 && speed < 24.5) {
    return 'strong gale';
  } else if (speed >= 24.5 && speed < 28.5) {
    return 'storm';
  } else if (speed >= 28.5 && speed < 32.7) {
    return 'violent storm';
  } else if (speed >= 32.7 && speed < 42) {
    return 'hurricane force';
  }
  return 'super typhoon';
}
 
export function describeHumidity(humidity) {
  if (humidity >= 0 && humidity <= 40) {
    return 'very dry';
  } else if (humidity >= 40 && humidity <= 70) {
    return 'dry';
  } else if (humidity >= 85 && humidity <= 95) {
    return 'humid';
  }
  return 'very humid';
}
 
export function describeTemperature(temp) {
  var celsius = convertKelvinToCelsius(temp);
  if (celsius >= 0 && celsius < 7) {
    return 'very cold';
  } else if (celsius >= 8 && celsius < 13) {
    return 'cold';
  } else if (celsius >= 13 && celsius < 18) {
    return 'cool';
  } else if (celsius >= 18 && celsius < 23) {
    return 'mild';
  } else if (celsius >= 23 && celsius < 28) {
    return 'warm';
  } else if (celsius >= 28 && celsius < 32) {
    return 'hot';
  }
  return 'very hot';
}
 
export function convertKelvinToCelsius(celsius) {
  return celsius - 273.15;
}
 
export function getTimeOfDay() {
  var hour = (new Date()).getHours();
  var time_of_day = 'night';
  if(hour >= 5 && hour <= 18){
    time_of_day = 'day';
  }
  return time_of_day;
}
 
export function getIcons(icon_names) {
  var icons = icon_names.map((name) => {
    return {
      'name': name,
      'icon': String.fromCharCode(constants.WEATHER_ICONS.neutral[name])
    };
  });
  return icons;
}

导航

导航模块是一个自定义帮助模块,它允许我们在应用程序的所有页面之间轻松导航。打开 common/navigation.ts 文件并添加以下内容:

import frame = require('ui/frame');
 
export function getStartPage() {
  return 'pages/main/main';
}
 
export function goToForecastPage() {
  frame.topmost().navigate('pages/forecast/forecast');
}
 
export function goToMainPage() {
  frame.topmost().goBack();
}

这使用Frame 模块 导航到应用程序的其他页面。该 getStartPage() 方法只返回应用程序主页面的位置。goToForecastPage()顾名思义,它允许用户导航到预测页面 。

在 NativeScript 中导航时,您需要参考当前所在的位置。这就是为什么您首先需要调用该topmost() 函数来获取当前或最上面的页面,然后再调用该navigate() 函数以转到另一个页面。这个函数接受你想去的页面的路径。 

请求者

该Requestor模块执行对 OpenWeatherMap API 的实际请求。正如NativeScript 简介一文中提到的, NativeScript使用 javaScript 虚拟机来运行 JavaScript 代码。这意味着我们也可以使用浏览器中可用的功能。 

一个这样的功能是fetch,它允许我们向远程服务器发出 HTTP 请求。该参数是您要发出请求的 URL。它返回一个承诺,因此我们then() 用来等待原始响应。注意“原始”一词的使用;该fetch函数返回带有标头和其他低级信息的响应——这就是为什么我们需要调用该json() 函数来获取实际的 JSON 数据。这将返回另一个承诺,因此我们再使用then() 一次来获取实际对象。

export function get(url){
  return fetch(
    url
  ).then(function(response){
    return response.json();
  }).then(function(json){
    return json;
  });
}

或者,您可以使用Http 模块,这是在 NativeScript 中发出 HTTP 请求的一种更强大的方式。

位置 商店

位置存储用作位置信息的存储。这允许我们从任何导入此模块的文件中更新并获取当前位置。

export var location;
 
export function saveLocation(loc) {
  location = loc;
}
 
export function getLocation() {
  return location;
}

主页

现在是时候查看应用程序每个页面的代码了。但在我们这样做之前,首先打开入口点文件 ( app.ts)。这使用导航模块来获取应用程序的起始页面:

import application = require("application");
import navigation = require('./common/navigation');
application.mainModule = navigation.getStartPage();
application.start();

接下来,让我们分解pages/main/main.xml 文件。

每次用户导航到此特定页面时,该navigatingTo 事件用于在 TypeScript 文件中执行类似命名的函数。CSS 类也是由 TypeScript 文件动态确定的。

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="{{ background_class }}">
...
</Page>

该ScrollView组件 用于包装所有内容,以便在内容超出屏幕大小可以显示的范围时自动生成垂直滚动条。 

而且因为我们要从远程服务器加载数据,所以该ActivityIndicator组件用于显示平台的默认加载动画。这需要您提供一个busy 属性,该属性接受一个布尔值来控制是否启动动画。默认情况下,它设置为true 并且仅false 在应用程序完成向服务器发出请求后才更新。 

该visibility 属性还用于确保组件在未设置动画时不占用任何空间。  

<StackLayout>
  <ScrollView>
    <ActivityIndicator busy="{{ is_loading }}" visibility="{{ is_loading ? 'visible' : 'collapsed' }}" />
    <StackLayout visibility="{{ !is_loading ? 'visible' : 'collapsed' }}">
      ...
    </StackLayout>
  </ScrollView>
</StackLayout>

对于主要内容,我们在顶部有当前天气的总体概述,在其下方有详细信息。总览显示代表当前天气、当前温度、天气描述和地点的图标。

<Label text="{{ icon }}" class="icon" />
<Label text="{{ temperature }}" class="temperature" />
<Label text="{{ weather }}" class="weather" textWrap="true"/>
<Label text="{{ place }}" class="place" textWrap="true"/>

对于细节,有一大堆关于当前天气的信息,你可以通过查看text 属性来猜测。每一个都伴随着自己的图标。 

在我之前向您展示的屏幕截图中,您看到它对两个页面都使用了两列布局。这就是我们使用GridLayout. 但正如您从下面的代码中看到的那样,我们也将 aGridLayout 用于每一行的第一列。 

您可能会问我们为什么要这样做,而不是创建一个三列布局,第一列是图标,第二列是天气属性,第三列是值。这是完全可以理解的,它会使代码更简洁。 

但问题是 NativeScript 2.1 版目前不允许其 GridLayout. 这意味着我们不能10%对图标使用类似的东西,而其他两列各消耗 45%。 

我们在下面使用的布局通过使用 aGridLayout 来包装图标和天气属性来解决该问题,其中图标消耗 30 像素,而天气属性消耗包含文本所需的空间量。还要注意在 the 上使用row andcol 属性GridLayout 。

<GridLayout columns="*,*" rows="auto,auto,auto,auto,auto,auto,auto" cssClass="details">
 
  <GridLayout columns="30,auto" rows="auto" row="0" col="0">
    <Label text="{{ wind_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Wind" textWrap="true" row="0" col="1" class="label" />
  </GridLayout>
  <Label text="{{ wind }}" textWrap="true" row="0" col="1" />
 
  <GridLayout columns="30,auto" rows="auto" row="1" col="0">
    <Label text="{{ cloud_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Cloudiness" textWrap="true" row="1" col="1" class="label" />
  </GridLayout>
  <Label text="{{ clouds }}" textWrap="true" row="1" col="1" />
 
  <GridLayout columns="30,auto" rows="auto" row="2" col="0">
    <Label text="{{ pressure_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Pressure" textWrap="true" row="2" col="1" class="label" />
  </GridLayout>
  <Label text="{{ pressure }}" textWrap="true" row="2" col="1" />
 
  <GridLayout columns="30,auto" rows="auto" row="3" col="0">
    <Label text="{{ humidity_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Humidity" textWrap="true" row="3" col="1" class="label" />
  </GridLayout>
  <Label text="{{ humidity }}" textWrap="true" row="3" col="1" />
 
  <GridLayout columns="30,auto" rows="auto" row="4" col="0">
    <Label text="{{ rain_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Rain" textWrap="true" row="4" col="1" class="label" />
  </GridLayout>
  <Label text="{{ rain }}" textWrap="true" row="4" col="1" />
 
  <GridLayout columns="30,auto" rows="auto" row="5" col="0">
    <Label text="{{ sunrise_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Sunrise" textWrap="true" row="5" col="1" class="label" />
  </GridLayout>
  <Label text="{{ sunrise }}" textWrap="true" row="5" col="1" />
 
  <GridLayout columns="30,auto" rows="auto" row="6" col="0">
    <Label text="{{ sunset_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Sunset" textWrap="true" row="6" col="1" class="label" />
  </GridLayout>
  <Label text="{{ sunset }}" textWrap="true" row="6" col="1" />
   
</GridLayout>

主页的最后一个标记是通向预测页面的按钮:

<Button text="5 day Forecast" tap="goToForecastPage" />

主页 JavaScript

打开pages/main/main.ts 文件并添加以下代码:

import { eventData } from "data/observable";
import { Page } from "ui/page";
import { MainViewModel } from "./main-view-model";
import navigation = require('../../common/navigation');
 
export function navigatingTo(args: EventData) {
  var page = <Page>args.object;
  page.bindingContext = new MainViewModel();
}
 
export function goToForecastPage () {
  navigation.goToForecastPage();
}

在上面的代码中,我们导入了几个内置的 NativeScript 模块、主视图模型和导航。 

对象是使用对象解构EventData 提取的,这是 TypeScript 提供的 ES6 功能。这 是我们作为参数传递给 函数的内容,以便它可以访问导航到该页面的任何页面传入的任何数据。 EventDatanavigatingTo

这有一个object 属性,基本上是触发事件的任何组件。在这种情况下,我们知道它是在Page组件上触发的,这就是我们使用<Page>type -assertion的原因。之后,我们将主视图模型绑定到页面。这将允许我们在类中添加或更新属性,它会立即反映在页面上。

在主视图模型 ( pages/main/main-view-model.ts) 中,首先导入我们将使用的所有模块:

import observable = require("data/observable");
import requestor = require("../../common/requestor");
import constants = require("../../common/constants");
import geolocation = require("nativescript-geolocation");
import moment = require('moment');
import utilities = require('../../common/utilities');
import locationStore = require('../../stores/locationStore');

通过扩展 Observable 模块创建视图模型,这使得该类中的所有属性都可观察。这意味着每次在此类中更改时,UI 中每个属性的所有引用都会更新。

export class MainViewModel extends observable.Observable {
   
  constructor() {
    ...
  }
 
}

在构造函数中,检查是否启用了地理定位。如果未启用,请尝试通过调用该enableLocationRequest() 函数来启用它。这会触发应用程序要求用户启用地理定位。 

super(); //call the constructor method of the parent class
//check if geolocation is not enabled
if (!geolocation.isEnabled()) {
  geolocation.enableLocationRequest(); //try to enable geolocation
}

接下来,判断是白天还是晚上,根据结果设置页面背景。然后设置页面中的图标。

var time_of_day = utilities.getTimeOfDay();
this.set('background_class', time_of_day);
this.setIcons();

之后,尝试确定当前位置。请注意,如果用户不允许地理定位,则应用程序将根本无法运行,因为天气取决于用户的位置。该应用程序将尝试在 10 秒内确定用户的位置。如果没有这样做,则会向用户显示一条错误消息。

var location = geolocation.getCurrentLocation({timeout: 10000}).
  then(
    (loc) => {
      if (loc) {
        ...
      }
    },
    (e) => {
      //failed to get location
      alert(e.message);
    }
);

如果位置请求成功,我们使用locationStore. 这使我们以后无需再次请求即可访问其他页面上的位置。

locationStore.saveLocation(loc);

供您参考,这是您在 NativeScript 中请求位置时可能会收到的示例响应。您可以查看 NativeScript 的Location 文档 ,以了解有关以下每个属性的更多信息。

{  
   "latitude":51.50853,
   "longitude":-0.12574,
   "altitude":0,
   "horizontalAccuracy":37.5,
   "verticalAccuracy":37.5,
   "speed":0,
   "direction":0,
   "timestamp":"2016-08-08T02:25:45.252Z",
   "android":{  
 
   }
}

我们可以使用模板文字构造完整的 API 请求 URL  ,并使用 Requestor 模块发出请求。

this.set('is_loading', true); //show the loader animation
var url = `${constants.WEATHER_URL}${constants.CURRENT_WEATHER_PATH}?lat=${loc.latitude}&lon=${loc.longitude}&apikey=${constants.WEATHER_APIKEY}`;
requestor.get(url).then((res) => {
  ...
});

一旦响应返回,提取并格式化它,然后将结果值设置为类的属性。并且因为类是可观察的,这将自动更新应用程序的 UI。

this.set('is_loading', false); //stop loader animation
var weather = res.weather[0].main.toLowerCase();
var weather_description = res.weather[0].description;
 
var temperature = res.main.temp;
var icon = constants.WEATHER_ICONS[time_of_day][weather];
 
var rain = '0';
if(res.rain){
  rain = res.rain['3h'];
}
 
this.set('icon', String.fromCharCode(icon));
this.set('temperature', `${utilities.describeTemperature(Math.floor(temperature))} (${utilities.convertKelvinToCelsius(temperature).toFixed(2)} °C)`);
this.set('weather', weather_description);
this.set('place', `${res.name}, ${res.sys.country}`);
this.set('wind', `${utilities.describeWindSpeed(res.wind.speed)} ${res.wind.speed}m/s ${utilities.degreeToDirection(res.wind.deg)} (${res.wind.deg}°)`);
this.set('clouds', `${res.clouds.all}%`);
this.set('pressure', `${res.main.pressure} hpa`);
this.set('humidity', `${utilities.describeHumidity(res.main.humidity)} (${res.main.humidity}%)`);
this.set('rain', `${rain}%`);
this.set('sunrise', moment.unix(res.sys.sunrise).format('hh:mm a'));
this.set('sunset', moment.unix(res.sys.sunset).format('hh:mm a'));

供您参考,以下是 API 可能返回的示例响应:

{  
   "coord":{  
      "lon":-0.13,
      "lat":51.51
   },
   "weather":[  
      {  
         "id":803,
         "main":"Clouds",
         "description":"broken clouds",
         "icon":"04d"
      }
   ],
   "base":"cmc stations",
   "main":{  
      "temp":291.44,
      "pressure":1031.7,
      "humidity":82,
      "temp_min":290.37,
      "temp_max":292.25
   },
   "wind":{  
      "speed":0.3,
      "deg":45,
      "gust":1
   },
   "rain":{  
      "3h":0.075
   },
   "clouds":{  
      "all":68
   },
   "dt":1470545747,
   "sys":{  
      "type":3,
      "id":1462694692,
      "message":0.0043,
      "country":"GB",
      "sunrise":1470544455,
      "sunset":1470598626
   },
   "id":2643743,
   "name":"London",
   "cod":200
}

您可以在当前天气数据的文档中找到有关每个属性的详细信息。

最后。有一个setIcons() 函数,它设置页面中使用的所有图标:

setIcons() {
  var icons = utilities.getIcons([
    'temperature', 'wind', 'cloud',
    'pressure', 'humidity', 'rain',
    'sunrise', 'sunset'
  ]);
  icons.forEach((item) => {
    this.set(`${item.name}_icon`, item.icon);
  });
}

主页样式和图标

以下是主页的样式:

.temperature {
  font-size: 40px;
  font-weight: bold;
  text-align: center;
}
 
.weather {
  font-size: 30px;
  text-align: center;
}
 
.place {
  font-size: 20px;
  text-align: center;
}
 
.icon {
  font-family: 'weathericons';
  font-size: 100px;
  text-align: center;
}
 
.small-icon {
  font-family: 'weathericons';
  font-size: 18px;
  margin-right: 5px;
}
 
.details {
  margin-top: 20px;
  padding: 30px;
  font-size: 18px;
}
 
.label {
  font-weight: bold;
}
 
.details Label {
  padding: 5px 0;
}
 
Button {
  margin: 20px;
}

注意 和 类weathericons 的font-family 使用。这就是我们在 NativeScript 中使用图标字体的方式。如果您喜欢 网页上的 Font Awesome等图标字体,您可以在 NativeScript 应用程序中以同样的方式使用它们。iconsmall-icon

首先,下载您要使用的图标字体。对于这个应用程序,使用天气图标字体 。提取 zip 存档并在提取的文件夹中转到该font 目录。将.ttf文件复制到fonts 应用程序中的目录并将其重命名为weathericons.ttf. 文件名是您font-family 每次要使用该特定字体图标时使用的值。除此之外,您还必须添加font-size 来控制图标的大小。

预测页面

现在让我们看一下预测页面 ( pages/forecast/forecast.xml) 的标记。在标题中,有一个返回按钮,允许用户返回主页。Button 请注意,我们使用的 不是通用 组件,而是ios后退按钮和 Android 导航按钮 NavigationButton的 NativeScript 等价物。

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="{{ background_class }}">
  <Page.actionBar>
    <ActionBar title="5-day Forecast" class="header">
      <NavigationButton text="Back" android.systemIcon="ic_menu_back" tap="goToMainPage" />
    </ActionBar>
  </Page.actionBar>
  ...
</Page>

对于主要内容,使用Repeater 组件而不是通常的ListView. 这两个组件都可以用来生成一个列表,但ListView 带有更多的花里胡哨。例如,当列表超出屏幕大小时,它会自动生成一个垂直滚动条。无限滚动功能 也是内置的。 

在这种情况下使用该Repeater 组件是因为没有真正需要我刚才提到的功能。我们所需要的只是一个简单的清单。

<StackLayout>
  <ScrollView>
    <ActivityIndicator busy="{{ is_loading }}" visibility="{{ is_loading ? 'visible' : 'collapsed' }}" />
    <Repeater items="{{ forecast }}">
      <Repeater.itemTemplate>
       ...
      </Repeater.itemTemplate>
    </Repeater>
  </ScrollView>
</StackLayout>

每个里面都有Repeater.itemTemplate 两GridLayout 列,一列用于一般天气信息,另一列用于详细信息。 

第一列StackLayout 包含日期、天气图标和天气描述。第二列也是一个StackLayout 包含四个的列GridLayouts ,它将包含四个天气属性(温度、风速、云量和气压)。 

第一个 GridLayout 包含三列用于包含图标、白天温度和夜间温度。其他三行只有两列——图标和天气属性的值。

<GridLayout class="item" columns="*,*" rows="auto">
  <StackLayout class="day-weather" row="0" col="0">
    <Label text="{{ day }}" class="date" />
    <Label text="{{ icon }}" class="icon" />
    <Label text="{{ description }}" textWrap="true" />
  </StackLayout>
 
  <StackLayout class="details" row="0" col="1">
    <GridLayout columns="30,auto,auto" rows="auto" row="0" col="0">
      <Label text="{{ $parents['Page'].temperature_icon }}" class="small-icon" row="0" col="0" />
      <Label text="{{ temperature.day }}" class="temp day-temp" row="0" col="1" />
      <Label text="{{ temperature.night }}" class="temp night-temp" row="0" col="2" />
    </GridLayout>
 
    <GridLayout columns="30,auto" rows="auto" row="1" col="0">
      <Label text="{{ $parents['Page'].wind_icon }}" class="small-icon" row="0" col="0" />
      <Label text="{{ wind }}" row="0" col="1" />
    </GridLayout>
 
    <GridLayout columns="30,auto" rows="auto" row="2" col="0">
      <Label text="{{ $parents['Page'].cloud_icon }}" class="small-icon" row="0" col="0" />
      <Label text="{{ clouds }}" row="0" col="1" />
    </GridLayout>
 
    <GridLayout columns="30,auto" rows="auto" row="3" col="0">
      <Label text="{{ $parents['Page'].pressure_icon }}" class="small-icon" row="0" col="0" />
      <Label text="{{ pressure }}" row="0" col="1" />
    </GridLayout>
 
  </StackLayout>
</GridLayout>

注意使用$parents['Page']. 使用Repeater orListView 组件时,您无法访问您为列表指定使用的数组之外的数据——除非您明确指定数据可用的父组件。这就是$parents['Page'] 出现的地方。 $parents 是 NativeScript 中的一个特殊变量,它允许您访问特定组件上可用的数据。在这种情况下,我们指定 Page 访问每个天气属性的图标。

<GridLayout columns="30,auto" rows="auto" row="1" col="0">
  <Label text="{{ $parents['Page'].wind_icon }}" class="small-icon" row="0" col="0" />
  <Label text="{{ wind }}" row="0" col="1" />
</GridLayout>

预测页面 JavaScript

预测页面的代码与主页的代码几乎相同。唯一的区别是导航功能用于返回主页面,我们使用的ForecastViewModel 是视图模型。

import { EventData } from "data/observable";
import { Page } from "ui/page";
import { ForecastViewModel } from "./forecast-view-model";
import navigation = require('../../common/navigation');
 
export function navigatingTo(args: EventData) {
  var page = <Page>args.object;
  page.bindingContext = new ForecastViewModel();
}
 
export function goToMainPage() {
  navigation.goToMainPage();
}

这是视图模型 ( pages/forecast/forecast-view-model.ts) 的代码:

import observable = require("data/observable");
import requestor = require("../../common/requestor");
import constants = require("../../common/constants");
import moment = require('moment');
import utilities = require('../../common/utilities');
import locationStore = require('../../stores/locationStore');
 
export class ForecastViewModel extends observable.Observable {
 
  constructor() {
    super();
    var location = locationStore.getLocation();
    var url = `${constants.WEATHER_URL}${constants.WEATHER_FORECAST_PATH}?cnt=6&lat=${location.latitude}&lon=${location.longitude}&apikey=${constants.WEATHER_APIKEY}`;
 
    var time_of_day = utilities.getTimeOfDay();
    this.set('is_loading', true);
    this.set('background_class', time_of_day);
    this.setIcons();
 
    requestor.get(url).then((response) => {
      this.set('is_loading', false);
      var forecast = this.getForecast(response);
      this.set('forecast', forecast);
    });
  }
 
  private getForecast(response) {
    var forecast = [];
    var list = response.list.splice(1); //remove first item from array of forecasts
    //format and push all the necessary data into a new array
    list.forEach((item) => {
      forecast.push({
        day: moment.unix(item.dt).format('MMM DD (ddd)'),
        icon: String.fromCharCode(constants.WEATHER_ICONS['day'][item.weather[0].main.toLowerCase()]),
        temperature: {
          day: `${utilities.describeTemperature(item.temp.day)}`,
          night: `${utilities.describeTemperature(item.temp.night)}`
        },
        wind: `${item.speed}m/s`,
        clouds: `${item.clouds}%`,
        pressure: `${item.pressure} hpa`,
        description: item.weather[0].description
      })
    });
 
    return forecast;
  }
 
  private setIcons() {
    var icons = utilities.getIcons(['temperature', 'wind', 'cloud', 'pressure']);
    icons.forEach((item) => {
      this.set(`${item.name}_icon`, item.icon);
    });
  }
 
}

在构造函数中,我们从位置存储中获取当前位置,并构造16 天天气预报的 URL 端点。但我们只需要 5 天而不是 16 天,因此我们指定 6 为计数 ( cnt)。我们使用 6 是因为时区取决于服务器而不是指定的位置。这意味着 API 有可能返回前一天或当天的天气。这就是为什么有一个额外的 1 天作为填充。

var location = locationStore.getLocation();
var url = `${constants.WEATHER_URL}${constants.WEATHER_FORECAST_PATH}?cnt=6&lat=${location.latitude}&lon=${location.longitude}&apikey=${constants.WEATHER_APIKEY}`;

接下来,通过调用 getForecast() 函数发出请求并使用 API 响应更新 UI:

requestor.get(url).then((response) => {
  this.set('is_loading', false);
  var forecast = this.getForecast(response);
  this.set('forecast', forecast);
});

这是 16 天预测端点返回的示例响应。请注意,为了使示例更简洁,我将计数设置为 1,这就是该list 属性仅包含一个对象的原因。

{  
   "city":{  
      "id":2643743,
      "name":"London",
      "coord":{  
         "lon":-0.12574,
         "lat":51.50853
      },
      "country":"GB",
      "population":0
   },
   "cod":"200",
   "message":0.0347,
   "cnt":1,
   "list":[  
      {  
         "dt":1470571200,
         "temp":{  
            "day":24.69,
            "min":17.37,
            "max":24.69,
            "night":17.37,
            "eve":23.29,
            "morn":19.02
         },
         "pressure":1029.77,
         "humidity":0,
         "weather":[  
            {  
               "id":500,
               "main":"Rain",
               "description":"light rain",
               "icon":"10d"
            }
         ],
         "speed":8.27,
         "deg":253,
         "clouds":0
      }
   ]
}

预测页面样式

以下是预测页面 ( pages/forecast/forecast.css) 的样式:

Page {
  font-size: 15px;
}
 
.item {
  padding: 20px 10px;
}
 
.day-weather {
  text-align: center;
}
 
.details {
  horizontal-align: left;
}
 
.date {
  font-size: 20px;
}
 
.icon {
  font-family: 'weathericons';
  font-size: 30px;
}
 
.temp {
  padding: 3px;
  text-align: center;
  font-size: 15px;
}
 
.day-temp {
  background-color: #d0c110;
}
 
.night-temp {
  background-color: #505050;
}
 
.small-icon {
  font-family: 'weathericons';
  margin-right: 5px;
  text-align: center;
}

全局应用样式

打开app.css 文件并添加以下样式:

.header {
  background-color: #333;
  color: #fff;
}
 
.day {
  background-color: #f48024;
  color: #fff;
}
 
.night {
  background-color: #924da3;
  color: #fff;
}

6.更改默认应用程序图标

您可以通过转到App_Resources文件夹来更改默认应用程序图标。在那里您可以看到一个 Android 和 iOS 文件夹。对于 Android,您可以替换以下每个文件夹中的图像文件来更改图标:

  • drawable-hdpi

  • drawable-ldpi

  • drawable-mdpi

对于 iOS,Assets.xcassets/AppIcon.appiconset 您要替换的是文件夹中的图像。

如果您想轻松地为 Android 和 iOS 创建图标,请查看MakeAppIcon。只需选择一个图像文件用作图标,它就会自动为 Android 和 iOS 生成不同的大小。然后,您可以将它们移动到上面提到的文件夹中。只需确保您获得正确的尺寸,并且名称与它们替换的图像相同。

7.运行应用程序

您可以通过执行以下tns 命令,像往常一样在您的设备或模拟器上运行该应用程序:

tns run {platform}
tns livesync {platform} --watch

现在我们使用 TypeScript 的唯一区别是,在每个任务开始时都有一个额外的步骤,将 TypeScript 文件编译成 JavaScript。TypeScript 强大的类型检查功能可以在 NativeScript 甚至编译应用程序之前捕获一些错误。

结论

在本教程中,您学习了如何使用 TypeScript 语言使用 NativeScript 构建应用程序


文章目录
  • 1.为什么选择 TypeScript?
  • 2.工装
  • 3.应用概述
  • 4.打开天气地图
  • 5.创建应用程序
    • 安装依赖项
    • 通用模块
      • 常数
      • 实用程序
      • 导航
      • 请求者
      • 位置 商店
    • 主页
      • 主页 JavaScript
      • 主页样式和图标
    • 预测页面
      • 预测页面 JavaScript
      • 预测页面样式
    • 全局应用样式
  • 6.更改默认应用程序图标
  • 7.运行应用程序
  • 结论