ASP.NET Core Blazor 核(he)心功能一:Blazor依賴注(zhu)入與狀態管理指南
大家好,我是碼農剛子。本(ben)文詳細介(jie)紹了(le)Blazor框(kuang)架中(zhong)的(de)(de)(de)依賴(lai)注入機(ji)制和狀(zhuang)態(tai)管理(li)方(fang)(fang)案(an)。依賴(lai)注入部分闡(chan)述了(le)服務注冊的(de)(de)(de)三種(zhong)(zhong)生命周期方(fang)(fang)式(shi)(Singleton/Scoped/Transient)及在組(zu)(zu)件(jian)(jian)中(zhong)的(de)(de)(de)使(shi)用方(fang)(fang)法。狀(zhuang)態(tai)管理(li)章節(jie)系統梳理(li)了(le)7種(zhong)(zhong)解(jie)決(jue)方(fang)(fang)案(an):從簡單的(de)(de)(de)組(zu)(zu)件(jian)(jian)內(nei)狀(zhuang)態(tai)到(dao)父(fu)子組(zu)(zu)件(jian)(jian)通(tong)信、級聯參(can)數,再到(dao)全局狀(zhuang)態(tai)容器和Flux/Redux模式(shi),并提供了(le)本(ben)地存(cun)儲持久化方(fang)(fang)案(an)。文章還介(jie)紹了(le)@ref指(zhi)令的(de)(de)(de)使(shi)用場景,包括組(zu)(zu)件(jian)(jian)引用、元素操作(zuo)和循環處(chu)理(li)等(deng)。最后給(gei)出了(le)不同場景下的(de)(de)(de)狀(zhuang)態(tai)管理(li)選擇建議,幫助開發者構建更健壯。
一(yi)、依賴(lai)注(zhu)入基礎
Blazor 提供了強大的依賴注入(Dependency Injection, DI)功能,用于將服務以解耦的方式注入到組件中,它幫(bang)助我們實現松耦合的代碼(ma)設計,提高可測試性(xing)和可維護性(xing)。
什么是依賴(lai)注入?
依賴(lai)注入是一種設計模式,它允(yun)許類從外部接(jie)收(shou)其依賴(lai)項,而不是自己創(chuang)建(jian)它們(men)。在 Blazor 中(zhong),這(zhe)意(yi)味著組件不需要知道如何(he)創(chuang)建(jian)服(fu)務(wu),而是通(tong)過構造函數或屬性接(jie)收(shou)這(zhe)些服(fu)務(wu)。
二、注冊(ce)和使用服務
1、創建自定義(yi)服務
1. 定義服務(wu)接口
public interface ICounterService { int Increment(int currentValue); int Decrement(int currentValue); void Reset(); }
2. 實現服務
public class CounterService : ICounterService { public int Increment(int currentValue) { return currentValue + 1; } public int Decrement(int currentValue) { return currentValue - 1; } public void Reset() { // 重(zhong)置邏輯 } }
2、注冊服(fu)務
在 Program.cs 文件中配置服(fu)務(wu)容器:
var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); // 注冊服(fu)務(wu) builder.Services.AddSingleton<ICounterService, CounterService>(); builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddTransient<IEmailService, EmailService>(); // 注冊內置服務 builder.Services.AddLocalStorage(); builder.Services.AddAuthorizationCore(); await builder.Build().RunAsync();
3、服務生命周期
Blazor 支持三(san)種服務生命周期(qi):
Singleton:整個應用(yong)生(sheng)命周(zhou)期(qi)內只有一個實例Scoped:每個用戶會話有(you)一(yi)個實(shi)例(Blazor Server)或每個瀏覽器標簽頁(Blazor WebAssembly)Transient:每次請求時(shi)創建新(xin)實(shi)例
4、在組件中(zhong)使用(yong)依賴(lai)注入
1. 使用 [Inject] 特性
@page "/counter" @inject ICounterService CounterService <h3>Counter</h3> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount = CounterService.Increment(currentCount); } }
2. 在代碼中使用(yong)注入的服務
@page "/user-profile" @inject IUserService UserService @inject NavigationManager Navigation <h3>User Profile</h3> @if (user != null) { <div> <p>Name: @user.Name</p> <p>Email: @user.Email</p> </div> } @code { private User user; protected override async Task OnInitializedAsync() { user = await UserService.GetCurrentUserAsync(); } private async Task UpdateProfile() { await UserService.UpdateUserAsync(user); Navigation.NavigateTo("/success"); } }
5、高級依(yi)賴注入(ru)用法
1. 工(gong)廠模式(shi)注冊(ce)
builder.Services.AddSingleton<ICounterService>(provider => { // 復雜的創建邏(luo)輯(ji) return new CounterService(); });
2. 選項模(mo)式
// 配置(zhi)選項類 public class ApiOptions { public string BaseUrl { get; set; } public int TimeoutSeconds { get; set; } } // 注冊選項 builder.Services.Configure<ApiOptions>(options => { options.BaseUrl = "//api.example.com"; options.TimeoutSeconds = 30; }); // 在服務中(zhong)使用 public class ApiService { private readonly ApiOptions _options; public ApiService(IOptions<ApiOptions> options) { _options = options.Value; } }
3. 條件注冊
#if DEBUG builder.Services.AddSingleton<ILogger, DebugLogger>(); #else builder.Services.AddSingleton<ILogger, ProductionLogger>(); #endif
三、組(zu)件狀態管理
在Blazor開發中,狀(zhuang)態管(guan)理是(shi)(shi)構建交(jiao)互式Web應用的(de)(de)核心挑戰。無論是(shi)(shi)簡單的(de)(de)計數器組件(jian)還是(shi)(shi)復雜的(de)(de)實時協作系(xi)統,選擇(ze)合適的(de)(de)狀(zhuang)態管(guan)理方案直接影響(xiang)應用性(xing)能和可維(wei)護性(xing)。
1、理解Blazor中的狀(zhuang)態管理
- 狀(zhuang)(zhuang)態是(shi)指應用程序或組件(jian)在(zai)某一(yi)(yi)時刻的(de)(de)數據(ju)或信(xin)息。例(li)如,一(yi)(yi)個(ge)(ge)(ge)(ge)計數器組件(jian)可(ke)以(yi)(yi)有(you)一(yi)(yi)個(ge)(ge)(ge)(ge)表示當前計數值的(de)(de)狀(zhuang)(zhuang)態,一(yi)(yi)個(ge)(ge)(ge)(ge)表單組件(jian)可(ke)以(yi)(yi)有(you)一(yi)(yi)個(ge)(ge)(ge)(ge)表示用戶(hu)輸入的(de)(de)狀(zhuang)(zhuang)態,一(yi)(yi)個(ge)(ge)(ge)(ge)購物車組件(jian)可(ke)以(yi)(yi)有(you)一(yi)(yi)個(ge)(ge)(ge)(ge)表示選中(zhong)商品的(de)(de)狀(zhuang)(zhuang)態等。狀(zhuang)(zhuang)態管(guan)理是(shi)指如何存(cun)儲、更新、獲取和傳遞這些數據(ju)或信(xin)息。
- 在(zai)Blazor中,每個(ge)組件都有自己(ji)的私有狀(zhuang)態,它只(zhi)能被該組件訪(fang)問和修(xiu)改。如果要將狀(zhuang)態從一(yi)(yi)個(ge)組件傳(chuan)遞(di)給另一(yi)(yi)個(ge)組件,或(huo)者在(zai)多(duo)個(ge)組件之間共享狀(zhuang)態,就(jiu)需要使(shi)用一(yi)(yi)些技(ji)術或(huo)模式來實現(xian)。下面我們將介紹一(yi)(yi)些常(chang)見的方法。
2、組件內狀態:最簡單的狀態管理
Blazor組件最基礎的(de)狀態(tai)(tai)管理方式是使用(yong)組件內部的(de)字段或屬性保存(cun)狀態(tai)(tai)。這種模式適用(yong)于狀態(tai)(tai)僅在(zai)單個(ge)組件內部使用(yong)且無需共享的(de)場景,如計數器、表單輸入等基礎交互。
@page "/counter" <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } }
上述代碼展示了典型的組件內狀態模式,currentCount字段存儲計數器狀態,IncrementCount方法修改狀態(tai)并(bing)自動觸發UI重新渲染。這(zhe)種模式的優勢在于實現簡(jian)單、零(ling)外部依賴,適合快速開發獨立功能組件。
3、父子組件通(tong)信:參數和事件回(hui)調
如果要將父組件的狀態傳遞給子組件,或者從子組件獲取更新后的狀態,可以使用參數和屬性(xing)來(lai)實現。
參數是指父組件向子組件傳遞數據或信息的方式。參數可以是任意類型的值,例如字符串、數字、布爾值、對象、委托等。要定義一個參數,需要在子組件中使用[Parameter]特性來標記一個公共屬性,并且該屬性的類型必須與父組件傳遞的值相同。例如:
這樣就定義了一個名為Counter的參數,在子組件中可以使用以下語法來獲取它的值:
<p>The counter value is @Counter</p>
在父(fu)組件中(zhong),可以使用以下(xia)語(yu)法來(lai)為參數賦值(zhi):
<CounterComponent Counter="@currentCount" /> @code { private int currentCount = 0; }
這(zhe)樣就將父組件中的(de)變量(liang)currentCount作為參數值(zhi)傳遞給了子(zi)組件。如(ru)果要實現從父到子(zi)單(dan)向(xiang)綁定(ding)。
屬(shu)性(xing)是(shi)指子(zi)組件(jian)向父組件(jian)傳遞數據(ju)或信息(xi)的方式。屬(shu)性(xing)可以是(shi)任意類型(xing)的值(zhi),但(dan)通常是(shi)一個事件(jian)回調(EventCallback)或一個動作(Action),用(yong)于在(zai)子(zi)組件(jian)中(zhong)觸(chu)發父組件(jian)定義(yi)的一個方法,從而將數據(ju)或信息(xi)傳遞給父組件(jian)。要(yao)(yao)定義(yi)一個屬(shu)性(xing),需要(yao)(yao)在(zai)子(zi)組件(jian)中(zhong)使(shi)用(yong)[Parameter]特性(xing)來標(biao)記(ji)一個公共屬(shu)性(xing),并且(qie)該(gai)屬(shu)性(xing)的類型(xing)必須是(shi)EventCallback<T>或Action<T>,其(qi)中(zhong)T是(shi)要(yao)(yao)傳遞的數據(ju)或信息(xi)的類型(xing)。例如:
<h3>CounterComponent</h3> <p>The counter value is @Counter</p> <button @onclick="CounterChangedFromChild">Update Counter from Child</button> @code { [Parameter] public int Counter { get; set; } [Parameter] public EventCallback<int> OnCounterChanged { get; set; } private async Task CounterChangedFromChild() { Counter++; await OnCounterChanged.InvokeAsync(Counter); } }
以上例子中就定義了一個名為OnCounterChanged的屬性,將子組件中的變量Counter作(zuo)為參數傳遞給了父組件。在父組件中,可以使(shi)用以下(xia)語法來為屬(shu)性賦值:
<CounterComponent OnCounterChanged="HandleCounterChanged" />
這樣就(jiu)將父組(zu)件(jian)中定義的一個方法名作為(wei)屬性(xing)值(zhi)傳(chuan)遞(di)給了子(zi)組(zu)件(jian)。該方法必須接(jie)受(shou)一個與屬性(xing)類型(xing)相(xiang)同的參(can)數(shu),并且可(ke)以(yi)在(zai)其中處(chu)理數(shu)據或信息。例如:
@code{ private void HandleCounterChanged(int counter) { Console.WriteLine($"The counter value is {counter}"); } }
這樣(yang)就實(shi)現(xian)了(le)從子到父單向傳遞(di)數(shu)據或信(xin)息,并且可以在任(ren)何時候觸發。
使用組件參(can)(can)數(shu)和屬性(xing)傳遞狀態:適合父子(zi)組件之間(jian)的簡單狀態傳遞,可以使用[Parameter]或者(zhe)級聯參(can)(can)數(shu)[CascadingParameter]特性(xing)來標記組件參(can)(can)數(shu),并且(qie)使用<Component Parameter="Value" />或者(zhe)<CascadingValue Value="Value"><Component /></CascadingValue>語法來傳遞狀態。
4、級聯參數和值
<!-- AppStateProvider.razor --> <CascadingValue Value="this"> @ChildContent </CascadingValue> @code { [Parameter] public RenderFragment? ChildContent { get; set; } private string theme = "light"; public string Theme { get => theme; set { if (theme != value) { theme = value; StateHasChanged(); } } } public event Action? OnThemeChanged; public void ToggleTheme() { Theme = Theme == "light" ? "dark" : "light"; OnThemeChanged?.Invoke(); } }
<!-- ConsumerComponent.razor --> <div class="@($"app-{appState.Theme}")"> <h3>當前主題: @appState.Theme</h3> <button @onclick="appState.ToggleTheme">切換主題</button> </div> @code { [CascadingParameter] public AppStateProvider appState { get; set; } = default!; protected override void OnInitialized() { if (appState != null) { appState.OnThemeChanged += StateHasChanged; } } public void Dispose() { if (appState != null) { appState.OnThemeChanged -= StateHasChanged; } } }
5、狀態容器模式(全局狀態)
創建狀態容器服務
// Services/AppState.cs public class AppState { private int _counter; private string _userName = string.Empty; public int Counter { get => _counter; set { _counter = value; OnCounterChanged?.Invoke(); } } public string UserName { get => _userName; set { _userName = value; OnUserNameChanged?.Invoke(); } } public event Action? OnCounterChanged; public event Action? OnUserNameChanged; public void IncrementCounter() { Counter++; } }
注冊服務
// Program.cs builder.Services.AddScoped<AppState>();
在組件中使用
@inject AppState AppState @implements IDisposable <h3>計數器: @AppState.Counter</h3> <h4>用戶: @AppState.UserName</h4> <button @onclick="AppState.IncrementCounter">增加計數</button> <input @bind="localUserName" @bind:event="onchange" placeholder="輸入用戶名(ming)" /> @code { private string localUserName { get => AppState.UserName; set { AppState.UserName = value; // 可(ke)以在(zai)這里添加其他邏(luo)輯 } } protected override void OnInitialized() { AppState.OnCounterChanged += StateHasChanged; AppState.OnUserNameChanged += StateHasChanged; } public void Dispose() { AppState.OnCounterChanged -= StateHasChanged; AppState.OnUserNameChanged -= StateHasChanged; } }
6、Flux/Redux 模式
什么是Flux模式?
Flux是一種應用(yong)程序架構模式(shi),專門(men)用(yong)于管(guan)理前端(duan)應用(yong)中的狀(zhuang)態(tai)。與(yu)常見的MVC模式(shi)不(bu)同(tong),Flux采用(yong)單向數據流的設計,使(shi)得狀(zhuang)態(tai)變化(hua)更加(jia)可預(yu)測和易于追蹤。
Flux模(mo)式(shi)的核心思想是(shi)將狀態管理與UI渲染分離,通過(guo)嚴格的規則來(lai)規范(fan)狀態變更的過(guo)程。這種模(mo)式(shi)最初由Facebook提出,后來(lai)被Redux等(deng)庫實現(xian),而Fluxor則是(shi)專門為(wei)Blazor應(ying)用設計的實現(xian)方(fang)案。
Flux模式的核心原則
- 狀態只讀原則
應用的狀(zhuang)態(tai)在任(ren)何情(qing)況下都不(bu)應該被直接修(xiu)改,這保證了(le)狀(zhuang)態(tai)變更的可控性。
- 動作驅動變更
任何(he)狀態變更都必須(xu)通過派發(dispatch)一個動作(action)來觸發。動作是一個簡(jian)單的對象(xiang),描述了發生了什么變化。
- 純函數處理
使用稱為"reducer"的純函數(shu)來(lai)處理動作,根據(ju)當前(qian)狀(zhuang)態(tai)和(he)動作生成新狀(zhuang)態(tai)。Reducer不會(hui)修改原(yuan)有狀(zhuang)態(tai),而是(shi)返回全(quan)新的狀(zhuang)態(tai)對象。
- 單向數據流
UI組件(jian)訂閱狀(zhuang)(zhuang)態變(bian)化,當狀(zhuang)(zhuang)態更(geng)新時自動(dong)(dong)重新渲染(ran)。用戶交互則通過派發(fa)動(dong)(dong)作來觸發(fa)狀(zhuang)(zhuang)態變(bian)更(geng),形成完整的單向(xiang)循環。
核心概念
- ?狀態(State)?:定義應用數據模型,不(bu)可直接修改,需通過動作(zuo)(Action)觸(chu)發更新。
- ?動作(Action)?:描述狀態(tai)變(bian)更意圖的對(dui)象,包含類型標識和有效載荷。
- ?歸約器(Reducer)?:純函數,根(gen)據當前狀(zhuang)態(tai)和動(dong)作生(sheng)成新狀(zhuang)態(tai)。
- ?效果(Effect)?:處理副作(zuo)用操作(zuo)(如 API 調用),監聽動作(zuo)并執(zhi)行異步任務。
- 中間件(Middleware):中間(jian)件可以在動作(zuo)被派(pai)發到reducer之前或之后執行自定義(yi)邏(luo)輯,用于日志記錄、性能監控等橫切(qie)關注點。
使用 Fluxor 庫
首先安裝 Fluxor:
Install-Package Fluxor.Blazor.Web
定義狀態和動作
// Store/CounterState.cs public record CounterState { public int Count { get; init; } } // Store/Actions/IncrementCounterAction.cs public record IncrementCounterAction; // Store/Reducers/CounterReducers.cs public static class CounterReducers { [ReducerMethod] public static CounterState OnIncrementCounter(CounterState state, IncrementCounterAction action) { return state with { Count = state.Count + 1 }; } }
在組件中使用
@using Fluxor @inherits Fluxor.Blazor.Web.Components.FluxorComponent <h3>計數器: @State.Value.Count</h3> <button @onclick="Increment">增加</button> @code { [Inject] private IState<CounterState> State { get; set; } = null!; [Inject] private IDispatcher Dispatcher { get; set; } = null!; private void Increment() { Dispatcher.Dispatch(new IncrementCounterAction()); } }
7、本地存儲持久化
使用 Blazor 本地存儲
@page "/counter2" @inject IJSRuntime JSRuntime <h3>持久化計數器: @count</h3> <button @onclick="Increment">增加并保存</button> @code { private int count = 0; private bool isInitialized = false; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await LoadFromStorage(); isInitialized = true; StateHasChanged(); // 確(que)保在加載后更新UI } } private async Task Increment() { count++; await SaveToStorage(); //StateHasChanged(); } private async Task SaveToStorage() { if (isInitialized) { await JSRuntime.InvokeVoidAsync("localStorage.setItem", "counter", count); } } private async Task LoadFromStorage() { try { var savedCount = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "counter"); if (int.TryParse(savedCount, out int result)) { count = result; } } catch (Exception ex) { // 處理預(yu)渲(xuan)染期間的 JS 互操作錯(cuo)誤 Console.WriteLine($"加載存儲時出錯: {ex.Message}"); } } }
8、狀態管理選擇指南

四、使用 @ref 引用組件
在 Blazor 中,@ref 指令用于(yu)獲取對組件(jian)或 HTML 元素的引用,讓你能夠(gou)在代碼中(zhong)直接(jie)操作它們。以下(xia)是詳細的使用方法(fa):
1、引用組件
基本用法
<!-- MyComponent.razor --> <h3>計數器: @count</h3> <button @onclick="Increment">增加</button> @code { private int count = 0; public void Increment() { count++; StateHasChanged(); } public void Reset() { count = 0; StateHasChanged(); } }
<!-- ParentComponent.razor --> @page "/parent" <MyComponent @ref="myComponentRef" /> <button @onclick="ResetChild">重置子組件</button> @code { private MyComponent? myComponentRef; private void ResetChild() { myComponentRef?.Reset(); } }
2、引用 HTML 元素
@page "/element-ref" <input @ref="usernameInput" placeholder="輸入用戶名" /> <button @onclick="FocusInput">聚焦輸入框</button> @code { private ElementReference usernameInput; private async Task FocusInput() { await usernameInput.FocusAsync(); } }
3、在循環中使用 @ref
@page "/loop-ref-example" <h3>循環中使用 ref 示例</h3> <button @onclick="ShowAllMessages" class="btn btn-primary">顯示所有消息</button> <button @onclick="UpdateAllMessages" class="btn btn-secondary">更新所有消息</button> @foreach (var item in items) { <ChildComponent @ref="componentRefs[item.Id]" Id="item.Id" Message="@item.Message" OnMessageChanged="HandleMessageChanged" /> } @code { private List<ItemModel> items = new(); private Dictionary<int, ChildComponent?> componentRefs = new(); protected override void OnInitialized() { items = new List<ItemModel> { new ItemModel { Id = 1, Message = "第一條消(xiao)息" }, new ItemModel { Id = 2, Message = "第(di)二條消息" }, new ItemModel { Id = 3, Message = "第三(san)條(tiao)消息" } }; // 預先初始化(hua)字(zi)典 foreach (var item in items) { componentRefs[item.Id] = null; } } private void ShowAllMessages() { foreach (var component in componentRefs.Values) { component?.ShowCurrentMessage(); } } private void UpdateAllMessages() { foreach (var item in items) { if (componentRefs.TryGetValue(item.Id, out var component) && component != null) { component.UpdateMessage($"更新后的消息(xi) {item.Id}"); } } } private void HandleMessageChanged((int Id, string Message) data) { Console.WriteLine($"收到消息更新 - ID: {data.Id}, 消息: {data.Message}"); var item = items.FirstOrDefault(i => i.Id == data.Id); if (item != null) { item.Message = data.Message; StateHasChanged(); } } public class ItemModel { public int Id { get; set; } public string Message { get; set; } = string.Empty; } }
<!-- ChildComponent.razor --> <div class="child-component"> <h5>子組件 ID: @Id</h5> <p>當前消息: <strong>@Message</strong></p> <input @bind="currentMessage" @bind:event="oninput" /> <button @onclick="UpdateMessage" class="btn btn-sm btn-info">更新消息</button> </div> @code { [Parameter] public int Id { get; set; } [Parameter] public string Message { get; set; } = string.Empty; [Parameter] public EventCallback<(int Id, string Message)> OnMessageChanged { get; set; } private string currentMessage = string.Empty; protected override void OnParametersSet() { currentMessage = Message; } private async Task UpdateMessage() { await OnMessageChanged.InvokeAsync((Id, currentMessage)); } public void ShowCurrentMessage() { Console.WriteLine($"組件 {Id} 的消(xiao)息: {Message}"); } public void UpdateMessage(string newMessage) { currentMessage = newMessage; UpdateMessage().Wait(); } }
4、使用 ref 回調
<CustomInput @ref="SetInputRef" /> @code { private CustomInput? inputRef; private void SetInputRef(CustomInput component) { inputRef = component; // 組件引(yin)用設置后的初(chu)始(shi)化邏輯 component?.Initialize(); } }
5、與 JavaScript 互操作
@inject IJSRuntime JSRuntime <div @ref="myDiv" style="width: 100px; height: 100px; background: red;"></div> <button @onclick="ChangeDivStyle">修改樣式</button> @code { private ElementReference myDiv; private async Task ChangeDivStyle() { await JSRuntime.InvokeVoidAsync("changeElementStyle", myDiv); } }
對(dui)應的 JavaScript 文件(jian):
// wwwroot/js/site.js window.changeElementStyle = (element) => { element.style.background = 'blue'; element.style.width = '200px'; };
本章節中用到:IJSRuntime ,后面會(hui)詳細講解。
以上(shang)就是關(guan)于《ASP.NET Core Blazor 核(he)心功能一:Blazor依賴注入與(yu)狀態管理指南(nan)》的全部內容,希望(wang)你有所收獲。關(guan)注我(wo),持續分享(xiang)。
