2026年4月29日 星期三

Blazor 10新功能介紹 - 2

 

在 .NET 10版本中,Blazor 在應用程式操作體驗與系統可觀察性方面都有明顯提升。其中包含更完善的 404 錯誤處理機制、可客製化的「ReconnectModal」重新連線UI,以及整合 .NET Aspire 指標(Metrics)的可觀察性能力。透過「NavigationManager.NotFound」方法,Blazor 能依不同渲染模式自動處理 404 回應,減少樣板程式碼並提供一致的行為。同時,新的「ReconnectModal」元件讓開發者可以完全客製化伺服器斷線時的使用者介面。此外,搭配 .NET Aspire 的儀表板,開發者能即時觀察應用程式的頁面導覽、UI 事件與連線狀態等指標,使 Blazor 應用程式更容易監控與維護。

本文將延續《Blazor 10新功能介紹 - 1》一文的情境,介紹Blazor 10這些新功能。



404處理(找不到網頁或資源)

.NET 10 Blazor應用程式中,404(找不到網頁或資源)的處理方式迎來了大幅度的改進,不僅大幅減少了繁瑣的樣板程式碼,還能在各種渲染模式下提供一致的行為。

 

在以前我們通常會使用一個if判斷式,來顯示找不到網頁或資源,例如以下圖書元件的程式碼,根據網址中的書籍 Id,顯示該書的詳細資料與作者列表:

@page "/book/{id:int}"

@rendermode InteractiveServer

@using Microsoft.AspNetCore.Components

@using BlazorServerApp.Models

 

<PageTitle>Book Details</PageTitle>

 

<h1>Book Details</h1>

 

@if (isLoading) {

    <p>Loading...</p>

} else if (!string.IsNullOrWhiteSpace(errorMessage)) {

    <div class="alert alert-danger" role="alert">

        @errorMessage

    </div>

} else if (book is null) {

    <div class="alert alert-warning" role="alert">

        Book not found.

    </div>

} else {

    <div class="card mb-4">

        <div class="card-body">

            <dl class="row mb-0">

                <dt class="col-sm-3">Id</dt>

                <dd class="col-sm-9">@book.Id</dd>

 

                <dt class="col-sm-3">Title</dt>

                <dd class="col-sm-9">@book.Title</dd>

 

            

            </dl>

        </div>

    </div>

 

    <h4>Authors</h4>

    @if (book.Authors.Count == 0) {

        <p>No authors.</p>

    } else {

        <ul>

            @foreach (var author in book.Authors) {

                <li>@author.AuthorName</li>

            }

        </ul>

    }

 

}

 

@code {

    [Parameter]

    public int Id { get; set; }

 

    private Book? book;

    private bool isLoading = true;

    private string? errorMessage;

    private readonly List<Book> books =

    [

        new Book

        {

            Id = 1,

            Title = "Clean Code",

            Authors =

            [

                new Author { Id = 1, AuthorName = "Robert C. Martin" }

            ]

        },

        new Book

        {

            Id = 2,

            Title = "The Pragmatic Programmer",

            Authors =

            [

                new Author { Id = 2, AuthorName = "Andrew Hunt" },

                new Author { Id = 3, AuthorName = "David Thomas" }

            ]

        },

        new Book

        {

            Id = 3,

            Title = "Design Patterns",

            Authors =

            [

                new Author { Id = 4, AuthorName = "Erich Gamma" },

                new Author { Id = 5, AuthorName = "Richard Helm" },

                new Author { Id = 6, AuthorName = "Ralph Johnson" }

            ]

        }

    ];

 

    protected override async Task OnParametersSetAsync() {

        isLoading = true;

        errorMessage = null;

 

        await Task.Delay(100);

 

        book = books.FirstOrDefault(b => b.Id == Id);

 

        isLoading = false;

    }

}

 

這個範例執行時,當存取到不存在的圖書資料時,就會顯示以下自訂的錯誤畫面,請參考下圖所示:

 

1:顯示自訂的錯誤畫面


現在.NET 10版的專案範本中包含一個「NotFound.razor」元件用來顯示 404(找不到網頁或資源)畫面,它的程式如下:

@page "/not-found"

@layout MainLayout

 

<h3>Not Found</h3>

<p>Sorry, the content you are looking for does not exist.</p>

 

當你發現資源不存在時,只要在元件中直接呼叫「NavigationManager.NotFound方法,Blazor會自動接管後續的處理

 

Blazor會根據目前的渲染模式(Render Mode)自動採取最適合的動作

● 靜態伺服器端渲染(Static SSR:會直接將 HTTP 狀態碼設定為 404

● 互動式渲染 Interactive rendering:會通知 Blazor 路由元件呈現 404 的內容。

● 串流渲染 Streaming rendering:在啟用增強型導覽Enhanced Navigation的情況下,不需要重新載入頁面,直接呈現 404 內容。

 

讓我們改寫一下上個圖書範例的程式碼來說明NavigationManager.NotFound方法的運作方式:

@page "/book/{id:int}"

@rendermode InteractiveServer

@using Microsoft.AspNetCore.Components

@using BlazorServerApp.Models

@inject NavigationManager Navigation

 

<PageTitle>Book Details</PageTitle>

 

<h1>Book Details</h1>

 

@if (isLoading) {

    <p>Loading...</p>

} else if (!string.IsNullOrWhiteSpace(errorMessage)) {

    <div class="alert alert-danger" role="alert">

        @errorMessage

    </div>

} else {

    <div class="card mb-4">

        <div class="card-body">

            <dl class="row mb-0">

                <dt class="col-sm-3">Id</dt>

                <dd class="col-sm-9">@book.Id</dd>

 

                <dt class="col-sm-3">Title</dt>

                <dd class="col-sm-9">@book.Title</dd>

 

            

            </dl>

        </div>

    </div>

 

    <h4>Authors</h4>

    @if (book.Authors.Count == 0) {

        <p>No authors.</p>

    } else {

        <ul>

            @foreach (var author in book.Authors) {

                <li>@author.AuthorName</li>

            }

        </ul>

    }

 

}

 

@code {

    [Parameter]

    public int Id { get; set; }

 

    private Book? book;

    private bool isLoading = true;

    private string? errorMessage;

    private readonly List<Book> books =

    [

        new Book

        {

            Id = 1,

            Title = "Clean Code",

            Authors =

            [

                new Author { Id = 1, AuthorName = "Robert C. Martin" }

            ]

        },

        new Book

        {

            Id = 2,

            Title = "The Pragmatic Programmer",

            Authors =

            [

                new Author { Id = 2, AuthorName = "Andrew Hunt" },

                new Author { Id = 3, AuthorName = "David Thomas" }

            ]

        },

        new Book

        {

            Id = 3,

            Title = "Design Patterns",

            Authors =

            [

                new Author { Id = 4, AuthorName = "Erich Gamma" },

                new Author { Id = 5, AuthorName = "Richard Helm" },

                new Author { Id = 6, AuthorName = "Ralph Johnson" }

            ]

        }

    ];

 

    protected override async Task OnParametersSetAsync() {

        isLoading = true;

        errorMessage = null;

 

        await Task.Delay(100);

 

        book = books.FirstOrDefault(b => b.Id == Id);

        if (book is null) {

            Navigation.NotFound();

            return;

        }

 

        isLoading = false;

    }

}

 

這個元件執行後,若找不到對應的圖書資料則顯示「NotFound.razor」頁面,請參考下圖所示:

2Blazor 路由元件呈現 404 的內容

 

 UI中更新伺服器端連線狀態

.NET 10版之前,當Blazor應用程式發生伺服器斷線事件時,系統會顯示一個對話方塊,向使用者提供有關重新連線狀態的資訊。然而,這個對話方塊外觀陽春、不容易客製化,並且會導致內容安全原則 Content Security Policy)的問題。       

.NET 10版徹底解決了這個問題,將控制權完全交還給開發者,Blazor Web App範本專案内包含一個新的「ReconnectModal」元件,在重新連線到伺服端時,提供視覺化向使用者顯示的重新連線對話方塊,必要時可以進行客製化。

Visual Studio中建立專案時,請參考下圖所示,如果將互動式轉譯模式的設定為「Server」或「Auto」,就會在專案建立這個元件(ReconnectModal.razor),以及一個用來定義樣式的CSS檔案ReconnectModal.razor.css),和一個執行邏輯的JavaScript ReconnectModal.js程式碼檔案,透過這三個檔案,可以隨心所欲地自訂重新連線元件

3:預設會包含三個檔案來構成元件

 

ReconnectModal」元件支援內容安全政策遵循CSP-compliant ,這是指網站實施了內容安全政策的電腦安全標準Content Security PolicyCSP,透過指定允許載入的資源來源,有效防止跨網站指令碼攻擊 XSS)與其他程式碼注入攻擊,提升整體應用程式安全性

 

ReconnectModal.razor」檔案內包含了重新連線對話方塊元件的HTML畫面配置。參考以下範例程式碼,第一行帶有 <script> 標籤的程式碼載入了「ReconnectModal.razor.js」檔案,以啟用元件的互動性能力。不需要變動這個檔案,檔案中的程式邏輯負責管理重新連線對話方塊所需的東西,包含和使用者互動的按鈕。

<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>

 

<dialog id="components-reconnect-modal" data-nosnippet>

    <div class="components-reconnect-container">

        <div class="components-rejoining-animation" aria-hidden="true">

            <div></div>

            <div></div>

        </div>

        <p class="components-reconnect-first-attempt-visible">

            Rejoining the server...

        </p>

        <p class="components-reconnect-repeated-attempt-visible">

            Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.

        </p>

        <p class="components-reconnect-failed-visible">

            Failed to rejoin.<br />Please retry or reload the page.

        </p>

        <button id="components-reconnect-button" class="components-reconnect-failed-visible">

            Retry

        </button>

        <p class="components-pause-visible">

            The session has been paused by the server.

        </p>

        <p class="components-resume-failed-visible">

            Failed to resume the session.<br />Please retry or reload the page.

        </p>

        <button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">

            Resume

        </button>

    </div>

</dialog>

 

<dialog> 元素用來顯示重新連線資訊給使用者。在<dialog>標籤內,可以隨心所欲修改的其中的版面配置,但限制僅能使用HTMLCSSJavaScript程式碼,因為這個對話方塊必須在線路 (Circuit) 斷掉時顯示。

 

此外<dialog>元素中使用到一些CSS樣式,例如「components-reconnect-show」、「components-reconnect-retrying」,它們定義在「ReconnectModal.razor.css檔案之中,用在不同的狀態下要顯示的樣式,允許你自行修改

 

預設應用程式與伺服器失去連線(例如 WebSockets 斷線)時,向使用者顯示的畫面如下:

4:預設「ReconnectModal」畫面


ReconnectModal」元件客製化

最後我們看一個客製化範例,修改「ReconnectModal.razor」程式碼如下:

<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>

 

<dialog id="components-reconnect-modal" data-nosnippet>

    <div class="components-reconnect-container">

        <div class="reconnect-visual" aria-hidden="true">

            <div class="reconnect-radar">

                <div class="reconnect-radar-grid"></div>

                <div class="reconnect-radar-sweep components-reconnect-first-attempt-visible components-reconnect-repeated-attempt-visible"></div>

                <div class="reconnect-radar-dot components-reconnect-first-attempt-visible components-reconnect-repeated-attempt-visible"></div>

                <div class="reconnect-status-badge">

                    <span class="status-symbol status-symbol-sync"></span>

                    <span class="status-symbol status-symbol-retry"></span>

                    <span class="status-symbol status-symbol-failed">!</span>

                    <span class="status-symbol status-symbol-paused"></span>

                    <span class="status-symbol status-symbol-resume-failed">!</span>

                </div>

            </div>

        </div>

 

        <div class="reconnect-copy">

            <h2 class="reconnect-title components-reconnect-first-attempt-visible">

                正在重新加入工作階段

            </h2>

            <p class="reconnect-text components-reconnect-first-attempt-visible">

                正在嘗試還原即時連線。

            </p>

 

            <h2 class="reconnect-title components-reconnect-repeated-attempt-visible">

                仍在重新連線

            </h2>

            <p class="reconnect-text components-reconnect-repeated-attempt-visible">

                下次重試將於 <span id="components-seconds-to-next-attempt" class="reconnect-countdown"></span> 秒後開始。

            </p>

 

            <h2 class="reconnect-title reconnect-title-alert components-reconnect-failed-visible">

                連線未恢復

            </h2>

            <p class="reconnect-text components-reconnect-failed-visible">

                請立即重試,或重新載入頁面以開始新的工作階段。

            </p>

 

            <h2 class="reconnect-title reconnect-title-warn components-pause-visible">

                工作階段已暫停

            </h2>

            <p class="reconnect-text components-pause-visible">

                伺服器已暫停此工作階段。準備好後請繼續。

            </p>

 

            <h2 class="reconnect-title reconnect-title-alert components-resume-failed-visible">

                繼續失敗

            </h2>

            <p class="reconnect-text components-resume-failed-visible">

                請重試或重新載入頁面以繼續。

            </p>

        </div>

 

        <div class="reconnect-actions">

            <button id="components-reconnect-button" class="reconnect-action reconnect-action-primary components-reconnect-failed-visible">

                重試

            </button>

            <button id="components-resume-button" class="reconnect-action reconnect-action-secondary components-pause-visible components-resume-failed-visible">

                繼續

            </button>

        </div>

    </div>

</dialog>

 

 

修改「ReconnectModal.razor.css程式碼如下:

.components-reconnect-first-attempt-visible,

.components-reconnect-repeated-attempt-visible,

.components-reconnect-failed-visible,

.components-pause-visible,

.components-resume-failed-visible,

.status-symbol {

    display: none;

}

 

#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,

#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,

#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible,

#components-reconnect-modal.components-reconnect-paused .components-pause-visible,

#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible {

    display: block;

}

 

#components-reconnect-modal.components-reconnect-show .status-symbol-sync,

#components-reconnect-modal.components-reconnect-retrying .status-symbol-retry,

#components-reconnect-modal.components-reconnect-failed .status-symbol-failed,

#components-reconnect-modal.components-reconnect-paused .status-symbol-paused,

#components-reconnect-modal.components-reconnect-resume-failed .status-symbol-resume-failed {

    display: flex;

}

 

#components-reconnect-modal {

    width: min(22rem, calc(100vw - 2rem));

    margin: 18vh auto;

    padding: 0;

    border: 0;

    border-radius: 1.25rem;

    overflow: hidden;

    color: #edf4ff;

    background:

        radial-gradient(circle at top, rgba(101, 145, 255, 0.18), transparent 42%),

        linear-gradient(180deg, rgba(12, 20, 38, 0.98), rgba(7, 13, 25, 0.98));

    box-shadow: 0 24px 60px rgba(3, 8, 20, 0.55);

    opacity: 0;

    transform: translateY(24px) scale(0.96);

    transition: display 0.35s allow-discrete, overlay 0.35s allow-discrete;

    animation: reconnect-modal-exit 0.2s ease-in both;

}

 

#components-reconnect-modal[open] {

    animation: reconnect-modal-enter 0.45s cubic-bezier(0.18, 0.89, 0.32, 1.18) both;

}

 

#components-reconnect-modal::backdrop {

    background: rgba(2, 6, 23, 0.58);

    backdrop-filter: blur(4px);

    animation: reconnect-backdrop-fade 0.25s ease-out;

}

 

.components-reconnect-container {

    position: relative;

    display: grid;

    justify-items: center;

    gap: 1.25rem;

    padding: 1.75rem 1.5rem 1.5rem;

}

 

.components-reconnect-container::after {

    content: "";

    position: absolute;

    inset: auto 1rem 0.75rem;

    height: 3px;

    border-radius: 999px;

    background: linear-gradient(90deg, rgba(96, 165, 250, 0.15), rgba(129, 140, 248, 0.9), rgba(96, 165, 250, 0.15));

    opacity: 0;

}

 

#components-reconnect-modal.components-reconnect-show .components-reconnect-container::after,

#components-reconnect-modal.components-reconnect-retrying .components-reconnect-container::after {

    opacity: 1;

    animation: reconnect-trace 1.6s linear infinite;

}

 

#components-reconnect-modal.components-reconnect-failed .components-reconnect-container::after,

#components-reconnect-modal.components-reconnect-resume-failed .components-reconnect-container::after {

    opacity: 1;

    background: linear-gradient(90deg, rgba(248, 113, 113, 0.15), rgba(248, 113, 113, 0.9), rgba(251, 146, 60, 0.15));

}

 

#components-reconnect-modal.components-reconnect-paused .components-reconnect-container::after {

    opacity: 1;

    background: linear-gradient(90deg, rgba(251, 191, 36, 0.15), rgba(251, 191, 36, 0.9), rgba(253, 224, 71, 0.15));

}

 

.reconnect-visual {

    display: grid;

    place-items: center;

    width: 8.5rem;

    height: 8.5rem;

}

 

.reconnect-radar {

    position: relative;

    width: 100%;

    height: 100%;

    border-radius: 50%;

    overflow: hidden;

    background:

        radial-gradient(circle at center, rgba(93, 188, 255, 0.24) 0 16%, rgba(19, 34, 60, 0.95) 16% 58%, rgba(6, 13, 25, 0.98) 58% 100%);

    box-shadow:

        inset 0 0 0 1px rgba(148, 184, 255, 0.12),

        inset 0 0 28px rgba(59, 130, 246, 0.14),

        0 18px 35px rgba(0, 0, 0, 0.35);

}

 

.reconnect-radar::before,

.reconnect-radar::after {

    content: "";

    position: absolute;

    inset: 50% 12%;

    border-top: 1px solid rgba(148, 163, 184, 0.16);

    transform: translateY(-50%);

}

 

.reconnect-radar::after {

    inset: 12% 50%;

    border-top: 0;

    border-left: 1px solid rgba(148, 163, 184, 0.16);

    transform: translateX(-50%);

}

 

.reconnect-radar-grid {

    position: absolute;

    inset: 14%;

    border-radius: 50%;

    border: 1px solid rgba(148, 163, 184, 0.14);

    box-shadow:

        0 0 0 16px rgba(148, 163, 184, 0.08),

        0 0 0 32px rgba(148, 163, 184, 0.05);

}

 

.reconnect-radar-sweep {

    position: absolute;

    inset: -12%;

    border-radius: 50%;

    background: conic-gradient(from 0deg, transparent 0deg, rgba(56, 189, 248, 0.5) 55deg, transparent 110deg);

    filter: blur(1px);

    mix-blend-mode: screen;

    animation: reconnect-radar-sweep 2.4s linear infinite;

}

 

.reconnect-radar-dot {

    position: absolute;

    top: 22%;

    right: 23%;

    width: 0.75rem;

    height: 0.75rem;

    border-radius: 50%;

    background: #7dd3fc;

    box-shadow: 0 0 0 0 rgba(125, 211, 252, 0.7);

    animation: reconnect-radar-dot 1.7s ease-out infinite;

}

 

.reconnect-status-badge {

    position: absolute;

    inset: 31%;

    border-radius: 50%;

    display: grid;

    place-items: center;

    background: linear-gradient(145deg, #4d7dff, #3258d4);

    box-shadow:

        inset 0 1px 0 rgba(255, 255, 255, 0.2),

        0 10px 22px rgba(50, 88, 212, 0.35);

}

 

#components-reconnect-modal.components-reconnect-show .reconnect-status-badge,

#components-reconnect-modal.components-reconnect-retrying .reconnect-status-badge {

    animation: reconnect-badge-breathe 1.8s ease-in-out infinite;

}

 

#components-reconnect-modal.components-reconnect-failed .reconnect-status-badge,

#components-reconnect-modal.components-reconnect-resume-failed .reconnect-status-badge {

    background: linear-gradient(145deg, #f97316, #ef4444);

    box-shadow:

        inset 0 1px 0 rgba(255, 255, 255, 0.2),

        0 10px 22px rgba(239, 68, 68, 0.28);

}

 

#components-reconnect-modal.components-reconnect-paused .reconnect-status-badge {

    background: linear-gradient(145deg, #fbbf24, #d97706);

    box-shadow:

        inset 0 1px 0 rgba(255, 255, 255, 0.2),

        0 10px 22px rgba(217, 119, 6, 0.28);

}

 

.status-symbol {

    align-items: center;

    justify-content: center;

    width: 100%;

    height: 100%;

    font-size: 1.9rem;

    font-weight: 700;

    color: white;

    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);

}

 

.status-symbol-sync {

    animation: reconnect-spin 1.4s linear infinite;

}

 

.status-symbol-retry {

    animation: reconnect-blink 1.1s ease-in-out infinite;

}

 

.status-symbol-failed,

.status-symbol-resume-failed {

    animation: reconnect-alert-pop 0.35s ease-out;

}

 

.reconnect-copy {

    display: grid;

    gap: 0.5rem;

    text-align: center;

}

 

.reconnect-title,

.reconnect-text {

    margin: 0;

}

 

.reconnect-title {

    font-size: 1.35rem;

    font-weight: 700;

    letter-spacing: 0.01em;

    color: #f8fbff;

}

 

.reconnect-title-alert {

    color: #ffd1dc;

}

 

.reconnect-title-warn {

    color: #ffe19a;

}

 

.reconnect-text {

    max-width: 18rem;

    color: #c6d3f3;

    font-size: 0.96rem;

    line-height: 1.55;

}

 

.reconnect-countdown {

    display: inline-grid;

    place-items: center;

    min-width: 2rem;

    padding: 0.05rem 0.45rem;

    margin-inline: 0.15rem;

    border-radius: 999px;

    background: rgba(96, 165, 250, 0.16);

    border: 1px solid rgba(147, 197, 253, 0.26);

    color: #ffffff;

    font-weight: 700;

}

 

.reconnect-actions {

    display: flex;

    justify-content: center;

    gap: 0.75rem;

    width: 100%;

}

 

.reconnect-action {

    display: none;

    align-items: center;

    justify-content: center;

    min-width: 8.5rem;

    padding: 0.72rem 1.2rem;

    border-radius: 999px;

    border: 1px solid transparent;

    font-size: 0.95rem;

    font-weight: 600;

    cursor: pointer;

    transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease, border-color 0.18s ease;

}

 

#components-reconnect-modal.components-reconnect-failed .reconnect-action.components-reconnect-failed-visible,

#components-reconnect-modal.components-reconnect-paused .reconnect-action.components-pause-visible,

#components-reconnect-modal.components-reconnect-resume-failed .reconnect-action.components-resume-failed-visible {

    display: inline-flex;

}

 

.reconnect-action-primary {

    background: linear-gradient(135deg, #5a87ff, #7c67ff);

    color: white;

    box-shadow: 0 8px 20px rgba(92, 99, 255, 0.3);

}

 

.reconnect-action-secondary {

    background: rgba(251, 191, 36, 0.14);

    border-color: rgba(251, 191, 36, 0.28);

    color: #ffe39c;

    box-shadow: 0 8px 20px rgba(217, 119, 6, 0.16);

}

 

.reconnect-action:hover {

    transform: translateY(-1px);

}

 

.reconnect-action:active {

    transform: translateY(0);

}

 

.reconnect-action-primary:hover {

    box-shadow: 0 10px 24px rgba(92, 99, 255, 0.38);

}

 

.reconnect-action-secondary:hover {

    background: rgba(251, 191, 36, 0.2);

    border-color: rgba(251, 191, 36, 0.36);

}

 

@keyframes reconnect-modal-enter {

    from {

        opacity: 0;

        transform: translateY(24px) scale(0.96);

    }

 

    to {

        opacity: 1;

        transform: translateY(0) scale(1);

    }

}

 

@keyframes reconnect-modal-exit {

    from {

        opacity: 1;

    }

 

    to {

        opacity: 0;

    }

}

 

@keyframes reconnect-backdrop-fade {

    from {

        opacity: 0;

    }

 

    to {

        opacity: 1;

    }

}

 

@keyframes reconnect-radar-sweep {

    from {

        transform: rotate(0deg);

    }

 

    to {

        transform: rotate(360deg);

    }

}

 

@keyframes reconnect-radar-dot {

    0% {

        box-shadow: 0 0 0 0 rgba(125, 211, 252, 0.72);

        opacity: 0.35;

    }

 

    35% {

        opacity: 1;

    }

 

    100% {

        box-shadow: 0 0 0 16px rgba(125, 211, 252, 0);

        opacity: 0.4;

    }

}

 

@keyframes reconnect-badge-breathe {

    0%,

    100% {

        transform: scale(1);

    }

 

    50% {

        transform: scale(1.05);

    }

}

 

@keyframes reconnect-spin {

    from {

        transform: rotate(0deg);

    }

 

    to {

        transform: rotate(360deg);

    }

}

 

@keyframes reconnect-blink {

    0%,

    100% {

        opacity: 0.4;

    }

 

    50% {

        opacity: 1;

    }

}

 

@keyframes reconnect-alert-pop {

    0% {

        transform: scale(0.7);

        opacity: 0;

    }

 

    100% {

        transform: scale(1);

        opacity: 1;

    }

}

 

@keyframes reconnect-trace {

    0% {

        transform: scaleX(0.2);

        transform-origin: left;

    }

 

    50% {

        transform: scaleX(1);

        transform-origin: left;

    }

 

    50.1% {

        transform-origin: right;

    }

 

    100% {

        transform: scaleX(0.2);

        transform-origin: right;

    }

}

 

客製化ReconnectModal」元件的執行結果,請參考下圖所示:

5:客製化「ReconnectModal」元件

 

.NET Aspire指標(Metrics

.NET 10版中,如果讓Blazor專案搭配 .NET Aspire 一起使用,開發者可以更容易了解應用程式目前的運作狀況,例如效能如何、使用者在做什麼、系統資源使用了多少。這種能力通常稱之為可觀察性(Observability)。換言之,在指標(Metrics)的收集與顯示方面,.NET 10版有很明顯的提升。

如果想在Blazor專案中看到這些指標,其實很簡單。只要在現有的專案中加入Aspire 協調器Orchestrator 的支援即可。

舉例來說,使用Visual Studio 開發工具建立Blazor Web專案時,可以勾選「Enlist in Aspire orchestration」項目來加入Aspire 協調器的支援,請參考下圖所示:

6:勾選「Enlist in Aspire orchestration」項目加入Aspire支援

 

既有的Blazor專案可以從Visual Studio 開發工具「Solution Explorer」視窗 > 專案名稱上方按右鍵 > 選取「Add> Aspire Orchestrator Support項目加入,請參考下圖所示:

7:在既有的Blazor專案加入Aspire支援


加入之後,會自動在你的解決方案新增兩個專案,請參考下圖所示:

8:新增Aspire支援


參考下圖所示,其中的AppHost 專案用來管理整個分散式應用程式,例如:後端 API、資料庫、其他服務等,負責把這些服務一起啟動與進行協調。ServiceDefaults專案內建了一些常用的設定,例如:OpenTelemetry、健康檢查(Health Checks)、服務探索(Service Discovery)等等。

 

9:新增兩個專案


Blazor應用程式啟動後,你可以打開 .NET Aspire 開發人員儀表板(Developer Dashboard)。

這是一個整合式介面,可以即時查看結構化日誌(Logs)、分散式追蹤(Traces)與各種應用程式指標(Metrics)。也就是說,你可以集中在同一個地方看到整個系統的運作狀況,請參考下圖所示: 

10.NET Aspire 開發人員儀表板(Developer Dashboard


透過 Aspire 儀表板,可以看到在 .NET 10版為 ASP.NET Core Blazor新增的許多指標,在頁面導覽(Page Navigations方面,可以追蹤使用者瀏覽某個頁面的次數例如可以看到 /counter 這個頁面被存取了幾次,請參考下圖所示 

11:頁面導覽(Page Navigations)相關指標


Blazor Server類型的專案可以額外觀察到UI 事件(UI Events,它記錄使用者在畫面上的操作,例如:onclickonsubmit,也可以看到某個事件處理程式執行了多久請參考下圖所示:

12UI 事件(UI Events)相關指標


Blazor Server 應用程式中,也可以看到目前有多少使用者正在連線。例如以下指標

   Active circuits:目前活躍的連線數(大致等同於線上使用者)。

   Connected:目前真的連線中的使用者。

   Reconnecting:暫時斷線、正在等待重新連線的使用者。

 

請參考下圖所示:

13:線路狀態(Circuits)相關指標


總結

整體而言,.NET 10 Blazor 在錯誤處理、使用者體驗與系統監控方面都有顯著進步。新的 404 處理方式透過「NavigationManager.NotFound」方法提供更簡潔且一致的錯誤頁面管理;「ReconnectModal」元件則讓開發者能自訂伺服器斷線時的重新連線 UI,並支援內容安全政策(CSP),提升安全性與可維護性。此外,結合 .NET Aspire Metrics Developer Dashboard,開發者可以即時觀察頁面導覽、UI 事件與連線狀態等資訊,強化系統可觀察性。這些改進使 Blazor 在現代 Web 應用開發中更加成熟,也讓開發與維運變得更加容易。

0 意見:

張貼留言