65.9K
CodeProject 正在变化。 阅读更多。
Home

Angular 基础 - 使用 .NET Core 2.2 构建 Angular 7 应用程序(全球天气)- 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (60投票s)

2019 年 1 月 17 日

CPOL

13分钟阅读

viewsIcon

114729

downloadIcon

2388

通过一个 Angular 7 应用程序演示一系列 Angular 基础知识。

引言

这是“全球天气”系列的第一篇文章,我们将使用 .NET Core 2.2 构建一个 Angular 7 应用程序。

这个基本应用程序具有 API 驱动应用程序应有的许多功能。它允许用户选择他们的位置并显示当前天气信息。

第二部分中,我们将构建一个 .NET Core API 微服务并与 Angular 应用程序集成。

第三部分中,我们将为 .NET Core 微服务和 Angular 应用程序构建单元测试。

设置 Angular CLI 环境

在开始之前,请访问Angular 教程获取设置 Angular CLI 环境的说明。

必备组件

在开始之前,请确保您的开发环境包含 Node.js 和 npm 包管理器。

Angular 需要 Node.js 版本 8.x 或 10.x。

要检查您的版本,请在终端/控制台窗口中运行 `node -v`。

要获取 Node.js,请访问Node.js

npm 包管理器

Angular、Angular CLI 和 Angular 应用程序依赖于 npm 包中提供的功能。要下载和安装 npm 包,您必须拥有一个 npm 包管理器。

要使用 npm 安装 CLI,请打开终端/控制台窗口并输入以下命令:

npm install -g @angular/cli

从 Visual Studio 2017 创建 ASP.NET Core Web 项目

请确保您已安装最新版本的 Visual Studio 2017(版本 15.9.5)和 .NET Core 2.2 SDK。在此处下载 .NET Core 2.2:此处

打开您的 Visual Studio 2017 -> 创建新项目 -> 选择 Core Web 应用程序。将解决方案命名为 Global Weather。

点击“确定”,然后在下一个窗口中,选择一个 API,如下图所示:

再次点击“确定”创建 `GlobalWeather` 解决方案。

使用 Angular CLI 创建天气客户端

API 项目创建完成后,打开 PowerShell 并导航到 `GlobalWeather` 项目文件夹,运行以下命令:

ng new WeatherClient

这将在一个 API 项目中创建一个 Angular 7 应用程序。现在解决方案结构应如下所示:

现在,我们需要对默认的 `Startup.cs` 类进行一些更改。

在 `ConfigureService` 方法中添加以下行:

services.AddSpaStaticFiles(configuration =>
{
    configuration.RootPath = "WeatherClient/dist";
});

在 `Configure` 方法中添加以下行:

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseHttpsRedirection();
app.UseMvc();
app.UseSpa(spa =>
{
    spa.Options.SourcePath = "WeatherClient";
    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start");
    }
});

从 `Properties/launchSettings.json` 中移除 `"launchUrl": "api/values"`。

好的。现在只需点击“IISExpress”即可运行它。

不行!它不起作用。基本上,异常是 **Failed to start npm**。但可以告诉您,它在 NetCore 2.1 中肯定有效。那么 NetCore 2.2 发生了什么?经过一些研究,坏消息是这是 NetCore 2.2 的一个 bug,好消息是有解决方法。

现在我们进行一种变通方法来修复它。首先,创建一个类 `CurrentDirectoryHelper.cs`。

using System;

namespace GlobalWeather
{
    internal class CurrentDirectoryHelpers
    {
        internal const string AspNetCoreModuleDll = "aspnetcorev2_inprocess.dll";

        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        private static extern IntPtr GetModuleHandle(string lpModuleName);

        [System.Runtime.InteropServices.DllImport(AspNetCoreModuleDll)]
        private static extern int http_get_application_properties
                      (ref IISConfigurationData iiConfigData);

        [System.Runtime.InteropServices.StructLayout
                   (System.Runtime.InteropServices.LayoutKind.Sequential)]
        private struct IISConfigurationData
        {
            public IntPtr pNativeApplication;
            [System.Runtime.InteropServices.MarshalAs
                    (System.Runtime.InteropServices.UnmanagedType.BStr)]
            public string pwzFullApplicationPath;
            [System.Runtime.InteropServices.MarshalAs
                    (System.Runtime.InteropServices.UnmanagedType.BStr)]
            public string pwzVirtualApplicationPath;
            public bool fWindowsAuthEnabled;
            public bool fBasicAuthEnabled;
            public bool fAnonymousAuthEnable;
        }

        public static void SetCurrentDirectory()
        {
            try
            {
                // Check if physical path was provided by ANCM
                var sitePhysicalPath = Environment.GetEnvironmentVariable
                                       ("ASPNETCORE_IIS_PHYSICAL_PATH");
                if (string.IsNullOrEmpty(sitePhysicalPath))
                {
                    // Skip if not running ANCM InProcess
                    if (GetModuleHandle(AspNetCoreModuleDll) == IntPtr.Zero)
                    {
                        return;
                    }

                    IISConfigurationData configurationData = default(IISConfigurationData);
                    if (http_get_application_properties(ref configurationData) != 0)
                    {
                        return;
                    }

                    sitePhysicalPath = configurationData.pwzFullApplicationPath;
                }

                Environment.CurrentDirectory = sitePhysicalPath;
            }
            catch
            {
                // ignore
            }
        }
    }
}

然后在 `Startup.cs` 类的 `Startup` 方法中添加以下行:

CurrentDirectoryHelpers.SetCurrentDirectory();

现在再次运行。

好了。我们已经让 Angular CLI 应用程序与 .NET Core 完美配合。

现在框架已完成。我们需要考虑应用程序需要做什么。

天气信息 REST API

我们正在开发一个显示天气信息的网站。用户可以选择任何位置并显示当前天气信息。

我决定使用 AccuWeather REST API 来获取应用程序数据。我们需要创建一个帐户以获得 API 密钥来使用这些 API。用户应该能够通过国家/地区缩小他们的位置搜索范围。

天气组件

从 `app.component.ts` 中移除所有内容,只留下 `<router-outlet></router-outlet>`。

在 PowerShell 中,转到 `WeatherClient` 文件夹。运行以下命令生成新组件:

ng generate component weather

Angular 路由

路由告诉路由器当用户单击链接或将 URL 粘贴到浏览器地址栏时显示哪个视图。

典型的 Angular 路由有两个属性:

  • path:一个 `string`,匹配浏览器地址栏中的 URL。
  • component:路由器在导航到此路由时应创建的组件。

我们打算从根 URL 导航到 `WeatherComponent`。

导入 `WeatherComponent`,以便您可以在 `Route` 中引用它。然后定义一个包含单个路由到该组件的路由数组。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { WeatherComponent } from './weather/weather.component';

const routes: Routes = [
  { path: '', redirectTo: 'weather', pathMatch: 'full' },
  { path: 'weather', component: WeatherComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { } 

好的。现在刷新浏览器,它会导航到 `WeatherComponent`。

响应式表单

响应式表单提供了一种模型驱动的方法来处理值随时间变化的表单输入。响应式表单与模板驱动表单在以下方面有所不同:响应式表单通过同步访问数据模型、使用可观察运算符实现不可变性以及通过可观察流进行更改跟踪,提供更高的可预测性。如果您更喜欢直接访问模板中的数据进行修改,模板驱动表单则不太明确,因为它们依赖于嵌入在模板中的指令以及用于异步跟踪更改的可变数据。

让我们为 Weather Component 构建响应式表单。

首先在 `app.module.ts` 中注册 `ReactiveFormsModule`。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { WeatherComponent } from './weather/weather.component';

@NgModule({
  declarations: [
    AppComponent,
    WeatherComponent
  ],
  imports: [
    FormsModule,
    ReactiveFormsModule,
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }   

在 `weather.component.ts` 的 `ngOnInit()` 中构建响应式表单。

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-weather',
  templateUrl: './weather.component.html',
  styleUrls: ['./weather.component.css']
})
export class WeatherComponent implements OnInit {

  private weatherForm: FormGroup;

  constructor(
    private fb: FormBuilder) {
  }

  ngOnInit() {
    this.weatherForm = this.buildForm();
  }

  buildForm(): FormGroup {
    return this.fb.group({
      searchGroup: this.fb.group({
        country: [
          null
        ],
        city: [
          null,
          [Validators.required]
        ],
      })
    });
  }
}   

表单是每个 Angular 应用程序的基础。表单最大的优点之一是您可以在将用户输入发送到服务器之前对其进行验证。在这里,我们使用内置的 Angular 验证器来验证“city”。只需将 `required` 验证器添加到验证器数组中。因为我们将 `required` 验证器添加到“city”,所以“city”输入的 `null` 值将使表单处于无效状态。

在 HTML 模板 `weather.component.html` 中使用 `[formGroup]` 和 `[formControl]`。

<div class="container content" style="padding-left: 0px; padding-top: 10px">
    <form [formGroup]="weatherForm">
        <div formgroupname="searchGroup">
            <div class="row">
                <div class="col-md-3 form-group"><input class="form-control" 
                 formcontrolname="country" id="country" 
                 placeholder="Country" type="text" />
                </div>
            </div>
                
            <div class="row">
                <div class="col-md-3 form-group"><input class="form-control" 
                 formcontrolname="city" id="city" 
                 placeholder="Location" type="text" />
                </div>
            </div>
            
            <div class="row">
                <div class="col-md-3"><input class="btn btn-primary" 
                type="button" /></div>
            </div>
        </div>
    </form>
</div>

再次运行它。

在 Angular 应用中使用 Bootstrap 4 样式

首先安装 Bootstrap 4。在 PowerShell 中,转到 `WeatherClient` 文件夹,然后运行以下命令:

npm install bootstrap --save

在 `src/styles.css` 中,添加以下行:

@import '../node_modules/bootstrap/dist/css/bootstrap.css';

现在再次运行。

Angular 服务

组件不应直接获取或保存数据,当然也不应有意呈现假数据。它们应该专注于呈现数据,并将数据访问委托给服务。

让我们添加一个位置服务来调用 AccuWeather REST API 来获取国家/地区列表。

在“`app`”文件夹下创建一个“`shared`”文件夹。然后在“`shared`”文件夹下创建“`services`”和“`models`”文件夹。

https://developer.accuweather.com/apis,您可以获得所有 API 参考。现在,我们要做的就是获取所有国家/地区。API URL 是 http://dataservice.accuweather.com/locations/v1/countries

在 `src/app/shared/models/` 文件夹下创建一个名为 `country.ts` 的文件。定义一个国家/地区接口并导出它。文件应如下所示:

export interface Country {
  ID: string;
  LocalizedName: string;
  EnglishName: string;
}

在 `src/app/` 文件夹中创建一个名为 `app.constants.ts` 的文件。定义 `locationAPIUrl` 和 `apiKey` 常量。文件应如下所示:

export class Constants {
  static locationAPIUrl = 'http://dataservice.accuweather.com/locations/v1';
  static apiKey = 'NmKsVaQH0chGQGIZodHin88XOpwhuoda';
}

我们将创建一个 `LocationService`,所有应用程序类都可以使用它来获取国家/地区。而不是通过 `new` 创建该服务,我们将依赖 Angular 的依赖注入将其注入 `WeatherComponent` 的构造函数中。

使用 Angular CLI,在 `src/app/shared/services/` 文件夹中创建一个名为 `location` 的服务。

ng generate service location

该命令在 `src/app/location.service.ts` 中生成骨架 `LocationService` 类。`LocationService` 类应如下所示:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LocationService {

  constructor() { }
}

在 Angular 可以将 `LocationService` 注入 `WeatherComponent` 之前,我们必须使其在依赖注入系统中可用。通过注册一个提供程序来完成此操作。提供程序是可以创建或提供服务的对象;在这种情况下,它会实例化 `LocationService` 类来提供服务。

查看 `LocationService` 类定义之前的 `@Injectable()` 语句,您可以看到 `providedIn` 元数据值为 `'root'`。当您在根级别提供服务时,Angular 会创建一个 `LocationService` 的单个共享实例,并将其注入到任何请求它的类中。在 `@Injectable` 元数据中注册提供程序也使 Angular 能够优化应用程序,如果服务最终未使用,则会将其删除。

打开 `WeatherComponent` 类文件。导入 `LocationService`。

import { LocationService } from '../shared/services/location.service';

并注入 `LocationService`。

constructor(
    private fb: FormBuilder,
    private locationService: LocationService) {
}

Angular HttpClient

`LocationService` 使用 HTTP 请求获取 `countries` 数据。`HttpClient` 是 Angular 通过 HTTP 与远程服务器通信的机制。

打开根 `AppModule`,从 `@angular/common/http` 导入 `HttpClientModule` 符号。

import { HttpClientModule } from '@angular/common/http';

将其添加到 `@NgModule.imports` 数组中。

imports: [
    FormsModule,
    ReactiveFormsModule,
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ]

使用 HttpClient 获取国家/地区

getCountries(): Observable<Country[]> {
    const uri = decodeURIComponent(
      `${Constants.locationAPIUrl}/countries?apikey=${Constants.apiKey}`
    );
    return this.http.get<Country[]>(uri)
      .pipe(
        tap(_ => console.log('fetched countries')),
        catchError(this.errorHandleService.handleError('getCountries', []))
      );
  }

`HttpClient.get` 默认返回响应体作为未类型化的 JSON 对象。应用可选的类型说明符 `<Country[]>`,可以得到一个类型化的结果对象。

JSON 数据的形状由服务器的数据 API 决定。Accuweather API 将国家/地区数据作为数组返回。

`getCountries` 方法将进入可观察值流。它将通过 RxJS 的 `tap` 运算符来实现,该运算符查看可观察值,对这些值进行一些操作,然后将它们传递出去。`tap` 回调本身不接触值。

当出现问题时,尤其是在从远程服务器获取数据时,`LocationService.getCountries()` 方法应捕获错误并执行适当的操作。

要捕获错误,您需要通过 RxJS `catchError()` 运算符来“管道化” `http.get()` 返回的可观察结果。我们将其封装到 `error-handle.service.ts` 类中。

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class ErrorHandleService {
  constructor() {}
  handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(error); // log to console instead
      // Let the app keep running by returning an empty result.
      return of(result as T);
    }
  }
}

因此,`LocationService` 类现在如下所示:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Constants } from '../../../app/app.constants';
import { Country } from '../../shared/models/country';
import { catchError, map, tap } from 'rxjs/operators';
import { ErrorHandleService } from '../../shared/services/error-handle.service';

@Injectable({
  providedIn: 'root'
})
export class LocationService {

  constructor(
    private http: HttpClient,
    private errorHandleService: ErrorHandleService) { }

  getCountries(): Observable<Country[]> {
    const uri = decodeURIComponent(
      `${Constants.locationAPIUrl}/countries?apikey=${Constants.apiKey}`
    );
    return this.http.get<Country[]>(uri)
      .pipe(
        tap(_ => console.log('fetched countries')),
        catchError(this.errorHandleService.handleError('getCountries', []))
      );
  }
}

在 `WeatherComponent` 文件中添加 `getCountries()` 以从服务检索国家/地区。

getCountries(): void {
    this.locationService.getCountries()
      .subscribe(countries => this.countries = countries);
}

在 `ngOnInit` 生命周期挂钩内调用 `getCountries()`,让 Angular 在构造 `WeatherComponent` 实例后在适当的时候调用 `ngOnInit`。

ngOnInit() {
    this.weatherForm = this.buildForm();
    this.getCountries();
  }

Promise

Promise 是我们都可以使用或自己构造来处理异步任务的 `Object` 的一种特殊类型。在 Promise 之前,回调是我们在进行 `async` 操作时使用的,例如上面订阅 http 服务结果。只要代码不复杂,回调是可以的。但是当你有许多层调用和许多错误需要处理时会怎样?你会遇到回调地狱!Promise 与异步操作一起工作,它们要么返回单个值(即 Promise 解析),要么返回错误消息(即 Promise 拒绝)。

现在我们承诺重写 `WeatherComponent.getCountries()`。

  async getCountries() {
    const promise = new Promise((resolve, reject) => {
      this.locationService.getCountries()
        .toPromise()
        .then(
          res => { // Success
            this.countries = res;
            resolve();
          },
          err => {
            console.error(err);
            this.errorMessage = err;
            reject(err);
          }
        );
    });
    await promise;
  }

因为 `getCountries()` 现在是 `async` 函数,所以我们需要在 `ngOnInit()` 中 `await` 该函数。

async ngOnInit() {
    this.weatherForm = this.buildForm();
    await this.getCountries();
  }

国家/地区输入自动完成

Ng-bootstrap 是完全使用 Bootstrap 4 CSS 和为 Angular 生态系统设计的 API 从头开始构建的 Angular 小部件。我们使用其中一个小部件“Typeahead”来实现 `Country AutoComplete`。

NgbTypeahead 指令提供了一种简单的方法,可以从任何文本输入创建功能强大的 typeahead。使用以下命令 `install ng-bootstrap`。

npm install --save @ng-bootstrap/ng-bootstrap

安装后,您需要导入我们的主模块。

import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
  declarations: [
    AppComponent,
    WeatherComponent
  ],
  imports: [
    NgbModule,
    FormsModule,
    ReactiveFormsModule,
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

焦点行为

可以获取 `focus` 事件及其当前的输入值,从而非常灵活地在焦点时发出结果。在输入为空时,将获取所有选项;否则,选项将根据搜索词进行过滤。

打开 `weather.component.html`,将“country”输入更改为使用 `NgbTypeahead`。

<input type="text" id="country" class="form-control" formControlName="country"
                 placeholder="Country"
                 [ngbTypeahead]="searchCountry" [resultFormatter]="countryFormatter"
                 [inputFormatter]="countryFormatter"
                 (focus)="focus$.next($event.target.value)"
                 (click)="click$.next($event.target.value)"
                 #instanceCountry="ngbTypeahead"
                 autocomplete="off" editable="false" [focusFirst]="false" />

打开 `weather.component.ts`,首先导入 `NgbTypeahead`。

import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';

然后添加以下代码:

countryFormatter = (country: Country) => country.EnglishName;

  searchCountry = (text$: Observable<string>) => {
    const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
    const clicksWithClosedPopup$ = this.click$.pipe
                   (filter(() => !this.instanceCountry.isPopupOpen()));
    const inputFocus$ = this.focus$;

    return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$).pipe(
      map(term => (term === ''
        ? this.countries
        : this.countries.filter(v => v.EnglishName.toLowerCase().indexOf
                               (term.toLowerCase()) > -1)).slice(0, 10))
    );
  }

现在通过 `IISExpress` 运行 `GlobalWeather` 项目。您可以看到预期的精确行为。当输入为空时加载所有国家/地区。

选项值根据非空值进行过滤。

搜索位置

在调用 API 获取天气当前状况之前,我们需要传递位置密钥。因此,我们首先需要调用城市搜索 API:http://dataservice.accuweather.com/locations/v1/cities/{countryCode}/{adminCode}/search

在 `src/app/shared/models/` 文件夹下创建一个名为 `city.ts` 的文件。定义一个城市接口并导出它。文件应如下所示:

import { Country } from './country';

export interface City {
  Key: string;
  EnglishName: string;
  Type: string;
  Country:Country;
}

打开 `src/app/shared/services/` 文件夹下的 `location.service.ts`,添加 `getCities` 方法。

  getCities(searchText: string, countryCode: string): Observable<City[]> {
    const uri = countryCode
      ? decodeURIComponent(
        `${Constants.locationAPIUrl}/cities/${countryCode}/search?
                        apikey=${Constants.apiKey}&q=${searchText}`)
      : decodeURIComponent(
        `${Constants.locationAPIUrl}/cities/search?apikey=${Constants.apiKey}&q=${searchText}`);
    return this.http.get<City[]>(uri)
      .pipe(
        map(res => (res as City[]).map(o => {
          return {
            Key: o.Key,
            EnglishName: o.EnglishName,
            Type: o.Type,
            Country: {
              ID: o.Country.ID,
              EnglishName: o.Country.EnglishName
            }
          }
        })),
        tap(_ => console.log('fetched cities')),
        catchError(this.errorHandleService.handleError('getCities', []))
      );
  }

如何将 Http JSON 响应映射到对象数组

HttpClient 是 Angular HTTP API 的演进版本,JSON 被假定为默认值,不再需要显式解析。将 JSON 结果映射到数组,特别是复杂数组,总是有点棘手。让我们看看如何将搜索位置 API 结果映射到 City 数组。

根据 API 参考,我们定义了 `city` 接口,它只包含我们需要的字段。对于 JSON 结果中的每个项,我们创建一个新对象并从 JSON 初始化字段。

map(res => (res as City[]).map(o => {
          return {
            Key: o.Key,
            EnglishName: o.EnglishName,
            Type: o.Type,
            Country: {
              ID: o.Country.ID,
              EnglishName: o.Country.EnglishName
            }
          }
        }))

获取天气当前状况

我们需要调用 http://dataservice.accuweather.com/currentconditions/v1/{locationKey} API 来获取当前状况。

在 `src/app/shared/models/` 文件夹下创建一个名为 `current-conditions.ts` 的文件。定义一个 `CurrentConditions` 接口并导出它。文件应如下所示:

export interface CurrentConditions {
  LocalObservationDateTime: string;
  WeatherText: string;
  WeatherIcon: number;
  IsDayTime: boolean;
  Temperature: Temperature;
}

export interface Metric {
  Unit: string;
  UnitType: number;
  Value:number;
}

export interface Imperial {
  Unit: string;
  UnitType: number;
  Value: number;
}

export interface Temperature {
  Imperial: Imperial;
  Metric: Metric;
}

打开 `src/app/app.constants.ts` 下的 `app.constants.ts` 文件。添加一个新常量。

static currentConditionsAPIUrl = 'http://dataservice.accuweather.com/currentconditions/v1';

在 `src/app/shared/services/` 文件夹中创建一个名为 current-conditions 的服务。

ng generate service currentConditions

该命令在 `src/app/current-conditions.service.ts` 中生成骨架 `CurrentConditionsService` 类。

然后,在 `CurrentConditionsService` 类中添加 `getCurrentConditions` 方法。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Constants } from '../../../app/app.constants';
import { CurrentConditions } from '../models/current-conditions';
import { catchError, map, tap } from 'rxjs/operators';
import { ErrorHandleService } from '../../shared/services/error-handle.service';

@Injectable()
export class CurrentConditionsService {

  constructor(
    private http: HttpClient,
    private errorHandleService: ErrorHandleService) { }

  getCurrentConditions(locationKey: string): Observable<CurrentConditions []> {
    const uri = decodeURIComponent(
      `${Constants.currentConditionsAPIUrl}/${locationKey}?apikey=${Constants.apiKey}`
    );
    return this.http.get<CurrentConditions []>(uri)
      .pipe(
        tap(_ => console.log('fetched current conditions')),
        catchError(this.errorHandleService.handleError('getCurrentConditions', []))
      );
  }
}

在 WeatherComponent 中获取 Location Key

打开 `src/app/weather` 文件夹下的 `weatherComponent.ts`。添加 `getCity()` 方法。

  async getCity() {
    const country = this.countryControl.value as Country;
    const searchText = this.cityControl.value as string;
    const countryCode = country ? country.ID : null;
    const promise = new Promise((resolve, reject) => {
      this.locationService.getCities(searchText, countryCode)
        .toPromise()
        .then(
          res => { // Success
            var data = res as City[];
            const cities = data;
            if (cities.length === 0) {
              this.errorMessage = 'Cannot find the specified location.';
              reject(this.errorMessage);
            } else {
              this.city = cities[0];
              resolve();
            } 
          },
          err => {
            console.error(err);
            this.errorMessage = err;
            reject(err);
          }
        );
    });
    await promise;
    if (this.city) {
      const country = this.countries.filter(x => x.ID === this.city.Country.ID)[0];
      this.weatherForm.patchValue({
        searchGroup: {
          country: country,
          city: this.city.EnglishName
        }
      });
    }
  }

修补表单控件的值

使用响应式表单,使用表单 API 设置模型值非常容易。更新 `FormGroup` 与 `FormControl` 时,实际上有两件事正在发生。

从组件获取表单控件非常容易。例如,我们可以像下面这样获取 `City` 和 `Country` 表单控件:

get cityControl(): FormControl {
    return <FormControl>this.weatherForm.get('searchGroup.city');
  }

get countryControl(): FormControl {
    return <FormControl>this.weatherForm.get('searchGroup.country');
 }

`patchValue` 允许您设置存在的值,并且会忽略当前迭代控件中不存在的值。

在 `getCity()` 函数中,当收到响应时,我们会修补天气表单的值。

    if (this.city) {
      const country = this.countries.filter(x => x.ID === this.city.Country.ID)[0];
      this.weatherForm.patchValue({
        searchGroup: {
          country: country,
          city: this.city.EnglishName
        }
      });
    }

在 WeatherComponent 中获取当前状况

在 `src/app/shared/models/` 文件夹下创建一个名为 `weather.ts` 的文件。定义一个 `Weather` 类并导出它。文件应如下所示:

import { CurrentConditions } from './current-conditions';
import { City } from './city';

export class Weather {
  public location: string;
  public weatherIconUrl: string;
  public weatherText: string;
  public temperatureValue: number;
  public temperatureUnit: string;
  public isDaytime: boolean;

  public constructor(currentConditions: CurrentConditions, city: City) {
    this.location = city.EnglishName;
    this.weatherText = currentConditions.WeatherText;
    this.isDaytime = currentConditions.IsDayTime;
    if (currentConditions.WeatherIcon)
      this.weatherIconUrl = `../assets/images/${currentConditions.WeatherIcon}.png`;
    this.temperatureValue = currentConditions.Temperature.Metric.Value;
    this.temperatureUnit = currentConditions.Temperature.Metric.Unit;
  }
}

打开 `weather.component.ts`,添加 `getCurrentConditions()` 方法。当收到 `CurrentConditionService` 的结果时,将其映射到 `weather` 类。

async getCurrentConditions() {
    if (!this.city)
      return;
    const promise = new Promise((resolve, reject) => {
      this.currentConditionService.getCurrentConditions(this.city.Key)
        .toPromise()
        .then(
          res => { // Success
            if (res.length > 0) {
              const data = res[0] as CurrentConditions;
              this.weather = new Weather(data, this.city);
              resolve();
            } else {
              this.errorMessage = "Weather is not available.";
              reject(this.errorMessage);
            }
          },
          err => {
            console.error(err);
            reject(err);
          }
        );
    });
    await promise;
  }

将 HTML 元素禁用状态绑定到表单组的有效性

input type="button" class="btn btn-primary" 
[disabled]="!weatherForm.valid" value="Go" (click)="search()" />

Go”按钮仅在 Weather 表单组有效时启用。在构建表单时,`city` 字段是必需的。

buildForm(): FormGroup {
    return this.fb.group({
      searchGroup: this.fb.group({
        country: [
          null
        ],
        city: [
          null,
          [Validators.required]
        ],
      })
    });
  }

这意味着如果 `City` 字段为空,`Weather` 表单组将无效。并且“Go”按钮仅在 `City` 字段有值时启用。并且“Click”此按钮将触发 `Search` 函数。

在 Weather HTML 模板中显示天气面板

在 `Search()` 之后,我们获取当前状况并将其存储在 `WeatherComponent` 类中的 weather 成员中。

现在我们需要在 `Weather` 模板中显示搜索结果。

打开 `src/app/weather` 文件夹下的 `weather.component.html`,在 `

` 之前添加以下更改。这是一个简单的 Angular 模板绑定。这里,我使用 `ng-template` 指令来显示“Daytime”或“Night”。

顾名思义,`ng-template` 指令代表一个 Angular 模板:这意味着此标签的内容将包含模板的一部分,然后可以与其他模板组合以构成最终的组件模板。

Angular 已经在我们一直在使用的许多结构指令(如 `ngIf`、`ngFor` 和 `ngSwitch`)的后台使用了 `ng-template`。

 <div class="city">
    <div *ngIf="weather">
      <h1>{{weather.location | uppercase }}</h1>
      <div class="row">
        <table>
          <tr>
            <td>
              <img src="{{weather.weatherIconUrl}}" class="img-thumbnail">
            </td>
            <td>
              <span>{{weather.weatherText}}</span>
            </td>
          </tr>
          <tr>
            <td>
              <div *ngIf="weather.isDaytime; then thenBlock else elseBlock"></div>
              <ng-template #thenBlock><span>Daytime</span></ng-template>
              <ng-template #elseBlock><span>Night</span></ng-template>
            </td>
            <td>
              <span>{{weather.temperatureValue}}&deg;{{weather.temperatureUnit}}</span>
            </td>
          </tr>
        </table>
      </div>
    </div>
    <div *ngIf="!weather">
      <div class="content-spacer-invisible"></div>
      <div> {{errorMessage}}</div>
    </div>
  </div>

现在再次运行应用程序。

哇!我们获得了墨尔本的当前状况。

仍然缺少一些东西。

组件样式

在 `src/app/weather` 文件夹下的 `weather.component.css` 中添加组件样式。

.city {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 400px;
  padding: 0px 20px 20px 20px;
  margin: 0px 0px 50px 0px;
  border: 1px solid;
  border-radius: 5px;
  box-shadow: 2px 2px #888888;
}

.city h1 {
  line-height: 1.2
}

.city span {
  padding-left: 20px
}

.city .row {
  padding-top: 20px
}

天气图标

在 `src/assets` 下创建一个“images”文件夹。从 http://developer.accuweather.com 下载所有天气图标,并将它们添加到“images”文件夹中。

再次运行应用程序。

从 Chrome 调试 Angular 应用

每个开发人员都知道调试对于开发非常重要。让我们看看如何从 Chrome 调试 Angular 应用程序。

使用 IIS Express 运行“GlobalWeather”项目。按“F12”键显示开发者工具。然后点击“Source”选项卡。

在左侧面板中,从 `webpack://` 查找源 TypeScript 文件。这里,我们以 `weather.componet.ts` 为例。

选择源文件后,源代码将显示在中间面板中。单击行号将切换断点。将断点放在您想要调试的位置。

从 UI 中,选择“Australia”并输入“Melbourne”,然后单击“Go”按钮,将命中断点。

如何使用源代码

npm install

源代码不包含任何外部包。因此,在从 Visual Studio 运行它之前,您需要运行 `npm install` 来安装所有依赖项。打开 PowerShell,然后转到 `GlobalWeather\GlobalWeather\WeatherClient` 文件夹。运行 `npm install`。

npm install

然后从 Visual Studio 构建并运行。

Accuweather API 密钥

我已经从源代码中删除了我的 API 密钥。因此,如果您想让源代码项目正常工作,请注册 http://developer.accuweather.com 以获取您自己的免费密钥。

结论

在本文中,我们使用 .NET Core 2.2 构建了一个 Angular 7 应用程序,并介绍了 Angular 的基础知识,如引导、NgModule、响应式表单、HTTP 客户端、Observable、Promise 和路由。

在下一篇文章全球天气第二部分中,我们将开始使用 .NET Core API 构建后端。我们将使用 .NET Core API 来保存用户选择的位置,并为后续访问自动填充。

© . All rights reserved.