ユーザーが頻繁に作成する一時的なデータの場合、一般的に使用されるストレージの場所は、ブラウザーの localStorage コレクションと sessionStorage コレクションです。
-
localStorageはブラウザーのインスタンスに限定されています。 ユーザーがページを再読み込みするか、ブラウザーを閉じて再び開くと、状態は維持されます。 ユーザーが複数のブラウザー タブを開くと、状態はすべてのタブで同じになります。 データは直接消去されるまでlocalStorageに残ります。 "プライベートブラウズ" または "シークレット" セッションに読み込まれたドキュメントのlocalStorageデータは、最後の [プライベート] タブが閉じられるとクリアされます。 -
sessionStorageの対象範囲はブラウザーのタブです。ユーザーがタブを再読み込みすると、状態は維持されます。 ユーザーがタブかブラウザーを閉じると、状態は失われます。 ユーザーが複数のブラウザー タブを開くと、それぞれのタブには、他に依存しないそのタブだけのバージョンのデータが保持されます。
一般に、sessionStorage を使用しておけば安全です。
sessionStorage の場合、ユーザーが複数のタブを開き、以下に遭遇するリスクが回避されます。
- タブ間の状態保存に含まれるバグ。
- タブで他のタブの状態が上書きされるときの紛らわしい動作。
ブラウザーを閉じて再び開いても状態を維持することがアプリに求められる場合、localStorage が最善の選択肢です。
ブラウザー ストレージ使用時の注意事項:
- サーバー側データベースの使用に似ていますが、データの読み込みと保存は非同期です。
- プリレンダリング中はローカル ストレージを利用できません。要求されたページがブラウザーに存在しないためです。
- サーバー側 Blazor アプリの場合、数キロバイトのデータをストレージに保持するのが妥当です。 数キロバイトを超えると、パフォーマンスに影響が出ることを考慮する必要があります。ネットワーク中でデータが読み込まれ、保存されるためです。
- ユーザーはデータを見たり、改ざんしたりするかもしれません。 ASP.NET Core のデータ保護で、このリスクを軽減できます。 たとえば、ASP.NET Core で保護されたブラウザー ストレージでは、ASP.NET Core のデータ保護が使用されます。
サードパーティ製 NuGet パッケージからは、localStorage と sessionStorage を使用するための API が与えられます。
ASP.NET Core のデータ保護を透過的に使用するパッケージを選択してみることもお勧めします。 データ保護を使用すると、保存データが暗号化され、保存データが改ざんされる潜在的リスクが減ります。 JSON でシリアル化されたデータがプレーンテキストで保存されている場合、ユーザーはブラウザー開発者ツールでデータを表示できます。また、保存データを変更できます。 些細なデータのセキュリティ保護は問題ではありません。 たとえば、UI 要素に保存されている色を読み取られたり、変更されたりしたところで、それはユーザーや組織にとって大きなセキュリティ リスクではありません。 "取り扱いに慎重を要するデータ" を見たり、改ざんしたりすることをユーザーに禁止します。
ASP.NET Core で保護されたブラウザー ストレージ
ASP.NET Core で保護されたブラウザー ストレージでは、 と localStorage に対して sessionStorageが使用されます。
注
保護されたブラウザー ストレージは、ASP.NET Core のデータ保護に依存しており、サーバー側 Blazor アプリでのみサポートされます。
Warnung
Microsoft.AspNetCore.ProtectedBrowserStorage はサポートのない試験用パッケージであり、運用環境での使用を意図したものではありません。
パッケージは、ASP.NET Core 3.1 アプリでのみ使用できます。
コンフィギュレーション
Microsoft.AspNetCore.ProtectedBrowserStorageへのパッケージ参照を追加します。注
.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。
_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 のデータ保護を使用して保護されます。 ブラウザーの開発者コンソールで 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 は、コンポーネントが最初にインスタンス化されたときに 1 回だけ呼び出されます。 後で、その同じページにいるとき、ユーザーが別の URL に移動しても OnInitializedAsync が再び呼び出されることはありません。 詳しくは、「ASP.NET Core Razor コンポーネントのライフサイクル」をご覧ください。
Warnung
このセクションの例は、サーバーでプリレンダリングが有効になっていない場合に機能します。 プリレンダリングが有効になっていると、コンポーネントがプリレンダリングされているために JavaScript 相互運用の呼び出しを発行できないことを示すエラーが生成されます。
プリレンダリングを無効にするか、プリレンダリングで使用するコードを追加します。 プリレンダリングと連動するコードを記述する方法の詳細については、「プリレンダリングを処理する」を参照してください。
読み込み状態を処理する
ブラウザー ストレージはネットワーク接続経由で非同期にアクセスされるため、データが読み込まれ、コンポーネントで利用できるようになるまでに、常に一定の時間があります。 最良の結果を得るには、空のデータや既定のデータを表示するのではなく、読み込みが進行中のとき、メッセージをレンダリングします。
1 つの手法は、データが 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 相互運用の呼び出しを発行できないことを示すエラーが生成されます。
このエラーを解決する方法の 1 つは、プリレンダリングを無効にすることです。 これは通常、ブラウザーベースのストレージがアプリで頻繁に使用される場合、最良の選択肢となります。 プリレンダリングによってさらに複雑になり、アプリにとっては良いことがありません。アプリでは 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);
}
}
状態保存を共通プロバイダーに分離する
さまざまなコンポーネントがブラウザーベースのストレージに依存しているとき、状態プロバイダー コードを何回も実装すると、コードが重複します。 コードの重複を回避する選択肢の 1 つは、状態プロバイダー ロジックをカプセル化する "状態プロバイダーの親コンポーネント" を作成することです。 状態保存メカニズムに関係なく、子コンポーネントは永続保存データとやりとりできます。
次の CounterStateProvider コンポーネントの例では、カウンター データは sessionStorageに保持され、状態の読み込みが完了するまで子コンテンツをレンダリングしないことで読み込みフェーズを処理します。
CounterStateProvider コンポーネントは、プリレンダリング中に実行されない、OnAfterRenderAsync ライフサイクル メソッドのでコンポーネントのレンダリング後まで状態を読み込まずにプリレンダリングを処理します。
このセクションのアプローチでは、同じページ上の複数のサブスクライブされたコンポーネントの再レンダリングをトリガーすることはできません。 サブスクライブされているコンポーネントの 1 つが状態を変更すると、再レンダリングされ、更新された状態を表示できますが、その状態を表示する同じページ上の別のコンポーネントには、それ自体の次の再レンダリングが行われるまで古いデータが表示されます。 そのため、このセクションで説明する方法は、ページ上の 1 つのコンポーネントで状態を使用する場合に最適です。
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 とやりとりするために必要ありませんし、"読み込み" 段階にも関係ありません。
一般に、次の場合、"状態プロバイダーの親コンポーネント" パターンが推奨されます。
- 多くのコンポーネントで状態を使用する。
- 最上位の状態オブジェクトを 1 つだけ保持する場合。
さまざまな状態オブジェクトを保持し、さまざまな場所でさまざまなオブジェクト サブセットを使用するには、状態をグローバルに保持しないことをお勧めします。
ASP.NET Core