این Prerendering است که امکان رندر یک کامپوننت تعاملی را در سمت سرور میسر میکند تا کاربر بتواند پیش از فعال شدن قابلیتهای پیشرفتهی یک کامپوننت، یک حداقل خروجی را از آن مشاهده کند و همچنین وجود آن برای موتورهای جستجو و بهبود SEO بسیار مفید است. اما ... در این بین مشکلی رخ میدهد که نمونهی آنرا در
قسمت قبل مشاهده کردیم: آغاز آن دوبار صورت میگیرد؛ یکبار در سمت سرور برای تهیهی یک خروجی SSR و یکبار هم پس از فعال شدن قابلیتهای تعاملی آن در سمت کلاینت. این آغاز دوباره، برای هر دو حالت کامپوننتهای تعاملی Blazor Server و Blazor WASM برقرار است. راهحلهایی از نحوهی مواجه شدن با یک چنین مشکلی را در
قسمت قبل بررسی کردیم. راهحل دیگری که در این بین ارائه شده و توسط خود مایکروسافت هم در مثالهای آن مورد استفاده قرار میگیرد، استفاده از سرویس PersistentComponentState است که جزئیات آنرا در این قسمت بررسی خواهیم کرد.
بررسی نحوهی عملکرد سرویس PersistentComponentState
سرویس PersistentComponentState، در داتنت 6، به Blazor اضافه شد و امکان جدیدی نیست. قسمتی از این مباحث جدید SSR که بهنظر مختص به Blazor 8x هستند، پیشتر هم وجود داشتند؛ تحت عنوان pre-rendering. برای مثال فقط کافی بودن تا در برنامههای Blazor Server قبلی، فایل Host.cshtml_ را به صورت زیر ویرایش کرد تا pre-rendering فعال شود:
<component type="typeof(App)" render-mode="ServerPrerendered" />
مشکلی که در این حالت بروز میکرد این بود که متد OnInitializedAsync یک کامپوننت، دوبار فراخوانی میشد؛ یکبار در زمان pre-rendering در سمت سرور، تا HTML استاتیکی برای ارائهی به مرورگر کاربر تولید شود و بار دیگر در زمان فعال شدن اتصال SignalR کامپوننت و ارائهی نهایی تعاملی آن. به همین جهت، کار سنگین آغازین یک کامپوننت، دوبار انجام میشد که تجربهی کاربری ناخوشایندی را هم به همراه داشت. برای رفع این مشکل، tag helper ویژهای را به نام persist-component-state تهیه کردند که به صورت زیر به فایل host.cshtml_ اضافه میشد:
<body>
<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
<persist-component-state />
</body>
این tag-helper فوق است که کار درج رمزنگاری شدهی اطلاعات کش شدهی pre-rendering سمت سرور را در انتهای فایل HTML صفحه انجام میدهد و برای نمونه، نحوهی استفادهی از آن به صورت زیر است:
@page "/"
@implements IDisposable
@inject PersistentComponentState applicationState
@inject IUserRepository userRepository
@foreach(var user in users)
{
<ShowUserInformation user="@item"></ShowUserInformation>
}
@code {
private const string cachingKey = "something_unique";
private List<User> users = new();
private PersistingComponentStateSubscription persistingSubscription;
protected override async Task OnInitializedAsync()
{
persistingSubscription = applicationState.RegisterOnPersisting(PersistData);
if (applicationState.TryTakeFromJson<List<User>>(cachingKey, out var restored))
{
users = restored;
}
else
{
users = await userRepository.GetAllUsers();
}
}
public void Dispose()
{
persistingSubscription.Dispose();
}
private Task PersistData()
{
applicationState.PersistAsJson(cachingKey, users);
return Task.CompletedTask;
}
}
توضیحات:
- ابتدا تزریق سرویس PersistentComponentState را مشاهده میکنید. این همان کامپوننتی است که کار کش کردن اطلاعات را مدیریت میکند.
- سپس فراخوانی متد RegisterOnPersisting انجام شدهاست. متدی که توسط آن ثبت میشود، در حین عملیات pre-rendering فراخوانی میشود تا اطلاعاتی را کش کند. نحوهی این کش شدن را در ادامهی مطلب بررسی میکنیم.
- سپس فراخوانی متد TryTakeFromJson رخدادهاست. اگر این متد اطلاعاتی را برگرداند، یعنی pre-rendering سمت سرور پیشتر انجام شده و اطلاعاتی کش شده، برای دریافت در سمت کلاینت، وجود داشته و نیازی به مراجعهی مجدد و دوباره، به بانک اطلاعاتی نیست.
- دراینجا یک قسمت Dispose را هم مشاهده میکنید تا اگر کاربر به صفحهی دیگری مراجعه کرد، متد ثبت شدهی در اینجا، از لیست مواردی که باید در حین pre-rendering سریالایز شوند، حذف گردد.
اگر از این روش استفاده نشود، به علت دوبار فراخوانی شدن متد OnInitializedAsync یک کامپوننت به همراه pre-rendering، اطلاعاتی که در حین pre-rendering کامپوننت از بانک اطلاعاتی، برای تولید محتوای استاتیک صفحه در سمت سرور دریافت شده، با فعالسازی آتی تعاملی سمت کلاینت آن کامپوننت، از دست خواهد رفت و در این حالت کلاینت باید مجددا این اطلاعات را از بانک اطلاعاتی درخواست کند. بنابراین هدف از persist-component-state، حفظ دادهها در بین دو رندر است؛ رندر اولی که در سمت سرور انجام میشود و رندر دومی که متعاقبا در سمت کلاینت رخ میدهد.
یک نکته: به یک چنین قابلیتی در فریمورکهای دیگر «hydration» هم گفته میشود که در اصل یک شیء دیکشنری است که مقدار شیء حالت را در سمت سرور دریافت کرده و آنرا در زمان فعال شدن سمت کلاینت کامپوننت، برای استفادهی مجدد مهیا میکند.
سؤال: اطلاعات سرویس PersistentComponentState کجا ذخیره میشوند؟
همانطور که در مثال فوق هم مشاهده کردید، سرویس PersistentComponentState، این state را به صورت JSON ذخیره میکند. اما ... این اطلاعات دقیقا کجا ذخیره میشوند؟
برای مشاهدهی آنها، فقط کافی است به source صفحه مراجعه کنید تا با دو مقدار مخفی رمزنگاری شدهی زیر در انتهای HTML صفحه مواجه شوید!
<!--Blazor-Server-Component-State:CfDJXyz->
<!--Blazor-WebAssembly-Component-State:eyJVc2Xyz->
فرمت این اطلاعات، JsonSerializer.SerializeToUtf8Bytes رمزنگاری شدهی توسط IDataProtectionProvider است. این هم یک روش نگهداری اطلاعات، بجای استفاده از کش مرورگر است؛ بدون نیاز به استفاده از جاوااسکریپت و کار با local storage و امثال آن.
مایکروسافت از این نوع کارها پیشتر در ASP.NET Web forms توسط ViewStateها انجام دادهاست! البته ViewStateها جهت نگهداری اطلاعات وضعیت کلاینت، به سمت سرور ارسال میشوند؛ اما این Component-Stateهای مخفی، فقط یکبار توسط قسمت کلاینت خوانده میشوند و هدف آنها ارسال اطلاعاتی به سمت سرور نیست.
یک نکته: همانطور که عنوان شد، در نگارشهای قبلی Blazor، از تگهلپری به نام persist-component-state برای درج این اطلاعات در انتهای صفحه استفاده میکردند. استفاده از این تگهلپر در Blazor 8x منسوخ شده و به صورت خودکار دادههای تمام سرویسهای PersistentComponentState فعالی که توسط PersistAsJson اطلاعاتی را ذخیره میکنند، جمع آوری و به صورت یکجا در انتهای صفحه به صورت رمزنگاری شده، درج میکنند.
روش خاموش کردن Pre-rendering
برای خاموش کردن pre-rendering نیاز است پارامتر همنامی را به نحو زیر با false مقدار دهی کرد:
@rendermode renderMode
@code
{
private static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false);
}
بازنویسی مثال قسمت قبل با استفاده از سرویس PersistentComponentState
در
قسمت قبل هرچند روش مواجه شدن با مشکل دوبار فراخوانی شدن متد آغازین یک کامپوننت را در سمت سرور و کلاینت بررسی کردیم، اما ... در نهایت دوبار مراجعه به بانک اطلاعاتی انجام میشود؛ یکبار در زمان pre-rendering و با مراجعهی مستقیم به یک سرویس سمت سرور و یکبار دیگر هم در زمان فراخوانی httpClient.GetFromJsonAsync در سمت کلاینت برای دریافت اطلاعات مورد نیاز از یک Web API Endpoint. اگر بخواهیم اطلاعات لیست محصولات دریافتی سمت سرور را به سمت کلاینت منتقل کنیم و مجددا آنرا از بانک اطلاعاتی دریافت نکنیم، راهحل سوم آن، استفاده از سرویس PersistentComponentState است.
در کدهای زیر، بازنویسی کامپوننت محصولات مشابه را مشاهده میکنید که کمی پیچیدهتر شدهاست. اینبار لیست محصولات مشابه، در همان بار اول رندر کامپوننت نمایش داده میشوند و نه پس از کلیک بر روی دکمهای. به همین جهت باید کار مدیریت دوبار فراخوانی متد رویدادگردان OnInitializedAsync را به درستی انجام داد. متد OnInitializedAsync یکبار در حین پیشرندر سمت سرور اجرا میشود و بار دیگر زمانیکه وباسمبلی جاری در مرورگر به صورت کامل دریافت شده و فعال میشود؛ یعنی برای بار دوم، کدهای اجرایی آن سمت کلاینت هستند.
در این مثال با استفاده از سرویس PersistentComponentState، اطلاعات دریافت شدهی در حین pre-rendering ابتدایی رخدادهی در سمت سرور، در متد OnPersistingAsync، کش شده و در حین فعال شدن وباسمبلی مرتبط در سمت کلاینت، با استفاده از متد PersistentState.TryTakeFromJson، از کش خوانده میشوند و دیگر دوبار رفت و برگشت به بانک اطلاعاتی را شاهد نخواهیم بود.
@implements IDisposable
@inject IProductStore ProductStore
@inject PersistentComponentState PersistentState
<h3>Related products</h3>
@if (_relatedProducts == null)
{
<p>Loading...</p>
}
else
{
<div class="mt-3">
@foreach (var item in _relatedProducts)
{
<a href="/ProductDetails/@item.Id">
<div class="col-sm">
<h5 class="mt-0">@item.Title (@item.Price)</h5>
</div>
</a>
}
</div>
}
@code{
private const string StateCachingKey = "products";
private IList<Product>? _relatedProducts;
private PersistingComponentStateSubscription _persistingSubscription;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnInitializedAsync()
{
_persistingSubscription = PersistentState.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);
if (PersistentState.TryTakeFromJson<IList<Product>>(StateCachingKey, out var restored))
{
_relatedProducts = restored;
}
else
{
await Task.Delay(500); // Simulates asynchronous loading
_relatedProducts = await ProductStore.GetRelatedProducts(ProductId);
}
}
private Task OnPersistingAsync()
{
PersistentState.PersistAsJson(StateCachingKey, _relatedProducts);
return Task.CompletedTask;
}
public void Dispose()
{
_persistingSubscription.Dispose();
}
}
در این پیاده سازی هنوز هم از سرویس IProductStore استفاده میشود که دو نگارش سمت سرور و سمت کلاینت آنرا در
قسمت قبل تهیه کردیم. برای مثال باتوجه به اینکه کدهای فوق در حین pre-rendering در سمت سرور اجرا میشوند، به صورت خودکار از نگارش سمت سرور IProductStore استفاده خواهد شد.
نکتهی مهم: فعلا کدهای فوق فقط برای حالت بارگذاری اولیهی کامپوننت درست کار میکنند. یعنی اگر نگارش وباسمبلی کامپوننت پس از وقوع پیشرندر سمت سرور، در مرورگر دریافت و بارگذاری کامل شود؛ اما برای دفعات بعدی مراجعهی به این صفحه با استفاده از enhanced navigation و راهبری بهبود یافته که در
قسمت ششم این سری بررسی کردیم ... دیگر کار نمیکنند و در این حالت کش شدنی رخ نمیدهد که در نتیجه، شاهد دوبار رفت و برگشت به بانک اطلاعاتی خواهیم بود و اساسا استفادهی از PersistentComponentState را زیر سؤال میبرد؛ چون در حین راهبری بهبود یافته، از آن استفاده نمیشود (قسمت PersistentState.TryTakeFromJson آن، هیچگاه اطلاعاتی را از کش نمیخواند).
اطلاعات بیشتر