Использование 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 и код, который мы написали, в основной модуль приложения.

Полученные результаты

В итоге имеем: