اشتراکها
اشتراکها
نقشه راه آنگولار
اشتراکها
کتاب Angular Essentials
اشتراکها
فرم ساز منعطف در انگولار 6
سلام و ضمن تشکر
کتابخانه کمکی ارائه شده در لینک برای استفاده در ASP.NET Core را مطالعه کردم و نمونه مثالی که برای دمو در لینک Demo.AspNetCore.JqGrid برای استفاده از این کتابخانه ارائه کرده است را بررسی کردم.
مشکلی که داشتم با نمونه مثال عنوان شده این کتابخانه در dot NET Core این بود :
مشکلی که داشتم با نمونه مثال عنوان شده این کتابخانه در dot NET Core این بود :
1. قبلا توسط
jqGridHelper که شما آن را توسعه داده بودید کوئریهای جستجو و فیلترینگ
داینامیک بر روی جداول اعمال میشد و نیاز به کدنویسیهای اضافهتر سمت سرور برای فیلترینگ هر جدول نبود ولی در این
مثالی که برای کتابخانه جدید dot NET Core در لینک ارائه کرده نیاز است تا برای
هر جدولی که میخواهیم نمایش دهیم چندین متد جداگانه برای فیلتر و مرتب سازی آن سمت سرور پیاده سازی شود ضمن اینکه کوئری جستجوها هم برای هر
فیلدی که میخواهید در گرید اضافه کنید باید جداگانه سمت سرور کد آن نوشته
شود که باعث پیچیدهتر شدن و کدنویسی بسیار زیاد سمت سرور میشود.
2. در کتابخانه jqGridHelper به دلیل نحوه داینامیک بودن و به صورت String بودن کوئریهای نهایی که به
Linq ترجمه میشد امکان فعال سازی و استفاده از آن بر روی بانکهای
اطلاعاتی NoSQL از قبیل MongoDB با استفاده از MongoDB Driver به راحتی فراهم میشد در حالی که در مثال ارائه شده برای dot Net Core به دلیل ماهیت ساخت داینامیک بودن آن توسط خود Linq و عدم پشتیبانی از این نحو فیلترینگ
در درایور دات نتی مونگو امکان استفاده از مثال برای زمانی که از بانک
اطلاعاتی مونگو استفاده میشود و میخواهیم بر روی جداول فیلترینگ انجام دهیم فراهم نمیباشد و با خطا مواجه میشود
به همین خاطر سعی کردم تا کدهای کتابخانه jqGridHelper قدیمی که بر روی نسخه قبلی ASP.NET MVC به راحتی برای انواع بانکهای اطلاعاتی از قبیل SQL Server و MongoDB کار میکرد را تغییراتی بدهم تا بتوان از این مثال برای استفاده در dot Net Core در انواع بانکهای اطلاعاتی SQL Server و MongoDB و غیره که قبلا استفاده میشد به راحتی استفاده کرد و هم کدنویسی سمت سرور برای نمایش و فیلترینگ انواع گریدها را به شدت کاهش میدهد
به همین خاطر سعی کردم تا کدهای کتابخانه jqGridHelper قدیمی که بر روی نسخه قبلی ASP.NET MVC به راحتی برای انواع بانکهای اطلاعاتی از قبیل SQL Server و MongoDB کار میکرد را تغییراتی بدهم تا بتوان از این مثال برای استفاده در dot Net Core در انواع بانکهای اطلاعاتی SQL Server و MongoDB و غیره که قبلا استفاده میشد به راحتی استفاده کرد و هم کدنویسی سمت سرور برای نمایش و فیلترینگ انواع گریدها را به شدت کاهش میدهد
کتابخانه قبلی jqGridHelper را همگام با کتابخانه Releasing Lib.AspNetCore.Mvc.JqGrid v1.0.0 تغییر دادم
دوستان از لینک زیر میتوانید بهینه سازی شده مثال لینک اول برای استفاده از jqGrid در dotNet Core همراه با قابلیت استفاده با بانک اطلاعاتی Mongo DB را استفاده نمایید.
Demo-AspNetCore-JqGrid.rar
Demo-AspNetCore-JqGrid.rar
در قسمت اول این سری، با مفاهیم توابع خالص و ناخالص آشنا شدیم و همچنین عنوان شد که هرگونه تعامل با یک API خارجی به عنوان یک اثر جانبی و یا side effect در نظر گرفته شده و توابع را ناخالص میکند. به علاوه Redux تنها امکان کار با توابع خالص قابل پیش بینی را دارد و همچنین dispatch تمام اکشنها توسط آن، به صورت پیشفرض synchronous است و نه asynchronous. اما در برنامههای واقعی نیاز است بتوان با یک API خارجی کارکرد و اطلاعاتی را از آن دریافت نمود و یا به آن ارسال کرد. برای رفع این مشکل، یک کتابخانهی کمکی به نام redux-thunk ایجاد شدهاست که جزئیات کار با آنرا در این قسمت بررسی میکنیم.
معرفی کتابخانهی Redux Thunk
thunk، تابعی است که خروجی تابعی دیگر است؛ مانند مثال زیر:
هدف اصلی از انجام یک چنین کاری، فراهم آوردن روشی برای اجرای به تاخیر افتادهاست. برای مثال زمانیکه برای اجرای آن مینویسیم ()definitelyNotAThunk، تابع درونی آن هنوز اجرا نشدهاست. اجرای کامل آن چنین شکلی را دارد: ()()definitelyNotAThunk. حالا فرض کنید میانافزاری پیش از اجرای reducer قرار گرفتهاست که به تمام اشیاء رسیدهی به آن (یا همان اکشنها در اینجا) گوش فرا میدهد. اگر اکشنی بجای یک شیء، یک تابع را بازگرداند، این میانافزار، آنرا اجرا میکند یا همان ()() که عنوان شد. این کل کاری است که میانافزار 14 سطری redux-thunk انجام میدهد. زمانیکه از این میانافزار استفاده میشود، تابع درونی، دو پارامتر dispatch و getState را دریافت میکند. پارامتر dispatch که در حقیقت یک متد است، امکان اجرای اعمال synchronous و ارسال آنها را به سمت reducer متناظر، میسر میکند.
برای مثال فرض کنید که نیاز است یک فراخوانی Ajax ای صورت گیرد و پس از پایان آن، جهت به روز رسانی state، یک شیء اکشن، به سمت reducer متناظری dispatch شود. مشکل اینجا است که نمیتوان به Redux، یک callback حاصل از دریافت نتیجهی عملیات Ajax ای و یا یک Promise را ارسال کرد. تمام اینها یک اثر جانبی یا side effect هستند که با توابع خالص Redux ای سازگاری ندارند. برای مدیریت یک چنین مواردی، یک میانافزار را به نام redux-thunk ایجاد کردهاند که اجازهی dispatch تابعی را میدهد (همان thunk در اینجا) که قرار است action اصلی را در زمانی دیگر dispatch کند. به این ترتیب Redux اطلاعاتی را در مورد یک عمل async نخواهد داشت؛ میانافزاری در این بین آنرا دریافت میکند و زمانیکه آنرا dispatch میکنیم، آنگاه اکشن متناظر با آن، به redux منتقل میشود. به این ترتیب امکان منتظر ماندن تا زمان رسیدن پاسخ از شبکه، میسر میشود.
فرض کنید یک action creator متداول به صورت زیر ایجاد شدهاست:
اکنون این سؤال مطرح میشود که چگونه میتوان متوجه شد، پاسخی از سمت API دریافت شدهاست؟
برای پاسخ به این سؤال، اینبار action creator فوق را بر اساس الگوی redux-thunk به صورت زیر بازنویسی میکنیم:
اینبار action creator ای را داریم که بجای بازگشت یک شیء، یک تابع را بازگشت دادهاست که به آن thunk گفته میشود و پارامتر dispatch را دریافت میکند. در این حالت زمانیکه یک Promise بازگشت داده میشود (امکان منتظر نتیجه شدن را فراهم میکند)، کار dispatch اکشن اصلی مدنظر (برای مثال ارسال آرایهای از اشیاء)، صورت میگیرد.
برپایی پیشنیازها
در اینجا برای افزودن کامپوننتی که اطلاعات خودش را از یک API خارجی تامین میکند، از همان برنامهی به همراه کامپوننت شمارشگر که در قسمت قبل آنرا تکمیل کردیم، استفاده میکنیم. فقط در آن کتابخانههای Axios و همچنین redux thunk را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
برنامهی backend مورد استفاده هم همان برنامهای است که از قسمت 22 شروع به توسعهی آن کردیم و کدهای کامل آنرا از پیوستهای انتهای بحث، میتوانید دریافت کنید. این برنامه که در مسیر شروع شدهی با https://localhost:5001/api قرار میگیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شدهاست. برای راه اندازی آن، به پوشهی این برنامه، مراجعه کرده و فایل dotnet_run.bat آنرا اجرا کنید، تا endpointهای REST Api آن قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts آن در مرورگر دسترسی یافت.
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api، ایجاد میکنیم:
در ادامه میخواهیم در برنامهی React خود، لیست مطالب برنامهی backend را از سرور دریافت کرده و نمایش دهیم.
افزودن میانافزار redux-thunk به برنامه
فرض کنید در قسمتی از صفحه، در کامپوننتی مجزا، دکمهای وجود دارد و با کلیک بر روی آن، قرار است اطلاعاتی از سرور دریافت شده و در کامپوننت مجزای دیگری نمایش داده شود:
چون نیاز به عملیات async وجود دارد، باید از میانافزار مخصوص thunk برای انجام آن استفاده کرد. برای این منظور به فایل src\index.js مراجعه کرده و میانافزار thunk را توسط تابع applyMiddleware، به متد createStore، معرفی میکنیم:
در اینجا چون نیاز است چندین تابع را به متد createStore ارسال کرد، باید از متد compose برای اعمال دسته جمعی آنها کمک گرفت.
دریافت اطلاعات از یک API خارجی به کمک redux-thunk
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux در قسمت قبل، پیاده سازی دریافت اطلاعات را بر اساس همان الگو، پیاده سازی میکنیم:
1) ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
به فایل src\constants\ActionTypes.js، نوع جدید دریافت مطالب را اضافه میکنیم:
در حین دریافت اطلاعات از API، حداقل سه اکشن نمایش loading (و یا GetPostsStarted در اینجا)، نمایش نهایی اطلاعات دریافت شدهی از سرور (و یا GetPostsSuccess در اینجا) و یا نمایش خطاهای حاصل (با نوع GetPostsFailure در اینجا) باید مدنظر باشند. به همین جهت سه نوع مختلف در اینجا تعریف شدهاند.
2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer دریافت اطلاعات، منطق نهایی را پیاده سازی کرد:
- در اینجا همان الگوی بازگشت یک تابع را بجای یک شیء، در توابع action creator، مشاهده میکنید.
- تابع fetchPosts، از همان روش قدیمی callback، برای مدیریت اطلاعات دریافتی از سرور استفاده میکند. زمانیکه اطلاعاتی دریافت شد، آنرا با فراخوانی dispatch و با قالبی که تابع getPostsSuccess ارائه میدهد، به reducer متناظر، ارسال میکند.
- تابع fetchPostsAsync، نمونهی به همراه async/await کار با کتابخانهی axios است. هر دو روش callback و یا async/await در اینجا پشتیبانی میشوند.
به صورت پیشفرض action creators کتابخانهی redux از اعمال async پشتیبانی نمیکنند. برای رفع این مشکل پس از ثبت میانافزار thunk، اینبار متدهای action creator، بجای بازگشت یک شیء، یک تابع را بازگشت میدهند که این تابع درونی در زمانی دیگر توسط میانافزار thunk و پیش از رسیدن به reducer، فراخوانی خواهد شد. این تابع درونی، دو پارامتر dispatch و getState را دریافت میکند. هر دوی اینها نیز متد هستند. برای مثال اگر نیاز به دریافت وضعیت فعلی state در اینجا وجود داشت، میتوان متد ()getState رسیده را فراخوانی کرد و حاصل آنرا بررسی نمود. برای مثال شاید تصمیم گرفته شود که بر اساس وضعیت فعلی state، نیازی نیست تا اطلاعاتی از سرور دریافت شود و بهتر است همان اطلاعات کش شدهی موجود در state را بازگشت دهیم. البته در این مثال فقط از متد dispatch ارسالی، برای بازگشت نتیجهی نهایی به reducer متناظر، استفاده شدهاست.
- در نهایت آرایهی اشیاء مطلب دریافتی از سرور، به عنوان مقدار خاصیت posts شیء منتسب به خاصیت payload شیء ارسالی به reducer، در متد getPostsSuccess تعریف شدهاست. یعنی reducer متناظر، اطلاعات را از طریق خاصیت action.payload.posts شیء رسیده، دریافت میکند.
- همچنین دو اکشن شروع به دریافت اطلاعات (getPostsStarted) و بروز خطا (getPostsFailure) نیز در ابتدا و در قسمت catch عملیات async، به سمت reducer متناظر، dispatch خواهند شد.
3) ایجاد تابع reducer مخصوص دریافت اطلاعات از سرور
اکنون در فایل جدید src\reducers\posts.js، بر اساس نوع شیء رسیده و مقدار action.payload.posts آن، کار تامین آرایهی posts موجود در state انجام میشود:
در این reducer با استفاده از یک switch، سه حالت ممکنی را که اکشنهای رسیدهی به آن میتوانند داشته باشند، مدیریت کردهایم:
- در حالت آغاز کار و یا GetPostsStarted، با تنظیم خاصیت loading به true، سبب نمایش یک div «لطفا منتظر بمانید» خواهیم شد.
- در حالت دریافت نهایی اطلاعات از سرور، خاصیت loading به false تنظیم میشود تا div «لطفا منتظر بمانید» را مخفی کند. همچنین آرایهی posts را نیز از payload رسیده استخراج کرده و به سمت کامپوننتها ارسال میکند.
- در حالت بروز خطا و یا GetPostsFailure، خاصیت error شیء action.payload استخراج شده و جهت نمایش div متناظری، بازگشت داده میشود.
پس از تعریف این reducer باید آنرا در فایل src\reducers\index.js به کمک combineReducers، با سایر reducerهای موجود، ترکیب و یکی کرد تا در نهایت این rootReducer در فایل index.js اصلی برنامه، جهت ایجاد store اصلی redux، مورد استفاده قرار گیرد:
تشکیل کامپوننتهای دکمهی دریافت اطلاعات و نمایش لیست مطالب
UI این قسمت از سه کامپوننت تشکیل شدهاست که کدهای کامل آنها را در ادامه مشاهده میکنید:
الف) کامپوننت src\components\FetchPosts.jsx
کار این کامپوننت، نمایش دکمهی Fetch Posts است. با کلیک بر روی آن قرار است action creator ای به نام fetchPostsAsync اجرا شود که کدهای آنرا پیشتر مرور کردیم.
همانطور که مشاهده میکنید، این کامپوننت هیچ اطلاعاتی از وجود کامپوننت دومی که قرار است لیست مطالب را نمایش دهد، ندارد. کارش تنها dispatch یک اکشن است.
بنابراین این کامپوننت از طریق props فقط یک اشارهگر به متد رویدادگردانی را دریافت میکند و اطلاعات دیگری را نیاز ندارد.
ب) کامپوننت src\components\Posts.jsx
این کامپوننت، آرایهای از اشیاء مطالب را دریافت کرده و با ایجاد حلقهای بر روی آنها، توسط کامپوننت Post، هر کدام را در صفحه درج میکند. بنابراین این کامپوننت اکشنی را dispatch نمیکند. فقط از طریق props، یک آرایهی posts، وضعیت جاری عملیات و خطاهای حاصل را باید دریافت کند.
در این کامپوننت اگر loading رسیده به true تنظیم شده باشد، یک div با عبارت loading نمایش داده میشود. در غیراینصورت، لیست مطالب را درج میکند. همچنین اگر خطایی نیز رخ داده باشد، آنرا نیز درون یک div در صفحه نمایش میدهد.
ج) کامپوننت src\components\Post.jsx
این کامپوننت کار نمایش جزئیات هر رکورد مطلب را به عهده دارد؛ مانند نمایش عنوان و بدنهی یک مطلب.
اتصال کامپوننتهای FetchPosts و Posts به مخزن redux
مرحلهی آخر کار، تامین state کامپوننتهای FetchPosts و Posts از طریق props است. به همین جهت باید دو دربرگیرنده را برای این دو کامپوننت ایجاد کنیم.
الف) ایجاد دربرگیرندهی کامپوننت FetchPosts
برای این منظور فایل جدید src\containers\FetchPosts.js را با محتوای زیر ایجاد میکنیم:
- کار این تامین کننده، اتصال action creator ای به نام fetchPostsAsync به props کامپوننت FetchPosts است.
- چون اطلاعات state ای قرار نیست به این کامپوننت ارسال شود، تابع mapStateToProps را در اینجا مشاهده نمیکنید و با نال مقدار دهی شدهاست.
ب) ایجاد دربرگیرندهی کامپوننت Posts
برای این منظور فایل جدید src\containers\Posts.js را با محتوای زیر ایجاد میکنیم:
- کار این تامین کننده، اتصال خاصیت posts بازگشت داده شدهی از طریق postsReducer، به props کامپوننت Posts است. البته چون کامپوننت Posts سه خاصیت { posts, loading, error } را از طریق props دریافت میکند، با استفاده از spread operator، هر سه خاصیت دریافتی از reducer را به صورت یک شیء جدید بازگشت دادهایم.
- کامپوننت Posts رویدادی را سبب نخواهد شد. به همین جهت تابع mapDispatchToProps را در اینجا تعریف و ذکر نکردهایم.
استفاده از کامپوننتهای دربرگیرنده جهت نمایش نهایی کامپوننتهای تحت کنترل Redux
اکنون به فایل src\App.js مراجعه کرده و دو تامین کنندهی فوق را درج میکنیم:
در اینجا FetchPostsContainer و PostsContainer سبب خواهند شد تا اتصالات مخزن اصلی redux، به کامپوننتهایی که توسط آنها دربرگرفته شدهاند، برقرار شود و کار تامین props آنها صورت گیرد.
یک نکته: برای مثال در انتهای کامپوننت FetchPosts، سطر export default FetchPosts را داریم. اگر این سطر را حذف کنیم و بجای آن export default connect فوق را قرار دهیم، دیگر نیازی نخواهد بود تا FetchPostsContainer را از دربرگیرندهها، import کرد و سپس بجای درج المان </FetchPosts> نوشت </FetchPostsContainer>. میتوان همانند قبل از همان نام متداول </FetchPosts> استفاده کرد و import انجام شده نیز همانند سابق از همان فایل ماژول کامپوننت صورت میگیرد. یعنی میتوان پوشهی containers را حذف کرد و کدهای آن را دقیقا ذیل کلاس کامپوننت درج نمود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part04-backend.zip و state-management-redux-mobx-part04-frontend.zip
معرفی کتابخانهی Redux Thunk
thunk، تابعی است که خروجی تابعی دیگر است؛ مانند مثال زیر:
function definitelyNotAThunk() { return function aThunk() { console.log('Hello, I am a thunk.'); } }
برای مثال فرض کنید که نیاز است یک فراخوانی Ajax ای صورت گیرد و پس از پایان آن، جهت به روز رسانی state، یک شیء اکشن، به سمت reducer متناظری dispatch شود. مشکل اینجا است که نمیتوان به Redux، یک callback حاصل از دریافت نتیجهی عملیات Ajax ای و یا یک Promise را ارسال کرد. تمام اینها یک اثر جانبی یا side effect هستند که با توابع خالص Redux ای سازگاری ندارند. برای مدیریت یک چنین مواردی، یک میانافزار را به نام redux-thunk ایجاد کردهاند که اجازهی dispatch تابعی را میدهد (همان thunk در اینجا) که قرار است action اصلی را در زمانی دیگر dispatch کند. به این ترتیب Redux اطلاعاتی را در مورد یک عمل async نخواهد داشت؛ میانافزاری در این بین آنرا دریافت میکند و زمانیکه آنرا dispatch میکنیم، آنگاه اکشن متناظر با آن، به redux منتقل میشود. به این ترتیب امکان منتظر ماندن تا زمان رسیدن پاسخ از شبکه، میسر میشود.
فرض کنید یک action creator متداول به صورت زیر ایجاد شدهاست:
export const getAllItems = () => ({ type: UPDATE_ALL_ITEMS, items, });
برای پاسخ به این سؤال، اینبار action creator فوق را بر اساس الگوی redux-thunk به صورت زیر بازنویسی میکنیم:
export const getAllItems = () => { return dispatch => { Api.getAll().then(items => { dispatch({ type: UPDATE_ALL_ITEMS, items, }); }); }; };
برپایی پیشنیازها
در اینجا برای افزودن کامپوننتی که اطلاعات خودش را از یک API خارجی تامین میکند، از همان برنامهی به همراه کامپوننت شمارشگر که در قسمت قبل آنرا تکمیل کردیم، استفاده میکنیم. فقط در آن کتابخانههای Axios و همچنین redux thunk را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios redux-thunk
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api، ایجاد میکنیم:
{ "apiUrl": "https://localhost:5001/api" }
افزودن میانافزار redux-thunk به برنامه
فرض کنید در قسمتی از صفحه، در کامپوننتی مجزا، دکمهای وجود دارد و با کلیک بر روی آن، قرار است اطلاعاتی از سرور دریافت شده و در کامپوننت مجزای دیگری نمایش داده شود:
چون نیاز به عملیات async وجود دارد، باید از میانافزار مخصوص thunk برای انجام آن استفاده کرد. برای این منظور به فایل src\index.js مراجعه کرده و میانافزار thunk را توسط تابع applyMiddleware، به متد createStore، معرفی میکنیم:
import { applyMiddleware, compose, createStore } from "redux"; import thunk from "redux-thunk"; //... const store = createStore( reducer, compose( applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ) ); //...
دریافت اطلاعات از یک API خارجی به کمک redux-thunk
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux در قسمت قبل، پیاده سازی دریافت اطلاعات را بر اساس همان الگو، پیاده سازی میکنیم:
1) ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
به فایل src\constants\ActionTypes.js، نوع جدید دریافت مطالب را اضافه میکنیم:
export const GetPostsSuccess = "GetPostsSuccess"; export const GetPostsStarted = "GetPostsStarted"; export const GetPostsFailure = "GetPostsFailure";
2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer دریافت اطلاعات، منطق نهایی را پیاده سازی کرد:
import axios from "axios"; import { apiUrl } from "../config.json"; import * as types from "../constants/ActionTypes"; export const fetchPosts = () => { return (dispatch, getState) => { dispatch(getPostsStarted()); axios.get(apiUrl + "/posts").then(response => { dispatch(getPostsSuccess(response.data)).catch(err => { dispatch(getPostsFailure(err)); }); }); }; }; export const fetchPostsAsync = () => { return async (dispatch, getState) => { dispatch(getPostsStarted()); try { const { data } = await axios.get(apiUrl + "/posts"); console.log(data); dispatch(getPostsSuccess(data)); } catch (error) { dispatch(getPostsFailure(error)); } }; }; const getPostsSuccess = posts => ({ type: types.GetPostsSuccess, payload: { posts } }); const getPostsStarted = () => ({ type: types.GetPostsStarted }); const getPostsFailure = error => ({ type: types.GetPostsFailure, payload: { error } });
- تابع fetchPosts، از همان روش قدیمی callback، برای مدیریت اطلاعات دریافتی از سرور استفاده میکند. زمانیکه اطلاعاتی دریافت شد، آنرا با فراخوانی dispatch و با قالبی که تابع getPostsSuccess ارائه میدهد، به reducer متناظر، ارسال میکند.
- تابع fetchPostsAsync، نمونهی به همراه async/await کار با کتابخانهی axios است. هر دو روش callback و یا async/await در اینجا پشتیبانی میشوند.
به صورت پیشفرض action creators کتابخانهی redux از اعمال async پشتیبانی نمیکنند. برای رفع این مشکل پس از ثبت میانافزار thunk، اینبار متدهای action creator، بجای بازگشت یک شیء، یک تابع را بازگشت میدهند که این تابع درونی در زمانی دیگر توسط میانافزار thunk و پیش از رسیدن به reducer، فراخوانی خواهد شد. این تابع درونی، دو پارامتر dispatch و getState را دریافت میکند. هر دوی اینها نیز متد هستند. برای مثال اگر نیاز به دریافت وضعیت فعلی state در اینجا وجود داشت، میتوان متد ()getState رسیده را فراخوانی کرد و حاصل آنرا بررسی نمود. برای مثال شاید تصمیم گرفته شود که بر اساس وضعیت فعلی state، نیازی نیست تا اطلاعاتی از سرور دریافت شود و بهتر است همان اطلاعات کش شدهی موجود در state را بازگشت دهیم. البته در این مثال فقط از متد dispatch ارسالی، برای بازگشت نتیجهی نهایی به reducer متناظر، استفاده شدهاست.
- در نهایت آرایهی اشیاء مطلب دریافتی از سرور، به عنوان مقدار خاصیت posts شیء منتسب به خاصیت payload شیء ارسالی به reducer، در متد getPostsSuccess تعریف شدهاست. یعنی reducer متناظر، اطلاعات را از طریق خاصیت action.payload.posts شیء رسیده، دریافت میکند.
- همچنین دو اکشن شروع به دریافت اطلاعات (getPostsStarted) و بروز خطا (getPostsFailure) نیز در ابتدا و در قسمت catch عملیات async، به سمت reducer متناظر، dispatch خواهند شد.
3) ایجاد تابع reducer مخصوص دریافت اطلاعات از سرور
اکنون در فایل جدید src\reducers\posts.js، بر اساس نوع شیء رسیده و مقدار action.payload.posts آن، کار تامین آرایهی posts موجود در state انجام میشود:
import * as types from "../constants/ActionTypes"; const initialState = { loading: false, posts: [], error: null }; export default function postsReducer(state = initialState, action) { switch (action.type) { case types.GetPostsStarted: return { loading: true, posts: [], error: null }; case types.GetPostsSuccess: return { loading: false, posts: action.payload.posts, error: null }; case types.GetPostsFailure: return { loading: false, posts: [], error: action.payload.error }; default: return state; } }
- در حالت آغاز کار و یا GetPostsStarted، با تنظیم خاصیت loading به true، سبب نمایش یک div «لطفا منتظر بمانید» خواهیم شد.
- در حالت دریافت نهایی اطلاعات از سرور، خاصیت loading به false تنظیم میشود تا div «لطفا منتظر بمانید» را مخفی کند. همچنین آرایهی posts را نیز از payload رسیده استخراج کرده و به سمت کامپوننتها ارسال میکند.
- در حالت بروز خطا و یا GetPostsFailure، خاصیت error شیء action.payload استخراج شده و جهت نمایش div متناظری، بازگشت داده میشود.
پس از تعریف این reducer باید آنرا در فایل src\reducers\index.js به کمک combineReducers، با سایر reducerهای موجود، ترکیب و یکی کرد تا در نهایت این rootReducer در فایل index.js اصلی برنامه، جهت ایجاد store اصلی redux، مورد استفاده قرار گیرد:
import { combineReducers } from "redux"; import counterReducer from "./counter"; import postsReducer from "./posts"; const rootReducer = combineReducers({ counterReducer, postsReducer }); export default rootReducer;
تشکیل کامپوننتهای دکمهی دریافت اطلاعات و نمایش لیست مطالب
UI این قسمت از سه کامپوننت تشکیل شدهاست که کدهای کامل آنها را در ادامه مشاهده میکنید:
الف) کامپوننت src\components\FetchPosts.jsx
import React from "react"; const FetchPosts = ({ fetchPostsAsync }) => { return ( <section className="card mt-5"> <div className="card-header text-center"> <button className="btn btn-primary" onClick={fetchPostsAsync}> Fetch Posts </button> </div> </section> ); }; export default FetchPosts;
همانطور که مشاهده میکنید، این کامپوننت هیچ اطلاعاتی از وجود کامپوننت دومی که قرار است لیست مطالب را نمایش دهد، ندارد. کارش تنها dispatch یک اکشن است.
بنابراین این کامپوننت از طریق props فقط یک اشارهگر به متد رویدادگردانی را دریافت میکند و اطلاعات دیگری را نیاز ندارد.
ب) کامپوننت src\components\Posts.jsx
import React from "react"; import Post from "./Post"; const Posts = ({ posts, loading, error }) => { return ( <> <section className="card mt-5"> <div className="card-header"> <h2>Posts</h2> </div> <div className="card-body"> {loading ? ( <div className="alert alert-info">Loading ...</div> ) : ( <div className="list-group list-group-flush"> {posts.map(post => ( <Post key={post.id} post={post} /> ))} </div> )} {error && <div className="alert alert-warning">{error.message}</div>} </div> </section> </> ); }; export default Posts;
در این کامپوننت اگر loading رسیده به true تنظیم شده باشد، یک div با عبارت loading نمایش داده میشود. در غیراینصورت، لیست مطالب را درج میکند. همچنین اگر خطایی نیز رخ داده باشد، آنرا نیز درون یک div در صفحه نمایش میدهد.
ج) کامپوننت src\components\Post.jsx
import React from "react"; const Post = ({ post }) => { return ( <article className="list-group-item"> <header> <h2>{post.title}</h2> </header> <p>{post.body}</p> </article> ); }; export default Post;
اتصال کامپوننتهای FetchPosts و Posts به مخزن redux
مرحلهی آخر کار، تامین state کامپوننتهای FetchPosts و Posts از طریق props است. به همین جهت باید دو دربرگیرنده را برای این دو کامپوننت ایجاد کنیم.
الف) ایجاد دربرگیرندهی کامپوننت FetchPosts
برای این منظور فایل جدید src\containers\FetchPosts.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux"; import { fetchPostsAsync } from "../actions"; import FetchPosts from "../components/FetchPosts"; const mapDispatchToProps = { fetchPostsAsync }; export default connect(null, mapDispatchToProps)(FetchPosts);
- چون اطلاعات state ای قرار نیست به این کامپوننت ارسال شود، تابع mapStateToProps را در اینجا مشاهده نمیکنید و با نال مقدار دهی شدهاست.
ب) ایجاد دربرگیرندهی کامپوننت Posts
برای این منظور فایل جدید src\containers\Posts.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux"; import Posts from "../components/Posts"; const mapStateToProps = state => { console.log("PostsContainer->mapStateToProps", state); return { ...state.postsReducer }; }; export default connect(mapStateToProps)(Posts);
- کامپوننت Posts رویدادی را سبب نخواهد شد. به همین جهت تابع mapDispatchToProps را در اینجا تعریف و ذکر نکردهایم.
استفاده از کامپوننتهای دربرگیرنده جهت نمایش نهایی کامپوننتهای تحت کنترل Redux
اکنون به فایل src\App.js مراجعه کرده و دو تامین کنندهی فوق را درج میکنیم:
import "./App.css"; import React from "react"; import CounterContainer from "./containers/Counter"; import FetchPostsContainer from "./containers/FetchPosts"; import PostsContainer from "./containers/Posts"; function App() { const prop1 = 123; return ( <main className="container"> <div className="row"> <div className="col"> <CounterContainer prop1={prop1} /> </div> <div className="col"> <FetchPostsContainer /> </div> <div className="col"> <PostsContainer /> </div> </div> </main> ); } export default App;
یک نکته: برای مثال در انتهای کامپوننت FetchPosts، سطر export default FetchPosts را داریم. اگر این سطر را حذف کنیم و بجای آن export default connect فوق را قرار دهیم، دیگر نیازی نخواهد بود تا FetchPostsContainer را از دربرگیرندهها، import کرد و سپس بجای درج المان </FetchPosts> نوشت </FetchPostsContainer>. میتوان همانند قبل از همان نام متداول </FetchPosts> استفاده کرد و import انجام شده نیز همانند سابق از همان فایل ماژول کامپوننت صورت میگیرد. یعنی میتوان پوشهی containers را حذف کرد و کدهای آن را دقیقا ذیل کلاس کامپوننت درج نمود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part04-backend.zip و state-management-redux-mobx-part04-frontend.zip
امکان دریافت تائید از کاربر، در حین ترک صفحهی یک فرم ذخیره نشده در دات نت 7
Blazor در دات نت 7 به همراه امکانات مدیریت بهتر تغییرات آدرس صفحات است. برای مثال توسط آن میتوان به کاربران در مورد کارهای ذخیره نشده، در صورت شروع به هدایت به صفحهای دیگر، هشدار داد.
برای مدیریت تغییرات آدرس صفحات، میتوان از سرویس NavigationManager و متد RegisterLocationChangingHandler آن به صورت زیر استفاده کرد:
- در این مثال اگر آدرس درخواستی به counter ختم شود، با فراخوانی متد PreventNavigation، مانع آن خواهیم شد.
- خروجی این متد برای مثال registration در اینجا، از نوع IDisposable است و dispose آن سبب حذف Handler ثبت شده میشود.
- باید بخاطر داشت که امکانات فوق تنها آدرسهای درون برنامهای را مدیریت میکند. برای مدیریت هدایت به آدرسهای خارجی باید از رخداد beforeunload جاوا اسکریپت استفاده کرد.
ساده سازی کار با سرویس مدیریت تغییرات آدرس با کامپوننت جدید NavigationLock
نکات عنوان شده را توسط کامپوننت جدید NavigationLock نیز میتوان به نحو سادهتری مدیریت کرد:
در این کامپوننت، OnBeforeInternalNavigation یک callback است که کار ردیابی و مدیریت تغییرات آدرسهای درون برنامهای را انجام میدهد. اگر میخواهید هدایت به آدرسهای خارجی را نیز کنترل کرده و همچنین علاقمند به پیاده سازی دستی رخداد beforeunload جاوا اسکریپت نیستید، میتوانید خاصیت ConfirmExternalNavigation را نیز قید کنید که با یک browser-specific prompt مدیریت میشود.
از این کامپوننت میتوان جهت مدیریت یک فرم ذخیره نشده درصورت شروع به هدایت کاربر به آدرسی دیگر، به صورت زیر استفاده کرد:
توضیحات:
- با استفاده از EditContext یک EditForm و متد IsModified آن میتوان تشخیص داد که اطلاعات فرم جاری توسط کاربر تغییر کردهاست و هنوز ذخیره شده یا نشده.
- در اینجا پیاده سازی OnBeforeInternalNavigation را توسط متد ConfirmNavigation مشاهده میکنید که در آن بررسی میشود آیا کاربر فرم را تغییر دادهاست یا خیر؟ اگر بله، متد confirm جاوا اسکریپت را جهت تائید ترک صفحه نمایش میدهد. اگر کاربر آنرا تائید نکند، توسط متد PreventNavigation، مانع تغییر آدرس صفحه و از دست رفتن اطلاعات خواهد شد.
Blazor در دات نت 7 به همراه امکانات مدیریت بهتر تغییرات آدرس صفحات است. برای مثال توسط آن میتوان به کاربران در مورد کارهای ذخیره نشده، در صورت شروع به هدایت به صفحهای دیگر، هشدار داد.
برای مدیریت تغییرات آدرس صفحات، میتوان از سرویس NavigationManager و متد RegisterLocationChangingHandler آن به صورت زیر استفاده کرد:
var registration = NavigationManager.RegisterLocationChangingHandler(async cxt => { if (cxt.TargetLocation.EndsWith("counter")) { cxt.PreventNavigation(); } });
- خروجی این متد برای مثال registration در اینجا، از نوع IDisposable است و dispose آن سبب حذف Handler ثبت شده میشود.
- باید بخاطر داشت که امکانات فوق تنها آدرسهای درون برنامهای را مدیریت میکند. برای مدیریت هدایت به آدرسهای خارجی باید از رخداد beforeunload جاوا اسکریپت استفاده کرد.
ساده سازی کار با سرویس مدیریت تغییرات آدرس با کامپوننت جدید NavigationLock
نکات عنوان شده را توسط کامپوننت جدید NavigationLock نیز میتوان به نحو سادهتری مدیریت کرد:
<NavigationLock OnBeforeInternalNavigation="ConfirmNavigation" ConfirmExternalNavigation />
از این کامپوننت میتوان جهت مدیریت یک فرم ذخیره نشده درصورت شروع به هدایت کاربر به آدرسی دیگر، به صورت زیر استفاده کرد:
<EditForm EditContext="editContext" OnValidSubmit="Submit"> ... </EditForm> <NavigationLock OnBeforeInternalNavigation="ConfirmNavigation" ConfirmExternalNavigation /> @code { private readonly EditContext editContext; ... // Called only for internal navigations. // External navigations will trigger a browser specific prompt. async Task ConfirmNavigation(LocationChangingContext context) { if (editContext.IsModified()) { var isConfirmed = await JS.InvokeAsync<bool>("window.confirm", "Are you sure you want to leave this page?"); if (!isConfirmed) { context.PreventNavigation(); } } } }
- با استفاده از EditContext یک EditForm و متد IsModified آن میتوان تشخیص داد که اطلاعات فرم جاری توسط کاربر تغییر کردهاست و هنوز ذخیره شده یا نشده.
- در اینجا پیاده سازی OnBeforeInternalNavigation را توسط متد ConfirmNavigation مشاهده میکنید که در آن بررسی میشود آیا کاربر فرم را تغییر دادهاست یا خیر؟ اگر بله، متد confirm جاوا اسکریپت را جهت تائید ترک صفحه نمایش میدهد. اگر کاربر آنرا تائید نکند، توسط متد PreventNavigation، مانع تغییر آدرس صفحه و از دست رفتن اطلاعات خواهد شد.
نظرات مطالب
EF Code First #12
وقت بخیر مهندس نصیری. خسته نباشید.
یک سوال.
در لایه سرویس اگر یک عملیات مشترک باشد (به عنوان مثال درهم سازی (Hash) کلمه عبور کاربر) به نظر شما بهتر است در کجا قرار گیرد.
1) به عنوان مثال اگر در Ef.....Service قرار گیرد خیلی جالب، زیبا و مربوط نیست.
2) میشه در یک بخش دیگر (مثلا مشترک) قرار گیره، که خوب بازم مسئله اینه که این متد همیشه به بخش کاربران سرویس میده و عملا نباید جدا باشه.
3) میشه از یک کلاس میانی انتزاعی استفاده کرد و متدهای مشترک در تمام انواع سرویس (EF، Fake، و یا ....) در دسترس باشه.
ممنون میشم که راهنمایی کنید.
فرض کنید میخواهیم دادههایی را از دیتابیس بخوانیم و در نهایت درون UI نمایش دهیم. شاید فکر کنید که متد render مکان مناسبی برای اینکار باشد:
render() { fetch('https://jsonplaceholder.typicode.com/users') .then(response => response.json()) .then(data => this.setState({ users: data })) return ( <ul> {this.state.users.map(user => <li key={user.id}>{user.name}</li>)} </ul> ); }
اما قرار دادن فراخوانی AJAX درون متد render ایدهی خوبی نیست؛ زیرا متد render نباید side effectی ایجاد کند. هدف متد render همانطور از نامش پیداست، رندر کردن یک کامپوننت است. در نتیجه اینچنین فراخوانیهایی باید درون lifecycle events تعریف شوند که در واقع یکسری رخداد هستند و این امکان را در اختیارمان قرار میدهند که بتوانیم کارهایی از این قبیل را در طول چرخهی حیات یک کامپوننت انجام دهیم. در React به فرآیند ایجاد یک کامپوننت و در نهایت render شدن محتوای آن برای اولین بار، mounting گفته میشود. در این فرآیند سه متد نقش دارند:
- constructor
- render
- componentDidMount
سازنده یک کامپوننت زمانی فراخوانی میشود که React نیاز به ایجاد یک وهله از کامپوننت داشته باشد. در اینجا کامپوننت فرصت این را خواهد داشت تا props مورد نیازش را از والدینش دریافت کند، state dataیش را تعریف کند و یا یکسری کارهای آمادهسازی را انجام دهد. در مرحلهی بعدی، متد render فراخوانی خواهد شد. در اینجا کامپوننت، محتوایی را که قرار است به DOM اضافه شود، در اختیار React قرار میدهد. در نهایت متد componentDidMount فراخوانی خواهد شد و به این معنا است که محتوا، به DOM اضافه شدهاست. در نتیجه متد componentDidMount محل مناسبی برای قرار دادن مثال ابتدای مطلب میباشد. به عنوان مثال در کد زیر هر بار که checkbox را فعال میکنیم، یک وهله از کامپوننت ایجاد خواهد شد. یعنی در واقع هر بار از فاز mounting عبور خواهد کرد.
//App.js import React, { Component } from 'react'; import { Users } from "./Users"; export default class App extends Component { constructor(props) { super(props); this.state = { toggle: false }; } handleChange = () => { this.setState({ toggle: !this.state.toggle }); } render() { return <div className="container text-center"> <div className="row p-2"> <div className="form-check"> <input type="checkbox" className="form-check-input" checked={this.state.toggle} onChange={this.handleChange} /> <label className="form-check-label">Toggle</label> </div> <div className="row p-2"> {this.state.toggle && <Users />} </div> </div> </div> } } //Users import React, { Component } from 'react'; export class Users extends Component { constructor(props) { super(props); this.state = { users: [] } } componentDidMount() { fetch('https://jsonplaceholder.typicode.com/users') .then(response => response.json()) .then(data => { this.setState({ users: data }); console.log(data) }) } render() { return ( <div className="bg-info text-white p-2"> {this.state.users && <p>{JSON.stringify(this.state.users, null, 2)}</p> } </div> ); } }
Update phase
فرآیند پاسخگویی به تغییرات و طی شدن reconciliation، به عنوان update phase شناخته میشود. در این فاز متد render جهت دریافت محتوا از کامپوننت، فراخوانی خواهد شد و سپس متد componentDidUpdate بعد از آن فراخوانی میشود:
import React, { Component } from 'react'; export class Users extends Component { constructor(props) { // as before } componentDidMount() { // as before } componentDidUpdate() { console.log("componentDidUpdate Users Component"); } render() { // as before } }
Unmounting phase
در این فاز متد componentWillUnmount زمانی فراخوانی خواهد شد که کامپوننت در حال حذف از DOM است. طبیعتاً این متد میتواند محل خوبی برای آزاد کردن منابع، بستن اتصالات شبکه و یا متوقف کردن درخواستهای asynchronous باشد؛ برای نمونه در مثال قبل در حین غیرفعال کردن checkbox فراخوانی خواهد شد:
import React, { Component } from 'react'; export class Users extends Component { constructor(props) { // as before } componentDidMount() { // as before } componentDidUpdate() { console.log("componentDidUpdate Users Component"); } componentWillUnmount() { console.log("componentWillUnmount Users Component"); } render() { // as before } }
Effect Hook
چرخهی فوق فقط مختص به کامپوننتهایی است که به صورت کلاس تعریف شدهاند. کامپوننتهایی که به صورت تابع هستند، نمیتوانند به روش فوق باشند. با استفاده از effect hook میتوانیم مشابه متدهای componentDidMount, componentDidUpdate, componentWillUnmount را پیادهسازی کنیم:
import React, { useState, useEffect } from 'react'; export const Users = () => { const [users, setUsers] = useState([]); useEffect(() => { console.log("Same as componentDidMount") fetch('https://jsonplaceholder.typicode.com/users') .then(response => response.json()) .then(data => { setUsers({ users: data }); }) }, []); useEffect(() => { console.log("Same as componentDidUpdate") }); useEffect(() => { return () => { console.log("Same as componentWillUnmount") } }, []); return ( <div className="bg-info text-white p-2"> {users && <p>{JSON.stringify(users, null, 2)}</p> } </div> ); }