克隆并熟悉代码存储库

已完成

在本单元中,你将克隆现有应用程序的源代码存储库。 使用源的本地克隆,你将熟悉现有的客户端轮询功能,并评估如何以最佳方式重构代码。

克隆存储库

无论你是使用 Visual Studio、Visual Studio Code 还是其他集成开发环境 (IDE),都将间接使用 Git 来克隆存储库。 本模块以 Blazor Workshop(其中包含一个披萨订购应用程序)为基础构建。

git clone https://github.com/MicrosoftDocs/mslearn-blazing-pizza-signalr

成功克隆存储库后,最好生成并运行应用。 你需要先将目录更改为存储库的 blazor-workshop/src 目录,然后才能使用 .NET CLI。

cd ./mslearn-blazing-pizza-signalr/blazing-pizza/src

运行应用程序

你可以随意使用 IDE 或 .NET CLI。 在 CLI 中,使用 dotnet run 命令:

dotnet run --project ./BlazingPizza.Server/BlazingPizza.Server.csproj

此应用程序用于学习目的。 经过身份验证后,你可以注册任何电子邮件地址。 主动开发应用时,通过选择链接来确认帐户即可完成注册过程,而无需验证电子邮件地址。 有关详细信息,请参阅 Blazor Workshop:注册用户并登录

订购披萨

登录后,便可下单订购披萨。 选择披萨和馅料,然后将其添加到订单中。 例如,考虑下图:

Screenshot of the Blazing Pizza window for adding extra toppings to a pizza order.

在向订单添加额外的配料后,通过选择“订单”按钮进行下单。

Screenshot of the Blazing Pizza window for placing order.

创建订单后,应用将重定向到订单状态页的“我的订单”。 此页面按顺序显示各种订单状态详细信息,从“备餐中”到“配送中”,再到最后的“已送达”。 当订单状态为“配送中”时,实时地图将通过逐渐更改并模拟送餐员的位置来进行更新。

请考虑下面的一系列图片,这些图片显示了实时地图上从起始位置到结束位置的进度。 下面是各个状态:

备餐中Screenshot of the Blazing Pizza 'My Orders' window with an order status of 'Preparing'.

配送中 1Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver is just leaving.

配送中 2Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver has gone about a quarter of the way.

配送中 3Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver has gone about halfway.

配送中 4Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver has gone about three quarters of the way.

配送中 5Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver has arrived at the destination.

最后,订单状态页显示订单状态为“已送达”:

Screenshot of the 'My Orders' window with an order status of 'Delivered'. The live map shows that the driver has arrived at the delivery location.

停止应用程序

控制台应会输出各种日志,告知你应用已成功生成,并且正在 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