🌐 Blazor 基礎入門
📌 什麼是 Blazor?
Blazor 讓你用 C# 寫前端!不用學 JavaScript,也能打造互動式的網頁應用程式。
想像以前蓋網站就像經營一家中日合作餐廳:
- 前台(前端)說日文(JavaScript)
- 後台(後端)說中文(C#)
- 兩邊溝通需要翻譯(API 呼叫)
有了 Blazor,就像整間餐廳都說中文——前後台無障礙溝通,用同一種語言(C#)搞定一切!
🔀 Blazor Server vs Blazor WebAssembly
Blazor Server(伺服器模式):
┌──────────┐ SignalR ┌──────────┐
│ 瀏覽器 │ ◄──────────► │ 伺服器 │
│ (只負責 │ 即時連線 │ (負責所有 │
│ 顯示畫面)│ │ 邏輯計算) │
└──────────┘ └──────────┘
像遙控電視——遙控器(瀏覽器)按鈕,電視(伺服器)換台
Blazor WebAssembly(瀏覽器模式):
┌─────────────────────────────┐
│ 瀏覽器 │
│ ┌───────┐ ┌────────────┐ │
│ │ .NET │ │ 你的 C# 程式│ │
│ │Runtime│ │ 整個跑在 │ │
│ │ │ │ 瀏覽器裡 │ │
│ └───────┘ └────────────┘ │
└─────────────────────────────┘
像離線遊戲——整個程式下載到你的電腦上執行
| 項目 | Blazor Server | Blazor WebAssembly |
|---|---|---|
| 首次載入 | 快(只下載少量 JS) | 慢(要下載 .NET Runtime) |
| 執行位置 | 伺服器 | 瀏覽器 |
| 離線支援 | 不支援 | 支援 |
| 伺服器負擔 | 高(每個使用者都佔連線) | 低(邏輯在用戶端) |
| 適用場景 | 企業內部系統 | 公開面向使用者的應用 |
🧩 Component 元件基礎
@* Components/Counter.razor - Blazor 元件範例 *@
@* 每個 .razor 檔案就是一個元件(像樂高積木,可以組合使用) *@
<h3>計數器</h3>
<p>目前計數:@currentCount</p> @* 用 @ 符號顯示 C# 變數 *@
<button class="btn btn-primary"
@onclick="IncrementCount"> @* 按鈕點擊事件綁定到 C# 方法 *@
點我加一
</button>
@code {
// 元件內的 C# 程式碼區塊
private int currentCount = 0; // 計數變數
private void IncrementCount() // 按鈕點擊時執行的方法
{
currentCount++; // 計數加一
// Blazor 會自動更新畫面!不需要手動操作 DOM
}
}
🔄 元件生命週期
@* Components/LifecycleDemo.razor - 生命週期範例 *@
@implements IDisposable @* 實作 IDisposable 以便清理資源 *@
<h3>@Title</h3>
<p>資料:@data</p>
@code {
[Parameter] // 標記為元件參數(像函式的參數,由父元件傳入)
public string Title { get; set; } = ""; // 接收父元件傳來的標題
private string data = "載入中..."; // 資料狀態
// 1. OnInitialized:元件初始化時呼叫(像開店前的準備工作)
protected override void OnInitialized()
{
Console.WriteLine("元件已初始化"); // 只在第一次建立時呼叫
}
// 1b. 非同步版本(適合需要呼叫 API 的情況)
protected override async Task OnInitializedAsync()
{
data = await LoadDataFromApi(); // 從 API 載入資料
}
// 2. OnParametersSet:參數變更時呼叫(像收到新的訂單就更新菜單)
protected override void OnParametersSet()
{
Console.WriteLine($"參數已更新:Title = {Title}"); // 每次參數改變都會呼叫
}
// 3. OnAfterRender:畫面渲染完成後呼叫(像裝潢完成後的驗收)
protected override void OnAfterRender(bool firstRender)
{
if (firstRender) // 只在第一次渲染後執行
{
Console.WriteLine("第一次渲染完成!"); // 適合做 JS Interop
}
}
// 4. Dispose:元件被移除時呼叫(像關店時的清理工作)
public void Dispose()
{
Console.WriteLine("元件已銷毀,清理資源"); // 取消訂閱、關閉連線等
}
private async Task<string> LoadDataFromApi() // 模擬 API 呼叫
{
await Task.Delay(1000); // 模擬延遲 1 秒
return "資料載入完成!"; // 回傳資料
}
}
🔗 @bind 雙向綁定
@* Components/BindingDemo.razor - 資料綁定範例 *@
<h3>雙向綁定示範</h3>
@* 雙向綁定:輸入框的值和 C# 變數自動同步(像對講機,兩邊都能講) *@
<input @bind="userName" /> @* 當輸入框改變時,userName 自動更新 *@
<p>你好,@userName!</p> @* userName 改變時,畫面自動更新 *@
@* 指定綁定事件:oninput 表示每打一個字就更新(像即時翻譯) *@
<input @bind="searchText"
@bind:event="oninput" /> @* 預設是 onchange(失去焦點時才更新) *@
<p>搜尋:@searchText</p>
@* 綁定到不同的資料型別 *@
<input type="number" @bind="quantity" /> @* 綁定到整數 *@
<input type="date" @bind="selectedDate" /> @* 綁定到日期 *@
<input type="checkbox" @bind="isChecked" /> @* 綁定到布林值 *@
@* 下拉選單綁定 *@
<select @bind="selectedCity"> @* 綁定選中的值 *@
<option value="">請選擇城市</option> @* 預設選項 *@
<option value="taipei">台北</option> @* 選項 1 *@
<option value="taichung">台中</option> @* 選項 2 *@
<option value="kaohsiung">高雄</option> @* 選項 3 *@
</select>
<p>你選的城市:@selectedCity</p>
@code {
private string userName = "世界"; // 使用者名稱
private string searchText = ""; // 搜尋文字
private int quantity = 1; // 數量
private DateTime selectedDate = DateTime.Today; // 選擇日期
private bool isChecked = false; // 是否勾選
private string selectedCity = ""; // 選擇的城市
}
📡 EventCallback:父子元件溝通
@* Components/ChildButton.razor - 子元件 *@
@* 子元件像一個按鈕零件,按下去會通知父元件 *@
<button class="btn btn-success"
@onclick="OnButtonClick"> @* 按鈕被點擊時執行 *@
@ButtonText @* 顯示按鈕文字 *@
</button>
@code {
[Parameter] // 從父元件接收按鈕文字
public string ButtonText { get; set; } = "按我"; // 預設文字
[Parameter] // EventCallback:當事件發生時通知父元件(像對講機呼叫總部)
public EventCallback<string> OnClicked { get; set; } // 可以傳送字串訊息
private async Task OnButtonClick() // 按鈕點擊處理
{
await OnClicked.InvokeAsync("子元件被點擊了!"); // 通知父元件,傳送訊息
}
}
@* Pages/ParentPage.razor - 父元件 *@
@page "/parent"
<h3>父元件</h3>
<p>收到的訊息:@message</p>
@* 使用子元件,並監聽事件 *@
<ChildButton ButtonText="點擊我"
OnClicked="HandleChildClick" /> @* 當子元件觸發事件時呼叫此方法 *@
@code {
private string message = "(等待點擊)"; // 訊息狀態
private void HandleChildClick(string msg) // 處理子元件的事件
{
message = msg; // 更新訊息
// 畫面會自動更新,顯示新的訊息
}
}
💉 在元件中使用依賴注入
@* Components/WeatherDisplay.razor - 使用 DI 的元件 *@
@inject HttpClient Http @* 注入 HttpClient 服務 *@
@inject ILogger<WeatherDisplay> Logger @* 注入日誌服務 *@
<h3>天氣預報</h3>
@if (forecasts == null) // 如果資料還沒載入
{
<p>載入中...</p> @* 顯示載入提示 *@
}
else
{
<table class="table">
<thead>
<tr>
<th>日期</th>
<th>溫度 (°C)</th>
<th>天氣</th>
</tr>
</thead>
<tbody>
@foreach (var f in forecasts) @* 走訪每筆天氣資料 *@
{
<tr>
<td>@f.Date.ToShortDateString()</td> @* 顯示日期 *@
<td>@f.TemperatureC</td> @* 顯示溫度 *@
<td>@f.Summary</td> @* 顯示天氣描述 *@
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts; // 天氣資料陣列
protected override async Task OnInitializedAsync() // 元件初始化時
{
try
{
Logger.LogInformation("正在載入天氣資料..."); // 記錄日誌
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("api/weather");
// 從 API 取得天氣資料
}
catch (Exception ex) // 如果載入失敗
{
Logger.LogError($"載入天氣資料失敗:{ex.Message}"); // 記錄錯誤
forecasts = Array.Empty<WeatherForecast>(); // 設定為空陣列
}
}
}
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:在 OnInitializedAsync 中阻塞 UI 執行緒
@code {
// ❌ 錯誤寫法:用 .Result 阻塞(像在高速公路上停車等人)
protected override void OnInitialized()
{
var data = Http.GetFromJsonAsync<Data[]>("api/data").Result; // 阻塞 UI!
// 整個頁面會凍結,使用者什麼都不能做
}
}
@code {
// ✅ 正確寫法:使用 async/await(像預約好時間再去取餐)
protected override async Task OnInitializedAsync()
{
var data = await Http.GetFromJsonAsync<Data[]>("api/data"); // 非阻塞
// 頁面會先顯示「載入中...」,資料來了再自動更新
}
}
解釋: Blazor 的 UI 渲染和你的程式碼跑在同一條線上。如果你用 .Result 或 .Wait() 阻塞,就像在單線道上停車——所有人(包括畫面更新)都被堵住了。使用 async/await 就像設置一個等候區,不影響其他車輛通行。
❌ 錯誤 2:沒有清理資源(Dispose)
@code {
// ❌ 錯誤寫法:訂閱了事件但沒有取消訂閱
private Timer? _timer; // 計時器
protected override void OnInitialized()
{
_timer = new Timer(UpdateTime, null, 0, 1000); // 每秒更新一次
// 當元件被移除時,Timer 還在跑!記憶體洩漏!
}
}
@implements IDisposable @* 實作 IDisposable *@
@code {
// ✅ 正確寫法:在 Dispose 中清理資源
private Timer? _timer; // 計時器
protected override void OnInitialized()
{
_timer = new Timer(UpdateTime, null, 0, 1000); // 每秒更新
}
public void Dispose() // 元件被移除時自動呼叫
{
_timer?.Dispose(); // 停止並釋放計時器
}
}
解釋: 不清理資源就像退房時不還鑰匙——你已經離開了,但鑰匙(Timer、事件訂閱)還在佔用資源。久了就會造成記憶體洩漏(Memory Leak),整個應用程式越來越慢。
❌ 錯誤 3:忘記通知 Blazor 更新畫面
@code {
// ❌ 錯誤情境:在非 Blazor 事件中修改狀態(像背景工作完成後)
private string status = "等待中"; // 狀態
protected override void OnInitialized()
{
var timer = new System.Threading.Timer(_ =>
{
status = "已更新"; // 修改了變數,但畫面不會自動更新!
// 因為這不是 Blazor 的事件,Blazor 不知道要重新渲染
}, null, 3000, Timeout.Infinite);
}
}
@code {
// ✅ 正確寫法:呼叫 StateHasChanged 通知 Blazor
private string status = "等待中"; // 狀態
protected override void OnInitialized()
{
var timer = new System.Threading.Timer(_ =>
{
InvokeAsync(() => // 切回 Blazor 的同步上下文
{
status = "已更新"; // 修改狀態
StateHasChanged(); // 通知 Blazor 重新渲染畫面
});
}, null, 3000, Timeout.Infinite);
}
}
解釋: Blazor 只有在自己的事件(@onclick、OnInitializedAsync 等)觸發後才會自動更新畫面。如果是外部的 Timer 或背景工作修改了資料,必須用 InvokeAsync + StateHasChanged 手動通知 Blazor「嘿,資料變了,該重新畫畫面了!」。