克隆并熟悉代码存储库
在本单元中,你将克隆现有应用程序的源代码存储库。 使用源的本地克隆,你将熟悉现有的客户端轮询功能,并评估如何以最佳方式重构代码。
克隆存储库
无论你是使用 Visual Studio、Visual Studio Code 还是其他集成开发环境 (IDE),都将间接使用 Git 来克隆存储库。 本模块以 Blazor Workshop(其中包含一个披萨订购应用程序)为基础构建。
成功克隆存储库后,最好生成并运行应用。 你需要先将目录更改为存储库的 blazor-workshop/src 目录,然后才能使用 .NET CLI。
运行应用程序
你可以随意使用 IDE 或 .NET CLI。 在 CLI 中,使用 dotnet run
命令:
此应用程序用于学习目的。 经过身份验证后,你可以注册任何电子邮件地址。 主动开发应用时,通过选择链接来确认帐户即可完成注册过程,而无需验证电子邮件地址。 有关详细信息,请参阅 Blazor Workshop:注册用户并登录。
订购披萨
登录后,便可下单订购披萨。 选择披萨和馅料,然后将其添加到订单中。 例如,考虑下图:
在向订单添加额外的配料后,通过选择“订单”按钮进行下单。
创建订单后,应用将重定向到订单状态页的“我的订单”。 此页面按顺序显示各种订单状态详细信息,从“备餐中”到“配送中”,再到最后的“已送达”。 当订单状态为“配送中”时,实时地图将通过逐渐更改并模拟送餐员的位置来进行更新。
请考虑下面的一系列图片,这些图片显示了实时地图上从起始位置到结束位置的进度。 下面是各个状态:
最后,订单状态页显示订单状态为“已送达”:
停止应用程序
控制台应会输出各种日志,告知你应用已成功生成,并且正在 https://localhost:5001/
提供内容。 若要停止应用程序,请关闭浏览器,然后在命令行会话中按 Ctrl+C。
熟悉代码
此模块的重点是重构客户端轮询,改用 ASP.NET Core SignalR。 订购披萨的过程将用户重定向到订单详细信息页。 此页面执行客户端轮询。 请确保你了解当前这是如何实现的,以便知晓需要重构哪些代码。 请参考 OrderDetails.razor 文件:
@page "/myorders/{orderId:int}"
@attribute [Authorize]
@using System.Threading
@inject OrdersClient OrdersClient
@implements IDisposable
<div class="main">
@if (invalidOrder)
{
<h2>Nope</h2>
<p>Sorry, this order could not be loaded.</p>
}
else if (orderWithStatus == null)
{
<text>Loading...</text>
}
else
{
<div class="track-order">
<div class="track-order-title">
<h2>
Order placed @orderWithStatus.Order.CreatedTime.ToLongDateString()
</h2>
<p class="ml-auto mb-0">
Status: <strong>@orderWithStatus.StatusText</strong>
</p>
</div>
<div class="track-order-body">
<div class="track-order-details">
<OrderReview Order="orderWithStatus.Order" />
</div>
<div class="track-order-map">
<Map Zoom="13" Markers="orderWithStatus.MapMarkers" />
</div>
</div>
</div>
}
</div>
@code {
[Parameter] public int OrderId { get; set; }
OrderWithStatus orderWithStatus;
bool invalidOrder;
CancellationTokenSource pollingCancellationToken;
protected override void OnParametersSet()
{
// If we were already polling for a different order, stop doing so
pollingCancellationToken?.Cancel();
// Start a new poll loop
PollForUpdates();
}
private async void PollForUpdates()
{
invalidOrder = false;
pollingCancellationToken = new CancellationTokenSource();
while (!pollingCancellationToken.IsCancellationRequested)
{
try
{
orderWithStatus = await OrdersClient.GetOrder(OrderId);
StateHasChanged();
if (orderWithStatus.IsDelivered)
{
pollingCancellationToken.Cancel();
}
else
{
await Task.Delay(4000);
}
}
catch (AccessTokenNotAvailableException ex)
{
pollingCancellationToken.Cancel();
ex.Redirect();
}
catch (Exception ex)
{
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
StateHasChanged();
}
}
}
void IDisposable.Dispose()
{
pollingCancellationToken?.Cancel();
}
}
前面的 Razor 标记将执行以下操作:
- 绑定来自
orderWithStatus
对象的值,作为组件模板的一部分。- 创建时间和状态文本值绑定在订单标题标记中。
orderWithStatus.Order
作为参数传递给OrderReview
组件。- 地图标记(表示实时地图上的标记)传递给
Map
组件。
- 设置
OrderId
参数后,PollForUpdates
将会启动。- 此方法每四秒向服务器发送一次 HTTP 请求。
- 最新的订单状态详细信息将重新分配给
orderWithStatus
变量。
备注
PollForUpdates
方法为 async void
,表示它是“触发并忘记”模式。这可能会导致意外行为,应尽可能避免。 它将作为更改的一部分进行重构。
每次收到订单时,它都会重新计算配送状态更新和相应的地图标记变化。 这是通过计算 OrderWithStatus
对象的属性来实现的。 请参考下面的 OrderWithStatus.cs C# 文件:
using BlazingPizza.ComponentsLibrary.Map;
using System;
using System.Collections.Generic;
namespace BlazingPizza
{
public class OrderWithStatus
{
public readonly static TimeSpan PreparationDuration = TimeSpan.FromSeconds(10);
public readonly static TimeSpan DeliveryDuration = TimeSpan.FromMinutes(1); // Unrealistic, but more interesting to watch
public Order Order { get; set; }
public string StatusText { get; set; }
public bool IsDelivered => StatusText == "Delivered";
public List<Marker> MapMarkers { get; set; }
public static OrderWithStatus FromOrder(Order order)
{
// To simulate a real backend process, we fake status updates based on the amount
// of time since the order was placed
string statusText;
List<Marker> mapMarkers;
var dispatchTime = order.CreatedTime.Add(PreparationDuration);
if (DateTime.Now < dispatchTime)
{
statusText = "Preparing";
mapMarkers = new List<Marker>
{
ToMapMarker("You", order.DeliveryLocation, showPopup: true)
};
}
else if (DateTime.Now < dispatchTime + DeliveryDuration)
{
statusText = "Out for delivery";
var startPosition = ComputeStartPosition(order);
var proportionOfDeliveryCompleted = Math.Min(1, (DateTime.Now - dispatchTime).TotalMilliseconds / DeliveryDuration.TotalMilliseconds);
var driverPosition = LatLong.Interpolate(startPosition, order.DeliveryLocation, proportionOfDeliveryCompleted);
mapMarkers = new List<Marker>
{
ToMapMarker("You", order.DeliveryLocation),
ToMapMarker("Driver", driverPosition, showPopup: true),
};
}
else
{
statusText = "Delivered";
mapMarkers = new List<Marker>
{
ToMapMarker("Delivery location", order.DeliveryLocation, showPopup: true),
};
}
return new OrderWithStatus
{
Order = order,
StatusText = statusText,
MapMarkers = mapMarkers,
};
}
private static LatLong ComputeStartPosition(Order order)
{
// Random but deterministic based on order ID
var rng = new Random(order.OrderId);
var distance = 0.01 + rng.NextDouble() * 0.02;
var angle = rng.NextDouble() * Math.PI * 2;
var offset = (distance * Math.Cos(angle), distance * Math.Sin(angle));
return new LatLong(order.DeliveryLocation.Latitude + offset.Item1, order.DeliveryLocation.Longitude + offset.Item2);
}
static Marker ToMapMarker(string description, LatLong coords, bool showPopup = false)
=> new Marker { Description = description, X = coords.Longitude, Y = coords.Latitude, ShowPopup = showPopup };
}
}
在前面的 C# 代码中,FromOrder
根据当前时间计算新的订单状态。 在了解实现方式后,你将能够重用 OrderWithStatus
对象,并了解应用的重构方式。
提取重构的代码
重构的代码位于名为 signalr
的独立分支中。
使用 git remote
命令确定 https://github.com/MicrosoftDocs/mslearn-blazing-pizza-signalr
存储库的名称:
git remote -v
与 https://github.com/MicrosoftDocs/mslearn-blazing-pizza-signalr
存储库对应的远程名称是你将需要使用的名称。 接下来,使用 git fetch
命令提取 signalr
分支。 这假定远程名称为 upstream
,但名称可能为 origin
。
git fetch upstream signalr
最后,使用 git checkout
命令执行上下文切换,切换到重构后的源代码:
git checkout signalr