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」頁面,請參考下圖所示:
圖 2:Blazor 路由元件呈現 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 Policy,CSP),透過指定允許載入的資源來源,有效防止跨網站指令碼攻擊 (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>標籤內,可以隨心所欲修改的其中的版面配置,但限制僅能使用HTML、CSS與JavaScript程式碼,因為這個對話方塊必須在線路 (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),它記錄使用者在畫面上的操作,例如:onclick、onsubmit,也可以看到某個事件處理程式執行了多久,請參考下圖所示:
圖 12:UI 事件(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 意見:
張貼留言