.NET 8 最小 API 和 React 前端






4.96/5 (13投票s)
一个响应式的 React 前端与 .NET 8 Minimal API 后端进行实时交互,以动态显示和管理模拟交通信号灯系统的状态转换。
引言
Minimal APIs 的概念在 .NET 中专注于通过减少样板代码来简化 Web API 的创建,使您能够更简洁地定义终结点。这些 API 利用了 ASP.NET Core 的核心功能,旨在以最小的编码工作量快速创建 HTTP API。它们非常适合微服务和小型应用程序,在这些场景下您希望避免完整 MVC 应用程序的复杂性。 在 ASP.NET Core Minimal APIs 中,您可以在 Program.cs 文件中直接定义终结点,而无需控制器或其他脚手架。
假设有 4组信号灯,如下所示。
信号灯 1:交通向南行驶
信号灯 2:交通向西行驶
信号灯 3:交通向北行驶
信号灯 4:交通向东行驶
同一轴线上的交通信号灯可以同时为绿色。在正常时段,所有信号灯都保持绿色 20 秒,但在高峰时段,南北方向的信号灯为绿色 40 秒,而东西方向的信号灯为绿色 10 秒。高峰时段为 08:00 至 10:00 和 17:00 至 19:00。黄灯在红灯显示前会亮 5 秒。红灯会一直亮着,直到交叉交通至少有 4 秒为红色,一旦红灯熄灭,就会按所需时间显示绿色。
在本文中,我们将实现一个 React 前端和 .Net 8 Minimal API 后端。后端将包含运行交通信号灯的逻辑和状态。前端将是交通信号灯的视觉表示,数据将从后端提供。
使用 Visual Studio “Standalone Typescript React Project” 模板创建解决方案
必备组件
- Visual Studio 2022 (17.1 或更高版本)。
- Node.js 18.20 或更高版本
创建 React 项目
启动 Visual Studio 2022,选择“Standalone TypeScript React Project”。
当您进入“Additional information”窗口时,选中“Add integration for Empty ASP.NET Web API Project”选项。此选项会将文件添加到您的 Angular 模板中,以便稍后与 ASP.NET Core 项目挂钩。
选择此选项,项目创建后将设置代理。本质上,此模板会运行“npx create-react-app”来创建一个 React 应用。
创建 Web API 项目
在同一解决方案中添加 ASP.NET Core Web API 项目。
将此后端项目命名为“TrafficLightsAPI”。选择 .NET 8.0 作为框架。
请注意,模板会在 program.cs 中生成 Minimal API 示例和 Weather Forecast。
设置启动项目
右键单击解决方案并选择“Set Startup Project”。将启动项目从“Single startup project”更改为“Multiple startup projects”。为每个项目的操作选择“Start”。
确保后端项目在前,这样它会先启动。
更改客户端代理设置
检查 TrafficLightsAPI 的 APP URL 中的 HTTPS 启动配置文件 UI。
然后打开 React 项目根文件夹中的 vite.config.ts。将 https://:5001 更新为 https://:7184 。
现在通过按“F5”或单击顶部菜单中的“Start”按钮来启动解决方案。
API 实现
让我们开始使用 .NET 8 实现交通信号灯系统的后端。我将指导您完成设置必要的模型、服务逻辑和控制器,以根据提供的规范管理交通信号灯。
定义模型和枚举
首先,我们将定义必要的模型和枚举来表示交通信号灯及其状态。
LightState 枚举
此枚举将表示交通信号灯的可能状态。
public enum LightState
{
Green,
Yellow,
Red
}
TrafficLight 模型
此模型将保存交通信号灯的当前状态以及可能与计时相关的其他属性。
public class TrafficLight
{
public string Direction { get; set; } = string.Empty;
public LightState CurrentState { get; set; }
public string CurrentStateColor => CurrentState.ToString();
public int GreenDuration { get; set; }
public int YellowDuration { get; set; } = 5;
public DateTime LastTransitionTime { get; set; } // The last state transition
public bool IsRightTurnActive { get; set; } = false; // Check the right-turn signal
public int GroupId { get; set; } // 1 for North-South, 2 for East-West
}
Traffic Light Service
此服务将处理交通信号灯的计时和状态转换逻辑。
using TrafficLightsAPI.Models;
namespace TrafficLightsAPI.Services
{
public class TrafficLightService
{
private List<TrafficLight> _lights;
public TrafficLightService()
{
var currentDateTime = DateTime.Now;
_lights = new List<TrafficLight>
{
new TrafficLight { Direction = "North", GreenDuration = 20, GroupId = 1, CurrentState = LightState.Green, LastTransitionTime = currentDateTime },
new TrafficLight { Direction = "South", GreenDuration = 20, GroupId = 1, CurrentState = LightState.Green, LastTransitionTime = currentDateTime },
new TrafficLight { Direction = "East", GreenDuration = 20, GroupId = 2, CurrentState = LightState.Red, LastTransitionTime = currentDateTime },
new TrafficLight { Direction = "West", GreenDuration = 20, GroupId = 2, CurrentState = LightState.Red, LastTransitionTime = currentDateTime }
};
}
public List<TrafficLight> RetrieveLights() => _lights;
public void UpdateLights()
{
lock (_lights)
{
DateTime currentTime = DateTime.Now;
bool isPeakHours = IsPeakHours(currentTime);
AdjustSouthboundForNorthRightTurn(currentTime);
foreach (var group in _lights.GroupBy(l => l.GroupId))
{
bool shouldSwitchToYellow = group.Any(l => l.CurrentState == LightState.Green && ShouldSwitchFromGreen((currentTime - l.LastTransitionTime).TotalSeconds, isPeakHours, l.Direction));
bool shouldSwitchToRed = group.Any(l => l.CurrentState == LightState.Yellow && (currentTime - l.LastTransitionTime).TotalSeconds >= 5);
if (shouldSwitchToYellow)
{
foreach (var light in group)
{
if (light.CurrentState == LightState.Red)
{
break;
}
light.CurrentState = LightState.Yellow;
light.LastTransitionTime = currentTime;
if (light.Direction == "North")
{
light.IsRightTurnActive = false;
}
}
}
else if (shouldSwitchToRed)
{
foreach (var light in group)
{
light.CurrentState = LightState.Red;
light.LastTransitionTime = currentTime;
}
SetOppositeGroupToGreen(group.Key);
}
}
}
}
#region Private Methods
private void SetOppositeGroupToGreen(int groupId)
{
int oppositeGroupId = groupId == 1 ? 2 : 1;
foreach (var light in _lights.Where(l => l.GroupId == oppositeGroupId))
{
light.CurrentState = LightState.Green;
light.LastTransitionTime = DateTime.Now;
}
}
private bool IsPeakHours(DateTime time)
{
return (time.Hour >= 8 && time.Hour < 10) || (time.Hour >= 17 && time.Hour < 19);
}
private bool ShouldSwitchFromGreen(double elapsedSeconds, bool isPeakHours, string direction)
{
int requiredSeconds = direction == "North" || direction == "South" ?
isPeakHours ? 40 : 20 :
isPeakHours ? 10 : 20;
return elapsedSeconds >= requiredSeconds;
}
private void AdjustSouthboundForNorthRightTurn(DateTime currentTime)
{
bool isPeakHours = IsPeakHours(currentTime);
var northLight = _lights.Single(l => l.Direction == "North");
if (northLight.CurrentState == LightState.Green && !northLight.IsRightTurnActive && ShouldActivateRightTurn((currentTime - northLight.LastTransitionTime).TotalSeconds, isPeakHours))
{
northLight.IsRightTurnActive = true;
foreach (var light in _lights.Where(l => l.Direction != "North"))
{
if (light.CurrentState != LightState.Red)
{
light.CurrentState = LightState.Red;
light.LastTransitionTime = currentTime;
}
}
}
}
private bool ShouldActivateRightTurn(double elapsedSeconds, bool isPeakHours)
{
// Activate right-turn signal for the last 10 seconds of the green phase
int greenDuration = isPeakHours ? 40 : 20;
return elapsedSeconds >= (greenDuration - 10);
}
#endregion
}
}
Minimal API 终结点
删除模板生成的 Weather Forecast 代码。然后设置新的 API 终结点并注册服务。
using TrafficLightAPI.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSingleton<TrafficLightService>();
builder.Services.AddHostedService<TrafficLightBackgroundService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/trafficlights", (TrafficLightService trafficLightService) =>
{
return Results.Ok(trafficLightService.RetrieveLights());
}).WithName("GetTrafficLights")
.WithOpenApi();
app.Run();
用于更新信号灯的后台服务
在 .NET 应用程序中实现后台作业以自动更新交通信号灯是模拟实时交通信号灯系统的有效方法。我们将使用一个后台服务,该服务定期调用 TrafficLightService 中的 UpdateLights 方法。这种方法允许交通信号灯的状态独立于 API 请求进行更新,模拟了交通信号灯自动改变的真实世界行为。
步骤 1:定义后台服务
您可以通过继承 BackgroundService 来在 .NET 中创建后台服务。此服务将运行一个计时器,该计时器以固定间隔触发 UpdateLights 方法。
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
public class TrafficLightBackgroundService : BackgroundService
{
private readonly TrafficLightService _trafficLightService;
private Timer _timer;
public TrafficLightBackgroundService(TrafficLightService trafficLightService)
{
_trafficLightService = trafficLightService;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_timer = new Timer(UpdateTrafficLights, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
return Task.CompletedTask;
}
private void UpdateTrafficLights(object state)
{
_trafficLightService.UpdateLights();
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_timer?.Change(Timeout.Infinite, 0);
await base.StopAsync(stoppingToken);
}
}
步骤 2:注册后台服务
在 Program.cs 或 Startup.cs(取决于您的项目设置)中,您需要注册此服务,以便在应用程序启动时启动它。
builder.Services.AddHostedService<TrafficLightBackgroundService>();
步骤 3:修改 TrafficLightService 以实现线程安全
由于 UpdateLights 现在将从后台服务调用,请确保 TrafficLightService 中的任何共享资源都以线程安全的方式进行访问。如果多个线程将修改交通信号灯的状态,您可能需要锁定资源或使用并发集合。
public void UpdateLights()
{
lock (_lights)
{
// Existing logic to update lights here
}
}
React 前端实现
让我们开始使用 TypeScript 设置 React 前端,以与您创建的交通信号灯后端进行交互。我们将构建一个简单的界面来显示交通信号灯,并根据从后端收到的数据实时更新它们。
使用 Visual Studio Code 打开 traffic-light 文件夹。
将 Material-UI(最近更名为 MUI)与 React 项目集成,将为交通信号灯系统提供精美的外观,并提供一致的用户体验。
安装 Material-UI
npm install @mui/material @emotion/react @emotion/styled
创建 TrafficLight 组件
创建一个新组件来显示单个交通信号灯。此组件将接收当前信号灯状态的 props 并显示适当的颜色。
// src/components/TrafficLight.tsx
import { Paper } from '@mui/material';
import { styled } from '@mui/system';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowsTurnRight } from '@fortawesome/free-solid-svg-icons';
interface TrafficLightProps {
direction: string;
currentState: string;
isRightTurnActive: boolean;
}
const Light = styled(Paper)(({ theme, color }) => ({
height: '100px',
width: '100px',
borderRadius: '50%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: color,
color: theme.palette.common.white,
fontSize: '1.5rem',
fontWeight: 'bold',
margin: '10px'
}));
const TrafficLight: React.FC<TrafficLightProps> = ({ direction, currentState, isRightTurnActive }) => {
const getColor = (state: string) => {
switch (state) {
case 'Green':
return 'limegreen';
case 'Yellow':
return 'yellow';
case 'Red':
return 'red';
default:
return 'grey'; // default color when state is unknown
}
};
return (
<div>
<Light color={getColor(currentState)} elevation={4}>
{direction}
</Light>
{isRightTurnActive && direction === 'North' && (
<div style={{ color: 'green', marginTop: '10px', fontSize: '24px' }}>
<FontAwesomeIcon icon={faArrowsTurnRight} /> Turn Right
</div>
)}
</div>
);
};
export default TrafficLight;
创建 TrafficLightsContainer 组件
此组件将管理从后端获取交通信号灯状态并相应更新 UI。
// src/components/TrafficLightsContainer.tsx
import React, { useEffect, useState } from 'react';
import TrafficLight from './TrafficLight';
import { Grid } from '@mui/material';
interface TrafficLightData {
direction: string;
currentStateColor: string;
isRightTurnActive: boolean;
}
const TrafficLightsContainer: React.FC = () => {
const [trafficLights, setTrafficLights] = useState<TrafficLightData[]>([]);
useEffect(() => {
const fetchTrafficLights = async () => {
try {
const response = await fetch('trafficlights');
const data = await response.json();
setTrafficLights(data);
} catch (error) {
console.error('Failed to fetch traffic lights', error);
}
};
fetchTrafficLights();
const interval = setInterval(fetchTrafficLights, 1000); // Poll every 5 seconds
return () => clearInterval(interval); // Cleanup on unmount
}, []);
return (
<Grid container justifyContent="center">
{trafficLights.map(light => (
<TrafficLight key={light.direction} direction={light.direction} currentState={light.currentStateColor} isRightTurnActive={light.isRightTurnActive} />
))}
</Grid>
);
};
export default TrafficLightsContainer;
更新 App 组件
更新主 App 组件以包含 TrafficLightsContainer。
// src/App.tsx
import './App.css';
import TrafficLightsContainer from './components/TrafficLightsContainer';
import { CssBaseline, Container, Typography } from '@mui/material';
function App() {
return (
<div className="App">
<CssBaseline />
<Container maxWidth="sm">
<header className="App-header">
<Typography variant="h4" component="h1" gutterBottom>
Traffic Lights System
</Typography>
<TrafficLightsContainer />
</header>
</Container>
</div>
);
}
export default App;
更新 Service Proxy
打开 vite.config.ts,将代理从 “/weatherforecast” 更改为 “/trafficlights”。
运行应用程序
回到 Visual Studio,现在的解决方案看起来是这样的
单击“Start”按钮或按 F5。
结论
RESTful API 消费
React 前端通过 RESTful API 与 .NET 后端通信。此交互涉及请求和接收交通信号灯数据,然后 React 使用这些数据相应地更新 UI。
解耦架构
前端和后端是松散耦合的。后端可以独立于前端运行,专注于逻辑和数据管理,而前端则专注于表示和用户交互。这种关注点分离增强了应用程序的可伸缩性和可维护性。
本文总结了 React 如何与 .NET 8 Minimal API 集成,以创建动态且响应迅速的交通信号灯管理系统,并采用解耦架构展示了现代 Web 开发实践,以实现高效且可伸缩的应用程序设计。