Global Weather - React 应用程序配合 ASP.NET Core 3.0 (第二部分)






4.89/5 (19投票s)
了解如何将 REST API 调用集成到 React-Redux 中进行客户端状态管理,使用 Redux 来管理数据,而不是直接访问组件状态。
引言
在 第一部分 中,我介绍了如何使用 .NET Core 构建 React 应用程序。现在我们开始接触有趣的部分,React-Redux。应用程序中到处都是状态。数据的变化随着时间推移而复杂化。为了在 React 中处理状态,Redux 通常被用作解决方案。在这篇文章中,我们将一步一步地用 react-redux 重写 Global Weather。
Redux
Redux 是一个状态管理工具。虽然它主要与 React 一起使用,但它也可以与任何其他 JavaScript/TypeScript 框架或库一起使用。在一个应用程序中,组件中的数据应该只存在于一个组件中。因此,在同级组件之间共享数据变得困难。
例如,在 React 中,为了在同级组件之间共享数据,状态必须存在于父组件中。父组件提供一个用于更新此状态的方法,并将其作为 props 传递给这些同级组件。这正是我们在 Global Weather React 应用程序中所做的。Weather View Model 存在于 Home 组件(父组件)中,并将其作为属性传递给 WeatherDetails 组件。状态将不得不向上提升到最近的父组件,然后再向上,直到到达需要状态的两个组件的共同祖先,然后向下传递。这使得状态难以维护且不那么可预测。这就是为什么我们需要像 Redux 这样的状态管理工具,它可以使这些状态更易于维护。
Redux 有三个部分:actions、store 和 reducers。
- Actions 是事件。它们是将数据从应用程序发送到 Redux store 的方式。
- Reducers 是函数,它们接收应用程序的当前状态,执行一个 action,并返回一个新的状态。
- Store 包含应用程序状态。在 Redux 应用程序中只有一个 store。
现在我们有了 Redux 的概念。让我们用 Redux 重写 Global Weather 应用程序。
Visual Studio Code
在这篇文章中,编码将专注于前端 React,“weatherclient
”。所以我将使用 Visual Studio Code 而不是 Visual Studio。Visual Studio Code 是一个轻量级但功能强大的源代码编辑器,可在您的桌面上运行,适用于 Windows、macOS 和 Linux。它内置了对 JavaScript、TypeScript 和 Node.js 的支持,并拥有丰富的其他语言(如 C++、C#、Java、Python、PHP、Go)和运行时(如 .NET 和 Unity)的扩展生态系统。您可以在 此处 下载 Visual Studio Code。
在 Visual Studio Code 中打开 weatherclient 文件夹。
在 powershell 终端中运行 npm install
。
然后转到父文件夹“GlobalWeatherReact”,从命令行启动 ASP.NET Core。
dotnet run
安装“Debugger Chrome”插件,并添加启动配置。
点击 **Debug** 按钮,GlobalWeather
应用程序将开始在 Chrome 中运行。
从 Visual Studio Code 调试 typescript/javascript 代码非常容易。我稍后会向您展示如何操作。
安装 React-Redux 和 Middleware
转到“weatherclient”文件夹以安装我们需要的库。
React-Redux
React-Redux 是 Redux 的官方 React 绑定。
npm install react-redux @types react-redux --save
Redux-thunk
Thunks 是推荐的中间件,用于基本的 Redux 侧边栏逻辑,包括需要访问 store 的复杂同步逻辑,以及简单的异步逻辑,如 AJAX 请求。Redux Thunk 中间件允许您编写返回函数而不是 action 的 action creators。Thunk 可用于延迟 action 的 dispatch,或仅在满足特定条件时才 dispatch。
npm install redux-thunk --save
Axios 和 redux-axios-middlware
Axios 是一个用于发出 HTTP 请求的库。与 Fetch 一样,Axios 基于 Promise。但是,它提供了更强大和灵活的功能集。相比原生 Fetch API 的优点包括:
- 请求和响应拦截
- 简化的错误处理
- 防止 XSRF
- 支持上传进度
- 响应超时
- 取消请求的能力
- 支持旧浏览器
- 自动 JSON 数据转换
Redux-axios-middleware
使用 axios HTTP 客户端获取数据的 Redux 中间件。
npm install axios redux-axios-middleware --save
创建 Actions
根据我们已经完成的工作,我们知道我们的状态需要具有 2 个状态属性:countries 和指定位置的当前天气。
所以,让我们开始创建 action types。创建一个 types/actions.ts 文件来存放我们的 action types。
import { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { Weather } from "../types/Weather"
export type AppActions =
{
type: 'GET_WEATHER',
} | {
type: 'GET_WEATHER_SUCCESS',
payload: Weather
} | {
type: 'GET_WEATHER_FAIL',
error: string
} | {
type: 'GET_COUNTRIES',
payload: {
request: AxiosRequestConfig
}
} | {
type: 'GET_COUNTRIES_SUCCESS',
payload: AxiosResponse
} | {
type: 'GET_COUNTRIES_FAIL',
error: AxiosError
};
export const GET_WEATHER = 'GET_WEATHER';
export const GET_WEATHER_SUCCESS = 'GET_WEATHER_SUCCESS';
export const GET_WEATHER_FAIL = 'GET_WEATHER_FAIL';
export const GET_COUNTRIES = 'GET_COUNTRIES';
export const GET_COUNTRIES_SUCCESS = 'GET_COUNTRIES_SUCCESS';
export const GET_COUNTRIES_FAIL = 'GET_COUNTRIES_FAIL';
然后创建一个 actions/actions.ts 文件来存放我们的 action creators。
getCountries
action creator
import { AppActions } from "../types/actions";
import { Constants } from "../Constants";
export const getCountries = (): AppActions => ({
type: "GET_COUNTRIES",
payload: {
request: {
url: `${Constants.locationAPIUrl}/countries?apikey=${Constants.apiKey}`
}
}
});
使用 redux axios middleware,我们只需要配置 axios 请求并指定正确的 url。
getWeather
action creator
export const getWeather = (countryCode: string, searchText: string) => {
return async (dispatch: ThunkDispatch<any, any, AppActions>) => {
try {
var res = await Axios.get(`${Constants.locationAPIUrl}/cities/
${countryCode}/search?apikey=${Constants.apiKey}&q=${searchText}`);
const cities = res.data as City[];
var weather = {} as Weather;
if (cities.length > 0) {
const city = cities[0];
weather = await getCurrentWeather(city);
}
return dispatch({
type: "GET_WEATHER_SUCCESS",
payload: weather
});
}
catch (e) {
return dispatch({
type: "GET_WEATHER_FAIL",
error: e.isAxiosError ? e.message : JSON.stringify(e)
});
}
}
}
export async function getCurrentWeather(city: City): Promise<Weather> {
const res = await Axios.get(`${Constants.currentConditionsAPIUrl}/
${city.Key}?apikey=${Constants.apiKey}`);
const currentConditions = res.data as CurrentCondition[];
if (currentConditions.length > 0) {
return new Weather(currentConditions[0], city);
}
return {} as Weather;
}
代码逻辑并不新,只是组合了一些原来在 Home 组件中的函数。但您可以看到我们在这里使用了 Thunk Dispatch。Thunk 是一个函数,它包装一个表达式以延迟其求值。action creator 的返回值将是 dispatch 本身的返回值。这对于编排异步控制流非常方便,thunk action creators 可以相互 dispatch 并返回 Promises 来等待彼此完成。
此外,此 creator 函数根据 fetch 结果返回不同的 action 类型和 payload。
创建 Reducers
定义了 action creators 后,我们现在编写 reducers,它们接收这个 action 并返回应用程序的新状态。
创建一个 reducers/countriesReducer.ts 文件。
import { Country } from "../types/Country";
import { AppActions } from "../types/actions";
import * as Actions from "../types/actions";
const countriesReducerDefaultState: Country [] = [];
const countriesReducer = (state = countriesReducerDefaultState, action: AppActions):
Country [] => {
switch (action.type) {
case Actions.GET_COUNTRIES_SUCCESS:
const data = action.payload.data;
return data as Country [];
case Actions.GET_COUNTRIES_FAIL:
return state;
default:
return state;
}
};
export { countriesReducer };
创建 reducers/weatherReducer.ts 文件。
import { Weather } from "../types/Weather";
import { AppActions } from "../types/actions";
import * as Actions from "../types/actions";
const weatherReducerDefaultState: Weather = {};
const weatherReducer = (state = weatherReducerDefaultState, action: AppActions): Weather => {
switch (action.type) {
case Actions.GET_WEATHER_SUCCESS:
const weather = action.payload;
return weather;
case Actions.GET_WEATHER_FAIL:
return {
error: action.error
} as Weather;
default:
return state;
}
};
export { weatherReducer };
配置 Store
本质上,redux store 执行以下操作:
- 持有应用程序状态
- 允许通过
getState()
访问状态。getState
方法返回应用程序的当前状态树。 - 允许通过
dispatch(action)
更新状态。dispatch
方法“dispatch”一个 action,从而触发状态更改。 - 通过
subscribe(listener)
注册侦听器。subscribe(listener)
添加一个更改侦听器。 - 通过
subscribe(listener)
返回的函数处理侦听器的注销。
让我们创建 store/configureStore.ts 文件,其中包含:
import axios, {AxiosRequestConfig} from 'axios';
import thunk, { ThunkMiddleware } from "redux-thunk";
import { createStore, combineReducers, applyMiddleware } from "redux";
import axiosMiddleware from 'redux-axios-middleware';
import { AppActions } from "../types/actions";
import { countriesReducer } from "../reducers/countriesReducer";
import { weatherReducer } from "../reducers/weatherReducer";
export const rootReducer = combineReducers({
countries: countriesReducer,
weather: weatherReducer
});
const config: AxiosRequestConfig = {
responseType: 'json'
};
const defaultClient = axios.create(config);
export type AppState = ReturnType<typeof rootReducer>;
export const store = createStore(rootReducer, applyMiddleware(axiosMiddleware(defaultClient),
thunk as ThunkMiddleware<AppState, AppActions>));
现在更改我们应用程序的 App.tsx 文件,以包含 <Provider />
、configureStore
,设置我们的 store 并将我们的应用程序包装起来以将 store 作为 props 传递下去。
import React from 'react';
import "./App.css";
import { Provider } from "react-redux";
import { store } from './store/configureStore';
import AppRouter from "./router";
const App: React.FC = () => {
return (
<Provider store={store}>
<AppRouter />
</Provider>
);
}
export default App;
在组件中使用 Redux Store 和方法
现在我们需要将 action creation dispatch 映射到组件,并将 app state 连接到组件。
Home 组件
在使用 redux 之前,我们在 Home
组件的 ComponentDidMount
中获取所有国家,然后将国家数组作为属性传递给 Form
组件。
下面是 Home
组件当前的 render 方法。不同的组件通过父组件共享数据。
render() {
return (
<div className="container content panel">
<div className="container">
<div className="row">
<div className="form-container">
<WeatherDetails weather={this.state.weather} />
<Form getWeather={this.getWeather} countries={this.state.countries} />
</div>
</div>
</div>
</div>
);
}
有了 redux,我们就无需再采用这种方法了。组件可以自行连接到 app state(共享数据)。
我们将 render
方法更改为如下所示:
render() {
return (
<div className="container content panel">
<div className="container">
<div className="row">
<div className="form-container">
<WeatherDetails />
<Form />
</div>
</div>
</div>
</div>
);
}
并且还删除所有 API 方法,如 getCountries
、getCity
、getWeather
等。我们将使用 redux actions 来实现这些功能。
首先,为 Home
组件添加 IDispatch
接口。
interface IDispatchProps {
getCountries: () => void;
}
然后将 Home
组件更改为继承这个 properties 接口。
class Home extends React.Component<IDispatchProps, IState>
Connect 允许我们将组件连接到 Redux 的 store,而 getCountries
是我们之前编写的 action creator。
将 IDispatchProp
映射到带 Thunk 中间件的 Action Creator。
class Home extends React.Component<IDiconst mapDispatchToProps =
(dispatch: ThunkDispatch<any, any, AppActions>): IDispatchProps => ({
getCountries: bindActionCreators(getCountries, dispatch),
});
export default connect(null, mapDispatchToProps)(Home);
Form Component
Form
组件需要做两件事。将 AppState.countries
映射到 form 属性,并连接 getWeather
action。
interface IFormProps {
countries: Country[];
}
interface IDispatchProps {
getWeather: (country: string, city: string) => void;
}
interface IProps extends IFormProps, IDispatchProps {}
连接到 Form
组件。
export default connect (mapStateToProps, mapDispatchToProps) (Form)
将 handleSubmit
函数更改为使用 dispatch
属性。
handleSubmit = async (e: any) => {
e.preventDefault();
if (this.state.searchText && this.state.country)
this.props.getWeather(this.state.country.ID, this.state.searchText);
}
WeatherDetails Component
在 WeatherDetails
组件中将 AppState.weather
映射到组件属性。
const mapStateToProps = (state: AppState) : IProp =>({
weather : state.weather
});
export default connect (mapStateToProps) (WeatherDetails)
就是这样。在终端运行“npm run build
”以确保一切都可以编译。
在 Visual Studio Code 中调试 React App
正如我之前所说的,安装“Debugger for Chrome”插件并创建一个 Json 配置。例如:
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "https://:5001",
"webRoot": "${workspaceFolder}"
}
]
}
现在您需要先启动服务器,对于我们的应用程序来说是 .NetCore API。我们可以在命令行中完成。
转到 GloablWeatherReact
(ASP.NET Core) 项目文件夹,运行“dotnet run
”。
您可以看到开发服务器已在 https://localhots:5001 和 https://:5000 启动。
这可以为您提供有关如何在 Chrome Debugger 配置中设置 url 的思路。
您可以从 GloblaWeatherReact/properties/launchSettings.json 检查所有 launch 设置。
现在我们在 actions/actions.ts 中设置断点。然后点击 launch 按钮。
当您选择一个国家并输入位置名称,然后点击“Go”时,断点会被触发。
您可以看到调用堆栈,并添加任何您想要监视的对象。
好的。让它继续,点击 **Continue** 按钮,或按“F5”。
它工作得非常完美。
GlobalWeatherReact GitHub
Global Weather React App 现在可以在 GitHub 上找到。master 分支是使用 react 实现的,redux 分支是用于 react-redux 实现的。
结论
在这篇文章中,我们学习了如何将 REST API 调用集成到 React-Redux 中进行客户端状态管理,使用 Redux 来管理数据,而不是直接访问组件状态。请记住,Redux 的全部目的是为我们管理全局状态,并为 React 应用程序提供真正强大的功能。我们还学习了如何使用出色的 VS Code 插件来调试 React-Redux 应用程序。
历史
- 2019 年 11 月 20 日:初始版本