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

使用 Create-React-App 和 .NET Core 3.0 构建 React 客户端(全球天气) - 第一部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (27投票s)

2019 年 10 月 9 日

CPOL

13分钟阅读

viewsIcon

132863

downloadIcon

2378

全球天气 - ASP.NET Core 3.0 的 React 应用

引言

我之前在“全球天气 – Angular 7 应用与 .NET Core 系列”(第一部分第二部分第三部分)中介绍了如何使用 .NET Core 构建 Angular 应用。现在我们来谈谈另一种流行的技术 React。React 和 Angular 之所以如此受欢迎,是因为这些单页应用框架能够构建完全独立运行在浏览器页面中的应用,功能非常强大。

React 与 Angular 对比

Angular 和 React 有许多相似之处,也有许多不同之处。Angular 是一个 MVC 框架,能够很好地组织应用程序结构,但灵活性较低。React 只提供了 MVC 中的“视图”部分——你需要自己解决模型(M)和控制器(C)的问题。因此,你可以根据自己的喜好选择任何库。

React 和 Angular 都是基于组件的。组件接收输入,并返回渲染的 UI 模板作为输出。

React 使用虚拟 DOM 来实现极快的速度。虚拟 DOM 只会比较之前和现在的 HTML 之间的差异,并只更新需要更改的部分。Angular 使用常规 DOM。它会更新整个 HTML 标签树结构,直到达到用户期望的更新程度。

React 决定将 UI 模板和内联 JavaScript/TypeScript 逻辑结合起来,这是以前任何公司都未曾做过的。结果被称为“JSX”(JavaScript)或“TSX”(TypeScript)。JSX/TSX 对于开发来说是一个巨大的优势,因为所有东西都在一个地方,代码补全和编译时检查效果更好。

React 使用单向绑定,并且通常使用 Redux 来处理状态。Angular 使用双向绑定。单向数据绑定意味着 React 的性能比 Angular 更高,并且调试 React 应用比调试 Angular 应用更容易。

Create-React-App

构建 React 应用的最佳解决方案是使用 `create-react-app`。`Create-React-App` 由 Facebook 维护。Create React App 设置了你的开发环境,以便你可以使用最新的 JavaScript/TypeScript 功能,提供良好的开发者体验,并为生产环境优化你的应用程序。你的机器上需要安装 Node >= 8.10 和 npm >= 5.6。

要创建 JavaScript 项目,请运行

npx create-react-app my-app
cd my-app
npm start

要创建 TypeScript 项目,请运行

npx create-react-app my-app --typescript
cd my-app
npm start

通过 Visual Studio 2019 创建 ASP.NET Core Web 项目

打开你的 Visual Studio 2019 -> **创建新项目** -> 选择 **ASP.NET Core Web 应用程序**。

将项目名称命名为“`GlobalWeatherReact`”。

点击“**创建**”,然后在下一个窗口中,选择 **ASP.NET Core 3.0** 和 **Empty**,如下图所示

点击“**创建**”,然后就创建了一个默认的 ASP.NET Core 项目。

暂时保留它。在我们创建 React 客户端之后,需要回来修改一些代码。

使用 Create-React-App 创建天气客户端

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

npx create-react-app weatherclient --typescript

这将创建一个最新版本的 React 应用程序。现在解决方案结构应该是这样的

TSX 是 TypeScript 的 JSX。JSX 是一种可嵌入的类 XML 语法。它旨在被转换为有效的 JavaScript,尽管这种转换的语义是特定于实现的。JSX 随着 React 框架的流行而兴起。TypeScript 支持嵌入、类型检查和将 JSX 直接编译为 JavaScript。

现在我们需要对 ASP.NET Core 项目进行一些更改,以使其与 React 客户端顺利集成。

首先,在 Nuget 包管理器中安装 ASP.NET Core SPA Services 和 ASP.NET Core SPA Services Extensions。

打开 *Startup.cs*,在 `ConfigureServices` 方法中添加 `SpaStaticFiles`。

services.AddMvc(option => option.EnableEndpointRouting = false);
services.AddSpaStaticFiles(configuration =>
{
    configuration.RootPath = "weatherclient/build";
});

并在 `Configuration` 方法中添加以下几行

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseMvc();
app.UseSpa(spa =>
{
    spa.Options.SourcePath = Path.Join(env.ContentRootPath, "weatherclient");

    if (env.IsDevelopment())
    {
        spa.UseReactDevelopmentServer(npmScript: "start");
    }
});

好了。现在只需运行它,点击“**IIS Express**”。

哇,React 客户端与 ASP.NET Core API 项目一起启动了!

React Router 和组件

Hello World 的 React 应用是一个单组件应用。但大多数实际应用都是多组件应用。React Router 是一组导航组件,可以声明式地与你的应用程序组合。在单页应用中,只有一个 HTML 页面。我们重用同一个 HTML 页面来根据导航渲染不同的组件。

React 组件

React 允许你将组件定义为类或函数。目前,类定义的组件提供更多功能。要定义一个 React 组件类,你需要继承 `React.Component`。

每个组件都有几个“生命周期方法”,你可以重写它们来在特定时间运行代码。组件生命周期如下所示。

  • 挂载 (Mounting)

    当组件实例被创建并插入到 DOM 时,这些方法会按以下顺序调用。

    constructor()
    static getDerivedStateFromProps()
    render()
    componentDidMount()
  • 更新

    由于 props 或 state 的变化都会导致更新。当组件被重新渲染时,这些方法会按以下顺序调用。

    static getDerivedStateFromProps()
    shouldComponentUpdate()
    render()
    getSnapshotBeforeUpdate()
    componentDidUpdate()
  • 卸载 (Unmounting)

    当组件被从 DOM 中移除时,会调用此方法。

    componentWillUnmount()
  • 错误处理

    当在渲染、生命周期方法或任何子组件的构造函数中发生错误时,会调用这些方法。

    static getDerivedStateFromError()
    componentDidCatch()

首先,我们先添加一个 Home 组件,将 *App.tsx* 中的代码移动到 Home 组件。

在 “weatherclient/src” 下创建 “components” 文件夹。然后添加 *Home.tsx*。

Home 组件继承 `React.Component`。我们从 *App.tsx* 复制 `render()` 方法。

import React from 'react';
import logo from '../logo.svg';

class Home extends React.Component {

    render() {
        return (
            <div className="App">
                <header className="App-header">
                    <img src={logo} className="App-logo" alt="logo" />
                    <p>
                        Edit <code>src/App.tsx</code> and save to reload.
                    </p>
                    <a
                        className="App-link"
                        href="https://reactjs.ac.cn"
                        target="_blank"
                        rel="noopener noreferrer"
                    >
                        Learn React
                    </a>
                </header>
            </div>
        );
    }
}

export default Home;

React-router-dom

`React-router-dom` 导出了 DOM 感知的组件,例如 ``(渲染一个 `` 标签)和 ``(与浏览器的 `window.history` 交互)。

`React-router-dom` 会重新导出 `react-router` 的所有导出项,因此你只需要在项目中从 `react-router-dom` 导入即可。

现在我们首先安装 `react-router-dom`。在 PowerShell 中,在 “weatherclient” 文件夹下安装 `react-router-dom`。

npm install @types/react-router-dom react-router-dom --save

将 Router 添加到 App

在 “weatherclient/src” 下创建 “router” 文件夹,然后添加 *index.tsx*。

import React from "react";
import { Router, Route, Switch, Link, NavLink } from "react-router-dom";
import * as createHistory from "history";
import Home from "../components/Home";

// Instead of BrowserRouter, we use the regular router,
// but we pass in a customer history to it.
export const history = createHistory.createBrowserHistory();

const AppRouter = () => (
    <Router history={history}>
        <div>
            <Switch>
                <Route path="/" component={Home} />
            </Switch>
        </div>
    </Router>
);

export default AppRouter;

然后我们回到 *App.tsx* 进行更改。

import React from 'react';
import "./App.css";
import AppRouter from "./router";
const App: React.FC = () => {
    return (
        <AppRouter />
    );
}
export default App;

基本上,它删除了默认的渲染代码,并替换为 `AppRouter`。

现在,点击“**IIS Express**”再次运行。

App Router 正在工作。它会自动导航到 Home 组件。

函数组件 vs. 类组件

你可能会注意到 `App` 组件是一个函数组件,而 `Home` 组件是一个类组件。

但是函数组件和类组件之间有什么区别呢?

函数组件是基本的 JavaScript/TypeScript 函数。它们通常是箭头函数,但也可以使用常规的 function 关键字创建。React 生命周期方法不能在函数组件中使用。函数组件中不使用 `render` 方法。如果你不需要使用 React 的 state,则应优先使用函数组件。

类组件使用 ES6 类并继承 React 中的 `Component` 类。React 生命周期方法可以在类组件内部使用。类组件具有内部 state。“`this.state`” 用于读取 state,“`this.setState`” 用于更新 state。

实现 WeatherDetails 和 Form 组件

我们要构建什么?

在介绍了基本的 React 知识后,现在是时候构建我们的天气应用了。我们将构建一个与我的系列文章 全球天气 - ASP.NET Core 2.2 的 Angular 7 应用 非常相似的应用。

所以,这个应用基本上有两个部分。一部分是显示天气详情的面板。另一部分是一个输入表单,我们可以在其中输入 `country` 和 `city`。现在让我们一步一步构建应用。

WeatherDetails 组件

在 “components” 文件夹下添加 *WeatherDetails.tsx*,包含以下代码

import React from 'react';
class WeatherDetails extends React.Component {
    render() {
        return (
            <div>
                Weather Details
            </div>
        );
    }
}
export default WeatherDetails

在 “components” 文件夹下添加 *Form.tsx*,包含以下代码

import React from 'react';
class Form extends React.Component {
    render() {
        return (
            <div>
                <input id="country" type="text" name="country" placeholder="  Country... " />
                <input id="city" type="text" name="city" placeholder="  City... " />
            </div>
        );
    }
}
export default Form

然后修改 `Home` 组件以使用 `WeatherDetails` 和 `Form`。

import React from 'react';
import Form from './Form';
import WeatherDetails from './WeatherDetails';
class Home extends React.Component {
    render() {
        return (
            <div className="App">
                <WeatherDetails />
                <Form />
            </div>
        );
    }
}
export default Home;

点击 “IISExpress” 运行。

就是这样。现在我们先创建类型,然后再回到组件进行更多工作。

Types

我们正在开发一个网站,通过 AccuWeather REST API 显示天气信息。用户可以选择任何位置并查看当前天气信息。

创建一个账户以获取 API 密钥以供 API 使用。

用户应该能够通过国家来缩小他们的位置搜索范围。

API 参考 API reference 介绍了不同 API 请求的所有响应详情。对于我们的应用,我们只关心国家、城市和当前天气。

因此,根据 JSON 响应,我们创建相应的 `type`,即 `Country`、`City` 和 `CurrentCondition` 类型。

在 “weatherclient/src” 下创建 “types” 文件夹。将 *Country.ts* 添加到 “types” 文件夹。

然后将以下代码复制到 *Country.ts*。

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

然后添加 *City.ts* 中的 `City` 类型。

import { Country } from './Country';
export interface City {
  Key: string;
  EnglishName: string;
  Type: string;
  Country: Country;
};

在 *CurrentCondition.ts* 中添加 `CurrentCondition` 类型。

export interface CurrentCondition {
    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;
};

对于 `WeatherDetails` 组件,我不想直接绑定 `CurrentCondition`,所以创建了一个视图模型类 `Weather`。

将 *Weather.ts* 添加到 “types” 文件夹。

import { CurrentCondition } from './CurrentCondition';
import { City } from './City'
import { Constants } from '../Constants';
export class Weather {
    public location?: string;
    public weatherIcon?: any;
    public weatherText?: string;
    public temperatureValue?: number;
    public temperatureUnit?: string;
    public isDaytime?: boolean;
    public error?: string;
    public constructor(currentConditions: CurrentCondition, city: City) {
        this.location = city.EnglishName!;
        this.weatherText = currentConditions.WeatherText;
        this.isDaytime = currentConditions.IsDayTime;
        if (currentConditions.WeatherIcon) {
            let icon = currentConditions.WeatherIcon.toString();
            if (icon.length === 1)
                icon = "0" + icon;
            this.weatherIcon = `${Constants.weatherIconUrl}${icon}-s.png`;
        }
        this.temperatureValue = currentConditions.Temperature.Metric.Value;
        this.temperatureUnit = currentConditions.Temperature.Metric.Unit;
    }
}

在这里,你可以看到我们自己创建了天气图标作为 HTML 链接 URL。你可以在 AccuWeather 图标 Accure Weather Icons 中找到所有天气图标。

使用 Bootstrap、React-Bootstrap 和 React-Bootstrap-Typeahead 样式化 UI

Bootstrap 是一个用于开发 HTML、CSS 和 JS 的开源工具包。使用我们的 Sass 变量和 mixin、响应式网格系统、广泛的预构建组件以及基于 jQuery 的强大插件,可以快速原型化你的想法或构建整个应用程序。

React-Bootstrap 取代了 Bootstrap 的 JavaScript。每个组件都从头开始构建为真正的 React 组件,没有 `jQuery` 等不必要的依赖。`React-Bootstrap` 提供了一系列组件,如 `Button`、`Form`、`Accordion`、`Badge`、`Alert`、`Dropdown`。

`React-Bootstrap-Typeahead` 是一个基于 React 的 `typeahead`,它依赖 Bootstrap 进行样式化。它支持单选和多选。

我们使用 Bootstrap 进行样式化,以及 `React-Bootstrap` 的输入和按钮组件。此外,我们使用 `React-Bootstrap-Typeahead` 来处理 `Country` 输入。

在 “weatherclient” 文件夹下安装 `Bootstrap`、`React-Bootstrap` 和 `React-Bootstrap-Typeahead`。

npm install react-bootstrap bootstrap –-save
npm install react-bootstrap-typeahead @types/react-bootstrap-typeahead --save

安装 `bootstrap` 后,在 src 文件夹下的 *index.tsx* 中添加以下行

import 'bootstrap/dist/css/bootstrap.min.css'

这一行将 `bootstrap` 样式应用于整个应用程序。

现在我们可以样式化我们的应用程序了,用以下样式替换 *App.css*

.panel {
    padding-left: 0px;
    padding-top: 10px
}
.field {
    padding-left: 10px;
    padding-right: 10px
}
.city {
    display: flex;
    background: linear-gradient(90deg, rgba(2,0,36,1) 0%, 
    rgba(25,112,245,0.6399510487788865) 0%, rgba(0,212,255,1) 100%);
    flex-direction: column;
    height: 40vh;
    justify-content: center;
    align-items: center;
    padding: 0px 20px 20px 20px;
    margin: 0px 0px 50px 0px;
    border: 1px solid;
    border-radius: 5px;
    box-shadow: 2px 2px #888888;
    font-family: 'Merriweather', serif;
}
.city h1 {
    line-height: 1.2
}
.city span {
    padding-left: 20px
}
.city .row {
    padding-top: 20px
}
.weatherError {
    color: #f16051;
    font-size: 20px;
    letter-spacing: 1px;
    font-weight: 200;
}

使用 IState 和 IProp 重构组件

Form 组件

我们将 `Form` 组件更改为 state 组件。React 类组件有一个内置的 state 对象。`state` 对象是存储组件属性值的地方。当 state 对象发生更改时,组件会重新渲染。

现在我们将 state 和 props 添加到我们的 form 组件中。

import React from "react";
import { Button, FormControl } from 'react-bootstrap';
import { AsyncTypeahead, Typeahead } from 'react-bootstrap-typeahead';
import { Country } from '../types/Country';
import { City } from '../types/City';
interface IState {
    city: City;
    country: Country;
    cities: City[];
    searchText: string
};

interface IProps {
    /* The http path that the form will be posted to */
    countries: Country[];
}
class Form extends React.Component<IProps, IState> {
    constructor(props: IProps) {
        super(props);
        this.state = {
            city: {} as City,
            country: {} as Country,
            cities: [],
            searchText: ""
        }
    };
    handleSubmit = async (e: any) => {
    }
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <div className="container-fluid">
                    <div className="row">
                        <div className="col-sm-4 form-group">
                            <Typeahead
                                id="country"
                                labelKey="EnglishName"
                                options={this.props.countries}
                                onChange={(s) => this.setState({ country: s[0] } as IState)}
                                placeholder="Country..."
                            />
                        </div>
                        <div className="col-sm-4 form-group field">
                            <FormControl id="city" type="text" name="city" 
                             onChange={(e: any) => this.setState
                             ({ searchText: e.target.value })} placeholder="  City... " />
                        </div>
                        <div className="col-sm-2 form-group field">
                            <Button variant="primary" type="submit"> Go </Button>
                        </div>
                    </div>
                </div>
            </form>
        );
    }
};
export default Form;

在这里,我们使用 `react-bootstrap-typeahead` 来处理 `country` 输入,并使用 `react-bootstrap FormControl` 来处理 `city` 文本输入,还使用 `react-bootstrap` Button 来处理“**Go**”按钮。我需要提的一点是输入字段的 `OnChange` 事件。

正如我之前提到的,React 是单向绑定,这意味着当你更改 UI 字段的值时,绑定数据模型不会像 Angular 那样自动更新。这就是为什么我们需要通过 `onChange` 事件更新绑定数据模型。另外,React 不允许你直接更改组件 state,你需要调用 `setState` 方法来更新 state。

目前我们将 `handleSubmit` 留为空函数,稍后会回来处理。

WeatherDetails 组件

`WeatherDetails` 组件非常直接,只需绑定 `Weather` 视图模型并显示即可。

import React from 'react';
import { Weather } from '../types/Weather'
interface IProp {
    weather: Weather,
};
class WeatherDetails extends React.Component<IProp> {
    render() {
        const weather = this.props.weather;
        return (
            <div>
                <div className="city col-sm-9">
                    {
                        weather.location && <div>
                            <h1>{weather.location}</h1>
                            <div className="row">
                                <table>
                                    <tbody>
                                        <tr>
                                            <td>
                                                {
                                                    weather.weatherIcon && 
                                                    <img src={weather.weatherIcon} 
                                                    className="img-thumbnail" />
                                                }
                                            </td>
                                            <td>
                                                <span>{weather.weatherText}</span>
                                            </td>
                                        </tr>
                                        <tr>
                                            <td>
                                                {weather.isDaytime && <span>
                                                    Daytime
                                    </span>}
                                                {!weather.isDaytime && <span>
                                                    Night
                                    </span>}
                                            </td>
                                            <td>
                                                <span>{weather.temperatureValue}&deg;
                                                      {weather.temperatureUnit}</span>
                                            </td>
                                        </tr>
                                    </tbody>
                                </table>
                            </div>
                        </div>
                    }
                </div>
                {
                    weather.error && <p className="weatherError">
                        {weather.error}
                    </p>
                }
            </div>
        );
    }
};

export default WeatherDetails;

在这里,我们将“`weather`”作为 prop 传递给 `WeatherDetails` 组件。在 `render` 函数中,将 `prop` 绑定到 HTML 字段。你还可以看到我们正在使用条件渲染。

React 中的条件渲染与 JavaScript 中的条件判断工作方式相同。使用 `if` 或三元运算符等 JavaScript 运算符来创建代表当前状态的元素,并让 React 更新 UI 以匹配它们。在条件 `render` 中,组件会根据一个或多个条件决定返回哪些元素。当组件具有条件渲染时,渲染组件的实例可能具有不同的外观。

Home 组件

首先,为 `Home` 组件定义 state 接口。

interface IState {
    weather: Weather,
    countries: Country[],
    city?: City
}

然后,在组件的构造函数中初始化 `state`。

public state: IState = {
    weather: {
        error: ""
    } as Weather,
    countries: [],
    city: undefined
}

在 `render` 函数中,将 `state` 值传递给 `WeatherDetails` 和 `Form` 组件。

render() {
    return (
        <div className="container content panel">
            <div className="container">
                <div className="row">
                    <div className="form-container">
                        <WeatherDetails weather={this.state.weather} />
                        <Form countries={this.state.countries} />
                    </div>
                </div>
            </div>
        </div>
    );
}

现在再次点击“**IIS Express**”运行。

如何消费 RESTful API

大多数现代 Web 应用程序使用 REST 协议进行相互通信。为此,数据以 JSON(JavaScript 对象表示法)的形式发送到 API。反过来,API 返回一个 JSON 负载,该负载可以是静态或动态数据。

Fetch API 提供了一个用于获取资源(包括网络资源)的接口。对于熟悉 `XMLHttpRequest` 的人来说,它会很熟悉,但新的 API 提供了更强大、更灵活的功能集。`Fetch` API 是 `Create-React-App` 的内置包。所以我们不需要安装任何东西,直接使用即可。

添加 *Constants.ts*,将 API URL 和 API 密钥放在 `Constants` 类中。

export class Constants {
    static locationAPIUrl = 'https://dataservice.accuweather.com/locations/v1';
    static citySearchAPIUrl = 'https://dataservice.accuweather.com/locations/v1/cities/';
    static currentConditionsAPIUrl = 'https://dataservice.accuweather.com/currentconditions/v1';
    static weatherIconUrl = 'http://developer.accuweather.com/sites/default/files/';
    static apiKey = 'XXXXXXXXXXXXXXXXXX';
}

你应该将 `apiKey` 替换为你的 API 密钥。

获取所有国家

调用 AccuWeather Locations API 的 `Country List` 端点。

资源 URL,

  • https://dataservice.accuweather.com/locations/v1/countries

将以下函数添加到 `Home` 组件。

async getCountries(): Promise<Country[]> {
    try {
        const res = await fetch
                    (`${Constants.locationAPIUrl}/countries?apikey=${Constants.apiKey}`);
        return await res.json() as Country[];
    } catch (error) {
        console.log(error);
        return [];
    }
}

在这里,我们通过“`fetch`”调用 http get,并将结果 JSON 反序列化为 `Country` 的数组。简而言之,`Promise` 对象表示异步操作的最终完成(或失败)及其结果值。

现在我们有了 `getCountries` 函数,但我们在哪里需要调用它?我们之前讨论了 React 组件的生命周期,我们需要在 `Home` 组件被创建并插入到 DOM 时调用 `getCountries()`。那么最佳位置就是 `componentDidMount()`。

async componentDidMount() {
    try {
        const countries = await this.getCountries();
        await this.setStateAsync({ countries: countries } as IState);
    } catch (error) {
    }
}

同时添加 `setStateAsync`。

async setStateAsync(state: IState) {
    return new Promise((resolve: any) => {
        this.setState(state, resolve);
    });
}

为什么我们使用 `async await`?

`Async` 和 `Await` 是 Promise 的扩展。`Async` 函数使我们能够编写基于 Promise 的代码,就像它是同步的一样,而不会阻塞执行线程。`await` 运算符用于等待一个 `Promise`。

好了。现在我们再次运行应用程序。

因为我们在 `Form` 组件中将“`Country`” typeahead 与“`countries`” prop 绑定,并将 countries 从 `Home` 组件传递到 `Form` 组件,所以 typeahead 按预期显示选项。

获取城市

选择一个 `country` 后,我们可以根据输入文本搜索 `city`。

`City` 搜索端点

  • http://dataservice.accuweather.com/locations/v1/cities/{countryCode}/search

将 `getCity` 函数添加到 `Home` 组件。

async getCity(searchText: string, countryCode: string): Promise<City> {
    const res = await fetch(`${Constants.locationAPIUrl}/cities/
                ${countryCode}/search?apikey=${Constants.apiKey}&q=${searchText}`);
    const cities = await res.json() as City[];
    if (cities.length > 0)
        return cities[0];
    return {} as City;
}

获取当前条件

`GetCity` 返回一个 `City` 类型对象,该对象具有 `location` 键。使用 `location` 键,我们可以调用 Current Conditions API 的 Current Conditions 端点。

当前条件端点

  • http://dataservice.accuweather.com/currentconditions/v1/{locationKey}

将 `getCurrentConditions` 函数添加到 `Home` 组件。

async getCurrentConditions(city: City) {
    try {
        const res = await fetch(`${Constants.currentConditionsAPIUrl}/
                                 ${city.Key}?apikey=${Constants.apiKey}`);
        const currentConditions = await res.json() as CurrentCondition[];
        if (currentConditions.length > 0) {
            const weather = new Weather(currentConditions[0], city);
            await this.setStateAsync({
                weather: weather,
                city: city
            } as IState);
        }
    } catch (error) {
        console.log(error);
    }
    return {} as Weather;
}

现在我们将 `getCity` 和 `getCurrentConditions` 放在 `getWeather` 中。

getWeather = async (e: any, countryCode: string, searchText: string) => {
        e.preventDefault();
        if (!countryCode && !searchText) {
            await this.setStateAsync
                 ({ weather: { error: "Please enter the value." } } as IState);
            return;
        }
        try {
            const city = await this.getCity(searchText, countryCode);
            if (city.Key) {
               await this.getCurrentConditions(city);
            }
        } catch (err) {
            await this.setStateAsync({ weather: { error: err } } as IState);
        }
    };

看起来 `getWeather` 有两个参数,`countryCode` 和 `searchText`。这些值来自 `Form` 组件,这意味着 `getWeather` 应该从 `Form` 组件调用。我们该怎么做?

只需将 `getWeather` 添加到 `Form` 组件的 prop 中,并从 `Home` 组件设置它。

首先,我们将 `getWeather` 添加到 `Form` prop。在 *Form.tsx* 中,我们更改 `Iprops interface`。

interface IProps {
    getWeather: (e: any, country: string, serachText: string) => Promise<void>;
    countries: Country[];
}

然后更改 `handleSubmit`

handleSubmit = async (e: any) => {
        this.props.getWeather(e, this.state.country.ID, this.state.searchText);
    }

别忘了在 `Home` 组件中设置 `getWeather` prop。

<div className="form-container">
    <WeatherDetails weather={this.state.weather} />
    <Form getWeather={this.getWeather} countries={this.state.countries} />
</div>

点击“**IISExpress**”再次运行。

选择“**Australia**”,输入“**Knoxfield**”,然后点击“**Go**”,天气状况如我们预期的那样显示。

调用 .NetCore API

现在我们转向后端 .NetCore API 控制器。我们的目标是将最后选择的位置保存到数据库。当加载 React 应用时,将检索最后输入的地点以自动获取天气信息。

所有后端代码与 全球天气 – ASP.NetCore 和 Angular 7(第二部分) 相同。我不再解释。只需快速复制代码。运行“Release 1.0.sql”来创建数据库。

基本上,我们创建了 `Cities` API,一个是 http get,另一个是 http post。

using System.Threading.Tasks;
using GlobalWeather.Services;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using Weather.Persistence.Models;
namespace GlobalWeather.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CitiesController : ControllerBase
    {
        private readonly ICityService _service;
        private readonly ILogger _logger;
        public CitiesController(ICityService service, ILogger logger)
        {
            _service = service;
            _logger = logger;
        }
        // GET api/cities
        [HttpGet]
        public async Task<ActionResult<City>> Get()
        {
            var city = await _service.GetLastAccessedCityAsync();
            return city;
        }
        // POST api/cities
        [HttpPost]
        public async Task Post([FromBody] City city)
        {
            await _service.UpdateLastAccessedCityAsync(city);
        }
    }
}

现在我们需要从 React 应用调用此 API。

首先,将 *CityMetaData.ts* 添加到 “types” 文件夹。

import { City } from './City';
export class CityMetaData {
  public id: string;
  public name: string;
  public countryId: string;
  public constructor(city: City) {
    this.id = city.Key;
    this.name = city.EnglishName;
    this.countryId = city.Country.ID;
  }
}

然后将 City API URL 添加到 *Constants.ts*

static cityAPIUrl = '/api/cities';

最后,将 `getLastAccessedCity` 和 `updateLastAccessedCity` 添加到 `Home` 组件。

async updateLastAccessedCity(city: City) {
    try {
        const data = new CityMetaData(city);
        await fetch(`${Constants.cityAPIUrl}`, {
            method: 'post',
            body: JSON.stringify(data)
        });
    } catch (error) {
        console.log(error);
    }
}

async getLastAccessedCity(): Promise<City> {
    try {
        const res = await fetch(`${Constants.cityAPIUrl}`);
        const data = await res.json() as CityMetaData;
        return {
            Key: data.id,
            EnglishName: data.name,
            Type: 'City',
            Country: {
                ID: data.countryId,
                EnglishName: ''
            }
        } as City;
    } catch (error) {
        console.log(error);
        return {} as City;
    }
}

在 `componentDidMount` 函数中调用 `getLastAccessedCity` 来检索最后访问的城市,如果存在最后一个访问的城市,则直接获取该 `city` 的天气。

async componentDidMount() {
    try {
        const countries = await this.getCountries();
        await this.setStateAsync({ countries: countries } as IState);
        const lastCity = await this.getLastAccessedCity();
        if (lastCity && lastCity.Key) {
            await this.getWeatherAsync(lastCity);
        }
    } catch (error) {
    }
}

更改 `getWeather` 函数以调用 `updateLastAccessedCity` 来保存当前城市。

getWeather = async (e: any, countryCode: string, searchText: string) => {
    e.preventDefault();
    if (!countryCode && !searchText) {
        await this.setStateAsync({ weather: { error: "Please enter the value." } } as IState);
        return;
    }
    try {
        const city = await this.getCities(searchText, countryCode);
        if (city.Key) {
            await this.updateLastAccessedCity(city);
            await this.getWeatherAsync(city);
        }
    } catch (err) {
        await this.setStateAsync({ weather: { error: err } } as IState);
    }
};

现在再次运行应用程序,它应该会记住你上次输入的城市。

 

如何使用源代码

  • 安装支持 .NET Core 3.0 发行的 Visual Studio 2019 版本 16.3(或更高版本)。
  • 安装最新版本的 NodeJs。
  • 下载并解压源代码。
  • 进入 “weatherclient” 文件夹,在命令行中运行 “npm install”。
  • 使用 Visual Studio 2019 打开 *GlobalWeatherReact.sln*,点击“**全部重新生成**”,然后以“**IIS Express**”启动。

结论

在本文中,我们讨论了客户端 React、服务器端 ASP. NET Core 和 REST API。我们使用 Create-React-App 构建 React 客户端,并使用 .NET Core 3.0 构建 API。React 应用和 .NetCore API 可以无缝集成。

第二部分 中,我们将开始接触有趣的部分,React Redux。没有 Redux,React 只能构建小型应用。但有了 Redux,React 就可以构建企业级应用了。

历史

  • 2019 年 10 月 9 日:初始版本
© . All rights reserved.