Использование Angular для установки погодного приложения за считанные минуты
OpenWeather - это погодный веб-сайт, который предоставляет людям собственный бесплатный API. Это удобно для создания небольших погодных приложений для нашего собственного использования. В этой истории мы создадим собственное погодное приложение с API OpenWeatherMap.
Сначала получите API-ключ здесь.
Мы создадим приложение, используя фреймворк Angular. Angular использует компонентную архитектуру, чтобы наш код был организован. Он также имеет библиотеку потоков для хранения данных в центральном месте. Это полезно, потому что он может делать много всего (структурирование кода, маршрутизацию и т. Д.), И в нем есть библиотеки (например, библиотека материалов Angular), чтобы наши приложения выглядели красиво. В нем также есть программа для построения и создания приложения с помощью Angular CLI.
Начнем строить
Для начала мы запускаем npm i -g @angular/cli
, чтобы установить Angular CLI. Затем мы добавляем библиотеки, необходимые для того, чтобы наше приложение работало и выглядело хорошо. Для этого мы используем Angular Material. Мы делаем это, запустив:
npm install --save @angular/material @angular/cdk @angular/animations
Нам также необходимо установить @ngrx/store
, запустив:
npm install @ngrx/store --save
Мы устанавливаем moment
для форматирования дат, запустив:
npm i moment
Затем мы приступаем к написанию нашего приложения. Давайте добавим несколько компонентов: нам нужна страница предупреждений для отображения УФ-индекса, компонент текущей погоды для отображения текущей погоды, компонент прогноза для отображения прогноза, страница для всего содержимого, верхняя панель для отображения имени приложения, и окно поиска.
Теперь мы создаем код, который используется несколькими частями приложения. Нам нужен редуктор для централизованного хранения данных. Для этого создайте файл с именем location-reducer.ts
и вставьте в него следующее:
import { Action } from '@ngrx/store'; export const initialState = ''; export const SET_LOCATION = 'SET_LOCATION'; export function locationReducer(state = initialState, action: any) { switch (action.type) { case SET_LOCATION: state = action.payload return state; default: return state; } }
Затем мы создаем функции для получения данных о погоде. Мы создаем сервис Angular под названием WeatherService
. Для этого запустите:
ng g service weather
В weather.service.ts
мы помещаем:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; import * as moment from 'moment'; const apiKey: string = environment.apiKey; @Injectable({ providedIn: 'root' }) export class WeatherService { constructor(private http: HttpClient) { } getCurrentWeather(loc: string) { return this.http.get(`${environment.apiUrl}/weather?q=${loc}&appid=${apiKey}`) } getForecast(loc: string) { return this.http.get(`${environment.apiUrl}/forecast?q=${loc}&appid=${apiKey}`) } getUv(lat: number, lon: number) { let startDate = Math.round(+moment(new Date()).subtract(1, 'week').toDate() / 1000); let endDate = Math.round(+moment(new Date()).add(1, 'week').toDate() / 1000); return this.http.get(`${environment.apiUrl}/uvi/history?lat=${lat}&lon=${lon}&start=${startDate}&end=${endDate}&appid=${apiKey}`) } }
В environments/environment.ts
мы помещаем:
export const environment = { production: false, apiKey: 'api key', apiUrl: 'http://api.openweathermap.org/data/2.5' };
apiKey
- это ключ, который вы получили на сайте.
Мы запускаем следующие команды для генерации наших компонентов:
ng g component uv ng g component currentWeather ng g component forecast ng g component homePage ng g component topBar
Теперь добавляем код в компоненты:
В uv.component.ts
мы помещаем:
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Store, select } from '@ngrx/store'; import { WeatherService } from '../weather.service'; @Component({ selector: 'app-uv', templateUrl: './uv.component.html', styleUrls: ['./uv.component.css'] }) export class UvComponent implements OnInit { loc$: Observable<string>; loc: string; currentWeather: any = <any>{}; uv: any[] = []; msg: string; constructor( private store: Store<any>, private weatherService: WeatherService ) { this.loc$ = store.pipe(select('loc')); this.loc$.subscribe(loc => { this.loc = loc; this.searchWeather(loc); }) } ngOnInit() { } searchWeather(loc: string) { this.msg = ''; this.currentWeather = {}; this.weatherService.getCurrentWeather(loc) .subscribe(res => { this.currentWeather = res; }, err => { }, () => { this.searchUv(loc); }) } searchUv(loc: string) { this.weatherService.getUv(this.currentWeather.coord.lat, this.currentWeather.coord.lon) .subscribe(res => { this.uv = res as any[]; }, err => { }) } resultFound() { return Object.keys(this.currentWeather).length > 0; } }
Он получает УФ-индекс из API.
В uv.component.html
мы помещаем:
<div *ngIf='resultFound()'> <h1 class="center">Current UV data for {{currentWeather.name}}</h1> <mat-card *ngFor='let l of uv' class="mat-elevation-z18"> <mat-card-header> <mat-card-title> <h2>{{l.date_iso | date:'MMM d, y, h:mm:ss a'}}</h2> </mat-card-title> </mat-card-header> <mat-card-content> <mat-list> <mat-list-item> <span> UV Index: {{l.value}} </span> </mat-list-item> </mat-list> </mat-card-content> </mat-card> </div> <div *ngIf='!resultFound()'> <h1 class="center">{{msg || 'Failed to get weather.'}}</h1> </div>
Для форматирования дат пишем:
{{l.date_iso | date:'MMM d, y, h:mm:ss a'}}
На жаргоне Angular это называется трубкой. Это функция, которая превращает один объект в другой. Его можно использовать в шаблонах и в логическом коде.
Аналогично в current-weather.component.ts
мы помещаем:
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Store, select } from '@ngrx/store'; import { WeatherService } from '../weather.service'; @Component({ selector: 'app-current-weather', templateUrl: './current-weather.component.html', styleUrls: ['./current-weather.component.css'] }) export class CurrentWeatherComponent implements OnInit { loc$: Observable<string>; loc: string; currentWeather: any = <any>{}; msg: string; constructor( private store: Store<any>, private weatherService: WeatherService ) { this.loc$ = store.pipe(select('loc')); this.loc$.subscribe(loc => { this.loc = loc; this.searchWeather(loc); }) } ngOnInit() { } searchWeather(loc: string) { this.msg = ''; this.currentWeather = {}; this.weatherService.getCurrentWeather(loc) .subscribe(res => { this.currentWeather = res; }, err => { if (err.error && err.error.message) { alert(err.error.message); this.msg = err.error.message; return; } alert('Failed to get weather.'); }, () => { }) } resultFound() { return Object.keys(this.currentWeather).length > 0; } }
Здесь мы получаем текущую погоду. В соответствующем шаблоне current-weather.component.html
мы помещаем:
<h1 class="center" *ngIf='resultFound()'>Current weather for {{currentWeather.name}}</h1> <table *ngIf='resultFound()'> <tbody> <tr> <td> <h3>Current Temperature:</h3> </td> <td> <h3>{{currentWeather.main?.temp - 273.15 | number:'1.0-0'}}<sup>o</sup>C</h3> </td> </tr> <tr> <td> <h3>Maximum Temperature:</h3> </td> <td> <h3>{{currentWeather.main?.temp - 273.15 | number:'1.0-0'}}<sup>o</sup>C</h3> </td> </tr> <tr> <td> <h3>Minimum Temperature:</h3> </td> <td> <h3>{{currentWeather.main?.temp_min - 273.15 | number:'1.0-0'}}<sup>o</sup>C</h3> </td> </tr> <tr> <td> <h3>Clouds:</h3> </td> <td> <h3>{{currentWeather.clouds?.all}}%</h3> </td> </tr> <tr> <td> <h3>Humidity</h3> </td> <td> <h3>{{currentWeather.main?.humidity}}%</h3> </td> </tr> <tr> <td> <h3>Pressure</h3> </td> <td> <h3>{{currentWeather.main?.pressure}}mb</h3> </td> </tr> <tr> <td> <h3>Sunrise</h3> </td> <td> <h3>{{currentWeather.sys?.sunrise*1000 | date:'long'}}</h3> </td> </tr> <tr> <td> <h3>Sunset</h3> </td> <td> <h3>{{currentWeather.sys?.sunset*1000 | date:'long'}}</h3> </td> </tr> <tr> <td> <h3>Visibility</h3> </td> <td> <h3>{{currentWeather.visibility}}m</h3> </td> </tr> </tbody> </table> <div *ngIf='!resultFound()'> <h1 class="center">{{msg || 'Failed to get weather.'}}</h1> </div>
Это просто таблица для отображения данных.
В forecast.component.ts
мы помещаем:
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Store, select } from '@ngrx/store'; import { WeatherService } from '../weather.service'; @Component({ selector: 'app-forecast', templateUrl: './forecast.component.html', styleUrls: ['./forecast.component.css'] }) export class ForecastComponent implements OnInit { loc$: Observable<string>; loc: string; currentWeather: any = <any>{}; forecast: any = <any>{}; msg: string; constructor( private store: Store<any>, private weatherService: WeatherService ) { this.loc$ = store.pipe(select('loc')); this.loc$.subscribe(loc => { this.loc = loc; this.searchWeather(loc); }) } ngOnInit() { } searchWeather(loc: string) { this.msg = ''; this.currentWeather = {}; this.weatherService.getCurrentWeather(loc) .subscribe(res => { this.currentWeather = res; }, err => { }, () => { this.searchForecast(loc); }) } searchForecast(loc: string) { this.weatherService.getForecast(loc) .subscribe(res => { this.forecast = res; }, err => { }) } resultFound() { return Object.keys(this.currentWeather).length > 0; } }
В соответствующем шаблоне forecast.component.html
мы помещаем:
<div *ngIf='resultFound()'> <h1 class="center">Current weather forecast for {{currentWeather.name}}</h1> <mat-card *ngFor='let l of forecast.list' class="mat-elevation-z18"> <mat-card-header> <mat-card-title> <h2>{{l.dt*1000 | date:'MMM d, y, h:mm:ss a'}}</h2> </mat-card-title> </mat-card-header> <mat-card-content> <table *ngIf='resultFound()'> <tbody> <tr> <td>Temperature</td> <td>{{l.main?.temp - 273.15 | number:'1.0-0'}}<sup>o</sup>C</td> </tr> <tr> <td>Minimum Temperature</td> <td>{{l.main?.temp_min - 273.15 | number:'1.0-0'}}<sup>o</sup>C</td> </tr> <tr> <td>Maximum Temperature</td> <td>{{l.main?.temp_max - 273.15 | number:'1.0-0'}}<sup>o</sup>C</td> </tr> <tr> <td>Pressure</td> <td>{{l.main?.pressure | number:'1.0-0'}}mb</td> </tr> <tr> <td>Sea Level</td> <td>{{l.main?.sea_level | number:'1.0-0'}}m</td> </tr> <tr> <td>Ground Level</td> <td>{{l.main?.grnd_level | number:'1.0-0'}}m</td> </tr> <tr> <td>Humidity</td> <td> {{l.main?.humidity | number:'1.0-0'}}%</td> </tr> <tr> <td>Weather</td> <td> <ul> <li *ngFor='let w of l.weather'> {{w?.main }}: {{w?.description }} </li> </ul> </td> </tr> <tr> <td>Wind Speed</td> <td>{{l.wind?.speed }}</td> </tr> <tr> <td>Wind Direction</td> <td>{{l.wind?.deg }}<sup>o</sup></td> </tr> </tbody> </table> </mat-card-content> </mat-card> </div> <div *ngIf='!resultFound()'> <h1 class="center">{{msg || 'Failed to get weather.'}}</h1> </div>
В home-page.component.ts
мы помещаем:
import { Component, OnInit } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Observable } from 'rxjs'; @Component({ selector: 'app-home-page', templateUrl: './home-page.component.html', styleUrls: ['./home-page.component.css'] }) export class HomePageComponent implements OnInit { loc$: Observable<string>; loc: string; constructor(private store: Store<any>) { this.loc$ = store.pipe(select('loc')); this.loc$.subscribe(loc => { this.loc = loc; }) } ngOnInit() { } }
А в home-page.component.html
у нас есть:
<app-top-bar></app-top-bar> <div id='container'> <div *ngIf='!loc' id='search'> <h1>Enter location to find weather info.</h1> </div> <mat-tab-group *ngIf='loc'> <mat-tab label="Current Weather"> <app-current-weather></app-current-weather> </mat-tab> <mat-tab label="Forecast"> <app-forecast></app-forecast> </mat-tab> <mat-tab label="UV Index"> <app-uv></app-uv> </mat-tab> </mat-tab-group> </div>
Наконец, в top-bar.component.ts
у нас есть:
import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { SET_LOCATION } from '../location-reducer'; import { NgForm } from '@angular/forms'; @Component({ selector: 'app-top-bar', templateUrl: './top-bar.component.html', styleUrls: ['./top-bar.component.css'] }) export class TopBarComponent implements OnInit { loc: string; constructor(private store: Store<any>) { } ngOnInit() { } search(searchForm: NgForm) { if (searchForm.invalid) { return; } this.store.dispatch({ type: SET_LOCATION, payload: this.loc }); } }
А в top-bar.component.html
у нас есть:
<mat-toolbar> <span id='title'>Weather App</span> <form (ngSubmit)='search(searchForm)' #searchForm='ngForm'> <mat-form-field class="form-field"> <input matInput placeholder="Search Location" [(ngModel)]='loc' #lo='ngModel' name='lo' required type='text' autocomplete="off"> <mat-error *ngIf="lo.invalid && (lo.dirty || lo.touched)"> <span *ngIf='lo.errors.required'> Location is required. </span> </mat-error> </mat-form-field> <button mat-button type='submit'>Search</button> </form> </mat-toolbar>
В top-bar.component.css
у нас есть:
#title{ margin-right: 30px; } .mat-form-field{ font-size: 13px; } .mat-button{ height: 33px; } .mat-toolbar{ background-color: green; color: white; } .mat-error, .mat-form-field-invalid .mat-input-element, .mat-warn .mat-input-element{ color: white !important; border-bottom-color: white !important; } .mat-focused .placeholder{ color: white; } ::ng-deep .form-field.mat-form-field-appearance-legacy .mat-form-field-underline, .form-field.mat-form-field-appearance-legacy .mat-form-field-ripple, .form-field.mat-form-field-appearance-legacy.mat-focused .mat-form-field-underline, .form-field.mat-form-field-appearance-legacy.mat-focused .mat-form-field-ripple { background-color: white !important; border-bottom-color: white !important; } /** Overrides label color **/ ::ng-deep .form-field.mat-form-field-appearance-legacy .mat-form-field-label, .form-field.mat-form-field-appearance-legacy.mat-focused .mat-form-field-label { color: white !important; border-bottom-color: white !important; } /** Overrides caret & text color **/ ::ng-deep .form-field.mat-form-field-appearance-legacy .mat-input-element { caret-color: white !important; color: white !important; border-bottom-color: white !important; } ::ng-deep .mat-form-field-underline, ::ng-deep .mat-form-field-ripple { background-color: white !important; }
Давайте добавим цветов к верхней панели.
В style.css
мы добавляем:
/* You can add global styles to this file, and also import other style files */ @import "~@angular/material/prebuilt-themes/indigo-pink.css"; body{ margin: 0px; font-family: 'Roboto', sans-serif; } .center{ text-align: center; } table{ width: 100% }
Сюда мы импортируем тему Material Design для стилизации. В index.html
давайте добавим следующий фрагмент кода, чтобы мы могли использовать указанный нами шрифт Roboto:
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
Обратите внимание, что у нас есть это:
search(searchForm: NgForm) { if (searchForm.invalid) { return; } this.store.dispatch({ type: SET_LOCATION, payload: this.loc }); }
Здесь и происходит поиск. Ключевое слово для поиска распространяется по всему приложению путем отправки ключевого слова в магазине. Где бы мы ни увидели ...
this.loc$ = store.pipe(select('loc')); this.loc$.subscribe(loc => { this.loc = loc; })
… Мы подписываемся на последнее ключевое слово поиска из магазина flux. Нам не нужно беспокоиться о передаче данных по приложению для распространения ключевого слова поиска. Это похоже на блок ниже:
this.loc$ = store.pipe(select('loc')); this.loc$.subscribe(loc => { this.loc = loc; this.searchWeather(loc); })
Это отправляет ключевое слово в сервисную функцию, которая ищет данные о погоде в соответствии с введенным нами ключевым словом.
Все, что начинается с mat
, является виджетом Angular Material. Все они стилизованы, поэтому нам не нужно делать это самим.
В app.module.ts
мы заменяем существующий код на:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule, MatToolbarModule, MatInputModule, MatTabsModule, MatCardModule, MatDividerModule, MatListModule } from '@angular/material'; import { HomePageComponent } from './home-page/home-page.component'; import { StoreModule } from '@ngrx/store'; import { locationReducer } from './location-reducer'; import { TopBarComponent } from './top-bar/top-bar.component'; import { FormsModule } from '@angular/forms'; import { WeatherService } from './weather.service'; import { CurrentWeatherComponent } from './current-weather/current-weather.component'; import { ForecastComponent } from './forecast/forecast.component'; import { UvComponent } from './uv/uv.component'; import { AlertsComponent } from './alerts/alerts.component'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent, HomePageComponent, TopBarComponent, CurrentWeatherComponent, ForecastComponent, UvComponent, AlertsComponent ], imports: [ BrowserModule, AppRoutingModule, BrowserAnimationsModule, MatButtonModule, MatToolbarModule, StoreModule.forRoot({ loc: locationReducer }), FormsModule, MatInputModule, MatTabsModule, MatCardModule, HttpClientModule, MatDividerModule, MatListModule ], providers: [ WeatherService ], bootstrap: [AppComponent] }) export class AppModule { }
чтобы включить Angular Material и код, который мы написали, в основной модуль приложения.
Полученные результаты
В итоге имеем: