Статьи

Создайте приложение погоды с TypeScript и NativeScript

В этом уроке я покажу вам, как создать приложение погоды в NativeScript, используя язык TypeScript.

В предыдущей статье этой серии мы создали приложение для создания заметок с использованием простого JavaScript. На этот раз мы будем использовать TypeScript. Сначала давайте выясним, почему TypeScript является хорошим вариантом для создания приложений NativeScript.

TypeScript — первоклассный гражданин в NativeScript. Он используется основной командой NativeScript для создания самой структуры NativeScript. Вот несколько причин, почему вы хотели бы использовать TypeScript для разработки приложений NativeScript:

  • TypeScript компилируется в JavaScript. Когда компилятор запускается, он улавливает любые ошибки, которые могут возникнуть в вашем коде, чтобы вы могли немедленно реагировать на них, не дожидаясь завершения компилятора NativeScript. Это означает большую продуктивность для вас как разработчика.
  • TypeScript позволяет использовать функции ES6, такие как классы, модули, функции стрелок, литералы шаблонов и многое другое . Это означает, что в вашем распоряжении будет больше инструментов для организации и написания лучшего кода.

Если я не смог убедить вас или вы хотите узнать больше о том, почему TypeScript хорош для разработки с NativeScript, вы можете проверить, как лучше создавать приложения NativeScript с помощью TypeScript .

Чтобы в полной мере воспользоваться возможностями, предлагаемыми TypeScript, я рекомендую использовать текстовый редактор кода Visual Studio . Он имеет функцию IntelliSense, которая обеспечивает интеллектуальное автозаполнение при написании кода TypeScript, он интегрируется с Git, а также имеет возможности отладки.

Кроме того, есть плагин NativeScript, который сделает вашу работу более продуктивной при разработке приложений NativeScript. Одна полезная функция — это интеграция с эмулятором. Это позволяет вам запускать эмулятор прямо из текстового редактора и отлаживать приложение из самого текстового редактора. Visual Studio Code бесплатен и доступен на всех основных платформах (Windows, Linux, OS X).

Однако, если вы не хотите выходить за рамки своего текстового редактора, вы также можете установить расширения, которые улучшат кодирование с помощью TypeScript. Для Atom есть плагин atom-typcript . Для Sublime Text есть плагин TypeScript Sublime .

Приложение, которое мы собираемся создать, является погодным приложением. Он будет иметь следующие страницы:

  • Главная страница, которая показывает текущую погоду вместе с некоторой соответствующей информацией, такой как температура, давление воздуха и влажность.
  • Страница прогноза, которая показывает пятидневный прогноз погоды на следующие пять дней.

Вот как будет выглядеть главная страница:

Главная страница

И вот страница прогноза:

страница прогноза

Вы можете найти полный исходный код этого приложения в репозитории GitHub .

Данные о погоде будут поступать из API OpenWeatherMap , и, как и любой другой API, вам необходимо зарегистрировать ключ API, чтобы использовать его. Давай и зарегистрируйся , я подожду. После входа перейдите на страницу ключей API , скопируйте значение поля ключа и сохраните его где-нибудь в безопасности. Это понадобится вам позже, как только вы начнете создавать приложение.

OpenWeatherMap генерирует ключи API

Теперь, когда вы знаете, как будет выглядеть приложение, пришло время приступить к его созданию. Начните с создания нового проекта NativeScript, который использует шаблон TypeScript:

1
tns create weatherApp —template typescript

После этого перейдите в папку app и создайте следующие папки и файлы. Для вашего удобства вы также можете загрузить или клонировать репозиторий GitHub и скопировать файлы из папки app .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
— 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 с помощью следующей команды:

1
tns plugin add nativescript-geolocation

И установите момент с:

1
npm install moment

Модуль Geolocation используется для определения текущего местоположения пользователя. Момент позволяет легко форматировать метки времени Unix, которые мы будем получать от API позже.

Прежде чем мы рассмотрим код для каждой страницы приложения, давайте сначала рассмотрим пользовательские модули, которые будут использоваться в приложении.

Модуль constants ( common/constants.ts ) содержит все значения констант, используемые в приложении: такие как базовый URL-адрес API OpenWeatherMap, ключ API, который вы получили ранее, пути к конечным точкам, которые мы будем использовать, коды символов значков погоды и направления ветра.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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»
];

Модуль утилит включает в себя все виды функций утилит: такие, как преобразование градусов в направления, определение описательного текста для скорости ветра, преобразование Кельвина в Цельсий и преобразование кодов символов в символы. Вы увидите, как все эти функции используются позже на страницах.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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 и добавьте следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
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 выполняет фактический запрос к API OpenWeatherMap. Как упоминалось в статье « Введение в NativeScript» , NativeScript использует виртуальную машину JavaScript для запуска кода JavaScript. Это означает, что мы также можем использовать функции, доступные в браузере.

Одной из таких функций является fetch , которая позволяет нам отправлять HTTP-запросы на удаленный сервер. Параметр — это URL, по которому вы хотите сделать запрос. Он возвращает обещание, поэтому мы используем then() для ожидания необработанного ответа. Обратите внимание на использование слова «сырье»; функция fetch возвращает ответ с заголовками и другой низкоуровневой информацией, поэтому нам нужно вызвать функцию json() чтобы получить фактические данные JSON. Это вернет другое обещание, поэтому мы используем then() еще раз, чтобы получить реальный объект.

1
2
3
4
5
6
7
8
9
export function get(url){
  return fetch(
    url
  ).then(function(response){
    return response.json();
  }).then(function(json){
    return json;
  });
}

Кроме того, вы можете использовать модуль Http , который является более надежным способом выполнения HTTP-запросов в NativeScript.

Хранилище местоположений служит хранилищем информации о местоположении. Это позволяет нам обновлять и получать текущее местоположение из любого файла, который импортирует этот модуль.

1
2
3
4
5
6
7
8
9
export var location;
 
export function saveLocation(loc) {
  location = loc;
}
 
export function getLocation() {
  return location;
}

Теперь пришло время взглянуть на код для каждой из страниц приложения. Но прежде чем сделать это, сначала откройте файл точки входа ( app.ts ). Это использует модуль навигации, чтобы получить начальную страницу приложения:

1
2
3
4
import application = require(«application»);
import navigation = require(‘./common/navigation’);
application.mainModule = navigation.getStartPage();
application.start();

Далее давайте pages/main/main.xml файл pages/main/main.xml .

Событие navigatingTo используется для выполнения функции с аналогичным именем в файле TypeScript каждый раз, когда пользователь переходит на эту конкретную страницу. Класс CSS также определяется динамически из файла TypeScript.

1
2
3
<Page xmlns=»http://schemas.nativescript.org/tns.xsd» navigatingTo=»navigatingTo» class=»{{ background_class }}»>
</Page>

Компонент ScrollView используется, чтобы обернуть все, так что вертикальная полоса прокрутки автоматически генерируется, когда содержание выходит за рамки, которые может отображать размер экрана.

И поскольку мы собираемся загрузить данные с удаленного сервера, компонент ActivityIndicator используется для отображения анимации загрузки платформы по умолчанию. Для этого необходимо указать атрибут busy , который принимает логическое значение, определяющее, запускать анимацию или нет. По умолчанию это значение равно true и обновляется до false только после того, как приложение выполнит запрос к серверу.

Атрибут visibility также используется, чтобы убедиться, что компонент не занимает места, пока он не анимируется.

1
2
3
4
5
6
7
8
<StackLayout>
  <ScrollView>
    <ActivityIndicator busy=»{{ is_loading }}» visibility=»{{ is_loading ? ‘visible’ : ‘collapsed’ }}» />
    <StackLayout visibility=»{{ !is_loading ? ‘visible’ : ‘collapsed’ }}»>
      …
    </StackLayout>
  </ScrollView>
</StackLayout>

Для основного контента у нас есть общий обзор текущей погоды в верхней части и подробности под ним. Общий обзор показывает значок, представляющий текущую погоду, текущую температуру, описание погоды и место.

1
2
3
4
<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 . Но, как видно из приведенного ниже кода, мы также используем GridLayout для первого столбца каждой строки.

Вы можете спросить, почему мы делаем это вместо того, чтобы просто создать макет из трех столбцов со значком в первом столбце, атрибутом погоды во втором и значением в третьем. Это вполне понятно, и это сделало бы код более кратким.

Но проблема в том, что в NativeScript версии 2.1 в настоящее время не разрешены процентные единицы для его GridLayout . Это означает, что мы не можем использовать что-то вроде 10% для значка, в то время как два других столбца потребляют 45% каждый.

Макет, который мы использовали ниже, решает эту проблему, используя GridLayout для переноса значка и атрибута погоды, при этом значок занимает 30 пикселей, а атрибут погоды — объем пространства, необходимого для размещения текста. Обратите внимание на использование атрибутов row и col в GridLayout .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<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>

Последняя разметка для главной страницы — кнопка, которая ведет на страницу прогноза:

1
<Button text=»5 day Forecast» tap=»goToForecastPage» />

Откройте файл pages/main/main.ts и добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
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 извлекается с использованием деструктуризации объекта , функции ES6, доступной благодаря TypeScript. EventData — это то, что мы передаем в качестве аргумента функции navigatingTo чтобы у него был доступ к любым данным, переданным любой страницей, которая переходит на эту страницу.

У этого есть свойство object , которое в основном является тем компонентом, который вызвал событие. В этом случае мы знаем, что он срабатывает на компоненте Page , и поэтому мы используем <Page> как утверждение типа . После этого мы привязываем основной вид-модель к странице. Это позволит нам добавить или обновить свойство в классе, и оно будет немедленно отражено на странице.

В модели основного вида ( pages/main/main-view-model.ts ) сначала импортируйте все модули, которые мы будем использовать:

1
2
3
4
5
6
7
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, который делает все свойства этого класса наблюдаемыми. Это означает, что все ссылки на каждое свойство в пользовательском интерфейсе будут обновляться при каждом изменении в этом классе.

1
2
3
4
5
6
7
export class MainViewModel extends observable.Observable {
   
  constructor() {
    …
  }
 
}

Внутри конструктора проверьте, включена ли геолокация. Если он не включен, попробуйте включить его, вызвав enableLocationRequest() . Это вызывает приложение, чтобы попросить пользователя включить геолокацию.

1
2
3
4
5
super();
//check if geolocation is not enabled
if (!geolocation.isEnabled()) {
  geolocation.enableLocationRequest();
}

Затем определите, день это или ночь, и установите фон страницы в соответствии с результатом. Затем установите значки на странице.

1
2
3
var time_of_day = utilities.getTimeOfDay();
this.set(‘background_class’, time_of_day);
this.setIcons();

После этого попробуйте определить текущее местоположение. Обратите внимание, что если пользователь не разрешил геолокацию, приложение вообще не будет работать, потому что погода зависит от местоположения пользователя. Приложение попытается определить местоположение пользователя за 10 секунд. Если это не удается сделать, то пользователю выводится сообщение об ошибке.

01
02
03
04
05
06
07
08
09
10
11
12
var location = geolocation.getCurrentLocation({timeout: 10000}).
  then(
    (loc) => {
      if (loc) {
        …
      }
    },
    (e) => {
      //failed to get location
      alert(e.message);
    }
);

Если запрос местоположения выполнен успешно, мы сохраняем его с помощью locationStore . Это позволяет нам получить доступ к местоположению на других страницах позже, не запрашивая его снова.

1
locationStore.saveLocation(loc);

Для справки, вот пример ответа, который вы можете получить при запросе местоположения в NativeScript. Вы можете ознакомиться с документацией Location для NativeScript, чтобы узнать больше о каждом из свойств ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
{
   «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»:{
 
   }
}

Мы можем создать полный URL-адрес запроса API с использованием литералов шаблона и выполнить запрос с помощью модуля Requestor.

1
2
3
4
5
this.set(‘is_loading’, true);
var url = `${constants.WEATHER_URL}${constants.CURRENT_WEATHER_PATH}?lat=${loc.latitude}&lon=${loc.longitude}&apikey=${constants.WEATHER_APIKEY}`;
requestor.get(url).then((res) => {
  …
});

Как только ответ возвращается, извлеките и отформатируйте его, а затем установите полученное значение как свойство класса. И поскольку этот класс является наблюдаемым, это автоматически обновит пользовательский интерфейс приложения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
this.set(‘is_loading’, false);
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
   «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() , которая устанавливает все значки, используемые на странице:

01
02
03
04
05
06
07
08
09
10
setIcons() {
  var icons = utilities.getIcons([
    ‘temperature’, ‘wind’, ‘cloud’,
    ‘pressure’, ‘humidity’, ‘rain’,
    ‘sunrise’, ‘sunset’
  ]);
  icons.forEach((item) => {
    this.set(`${item.name}_icon`, item.icon);
  });
}

Вот стили для главной страницы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.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 для классов icon и small-icon . Вот как мы используем иконки шрифтов в NativeScript . Если вам нравятся такие шрифты, как Font Awesome на ваших веб-страницах, вы можете использовать их в приложениях NativeScript.

Сначала загрузите значок шрифта, который вы хотите использовать. Для этого приложения используется шрифт Weather Icons . Распакуйте zip-архив и внутри извлеченной папки перейдите в каталог font . Скопируйте файл .ttf в каталог fonts в вашем приложении и переименуйте его в weathericons.ttf . Имя файла — это то, что вы используете в качестве значения для семейства font-family каждый раз, когда хотите использовать этот конкретный значок шрифта. Кроме того, вы также должны добавить font-size чтобы контролировать размер значков.

Теперь давайте посмотрим на разметку для страницы прогноза ( pages/forecast/forecast.xml ). В заголовке есть кнопка «Назад», которая позволяет пользователю вернуться на главную страницу. Обратите внимание, что вместо компонента Button общего назначения мы используем NavigationButton , который является эквивалентом NativeScript кнопки возврата iOS и кнопки навигации Android.

1
2
3
4
5
6
7
8
<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>

Для основного контента вместо обычного ListView используется компонент Repeater . Оба компонента могут быть использованы для создания списка, но ListView поставляется с большим количеством наворотов. Например, он автоматически генерирует вертикальную полосу прокрутки, когда список выходит за размер экрана. Бесконечные функции прокрутки также встроены.

В этом случае используется компонент Repeater потому что нет особой необходимости в функциях, которые я только что упомянул. Все, что нам нужно, это пустой список.

01
02
03
04
05
06
07
08
09
10
<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 имеет три столбца для содержания значка, дневной температуры и ночной температуры. Остальные три строки имеют только два столбца — для значка и значения атрибута погоды.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<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 или ListView вас не будет доступа к данным за пределами массива, который вы указали для использования в списке, — если только вы явно не укажете родительский компонент, в котором доступны данные. Вот где приходит $parents['Page'] . $parents — это специальная переменная в NativeScript, которая позволяет вам получать доступ к данным, доступным для определенного компонента. В этом случае мы указали Page для доступа к значкам для каждого атрибута погоды.

1
2
3
4
<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>

Код для страницы прогноза почти такой же, как код для главной страницы. Единственное отличие состоит в том, что функция навигации предназначена для возврата на главную страницу, и мы используем ForecastViewModel в качестве модели представления.

01
02
03
04
05
06
07
08
09
10
11
12
13
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 ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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);
    //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);
    });
  }
 
}

Внутри конструктора мы получаем текущее местоположение из хранилища местоположений и создаем конечную точку URL для 16-дневного прогноза погоды . Но вместо 16 мы хотим только пять дней, поэтому мы указываем 6 для подсчета ( cnt ). Мы используем 6, потому что часовой пояс зависит от сервера, а не от указанного местоположения. Это означает, что существует вероятность того, что API вернет погоду за предыдущий или текущий день. Вот почему есть дополнительный 1 день, который служит дополнением.

1
2
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}`;

Затем выполните запрос и обновите интерфейс с ответом API, вызвав getForecast() :

1
2
3
4
5
requestor.get(url).then((response) => {
  this.set(‘is_loading’, false);
  var forecast = this.getForecast(response);
  this.set(‘forecast’, forecast);
});

Вот пример ответа, возвращаемого конечной точкой прогноза на 16 дней. Обратите внимание: чтобы сделать образец более сжатым, я установил счетчик в 1, поэтому свойство list содержит только один объект.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
   «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 ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 и добавьте следующие стили:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
.header {
  background-color: #333;
  color: #fff;
}
 
.day {
  background-color: #f48024;
  color: #fff;
}
 
.night {
  background-color: #924da3;
  color: #fff;
}

Вы можете изменить значок приложения по умолчанию, перейдя в папку App_Resources . Там вы можете увидеть папку Android и iOS. Для Android вы можете заменить файл изображения в каждой из следующих папок, чтобы изменить значок:

  • drawable-hdpi
  • drawable-ldpi
  • drawable-mdpi

Для iOS это изображения в Assets.xcassets/AppIcon.appiconset которые вы хотите заменить.

Если вы хотите легко создавать иконки для Android и iOS, проверьте MakeAppIcon . Просто выберите файл изображения для использования в качестве значка, и он автоматически создаст разные размеры для Android и iOS. Затем вы можете переместить их в папки, упомянутые выше. Просто убедитесь, что вы получили правильные размеры, и имена совпадают с изображениями, которые они заменяют.

Вы можете запустить приложение на своем устройстве или эмуляторе, как обычно, выполнив следующие команды tns :

1
2
tns run {platform}
tns livesync {platform} —watch

Единственное отличие теперь, когда мы используем TypeScript, заключается в том, что в начале каждой задачи есть дополнительный шаг для компиляции файлов TypeScript в JavaScript. Строгая проверка типов в TypeScript служит защитной сеткой для выявления некоторых ошибок до того, как NativeScript даже скомпилирует приложение.

В этом руководстве вы узнали, как создать приложение с использованием NativeScript с использованием языка TypeScript. В частности, вы узнали следующие понятия:

  • Структурирование вашего приложения путем помещения связанных файлов в их собственную папку.
  • Повторное использование кода с использованием модулей.
  • Использование view-моделей для предоставления данных и функциональности страницам приложения.
  • Определение местоположения с помощью плагина геолокации.
  • Делать HTTP-запросы.
  • Использование значков шрифтов.
  • Навигация между страницами.

Я оставлю вам несколько ресурсов, чтобы продолжить ваше путешествие по разработке потрясающих приложений с помощью NativeScript: