با سلام و ممنون از مقاله خوبتون
نظرات مطالب
آشنایی با NuGet - قسمت اول
بسیار مفید است و اگر ادامه پیدا کند خواهد بود;)
برام یه تلنگر بود برم سمت NuGet
راستی چطور تلفظ میشه؟
برام یه تلنگر بود برم سمت NuGet
راستی چطور تلفظ میشه؟
پاسخ به بازخوردهای پروژهها
عدم ساخت template کامل
- نیازی نیست قرار بگیرند. NuGet جدید امکان بازیابی تمام بستهها را دارد. فقط فایل packages.config باید پیوست شود که میشود.
در جهت تکمیل بحث بارگذاری اطلاعات وابسته: اضافه شدن Lazy Loading به نگارش 2.1
برخلاف نگارشهای پیشین EF، اینبار Lazy loading به صورت پیشفرض فعال نیست که در بسیاری از موارد یک مزیت مهم، در جهت بهبود کارآیی برنامه به حساب میآید؛ چون پیشتر مدام میبایستی توسط ابزارهای profiler، برنامه را بررسی میکردیم تا از وجود مشکلی به نام select n+1 مطلع میشدیم (lazy loading اشتباه، در جائی که نیازی به آن نبوده و رفت و برگشت بیش از اندازهای را به بانک اطلاعاتی سبب شدهاست).
در این حالت ابتدا نیاز است بستهی نیوگت Microsoft.EntityFrameworkCore.Proxies را نصب کنید. سپس در متد OnConfiguring مربوط به Context برنامه، متد UseLazyLoadingProxies را فراخوانی نمائید:
و یا اینکار در فایل آغازین برنامه نیز میسر است:
اکنون EF Core 2.1 خواص راهبری (navigation properties) را که قابل بازنویسی باشند (همان مباحث AOP و تشکیل پروکسیها)، lazy load میکند.
این خواص نیز حتما باید به صورت virtual معرفی شوند تا قابلیت بازنویسی را داشته باشند؛ مانند:
در این مثال با فعال بودن lazy loading، به محض لمس خاصیت Blog، اطلاعات مرتبط با آن از بانک اطلاعاتی واکشی خواهند شد و نه پیش از آن مانند eager loading که تمام اطلاعات وابستهی به یک موجودیت را نیز واکشی میکند.
هرچند این قابلیت بارگذاری اطلاعات وابسته در آینده، جذاب به نظر میرسد اما در عمل در حین رندر یک گرید و یا بکارگیری حلقهها، چون سبب رفت و برگشت بیش از اندازهای به بانک اطلاعاتی خواهد شد، باید با دقت مورد استفاده قرار گیرد و اساسا استفادهی از آن در برنامههای وب توصیه نمیشود (با بررسیهای پروژههای بسیاری مشخص شدهاست که این قابلیت ضررش بیشتر از نفعش است).
ب) فعالسازی Lazy loading بدون استفاده از Proxyها
در این حالت نیازی به نصب بستهی AOP جدید تشکیل پروکسیها نیست. در اینجا در کلاس موجودیت خود باید سرویس ILazyLoader را تزریق کنید:
در این روش نیازی به virtual معرفی کردن خواص راهبری نیست. اما در این حالت به علت استفادهی از سرویس ILazyLoader، نیاز خواهید داشت تا بستهی نیوگت Microsoft.EntityFrameworkCore.Abstractions را نیز نصب کنید.
برخلاف نگارشهای پیشین EF، اینبار Lazy loading به صورت پیشفرض فعال نیست که در بسیاری از موارد یک مزیت مهم، در جهت بهبود کارآیی برنامه به حساب میآید؛ چون پیشتر مدام میبایستی توسط ابزارهای profiler، برنامه را بررسی میکردیم تا از وجود مشکلی به نام select n+1 مطلع میشدیم (lazy loading اشتباه، در جائی که نیازی به آن نبوده و رفت و برگشت بیش از اندازهای را به بانک اطلاعاتی سبب شدهاست).
برای فعالسازی lazy loading در EF Core 2.1 (اگر واقعا به آن نیاز دارید البته) دو روش وجود دارد:
الف) فعالسازی Lazy loading توسط Proxyها در این حالت ابتدا نیاز است بستهی نیوگت Microsoft.EntityFrameworkCore.Proxies را نصب کنید. سپس در متد OnConfiguring مربوط به Context برنامه، متد UseLazyLoadingProxies را فراخوانی نمائید:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseLazyLoadingProxies() .UseSqlServer(myConnectionString);
.AddDbContext<BloggingContext>( b => b.UseLazyLoadingProxies() .UseSqlServer(myConnectionString));
این خواص نیز حتما باید به صورت virtual معرفی شوند تا قابلیت بازنویسی را داشته باشند؛ مانند:
public class Blog { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public virtual Blog Blog { get; set; } }
هرچند این قابلیت بارگذاری اطلاعات وابسته در آینده، جذاب به نظر میرسد اما در عمل در حین رندر یک گرید و یا بکارگیری حلقهها، چون سبب رفت و برگشت بیش از اندازهای به بانک اطلاعاتی خواهد شد، باید با دقت مورد استفاده قرار گیرد و اساسا استفادهی از آن در برنامههای وب توصیه نمیشود (با بررسیهای پروژههای بسیاری مشخص شدهاست که این قابلیت ضررش بیشتر از نفعش است).
ب) فعالسازی Lazy loading بدون استفاده از Proxyها
در این حالت نیازی به نصب بستهی AOP جدید تشکیل پروکسیها نیست. در اینجا در کلاس موجودیت خود باید سرویس ILazyLoader را تزریق کنید:
public class Blog { private ICollection<Post> _posts; public Blog() { } private Blog(ILazyLoader lazyLoader) { LazyLoader = lazyLoader; } private ILazyLoader LazyLoader { get; set; } public int Id { get; set; } public string Name { get; set; } public ICollection<Post> Posts { get => LazyLoader?.Load(this, ref _posts); set => _posts = value; } } public class Post { private Blog _blog; public Post() { } private Post(ILazyLoader lazyLoader) { LazyLoader = lazyLoader; } private ILazyLoader LazyLoader { get; set; } public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get => LazyLoader?.Load(this, ref _blog); set => _blog = value; } }
مطالب دورهها
نحوهی ارتقاء برنامههای SignalR 1.x به SignalR 2.x
1) اگر هم اکنون یک پروژه جدید SignalR را آغاز و از طریق NuGet وابستگیهای آنرا اضافه کنید، به صورت خودکار SignalR نگارش 2 را در این تاریخ دریافت خواهید کرد. این نگارش صرفا با دات نت 4 و نیم به بعد سازگار است. بنابراین اولین کاری که باید برای ارتقاء پروژههای SignalR 1.x به نگارش جدید انجام دهید، تغییر Target framework پروژه به نگارش 4.5 است.
2) حذف وابستگیهای قدیمی
فرمان فوق را اگر در کنسول پاورشل نیوگت اجرا کنید، به صورت خودکار وابستگیهای قدیمی SignalR را حذف میکند.
3) نصب فایلهای جدید SignalR
برای این منظور تنها کافی است دستور فوق را اجرا نمائید.
4) به روز رسانی ارجاعات اسکریپتی
ارجاع به افزونه جیکوئری SignalR نیز باید به نگارش 2 ارتقاء یابد.
5) حذف نحوهی تعریف مسیریابی هابهای SignalR از فایل global.asax برنامه.
فایل یاد شده را گشوده و سطر فوق را از آن حذف کنید. سپس یک کلاس دلخواه جدید را مثلا به نام Startup، ایجاد و محتوای آن را به نحو ذیل تغییر دهید:
این فایل به صورت خودکار در زمان آغاز برنامههای SignalR 2 مورد استفاده قرار میگیرد (با کمک ویژگی assembly: OwinStartup آن).
اگر از آخرین نگارش VS.NET استفاده میکنید، این کلاس را توسط گزینه Add -> New Item -> Owin Startup Class نیز میتوانید اضافه نمائید.
2) حذف وابستگیهای قدیمی
Uninstall-Package Microsoft.AspNet.SignalR -RemoveDependencies
3) نصب فایلهای جدید SignalR
Install-Package Microsoft.AspNet.SignalR
4) به روز رسانی ارجاعات اسکریپتی
<script src="Scripts/jquery.signalR-2.0.0.min.js"></script>
5) حذف نحوهی تعریف مسیریابی هابهای SignalR از فایل global.asax برنامه.
protected void Application_Start(object sender, EventArgs e) { //RouteTable.Routes.MapHubs(); }
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(SignalRChat.Startup))] namespace SignalRChat { public class Startup { public void Configuration(IAppBuilder app) { app.MapSignalR(); } } }
اگر از آخرین نگارش VS.NET استفاده میکنید، این کلاس را توسط گزینه Add -> New Item -> Owin Startup Class نیز میتوانید اضافه نمائید.
در قسمتهای 22 تا 25 این سری، روش برقراری ارتباط با سرور را در برنامههای React، توسط کتابخانهی معروف Axios، بررسی کردیم. در این قسمت میخواهیم همان نکات را زمانیکه قرار است از کامپوننتهای تابعی، به همراه useState hook و useEffect hook استفاده کنیم، مرور نمائیم.
برپایی پیشنیازها
در اینجا نیز از همان برنامهای که در قسمت 30، برای بررسی مثالهای React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانهی Axios را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
برنامهی backend مورد استفاده هم همان برنامهای است که از قسمت 22 شروع به توسعهی آن کردیم و کدهای کامل آنرا از پیوستهای انتهای بحث، میتوانید دریافت کنید. این برنامه که در مسیر شروع شدهی با https://localhost:5001/api قرار میگیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شدهاست. برای راه اندازی آن، به پوشهی این برنامه، مراجعه کرده و فایل dotnet_run.bat آنرا اجرا کنید، تا endpointهای REST Api آن قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts آن در مرورگر دسترسی یافت.
در ادامه میخواهیم در برنامهی React خود، لیست مطالب برنامهی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمههای search و clear نیز به آن اضافه کنیم.
دریافت اطلاعات اولیه از سرور، درون useEffect Hook
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد میکنیم:
سپس فایل جدید src\components\part03\Search.jsx را جهت توسعهی کامپوننت جستجوی این قسمت ایجاد میکنیم و ساختار ابتدایی آنرا با import وابستگیهای React و مسیر فوق، به صورت یک function که در همان محل قابل export است، ایجاد میکنیم، تا فعلا یک React.Fragment را بازگشت دهد:
بر این اساس، کامپوننت App فایل index.js را به صورت زیر از کامپوننت App فوق، تامین خواهیم کرد:
اکنون میخواهیم اولین درخواست خود را به سمت backend server ارسال کنیم. برای این منظور در کامپوننتهای تابعی، از useEffect Hook استفاده میشود؛ چون کار با یک API خارجی نیز یک side effect محسوب میگردد. بنابراین متد useEffect را import کرده و سپس آنرا بالای return، فراخوانی میکنیم. درون آن نیاز است اطلاعات را از سرور دریافت کنیم و برای اینکار از کتابخانهی axios که آنرا در قسمت 23 معرفی کردیم، استفاده خواهیم کرد. به همین جهت import آنرا نیز در این ماژول خواهیم داشت:
در اینجا با استفاده از متد get کتابخانهی axios، درخواستی را به آدرس https://localhost:5001/api/posts/search، با یک کوئری استرینگ خالی، ارسال کردهایم تا تمام دادهها را بازگشت دهد. روش قدیمی استفادهی از axios را که با استفاده از Promiseها و متد then آن است، در اینجا ملاحظه میکنید که خروجی خاصیت data شیء response دریافتی را لاگ کردهاست:
اکنون میخواهیم این اطلاعات دریافتی را در برنامهی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقهای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آنرا import کرده و سپس آنرا تعریف و در قسمت then، فراخوانی میکنیم:
چون اطلاعات بازگشتی به صورت یک آرایهاست، مقدار اولیهی متد useState را با یک آرایهی خالی مقدار دهی کردهایم. سپس برای مقدار دهی متغیر results موجود در state، به متد setResults تعریف شدهی توسط useState، مقدار response.data را ارسال میکنیم. در این حالت اگر برنامه را ذخیره کرده و اجرا کنید .... برنامه و همچنین مرورگر، هنگ میکنند!
همانطور که مشاهده میکنید، یک حلقهی بی پایان در اینجا رخ دادهاست! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامهی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرندهی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا میشود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننتهای کلاسی را با هم پوشش میدهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شدهاست و پس از آن، مجددا effect method فراخوانی میشود و ... این حلقه هیچگاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ میکنند!
در این برنامه فعلا میخواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایهی خالی مقدار دهی میکنیم:
تا اینجا موفق شدیم متد setResults را تنها در اولین بار نمایش کامپوننت، فراخوانی کنیم که در نتیجهی آن، متغیر results موجود در state، مقدار دهی شده و همچنین کار رندر مجدد کامپوننت در صف قرار میگیرد. بنابراین مرحلهی بعد، تکمیل قسمت return کامپوننت تابعی است تا آرایهی results را نمایش دهد:
در اینجا ابتدا یک فرگمنت را توسط </><> تعریف کردهایم و سپس در داخل آن میتوان المانهای فرزند را قرار داد. سپس برای ایجاد trهای جدول، یک حلقه را توسط results.map، بر روی عناصر دریافتی از آرایهی مطالب، تشکیل دادهایم. چون این حلقه بر روی trهای پویا تشکیل میشود، هر tr، نیاز به یک key دارد، تا در DOM مجازی React قابل شناسایی و ردیابی شود که در آخر یک چنین شکلی را ایجاد میکند:
استفاده ازAsync/Await برای دریافت اطلاعات، درون یک useEffect Hook
اکنون میخواهیم درون effect method یک useEffect Hook، روش قدیمی استفادهی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آنرا بیشتر بررسی کردیم، جایگزین کنیم.
خروجی متد axios.get، یک شیء Promise است که نتیجهی عملیات async را بازگشت میدهد. در جاوا اسکریپت مدرن، میتوان از واژهی کلیدی await برای دسترسی به شیء response دریافتی از آن، استفاده کرد. سپس هر جائیکه از واژهی کلیدی await استفاده میشود، متد جاری را باید با واژهی کلیدی async نیز مزین کرد. با انجام اینکار و اجرای برنامه، اخطار زیر در کنسول توسعه دهندگان مرورگر ظاهر میشود؛ هرچند نتیجه نهایی هم هنوز نمایش داده میشود:
این اخطار به این معنا است که effect function تعریف شده را نمیتوان به صورت async تعریف کرد و از چنین قابلیتی پشتیبانی نمیشود. یک effect function حداکثر میتواند یک متد دیگر را بازگشت دهد (و یا هیچ چیزی را بازگشت ندهد) که نمونهی آنرا در قسمت 30، با متدهایی که کار پاکسازی منابع را انجام میدادند، بررسی کردیم. اگر متدی را مزین به واژهی کلیدی async کردیم، یعنی این متد در اصل یک Promise را بازگشت میدهد؛ اما یک effect function، حداکثر یک تابع دیگر را میتواند بازگشت دهد تا componentWillUnmount را پیاده سازی کند.
برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
مشکل یا محدودیتی برای ایجاد متدهای async، در خارج از یک effect function وجود ندارد. به همین جهت اعمالی را که نیاز به Async/Await دارند، در این متدهای مجزا انجام داده و سپس میتوان آنها را درون effect function، به نحوی که ملاحظه میکنید، فراخوانی کرد. با این تغییر، هنوز هم اطلاعات نهایی، بدون مشکل دریافت میشوند، اما دیگر اخطاری در کنسول توسعه دهندگان مرورگر درج نخواهد شد.
پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا
تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آنرا از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقهی بینهایت وارد نشود. اکنون این سؤال مطرح میشود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شدهی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آنرا با استفاده از useState Hook و یک مقدار اولیهی دلخواه تنظیم میکنیم:
سپس المان textbox زیر را هم به بالای المان جدول، اضافه میکنیم:
این کنترل توسط رویداد onChange، عبارت تایپ شده را به متد setQuery ارسال کرده و در نتیجهی آن، کار تنظیم متغیر query در state کامپوننت جاری، صورت میگیرد. همچنین با تنظیم value={query}، سبب خواهیم شد تا این کنترل، به یک المان کنترل شدهی توسط state تبدیل شود و در ابتدای نمایش فرم، مقدار ابتدایی useState را نمایش دهد.
اکنون که متغیر query دارای مقدار شدهاست، میتوان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
استفاده از تابع encodeURIComponent، سبب میشود تا اگر کاربر برای مثال "Text 1" را وارد کرد، فاصلهی بین دو عبارت، به درستی encode شده و یک کوئری مانند https://localhost:5001/api/posts/search?query=Title%201 به سمت سرور ارسال گردد.
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمیشود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get میشود، طوری تنظیم شدهاست که فقط یکبار، آنهم پس از رندر اولیهی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایهای که به عنوان پارامتر دوم متد useEffect تعریف شده، میتوان اجرای مجدد effect function آنرا وابستهی به تغییرات متغیر query در state کامپوننت کرد:
اکنون اگر برنامه را ذخیره کرده و اجرا کنید، با هربار ورود اطلاعات درون textbox جستجو، یک کوئری جدید به سمت سرور ارسال شده و نتیجهی جستجوی انجام شده، به صورت یک جدول رندر میشود:
دریافت اطلاعات جستجو، تنها با ارسال اطلاعات یک فرم به سمت سرور
تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد میکند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن میتوان یک دکمهی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف میکنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آنرا مجددا به یک آرایهی خالی تنظیم میکنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره میکند، در بالای جدول نتایج مطالب، قرار میدهیم:
تا اینجا اگر کاربر اطلاعاتی را وارد کرده و سپس بر روی دکمهی Search فوق کلیک کند، نتایج جستجوی خودش را در جدول ذیل آن مشاهده میکند. اکنون میخواهیم این امکان را به کاربران بدهیم که با فشردن دکمهی enter درون textbox جستجو، همین قابلیت جستجو را در اختیار داشته باشند؛ تا دیگر الزامی به کلیک بر روی دکمهی Search، نباشد. برای اینکار تنها کافی است، کل مجموعهی textbox و دکمه را درون یک المان form قرار دهیم و نوع button را نیز به submit تغییر دهیم. سپس onClick دکمه را حذف کرده و بجای آن رویداد onSubmit فرم را پیاده سازی میکنیم:
در اینجا یک المان فرم، به همراه یک textbox و button از نوع submit تعریف شدهاند. رویداد onSubmit نیز به متد منتسب به متغیر handleSearch، متصل شدهاست تا با فشردن دکمهی enter توسط کاربر در این textbox، کار جستجوی مجدد، صورت گیرد:
تا اینجا اگر برنامه را ذخیره کرده و "Text 1" را در textbox جستجو، وارد کرده و enter کنیم، همانند تصویر فوق، رکورد متناظری از سرور دریافت و نمایش داده میشود.
افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook
در ادامه میخواهیم یک دکمهی جدید را در کنار دکمهی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شدهی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشتهی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آنرا با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم میکنیم». برای انجام یک چنین کاری در اینجا، Hook ویژهای به نام useRef معرفی شدهاست. بنابراین برای پیاده سازی این نیازمندیها، ابتدا دکمهی Clear را در کنار دکمهی Search قرار میدهیم:
سپس رویداد onClick آنرا به متد منتسب به متغیر handleClearSearch، مرتبط میکنیم:
در اینجا ابتدا useRef را import کردهایم، تا توسط آن بتوان یک متغیر از نوع React.MutableRefObject را ایجاد کرد. سپس در متد منتسب به handleClearSearch، ابتدا با فراخوانی setQuery، مقدار query را در state کامپوننت، پاک کرده و سپس به کمک این شیء Ref، دسترسی مستقیمی به شیء textbox یافته و متد focus آنرا فراخوانی میکنیم (شیء current آن، معادل DOM Element متناظر است).
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آنرا به ویژگی ref المان، انتساب داد:
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنیم، با کلیک بر روی دکمهی Clear، متن textbox جستجو حذف شده و سپس کرسر مجددا به همان textbox برای ورود اطلاعات، منتقل میشود.
نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور
البته در اینجا با هر بار کلیک بر روی دکمهی جستجو، نتیجهی نهایی به سرعت نمایش داده میشود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ میدهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه میکنیم:
تا با فراخوانی متد setLoading آن بتوان سبب رندر مجدد UI شد و پیامی را نمایش داد و یا مخفی کرد:
متد setLoading در ابتدای متد منتسب به متغیر getResults، مقدار متغیر loading را در state به true تنظیم میکند و در پایان عملیات، به false. اکنون بر این اساس میتوان UI متناظری را نمایش داد:
در اینجا با استفاده از یک ternary operator، اگر loading به true تنظیم شده باشد، یک div به همراه عبارت Loading results، نمایش داده میشود؛ در غیراینصورت، جدول اطلاعات مطالب، نمایش داده خواهد شد.
برای آزمایش آن میتوان سرعت اتصال را در برگهی شبکهی ابزارهای توسعه دهندگان مرورگر، تغییر داد:
مدیریت خطاها در حین اعمال async
آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت میگیرد:
در حین فراخوانی await axios.get، اگر خطایی رخ دهد، این کتابخانه استثنایی را صادر خواهد کرد که میتوان به جزئیات آن در بدنهی catch نوشته شده دسترسی یافت و برای مثال آنرا به کاربر نمایش داد. برای این منظور ابتدا state مخصوص آنرا ایجاد میکنیم و سپس توسط فراخوانی متد setError آن، کار رندر مجدد کامپوننت را در صف انجام قرار خواهیم داد.در نهایت برای نمایش آن میتوان یک div را به پایین جدول اضافه نمود:
برای آزمایش آن، برنامهی backend را که در حال اجرا است، خاتمه دهید و سپس در برنامه سعی کنید به آن متصل شوید:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip
برپایی پیشنیازها
در اینجا نیز از همان برنامهای که در قسمت 30، برای بررسی مثالهای React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانهی Axios را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
در ادامه میخواهیم در برنامهی React خود، لیست مطالب برنامهی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمههای search و clear نیز به آن اضافه کنیم.
دریافت اطلاعات اولیه از سرور، درون useEffect Hook
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد میکنیم:
{ "apiUrl": "https://localhost:5001/api" }
import React from "react"; import {apiUrl} from "../../config.json"; export default function App() { return <></>; }
import App from "./components/part03/Search";
import axios from "axios"; import React, { useEffect, useState } from "react"; import { apiUrl } from "../../config.json"; export default function App() { useEffect(() => { axios .get(apiUrl + "/posts/search?query=") .then(response => console.log(response.data)); }); return <></>; }
اکنون میخواهیم این اطلاعات دریافتی را در برنامهی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقهای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آنرا import کرده و سپس آنرا تعریف و در قسمت then، فراخوانی میکنیم:
import axios from "axios"; import React, { useEffect, useState } from "react"; import { apiUrl } from "../../config.json"; export default function App() { const [results, setResults] = useState([]); useEffect(() => { axios.get(apiUrl + "/posts/search?query=").then(response => { console.log(response.data); setResults(response.data); }); });
همانطور که مشاهده میکنید، یک حلقهی بی پایان در اینجا رخ دادهاست! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامهی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرندهی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا میشود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننتهای کلاسی را با هم پوشش میدهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شدهاست و پس از آن، مجددا effect method فراخوانی میشود و ... این حلقه هیچگاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ میکنند!
در این برنامه فعلا میخواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایهی خالی مقدار دهی میکنیم:
useEffect(() => { // ... }, []);
//... export default function App() { // ... return ( <> <table className="table"> <thead> <tr> <th>Title</th> </tr> </thead> <tbody> {results.map(post => ( <tr key={post.id}> <td>{post.title}</td> </tr> ))} </tbody> </table> </> ); }
استفاده ازAsync/Await برای دریافت اطلاعات، درون یک useEffect Hook
اکنون میخواهیم درون effect method یک useEffect Hook، روش قدیمی استفادهی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آنرا بیشتر بررسی کردیم، جایگزین کنیم.
useEffect(async () => { const { data } = await axios.get(apiUrl + "/posts/search?query="); console.log(data); setResults(data); }, []);
Warning: An effect function must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise.
برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
useEffect(() => { getResults(); }, []); const getResults = async () => { const { data } = await axios.get(apiUrl + "/posts/search?query="); console.log(data); setResults(data); };
پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا
تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آنرا از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقهی بینهایت وارد نشود. اکنون این سؤال مطرح میشود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شدهی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آنرا با استفاده از useState Hook و یک مقدار اولیهی دلخواه تنظیم میکنیم:
export default function App() { // ... const [query, setQuery] = useState("Title");
<input type="text" name="query" className="form-control my-3" placeholder="Search..." onChange={event => setQuery(event.target.value)} value={query} />
اکنون که متغیر query دارای مقدار شدهاست، میتوان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` );
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمیشود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get میشود، طوری تنظیم شدهاست که فقط یکبار، آنهم پس از رندر اولیهی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایهای که به عنوان پارامتر دوم متد useEffect تعریف شده، میتوان اجرای مجدد effect function آنرا وابستهی به تغییرات متغیر query در state کامپوننت کرد:
useEffect(() => { getResults(); }, [query]);
دریافت اطلاعات جستجو، تنها با ارسال اطلاعات یک فرم به سمت سرور
تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد میکند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن میتوان یک دکمهی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف میکنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آنرا مجددا به یک آرایهی خالی تنظیم میکنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره میکند، در بالای جدول نتایج مطالب، قرار میدهیم:
<button className="btn btn-primary" type="button" onClick={getResults} > Search </button>
<form onSubmit={handleSearch}> <div className="input-group my-3"> <label htmlFor="query" className="form-control-label sr-only"></label> <input type="text" id="query" name="query" className="form-control" placeholder="Search ..." onChange={event => setQuery(event.target.value)} value={query} /> <div className="input-group-append"> <button className="btn btn-primary" type="submit"> Search </button> </div> </div> </form>
const handleSearch = event => { event.preventDefault(); getResults(); };
افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook
در ادامه میخواهیم یک دکمهی جدید را در کنار دکمهی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شدهی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشتهی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آنرا با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم میکنیم». برای انجام یک چنین کاری در اینجا، Hook ویژهای به نام useRef معرفی شدهاست. بنابراین برای پیاده سازی این نیازمندیها، ابتدا دکمهی Clear را در کنار دکمهی Search قرار میدهیم:
<button type="button" onClick={handleClearSearch} className="btn btn-info" > Clear </button>
import React, { useEffect, useRef, useState } from "react"; // ... export default function App() { // ... const searchInputRef = useRef(); const handleClearSearch = () => { setQuery(""); searchInputRef.current.focus(); };
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آنرا به ویژگی ref المان، انتساب داد:
<input type="text" id="query" name="query" className="form-control" placeholder="Search ..." onChange={event => setQuery(event.target.value)} value={query} ref={searchInputRef} />
نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور
البته در اینجا با هر بار کلیک بر روی دکمهی جستجو، نتیجهی نهایی به سرعت نمایش داده میشود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ میدهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه میکنیم:
const [loading, setLoading] = useState(false);
const getResults = async () => { setLoading(true); const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` ); console.log(data); setResults(data); setLoading(false); };
{loading ? ( <div className="alert alert-info">Loading results...</div> ) : ( <table className="table"> <thead> <tr> <th>Title</th> </tr> </thead> <tbody> {results.map(post => ( <tr key={post.id}> <td>{post.title}</td> </tr> ))} </tbody> </table> )}
برای آزمایش آن میتوان سرعت اتصال را در برگهی شبکهی ابزارهای توسعه دهندگان مرورگر، تغییر داد:
مدیریت خطاها در حین اعمال async
آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت میگیرد:
// ... export default function App() { // ... const [error, setError] = useState(null); // ... const getResults = async () => { setLoading(true); try { const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` ); console.log(data); setResults(data); } catch (err) { setError(err); } setLoading(false); };
{error && <div className="alert alert-warning">{error.message}</div>}
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip
اشتراکها
معرفی SafeNuGet
نظرات مطالب
تبدیل عدد به حروف
در طی دو مطلب (^ و ^) با نحوهی قرار دادن خواص محاسباتی، درون کلاسهای مدلهای بانک اطلاعاتی مورد استفادهی توسط Entity Framework آشنا شدیم. در اینجا قصد داریم این خواص محاسباتی را از کلاسهای اصلی مدلهای بانک اطلاعاتی خود خارج و به ViewModelها منتقل کنیم؛ چون اساسا هدف از این نوع خواص ویژه، ارائه اطلاعات نمایشی است به کاربر و نه ذخیره سازی آنها در بانک اطلاعاتی.
مدلها و تنظیمات برنامه
مدلها و تنظیمات مورد استفادهی در مثال جاری، با مدلهای مطلب «لغو Lazy Loading در حین کار با AutoMapper و Entity Framework» یکی است. فقط ViewModel مورد استفاده اینبار یکچنین ساختاری را دارد:
در اینجا میخواهیم در حین نگاشت اطلاعات جدول کاربران بانک اطلاعاتی به UserViewModel :
- خاصیت CustomName از جمع نام و سن شخص تشکیل شود.
- خاصیت PostsCount بیانگر جمع مطالب ارسالی آن شخص باشد.
نگاشتهای AutoMapper میتوانند حاوی توابع تجمعی نیز باشند
برای حل مسالهی فوق تنها کافی است نگاشت ذیل را تهیه کنیم:
در این نگاشت عنوان شدهاست که اطلاعات CustomName را مطابق فرمول خاص جمع نام شخص و سن او تهیه کن. همچنین مقدار PostsCount، باید از جمع تعداد مطالب ارسالی او تشکیل شود.
کوئری نهایی استفاده کننده از تنظیمات نگاشت تهیه شده
در ادامه متدهای Project To را جهت استفادهی از تنظیمات نگاشت فوق بکار میگیریم:
این کوئری یک چنین خروجی SQL ایی را به همراه دارد:
همانطور که مشاهده میکنید، تنظیمات نگاشت تهیه شده (نحوهی تهیهی نام و جمع تعداد مطالب شخص) به SQL ترجمه شدهاند.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
مدلها و تنظیمات برنامه
مدلها و تنظیمات مورد استفادهی در مثال جاری، با مدلهای مطلب «لغو Lazy Loading در حین کار با AutoMapper و Entity Framework» یکی است. فقط ViewModel مورد استفاده اینبار یکچنین ساختاری را دارد:
public class UserViewModel { public int Id { set; get; } public string CustomName { set; get; } public int PostsCount { set; get; } }
- خاصیت CustomName از جمع نام و سن شخص تشکیل شود.
- خاصیت PostsCount بیانگر جمع مطالب ارسالی آن شخص باشد.
نگاشتهای AutoMapper میتوانند حاوی توابع تجمعی نیز باشند
برای حل مسالهی فوق تنها کافی است نگاشت ذیل را تهیه کنیم:
public class TestProfile : Profile { protected override void Configure() { this.CreateMap<User, UserViewModel>() .ForMember(dest => dest.CustomName, opt => opt.MapFrom(src => src.Name + "[" + src.Age + "]")) .ForMember(dest => dest.PostsCount, opt => opt.MapFrom(src => src.BlogPosts.Count())); } public override string ProfileName { get { return this.GetType().Name; } } }
کوئری نهایی استفاده کننده از تنظیمات نگاشت تهیه شده
در ادامه متدهای Project To را جهت استفادهی از تنظیمات نگاشت فوق بکار میگیریم:
using (var context = new MyContext()) { var user1 = context.Users .Project() .To<UserViewModel>() .FirstOrDefault(); if (user1 != null) { Console.Write(user1.CustomName); Console.Write(user1.PostsCount); } }
SELECT [Limit1].[Id] AS [Id], [Limit1].[C1] AS [C1], [Limit1].[C2] AS [C2] FROM ( SELECT TOP (1) [Project1].[Id] AS [Id], CASE WHEN ([Project1].[Name] IS NULL) THEN N'' ELSE [Project1].[Name] END + N'[' + CAST( [Project1].[Age] AS nvarchar(max)) + N']' AS [C1], [Project1].[C1] AS [C2] FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Age] AS [Age], (SELECT COUNT(1) AS [A1] FROM [dbo].[BlogPosts] AS [Extent2] WHERE [Extent1].[Id] = [Extent2].[UserId]) AS [C1] FROM [dbo].[Users] AS [Extent1] ) AS [Project1] ) AS [Limit1]
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.