針對使用者正在主動建立的暫時性資料,常用的儲存位置是瀏覽器的 localStorage 和 sessionStorage 集合:
-
localStorage的範圍限定於瀏覽器實例。 如果使用者重新載入頁面或關閉並重新開啟瀏覽器,會保存狀態。 如果使用者開啟多個瀏覽器索引標籤,狀態會跨索引標籤共用。 資料會持續存在localStorage中,直到明確清除為止。 當最後一個「私人」分頁關閉時,載入於「私人瀏覽」或「無痕」會話的文件的localStorage數據會被清除。 -
sessionStorage範圍設定為瀏覽器索引標籤。如果使用者重新載入索引標籤,則狀態會持續。 如果使用者關閉索引標籤或瀏覽器,狀態就會遺失。 如果使用者開啟多個瀏覽器索引標籤,則每個索引標籤都有自己的獨立資料版本。
一般而言,sessionStorage 使用上更安全。
sessionStorage 可避免使用者開啟多個索引標籤並遇到下列風險:
- 標籤頁的狀態儲存錯誤。
- 當索引標籤覆寫其他索引標籤的狀態時,造成混淆的行為。
如果應用程式必須在關閉並重新開啟瀏覽器時保存狀態,則 localStorage 為較佳的選擇。
使用瀏覽器儲存體的注意事項:
- 類似於使用伺服器端資料庫,載入及儲存資料為非同步。
- 由於在預先轉譯期間,要求的頁面不存在於瀏覽器中,因此預先轉譯期間不能使用本地存儲空間。
- 對於伺服器端 Blazor 應用程式,儲存幾個 KB 的數據是合理的。 除了幾 KB 之外,您還必須考量效能影響,因為資料會透過網路載入並儲存。
- 使用者可以檢視或竄改資料。 ASP.NET Core Data Protection 可以降低風險。 例如,ASP.NET Core Protected Browser Storage 會使用 ASP.NET Core Data Protection。
第三方 NuGet 套件提供用於 localStorage 和 sessionStorage 的 API。 建議您考慮選擇以透明方式使用 ASP.NET Core Data Protection 的套件。 資料保護會加密儲存的資料,並降低竄改已儲存資料的潛在風險。 如果 JSON 序列化資料以純文字儲存,則使用者可以使用瀏覽器開發人員工具查看資料,也可以修改儲存的資料。 保護一般數據不是問題。 例如,讀取或修改 UI 元素的預存色彩,對使用者或組織來說並不具有重大的安全性風險。 避免允許使用者檢查或竄改敏感性資料。
ASP.NET Core 受保護的瀏覽器儲存
ASP.NET Core Protected Browser Storage 利用 ASP.NET Core Data Protection 來對 localStorage 和 sessionStorage 進行保護。
備註
受保護的瀏覽器儲存體會仰賴 ASP.NET Core Data Protection,且僅支援伺服器端 Blazor 應用程式。
警告
Microsoft.AspNetCore.ProtectedBrowserStorage 是不受支援的實驗性套件,不適合用於生產環境。
套件僅適用於 ASP.NET Core 3.1 應用程式。
設定
在
_Host.cshtml檔案中,在結尾</body>標籤內新增下列指令碼:<script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/protectedBrowserStorage.js"></script>在
Startup.ConfigureServices中,呼叫AddProtectedBrowserStorage以將localStorage和sessionStorage服務新增至服務集合:services.AddProtectedBrowserStorage();
儲存及載入元件中的資料
在任何需要將資料載入或儲存至瀏覽器儲存體的元件中,使用 @inject 指示詞插入下列任一項的執行個體:
ProtectedLocalStorageProtectedSessionStorage
選擇取決於您想要使用的瀏覽器儲存位置。 在以下範例中,已使用 sessionStorage:
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@using 指示詞可以放在應用程式的 _Imports.razor 檔案中,而不是放在元件中。 使用 _Imports.razor 檔案會使命名空間可供應用程式的較大區段或整個應用程式使用。
若要在以 currentCount 專案範本為基礎的應用程式中,使 Counter 值於 Blazor 元件中持續存在,請修改 方法來使用 IncrementCount。
private async Task IncrementCount()
{
currentCount++;
await ProtectedSessionStore.SetAsync("count", currentCount);
}
在較大型、更現實的應用程式中,個別欄位的儲存是不太可能發生的案例。 應用程式更可能儲存包含複雜狀態的整個模型物件。
ProtectedSessionStore 會自動序列化及還原序列化 JSON 資料,以儲存複雜的狀態物件。
在上述程式碼範例中,currentCount 資料會在使用者的瀏覽器中儲存為 sessionStorage['count']。 資料不會以純文字儲存,而是使用 ASP.NET Core Data Protection 來保護資料。 若在瀏覽器的開發人員主控台中評估 sessionStorage['count'],則可以查看加密的資料。
若要在稍後使用者返回 currentCount 元件時復原 Counter 資料,包括使用者位於新的線路上,請使用 ProtectedSessionStore.GetAsync:
protected override async Task OnInitializedAsync()
{
var result = await ProtectedSessionStore.GetAsync<int>("count");
currentCount = result.Success ? result.Value : 0;
}
protected override async Task OnInitializedAsync()
{
currentCount = await ProtectedSessionStore.GetAsync<int>("count");
}
如果元件的參數包含瀏覽狀態,請呼叫 ProtectedSessionStore.GetAsync 並將非 null 的結果指派給 OnParametersSetAsync,而不是 OnInitializedAsync。 僅在第一次具現化元件時,才會呼叫 OnInitializedAsync 一次。 如果使用者在相同頁面上繼續瀏覽至不同的 URL,則稍後不會再次呼叫 OnInitializedAsync。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件生命週期。
警告
本節中的範例只有在伺服器未啟用預先轉譯時才能運作。 啟用預先轉譯時,會產生錯誤,說明無法發出 JavaScript Interop 呼叫,因為元件已預先轉譯。
停用預先轉譯或新增其他程式碼以使用預先轉譯。 若要深入了解撰寫可與預先轉譯搭配運作的程式碼,請參閱處理預先轉譯一節。
處理載入狀態
由於瀏覽器儲存體是透過網路連線以非同步方式存取,因此在載入資料並提供給元件之前,一律會有一段時間。 為了獲得最佳結果,在載入進行時轉譯訊息,而不是顯示空白或預設資料。
其中一種方法是追蹤資料是否為 null,這表示資料仍在載入中。 在預設 Counter 元件中,計數會保留在 int 中。
currentCount:
private int? currentCount;
只有在確認資料已載入後,才會顯示計數和 Increment 按鈕,而不是無條件顯示這些元素 HasValue。
@if (currentCount.HasValue)
{
<p>Current count: <strong>@currentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>
}
else
{
<p>Loading...</p>
}
處理預渲染
在預渲染期間:
- 使用者瀏覽器中不存在互動式連線。
- 瀏覽器還沒有可以執行 JavaScript 程式碼的頁面。
localStorage 或 sessionStorage 無法在預先轉譯期間使用。 如果元件嘗試與儲存體互動,就會產生錯誤,說明無法發出 JavaScript Interop 呼叫,因為元件已預先轉譯。
解決錯誤的其中一種方法是停用預先轉譯。 如果應用程式大量使用瀏覽器型儲存體,這通常是最佳選擇。 預先轉譯會增加複雜度,且不會使應用程式受益,因為應用程式在 localStorage 或 sessionStorage 可用之前,無法預先轉譯任何有用的內容。
若要停用預先轉譯,請指出轉譯模式,並在應用程式元件階層的最高層元件 (不是根元件) 中將 prerender 參數設定為 false。
備註
不支援讓根元件成為互動式,例如 App 元件。 因此,App 元件無法直接停用預先轉譯。
針對以 Blazor Web App 專案範本為基礎的應用程式,通常會停用預先轉譯,會在 Routes 元件中使用 App 元件 (Components/App.razor):
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
此外,停用 HeadOutlet 元件的預先轉譯:
<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
如需詳細資訊,請參閱 Prerender ASP.NET Core Razor 元件。
若要停用預先轉譯,請開啟 _Host.cshtml 檔案,並將 render-mode 的 屬性變更為 Server:
<component type="typeof(App)" render-mode="Server" />
當預先轉譯被停用時,<head>內容的預先轉譯會被停用。
預先轉譯對於不使用 localStorage 或 sessionStorage 的其他頁面可能很有用。 若要保留預先轉譯,請延遲載入作業,直到瀏覽器連線到線路為止。 以下是儲存計數器值的範例:
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore
@if (isConnected)
{
<p>Current count: <strong>@currentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>
}
else
{
<p>Loading...</p>
}
@code {
private int currentCount;
private bool isConnected;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
isConnected = true;
await LoadStateAsync();
StateHasChanged();
}
}
private async Task LoadStateAsync()
{
var result = await ProtectedLocalStore.GetAsync<int>("count");
currentCount = result.Success ? result.Value : 0;
}
private async Task IncrementCount()
{
currentCount++;
await ProtectedLocalStore.SetAsync("count", currentCount);
}
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore
@if (isConnected)
{
<p>Current count: <strong>@currentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>
}
else
{
<p>Loading...</p>
}
@code {
private int currentCount = 0;
private bool isConnected = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
isConnected = true;
await LoadStateAsync();
StateHasChanged();
}
}
private async Task LoadStateAsync()
{
currentCount = await ProtectedLocalStore.GetAsync<int>("count");
}
private async Task IncrementCount()
{
currentCount++;
await ProtectedLocalStore.SetAsync("count", currentCount);
}
}
將狀態提取到共用提供者
如果許多元件仰賴瀏覽器型儲存體,則實作狀態提供者程式碼在大多數情況下會建立程式碼重複。 避免程式碼重複的其中一個選項是建立封裝狀態提供者邏輯的狀態提供者父元件。 子元件可以處理持續化的資料,而不必考慮狀態保存機制。
在下列 CounterStateProvider 元件範例中,計數器資料會儲存至 sessionStorage,而且在狀態載入完成之前,暫不渲染其子內容以處理載入階段。
CounterStateProvider 元件處理預先呈現的方式是直到組件在 OnAfterRenderAsync 生命週期方法渲染後才載入狀態,而該方法在預先呈現期間不會執行。
本節中的方法無法觸發相同頁面上多個訂閱元件的重新呈現。 如果某個訂閱的元件變更狀態,它會重新渲染並且可以顯示更新後的狀態,但在相同頁面中顯示該狀態的其他元件則會顯示過時的資料,直到它自己下一次重新渲染為止。 因此,本節所述的方法最適合在頁面上的單一元件中使用狀態。
CounterStateProvider.razor:
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@if (isLoaded)
{
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
}
else
{
<p>Loading...</p>
}
@code {
private bool isLoaded;
[Parameter]
public RenderFragment? ChildContent { get; set; }
public int CurrentCount { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
isLoaded = true;
await LoadStateAsync();
StateHasChanged();
}
}
private async Task LoadStateAsync()
{
var result = await ProtectedSessionStore.GetAsync<int>("count");
CurrentCount = result.Success ? result.Value : 0;
isLoaded = true;
}
public async Task IncrementCount()
{
CurrentCount++;
await ProtectedSessionStore.SetAsync("count", CurrentCount);
}
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@if (isLoaded)
{
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
}
else
{
<p>Loading...</p>
}
@code {
private bool isLoaded;
[Parameter]
public RenderFragment ChildContent { get; set; }
public int CurrentCount { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
isLoaded = true;
await LoadStateAsync();
StateHasChanged();
}
}
private async Task LoadStateAsync()
{
CurrentCount = await ProtectedSessionStore.GetAsync<int>("count");
isLoaded = true;
}
public async Task IncrementCount()
{
CurrentCount++;
await ProtectedSessionStore.SetAsync("count", CurrentCount);
}
}
備註
如需關於 RenderFragment 的詳細資訊,請參閱 ASP.NET Core Razor 元件。
為了讓應用程式中的所有元件都能存取狀態,請使用全域互動式伺服器端渲染(互動式 SSR),將 CounterStateProvider 元件包裹在 Router 元件中的 <Router>...</Router>(Routes)周圍。
在 App 元件 (Components/App.razor) 中:
<Routes @rendermode="InteractiveServer" />
在 Routes 元件 (Components/Routes.razor) 中:
若要使用 CounterStateProvider 元件,請將其實例包裹在需要存取計數器狀態的其他任何元件周圍。 若要讓應用程式中的所有元件都能存取狀態,請在 CounterStateProvider 元件 (Router) 中,以 App 包裝 App.razor 元件:
<CounterStateProvider>
<Router ...>
...
</Router>
</CounterStateProvider>
備註
針對 .NET 5.0.1 和任何其他 5.x 版本,Router 元件中包含的一個參數為 PreferExactMatches,此參數已設定為 @true。 如需詳細資訊,請參閱 從 ASP.NET Core 3.1 移轉至 .NET 5。
被包裝的元件會接收並可修改持久化的計數器狀態。 下列 Counter 元件會實作模式:
@page "/counter"
<p>Current count: <strong>@CounterStateProvider?.CurrentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>
@code {
[CascadingParameter]
private CounterStateProvider? CounterStateProvider { get; set; }
private async Task IncrementCount()
{
if (CounterStateProvider is not null)
{
await CounterStateProvider.IncrementCount();
}
}
}
不需要上述元件即可與 ProtectedBrowserStorage 互動,且上述元件也不會處理「載入」階段。
一般而言,建議使用狀態提供者父元件模式:
- 若要在多個元件之間取用狀態。
- 如果只有一個最上層狀態物件需要持久化。
若要保存許多不同的狀態物件,並在不同位置取用不同的物件子集,最好避免全域保存狀態。