ساخت چت روم با Blazor Web Assembly و SignalR قسمت دوم: ساخت کلاینت

در این مقاله قصد داریم به ادامه ساخت چت روم با ASP Net Core Web API و Blazor Web Assembly بپردازیم.

ابتدا به سراغ ساخت صفحه لاگین و رجیستر میرویم. در پوشه Pages یک razor component تحت عنوان Login میسازیم. سپس برای داشتن css isolation یک فایل دیگر با نام Login.razor.css ایجاد میکنیم. برای اینکه css isolation به درستی کار کند، قطعه کد زیر را در فایل index.html به قسمت head اضافه میکنیم.

https://gist.github.com/babaktaremi/614f0483aee9a5d436907841e78fc173


قسمت ChatApplication.client در واقع نام assembly پروژه میباشد. سپس فایل های استاتیک شامل Font Awsome و Bootstrap را به index.html اضافه میکنیم. در حال حاضر index.html شامل کد های زیر می باشد.

https://gist.github.com/babaktaremi/c5fb91a3ddd1764e6d9fcaaa94f5781b

پروژه Client را اجرا میکنیم. به صفحه لاگین میرویم و مشاهده میکنیم که استایل ها به درستی لود شده اند


استفاده از Local Storage در Blazor

برای استفاده از Access Token و ارتباط با Server نیاز داریم که توکن دریافتی را در جایی ذخیره کنیم. برای اینکار بهترین کار استفاده از قابلیت Local Storage در Browser می باشد. میتوانیم از قابلیت Javascript Interop در Blazor برای کار با Local Storage استفاده کنیم. اما پکیجی با نام Blazored.LocalStorage وجود دارد که کار با Local Storage در Blazor را بسیار راحت میکند. این پکیج را در پروژه Client نصب میکنیم.

همچنین در Package Manager Console نیز میتوانیم با دستور زیر این پکیج را نصب کنیم

Install-Package Blazored.LocalStorage -Version 4.1.5

سپس قطعه کد زیر را برای استفاده Local Storage به کلاس Program اضافه میکنیم.

https://gist.github.com/babaktaremi/3148a94f25533ea35547b60c29536526

جداسازی Code از UI

برای اینکه کد تمیزتری داشته باشیم میتوانیم کد مربوط به صفحه UI را از منطق پروژه جدا کنیم. می دانیم که هرکدام از razor فایل ها پس از کامپایل به یک partial class تبدیل میشوند. با ایجاد فایل با پسوند razor.cs و هم نام با فایل razor اصلی میتوانیم به نوعی یک code behind برای هر صفحه razor داشته باشیم. پس فایل Login.razor.cs را به شکل زیر ایجاد میکنیم.

https://gist.github.com/babaktaremi/4450e4fd79e3e0c615ba52fc20e3310a


حال میتوانیم منطق پروژه Client و قسمت های مربوط به بخش Login را در این کلاس بنویسیم.

ایجاد سرویس مربوط به Login

ابتدا در پروژه Client یک پوشه با نام Service ایجاد میکنیم. در پوشه یک پوشه دیگر با نام Login ایجاد میکنیم.

اینترفیس ILoginService را به شکل زیر ایجاد میکنیم.

https://gist.github.com/babaktaremi/597e36d517deaa0229548c456a97b17a

حال کلاس LoginService را که این اینترفیس را پیاده سازی میکند به شکل زیر مینویسیم

https://gist.github.com/babaktaremi/5fedaf5a53efd772688628a7a04d5e14

ابتدا یک instance از HttpClient را تزریق میکنیم. از Extension Method ای به نام PostAsJsonAsync که مدل را به صورت JSON در Body به URL داده شده پست میکند استفاده میکنیم. اگر Response داده شده دارای Status Code 200 نبود null بر میگردانیم و در غیر این صورت Content آن را به صورت stream خوانده و به Access Token مدل را Deserialize میکنیم.

برای آنکه HttpClient در سرویس ما تزریق شود، تکه کد زیر را در کلاس Program پروژه Client اضافه میکنیم. قبل از آن نیاز داریم که پکیج Microsoft.Extensions.Http را به پروژه Client آضافه کنیم.

همچنین میتوانیم از دستور زیر در Package Manager Console استفاده کنیم.

Install-Package Microsoft.Extensions.Http -Version 5.0.0

استفاده از Toaster برای اعلام Notification ها به کاربر

مانند Local Storage ، برای استفاده از Toaster در Blazor نیز پکیج وجود دارد که نصب و تنظیم آن مانند پکیج قبلی کار بسیار سرراست و ساده ای است.

ابتدا پکیج Sotsera.Blazor.Toaster را دانلود و نصب میکنیم.

با استفاده از دستور زیر در Package Manager Console نیز میتوانیم این پکیج را نصب کنیم.

Install-Package Sotsera.Blazor.Toaster -Version 3.0.0

سپس در کلاس Program قطعه کد زیر را برای تنظیم Toaster اضافه میکنیم.

https://gist.github.com/babaktaremi/a4f2ce0323b785186ea7d35dff3c7ef5

در اینجا Postion مربوط به Toaster را در قسمت بالا وسط تعیین کرده ایم. سپس به فایل index.html قطعه کد زیر را اضافه میکنیم.

https://gist.github.com/babaktaremi/68c3c0586a066d97186978eddcc256b2

سپس به فایل App.razor در قسمت بالا قطعه کد زیر را اضافه میکنیم

https://gist.github.com/babaktaremi/b02e38416dc5303833f8d34cbef73536

تکمیل کد های مربوط به بخش Login

حال که همه سرویس های مربوط به بخش Login آماده است به سراغ تکمیل آن می رویم. یادتان باشد که ما بخش Code را از UI جدا کرده ایم پس تمام کد های مربوط به بخش Login را در کلاس Login.razor.cs مینویسیم.

ابتدا سرویس های مورد نیاز را به این بخش تزریق میکنیم

https://gist.github.com/babaktaremi/b10ab8536eb23e60952fd5498f9a63db

در اینجا سرویس های مربوط به Local Storage و Toaster و Login Service را تزریق کرده ایم. صفت Inject در واقع کار تزریق را برای ما در Blazor انجام میدهد. برای Navigate کردن به سایر صفحات نیز Navigation Manager را Inject میکنیم

سپس یک پراپرتی از جنس LoginViewModel را new کرده و در کلاس قرار میدهیم. برای اینکه در تگ EditForm در Blazor این مدل قابل استفاده باشد نیاز است که آن را new کنیم.

https://gist.github.com/babaktaremi/ed996bd3c75f84816160a60178573f00

حال متد OnInitializedAsync را override میکنیم. این متد زمانی که صفحه رندر و Initialize می شود اجرا میشود.

https://gist.github.com/babaktaremi/f656fe021745b3930a54b5ef5668bfb4

در اینجا در Local Storage چک میکنیم که آیا کلیدی با نام userToken وجود دارد یا خیر. اگر وجود داشت به معنی این است که یوزر از قبل لاگین کرده است و آن را به صفحه اصلی میفرستیم.

در قسمت UI برای اینکه فرم Login به درستی کار کند تغییرات زیر را انجام میدهیم

https://gist.github.com/babaktaremi/30a6f0d1b3313bbd2f6df8830e4328e4

در خط 4 با استفاده از Component ای به نام DataAnnotationsValidator قابلیت Data Annotation Validation را به فرم اضافه میکنیم.

در خط 8 و 12 با استفاده از InputText Componenet مقادیر را به پراپرتی های LoginViewModel بایند میکنیم و همچنین با استفاده از ValidationMessage پیامی که هنگام خالی بودن InputText باید نمایش داده شود را نمایش میدهیم. متد HandleValidSubmit را به شکل زیر پیاده سازی میکنیم.

https://gist.github.com/babaktaremi/c2f35582b6fae0e50d73409257e91a9c

اگر Token برابر null بود با Toaster به کاربر نشان میدهیم که کاربر یافت نشده است و در غیر این صورت مقدار Local Storage را برابر Access Token قرار میدهیم.

قبل از هرچیزی کلاس AccessToken را به شکل زیر باید تغییر دهیم

https://gist.github.com/babaktaremi/819ea908c598a7b2e74b5c1d084ad32d

باید یک Constructor هنگام deserialize کردن مقدار Json در نظر بگیریم.

حال فرم Login را چک میکنیم. پروژه را اجرا میکنیم. ابتدا مقادیر username و password را خالی می گذاریم و مشاهده میکنیم که validation ها درست کار میکنند.

حال مقدار username و password را با مقادیر اشتباه پر میکنیم و مشاهده میکنیم که Toaster هم درست کار میکند.

حال با مقادیر درست username و password را پر میکنیم. مشاهده میشود که در local storage مرورگر مقدار Token ذخیره شده است.

به user_id برای جداسازی چت های کاربر و سایر کاربران نیاز داریم. در اینجا برای راحتی کار user_id را داخل توکن قرار داریم ولی این کار باعث بوجود آمدن مشکلات امنیتی میشود. پیشنهاد میکنم که از GUID برای اینکار استفاده کنید و یا راه حل دیگری برای تشخیص یوزر جاری در نظر بگیرید.

به همین منوال میتوانیم صفحه مربوط به رجیستر و ثبت نام کاربر را نیز بسازیم.

ساخت صفحه چت کاربران

در فولدر Pages یک razor Component به نام Index می سازیم. کد اولیه این صفحه به صورت زیر خواهد بود.

https://gist.github.com/babaktaremi/9663b4e21c8bfb6c1c858d92418cacf6

نوشتن کد های مربوط به Chat و SignalR Client

ابتدا پکیج مربوط به SignalR Client را نصب میکنیم.

از طریق Package Manager Console نیز با دستور زیر میتوانیم SignalR را نصب کنیم.

Install-Package Microsoft.AspNetCore.SignalR.Client -Version 5.0.9

سپس سرویس مربوط به Chat را مینویسیم. در فولدر Service یک فولدر دیگر با نام Chat ایجاد میکنیم و در آن اینترفیس IChatService را به شکل زیر مینویسیم.

https://gist.github.com/babaktaremi/c71258dc8df3bb191ee0084ba54c608d

سپس کلاس ChatService را به که این اینترفیس را پیاده سازی میکند به شکل زیر مینویسیم.

https://gist.github.com/babaktaremi/5ad6462f9ede4e36128f5d0f6b4b7d9a

در خط 14 برای احراز هویت، توکن را به صورت bearer در Header قرار داده ایم.

سپس در کلاس Program.cs این سرویس را به شکل زیر رجیستر میکنیم.

https://gist.github.com/babaktaremi/5049a917b85f69f7109134e3216368f2

سپس کلاس Index.razor.cs را به شکل زیر میسازیم

https://gist.github.com/babaktaremi/ac29a7b8877cb3ff39b71ef0ca60dea8


ابتدا باید از IAsyncDisposable ارث بری کنیم چرا که نیاز داریم در پایان طول عمر Component مقادیر Hub و Debounce Timer را Dispose کنیم.

سپس در خط 5 یک فیلد از Hub Connection تعریف میکنیم.

سپس برای اینکه یک تاخیر هنگام تایپ کاربر و Notify کردن سایر کاربران ایجاد کنیم یک Debounce Timer تعریف میکنیم که پس از هر 500 میلی ثانیه یک متد را صدا میزند.

در خط 13 یک فیلد از جنس string تعریف میکنیم که مقدار پیام وروردی کاربر به آن bind می شود.

در خط 14 یک فیلد از جنس int داریم که ID کاربر را در آن ذخیره میکنیم. از این فیلد بعد ها برای تمایز بین پیام های کاربر و سایر کاربران استفاده خواهیم کرد.

در خط 15 یک لیست از جنس UserMessageViewModel داریم که تاریخچه چت را به کاربر نشان میدهد.

از خط 19 تا 22 سرویس های مورد نیاز را تزریق میکنیم.

کار اصلی از override کردن متد OnInitializedAsync شروع میشود. ابتدا بررسی میکنیم که آیا کاربر در Browser و Local Storage مقادیر توکن را دارد یا خیر و اگر نداشت، وی را به صفحه Login هدایت میکنیم. سپس فیلد _userId را با استفاده از متد GetUserIdAsync که در خط 82 تعریف شده است از Local Storage خوانده و مقدار دهی میکنیم. سپس مقدار _chatHistory را بوسیله سرویسی که قبلا نوشتیم از Web API خوانده و مقدار دهی میکنیم. سپس event مربوطه به هنگامی که debounce timer مقدار 500 میلی ثانیه را سپری میکند را صدا میزند مقدار دهی میکنیم. متد IsTyping فانکشن مربوطه را در SignalR Server فراخوانی میکند. سپس نوبت به مقداردهی _hub میرسد. در خط 39 مقادیر مربوط به URL را مقداردهی میکنیم. در خط 40 مقدار Access Token را بوسیله متد GetAccessTokenValueAsync از Local Storage خوانده و مقدار دهی میکنیم. سپس متد هایی که Hub روی آنها قرار است invoke بشود را تعریف میکنیم. ابتدا بوسیله Toaster هنگامی که کاربری به SignalR Server متصل میشود را نشان میدهیم. سپس متدی که هنگام دریافت پیام جدید صدا زده می شود را تعریف میکنیم. هنگام دریافت پیام جدید ، آن را به Chat History اضافه میکنیم و برای rerender کردن UI متد StateHasChanged را صدا میزنیم. سپس برای Notify کردن سایر کاربران هنگامی که یک یوزر تایپ میکند ، متد UpdateUserIsTypingAsync را می سازیم .متد InvokeAsync در خط 56 یک اکشن ورودی برای رندر کردن UI دریافت میکند. در آن مقدار typing user را از Server Hub دریافت کرده و برای نمایش آن سمت UI متد StateHasChanged را صدا میزنیم. پس از گذشت 1 ثانیه مقدار typing user را خالی کرده و مجددا StateHasChanged را صدا میزنیم . در نهایت در خط 48 Hub را استارت میکنیم.

در خط 89 متدی که هنگام زدن دکمه ارسال باید فراخوانی بشود را تعریف میکنیم. اگر مقدار message خالی نبود آن را به متد OnNewMessage در SignalR Server میفرستیم. سپس مقدار message را خالی میکنیم و متد StateHasChanged را صدا میزنیم.

در خط 101 یک متد برای on input event تعریف میکنیم که وقتی کاربر در حال تایپ در Text Area است صدا زده میشود. در آن یک بار Debounce Timer را متوقف میکنیم و مجددا استارت میزنیم.

در نهایت در خط 114 مقدار Hub و Timer را Dispose میکنیم.

نوشتن کد های مربوط به UI صفحه چت

فایل Index.razor را به شکل زیر بازنویسی میکنیم.

https://gist.github.com/babaktaremi/3b874bf665d0c28db72d5276dabbb2b5

ابتدا در خط 6 چک میکنیم که اگر مقدار typing user خالی نبود ، آن را در قسمت بالای صفحه نمایش دهد.

سپس بین چت های موجود در Chat History پیمایش میکنیم و مسیج های کاربر و سایر کاربرها را نمایش میدهیم.

در خط 46 مقدار text area را به message بایند میکنیم و event مربوط به را به متد InitiateUserIsTyping که قبلا تعریف کرده بودیم Assign میکنیم.

در خط 47 نیز event مربوط به که برای Button تعریف شده است را به متد SendMessage که قبلا تعریف کرده ایم Assign میکنیم.

تست نهایی

ابتدا دو مرورگر باز میکنیم و در هر دو آنها Login میکنیم. سپس مشاهده میکنیم که نشان دادن کاربر آنلاین و همچنین ارسال پیام و نشان دادن کاربر در حال تایپ به درستی کار میکند.


جمع بندی

در این پروژه یک سرور از صفر با استفاده از Identity و Signal R و همچنین JWT Authentication ساختیم. همچنین به طور اجمالی با Blazor web assembly و نحوه استفاده از JWT Token و ارتباط با Signal R آشنا شدیم. شدیدا توصیه میکنم که کد های مربوط به این مقاله را از گیت هاب دریافت کرده و آن را روی سیستم خود اجرا و دیباگ کنید. در نهایت اگر سوال یا نظری داشتید، خوشحال میشوم که آن را در بخش نظرات این مقاله مطرح کنید.

https://github.com/babaktaremi/Blazor-Chat-Application

مقالات بیشتر در دات نت زوم

https://t.me/DotNetZoom