برنامه نویسی 39 بازدید

در این مقاله در مورد اتصال کامپوننت‌ها به رویدادهای زنده socket.io با استفاده از React Context و Redux store صحبت خواهیم کرد. بدین ترتیب با شیوه مدیریت وب سوکت با Redux و Context آشنا می‌شویم و می‌توانیم فیدبک آنی درون کامپوننت‌ها ارائه کنیم. برای دست یافتن به یک مدیریت حالت (State) مؤثر، اقدام به بررسی ریداکس و کانتکست برای نگهداری بخش‌های خاصی از داده می‌کنیم که در کامپوننت‌های مختلف در درخت کامپوننت اپلیکیشن نمایش خواهند یافت.

پیش از بررسی کد، ابتدا به بررسی دقیق چگونگی استفاده از کانتکست ری‌اکت و استور ریداکس برای به دست آوردن داده‌های حالت به‌روز می‌پردازیم. سورس کد کامل همه موارد مطرح‌شده در این مقاله در انتهای این راهنما ارائه شده است.

React Context در برابر Redux Store

ارائه‌دهنده‌های React Context را می‌توان همزمان با Redux Store برای راه‌اندازی پروژه‌هایی که از هر دو راه‌حل مدیریت حالت بهره می‌گیرند، مورد استفاده قرار دارد. این اتفاق در مواردی رخ می‌دهد که از وب سوکت برای واکشی نوعی داده‌های آنی مانند داده‌های قیمت بازار استفاده کنیم.

فرض کنید می‌خواهیم داده‌های قیمتی را برای دسته‌ای از محصولات واکشی کنیم که با ارزهای مختلفی معامله می‌شوند.

مدیریت وب سوکت با Redux و Context

پاسخ دریافتی از وب سوکت معمولاً یک شیء JSON است که هر market را با محصولاتش تطبیق می‌دهد و سپس با مقادیرشان در یک جا گردآوری می‌کند:

فراداده (Metadata) نیز از سرویس‌های مختلف واکشی می‌شود. در این مورد stats را داریم که برابر با کل محصولات مورد معامله است. این معمولاً همان چیزی است که از این نوع داده‌های پاسخ انتظار داریم.

در یک وب سوکت معمولی، این شیء هر زمان که تغییر قیمت رخ می‌دهد ارائه می‌شود. زمانی که این داده‌ها وارد پروژه ری‌اکت شوند، باید آن‌ها را مورد پردازش قرار دهیم. بدین ترتیب کامپوننت‌هایی که باید داشته باشیم به شرح زیر هستند:

  • یک Context Provider: برای ذخیره‌سازی همه داده‌های قیمتی که از وب سوکت می‌آید. این داده‌ها در قالب خام هستند و یک شیء JSON بزرگ را برای هر دسته از قیمت‌های محصول در هر بازار تشکیل می‌دهند.
  • یک Redux Store: که مسئول ذخیره‌سازی قیمت‌های کوچک‌تر داده‌های کلیدی بازار است که در سراسر اپلیکیشن در کامپوننت‌های دیگر نمایش می‌یابد.

روش جداسازی این دو راه‌حل چنین است که داده‌های بازار عمده در کانتکست ری‌اکت ذخیره می‌شوند و بخش‌های منتخب آمارهای دیگر نیز که از روی آن‌ها اقدام به به‌روزرسانی مقادیر Redux store می‌کنیم، در کامپوننت‌های دیگر ذخیره خواهند شد.

قبل از هر چیز می‌توانیم یک وب سوکت زنده را از طریق Socket.io (+) متصل کنیم و داده‌های قیمتی به‌روزرسانی شده را هر چند ثانیه یک بار به اپلیکیشن ارسال نماییم. فرض کنید سرور ما هر 3 ثانیه یک بار به‌روزرسانی‌ها را بررسی می‌کند. این داده‌ها می‌توانند در یک Context Provider ذخیره شوند و در طیفی از کامپوننت‌ها مانند یک کامپوننت <LiveAssetTable /‎> نمایش یابند.

زمانی که این ترکیب Context +Websocket داده‌ها را به طور یکنواخت واکشی کرد، می‌توانیم ریداکس را نیز به این آمیزه اضافه کنیم. بنابراین کار دیگری که درون کامپوننت Context انجام خواهیم داد، این است که آمارهایی را از داده‌های قیمتی جمع‌آوری می‌کنیم و آن‌ها را درون یک استور ریداکس برای استفاده کامپوننت‌های دیگر قرار می‌دهیم:

از آنجا که کامپوننت کانتکست نیز از حالت خاص خود استفاده می‌کند، هر تغییری در این منابع داده رخ دهد، موجب رندر مجدد کامپوننت می‌شود و بدین ترتیب UI به‌روز باقی می‌ماند.

با این که می‌توان از یک Context استفاده کرد و یا صرفاً بر استور ریداکس برای مدیریت چنین الزامی تکیه نمود، اما در واقعیت اغلب اپلیکیشن‌ها که از حالت سراسری بهره می‌گیرند، یک استور ریداکس نیز پیکربندی کرده‌اند. به جای افزودن کدهای قالبی دیگر به استور، می‌توانیم داده‌ها را در یک Context جدا کنیم که راه‌حل ساده‌تری ارائه می‌کند و بدین ترتیب داده‌ها بخش‌بندی شده و قابلیت مدیریت بیشتری می‌یابند.

از سوی دیگر، Context روشی عالی برای ارائه قابلیت حالت سراسری در این اپلیکیشن محسوب می‌شود که از ریداکس بهره نمی‌گیرند. نتیجه نهایی این تنظیمات آن است که کامپوننت Context حالت اپلیکیشن ما را به‌روزرسانی می‌کند و موجب رندر مجدد کامپوننت متصل و به‌روز ماندن UI می‌شود:

بدین ترتیب اینک که درکی نظری از شیوه کار اپلیکیشن به دست آوردیم، در ادامه به بررسی سورس کد می‌پردازیم. پیاده‌سازی این اپلیکیشن را در چند بخش به شرح زیر انجام خواهیم داد:

  • راه‌اندازی کامپوننت </ SocketManager> که مسئول تعریف کردن context و context provider است. این ارائه‌دهنده پیرامون کامپوننت </ App> قرار می‌گیرد و دسترسی کاملی به درخت کامپوننت فراهم می‌سازد.
  • استور ریداکس نیز در این گردش داده وارد شده و برای به‌روزرسانی استور با آمارهای بازار برای استفاده کامپوننت‌های دیگر مورد استفاده قرار می‌گیرد.
  • کامپوننت‌های دیگر به استور ریداکس وصل شده و این داده‌ها را نمایش می‌دهند.

کار خود را از جایی آغاز می‌کنیم که گردش داده آغاز می‌شود و آن کامپوننت ارائه‌دهنده کانتکست برای مدیریت وب سوکت یعنی </ SocketManager> است.

مدیریت سوکت از طریق Websocket به همراه Context Provider

کامپوننت </ SocketManager> را می‌توان به صورت یک موتور وب سوکت و راه‌حل مدیریت حالت تصور کرد. در این مقاله در مورد این راه‌حل صحبت کرده و سپس کد کامل آن را ارائه می‌کنیم.

موارد زیر را درون </ SocketManager> تعریف می‌کنیم:

  • خود React context و قلاب ()useContext (+) که گزینه‌ای برای استفاده از کانتکست در کامپوننت‌های تابعی نیز در اختیار توسعه‌دهندگان قرار می‌دهد.
  • خود کامپوننت </ SocketManager> که برای پوشش بقیه اپلیکیشن مورد استفاده قرار می‌گیرد و Context Provider را در اختیار کل درخت کامپوننت قرار می‌دهد.
  • اتصال وب سوکت ما درون متدهای چرخه عمری کامپوننت </ SocketManager> مدیریت می‌شود. در زمان مقداردهی کامپوننت به وب سوکت وصل می‌شویم  و زمانی که unmount شود قطع می‌شویم.
  • رویداد های جدید سوکت موجب به‌روزرسانی حالت می شوند و از این رو مقدار Context به روز می‌شود و لذا داده‌های بازار به‌روز می‌مانند.

در چنین تنظیماتی نیازمند یک پکیج socket.io-client همراه با react-redux (+) برای کاربردهای آتی هستیم. آن‌ها را در دایرکتوری پروژه نصب می‌کنیم:

در این تنظیمات فرض شده از کلاینت سرور socket.io استفاده می‌شود که همراه با NodeJS اجرا می‌شود و کار آن عرضه داده‌های بازار در اپلیکیشن شما است راه‌حل بک‌اند برای این پروژه به بخش دیگری مربوط است، اما لازم به ذکر است که socket.io هر دو API سمت سرور و کلاینت برای وب سوکت‌ها را ارائه می‌کند.

تعریف Context

تعریف خود Context کار ساده‌ای است، همچنین پیکربندی قلاب کانتکست برای این که بتواند جهت استفاده در اختیار کامپوننت‌های تابعی قرار گیرد نیز کار آسانی محسوب می‌شود:

بدین ترتیب به کامپوننت‌های دیگر امکان می‌دهیم که به Context ما دسترسی داشته باشند و از این رو به فید داده‌های زنده بازار دسترسی داشته باشیم.

درون کامپوننت کلاس با استفاده از مشخصه static contextType روی یک کلاس به کانتکست ارجاع می‌دهیم:

بدین ترتیب هر جایی درون کامپوننت‌های تابعی می‌توانیم از قلاب تعریف‌شده جدید ()useSocket بهره بگیریم:

پروژه شما چه از کامپوننت کلاسی و چه تابعی استفاده کند، این تنظیمات برایتان مناسب خواهد بود.

کامپوننت </ SocketManager>

باید این نکته را مورد اشاره قرار دهیم که </ SocketManager> حالت درونی خاص خود را دارد که محتوای Context Provider ما را تعیین می‌کند. قبل از هر چیز از کد قالبی زیر استفاده می‌کنیم:

مشخصه کلاس عمومی socket نیز به صورت null مقداردهی شده است. این مشخصه با اتصال socket.io در سازنده کلاس به‌روزرسانی می‌شود. مشخصه‌های کلاس از هر تابع کلاس قابل دسترسی هستند که شامل متدهای چرخه عمری، render و دیگر متدهای سفارشی می‌شود. این وضعیت برای یک اتصال سوکت ایده‌آل است و از این رو متدهای چرخه عمری و render، آن را require می‌کنند.

اگر این راه‌حل را در محیط تایپ اسکریپت مورد استفاده قرار دهید، می‌توانید مشخصه را به صورت خصوصی نیز درآورده آن را در برابر دستکاری خارجی مصون سازید:

()render در </ SocketManager> صرفاً فرزندان کامپوننت را که درون Context Provider قرار دارند بازگشت می‌دهد:

توجه داشته باشید چنان که پیش‌تر اشاره کردیم، حالت </ SocketManager> مقدار Context Provider ما را تعیین می‌کند و بدین ترتیب داده‌های وب سوکت با به‌روزرسانی قیمت جدید فید های سوکت، تغییر می‌یابند.

چنان که ممکن است حدس زده باشید، اینک می‌توانیم کل اپلیکیشن را درون </ SocketManager> یا کلاسترهای منفرد کامپوننت‌هایی که می‌خواهیم context را در اختیارشان قرار دهیم، بگذاریم:

اگر دقیقاً بدانید که کدام بخش از اپلیکیشن شما نیازمند بهره‌گیری از </ SocketManager> است، بهتر است که آن را در اختیار کامپوننت‌های مجزایی که لازم دارند قرار دهید تا این که کل اپلیکیشن را پوشش دهید.

وهله‌سازی وب سوکت درون ()constructor

اکنون به‌روزرسانی‌های آنی را پیکربندی می‌کنیم، می‌توانیم از متد ()constructor کلاس‌ها برای وهله‌سازی وب سوکت بهره بگیریم. زمانی که یک سوکت متصل داشته باشیم، می‌توانیم به رویداد receive prices گوش کنیم که به‌روزرسانی حالت در آن رخ می‌دهد:

با بررسی دقیق‌تر قطعه کد فوق، ابتدا مشخصه کلاس socket را با ()io.connect مقداردهی می‌کنیم. به شیوه استفاده از متغیر محیطی process.env.NODE_ENV برای تعیین نقطه انتهایی که باید وصل شویم توجه کنید:

توضیح در مورد شیوه راه‌اندازی اتصال وب سوکت رمزنگاری‌شده را در محیط توسعه و پروداکشن با تکیه بر Nginx Proxy به مقاله دیگری وا‌می‌گذاریم. با این حال این قطعه کد کوچک برای مقاصد توسعه کارآمد است. همچنین در صورتی که بخواهید داده‌های زنده را به بیلد توسعه ارائه کنید، قرار دادن URL پروداکشن به صورت پیش‌فرض مفید خواهد بود.

در صورتی که هیچ پیکربندی برای transport عرضه نشده باشد، وب سوکت به صورت پیش‌فرض از polling استفاده می‌کند. در قطعه کد فوق می‌خواهیم از طریق websocket اتصال یابیم. به طور کلی polling می‌تواند سریع‌تر مقداردهی شود و از این رو پاسخ اولیه سریع‌تری از سرور دریافت می‌شود. با این حال، websocket یک اتصال زنده به روشی پایدار در اختیار ما قرار می‌دهد که در محیط‌های پروداکشن بسیار کارآمدتر است.

در نهایت، به رویداد receive prices گوش می‌کنیم که حالت ما را بر مبنای رویداد تحریک‌شده به‌روزرسانی می‌کند:

مدیریت قطع اتصال سوکت

()componentWillUnmount مکانی عالی برای قطع اتصال وب سوکت محسوب می‌شود. از آنجا که این احتمال وجود دارد که سوکت از قبل به هر دلیلی قطع شده باشد، متد ()socket.disconnect را در یک گزاره try catch قرار می‌دهیم:

نکته: یکپارچه‌سازی React Router DOM

در حالتی که تنها بخواهیم وب سوکت به صفحه‌های خاصی از اپلیکیشن وصل شود، می‌توانیم همواره به مشخصه location مربوط به react-router-dom (+) اشاره کنیم. کافی است </ SocketManager> را درون کامپوننت مرتبه بالاتر withRouter()‎ که از سوی پکیج ارائه شده است قرار دهیم:

اکنون فرض می‌کنیم که تنها می‌خواهیم وب سوکت را به صفحه فرود اپلیکیشن خود وصل کنیم. بدین ترتیب می‌توانیم مقدار pathname را در location به این منظور درون سازنده تست کنیم:

یا این که می‌توانیم در صوتی که در صفحه فرود نباشیم، مقدار false را پیش از اجرای بقیه تابع، بازگشت دهیم:

البته محدود به مشخصه‌های location نیستیم، زیرا می‌توان از props کامپوننت نیز برای تعیین این که اتصال وب سوکت مقداردهی شده یا نه استفاده کنیم. بدین ترتیب اینک </ SocketManager> از دسترسی کامپوننت‌ها به قیمت‌های زنده بازار پشتیبانی می‌کند. در ادامه تلاش می‌کنیم آن را با ریداکس نیز ادغام کنیم.

یکپارچه‌سازی ریداکس با رویدادهای وب سوکت

در این بخش یکپارچه‌سازی ریداکس قبلی را بسط می‌دهیم. فرض کنید می‌خواهیم آماری مانند سقف بازار کل سراسری را از یک رویداد receive prices به دست آوریم. این بار به جای شیء prices از شیء stats کمک می‌گیریم. همچنین به جای تکیه بر Context می‌توانیم داده‌ها را درون یک استور ریداکس نیز تزریق کنیم.

راه‌اندازی یک اکشن و کاهنده

در این بخش به طور خلاصه یک جفت اکشن و کاهنده (Reducer) را بررسی می‌کنیم که امکان عملیاتی شدن مثال فوق را فراهم می‌سازند. ابتدا اکشن updateTotalMarketCap را وارد می‌کنیم:

همچنین یک کاهنده برای مدیریت این اکشن می‌گنجانیم:

فرض ما بر این است که استور ریداکس در اینجا سقف کل بازار را در stats.totalMarketcap نگهداری می‌کند. همچنین می‌خواهیم از combineReducers برای بسط آسان ریداکس استفاده کنیم:

در نهایت استور ریداکس را پیرامون اپلیکیشن قرار می‌دهیم:

مقدار اولیه یک استور ممکن است برای اپلیکیشن چندان مهم نباشد، اما رویه مناسب این است که یک ساختار پایه تعریف کنیم تا توسعه‌دهندگان همکار از آن چه استور مدیریت می‌کند آگاه باشند. این وضعیت در پروژه‌های مبتنی بر تایپ اسکریپت اهمیت بیشتری دارد، چون رویه مناسب در این پروژه‌ها برای ساختارهای استور آن است که با اینترفیس‌ها هماهنگ باشند تا شفافیت بیشتری ایجاد شده و احتمال بروز باگ کاهش یابد.

بدین ترتیب کد قالبی کافی برای دریافت سقف بازار کل در یک راه‌حل ریداکس به دست آورده‌ایم. در ادامه تابع‌های dispatch را درون </ SocketManager> ادغام می‌کنیم.

اتصال </ SocketManager> به ریداکس

کاری که در این بخش انجام می‌دهیم آن است که با قرار دادن </ SocketManager> درون یک کامپوننت کانتینر و در نتیجه در متد connect()‎ ریداکس به آن وصل می‌کنیم. به بیان ساده، تابع ()connect ریداکس یک کامپوننت ری‌اکت را به استور ریداکس وصل می‌کند. ما می‌توانیم props و متدهای dispatch را از طریق دو پارامتر ارائه شده به نام‌های mapStateToProps و mapDispatchToProps ارسال کنیم.

به این ترتیب می‌توانیم تابع‌های dispatch حالت موجود یعنی تابع‌هایی که یک اکشن را می‌گیرند و کاهنده‌ها را برای به‌روزرسانی استور ریداکس مورد بحث فرامی‌خوانند، مستقیماً به </ SocketManager> ارسال کنیم.

در ادامه </ SocketManager> را با ایمپورت‌های دیگر ریداکس که برای به‌روزرسانی سقف کل بازار مورد نیاز هستند بسط می‌دهیم:

اکنون نام کامپوننت را تغییر می‌دهیم تا نشان دهیم که درون یک کامپوننت کانتینر قرار گرفته است:

در نهایت کامپوننت کانتینر را با تابع ()connect که قبلاً اشاره کردیم، تعریف می‌کنیم:

SocketManager همچنان کامپوننت پیش‌فرضی است که اکسپورت می‌شود، اما این گزاره اکسپورت اکنون به کامپوننت کانتینری که تعریف کردیم اشاره می‌کند. بدین ترتیب اکشن را به صورت props به دست می‌آوریم و فیلد mapStateAsProps را رها می‌کنیم، یعنی آرگومان نخست ()connect به صورت null خواهد بود.

آخرین تکه پازل، فراخوانی تابع dispatch تزریق شده در زمان دریافت یک رویداد جدید وب سوکت است. می‌توانیم این بخش از منطق را در جایی که حالت </ SocketManager> به‌روزرسانی می‌شود، یعنی درون receive prices قرار دهیم:

اینک به‌روزرسانی‌های Context و Redux به صورت همزمان با هم عمل می‌کنند.

تزریق حالت Redux درون کامپوننت‌های دیگر

پردازش ()connect که در بخش قبل توضیح دادیم، می‌تواند در مورد هر کامپوننتی که می‌خواهد به استور ریداکس وصل شود تکرار شود. برای نمونه اگر بخواهیم یک مقدار stats.totalMarketCap موجود را درون یک کامپوننت بیاوریم، می‌توانیم از پارامتر mapStateToProps نخست ()connect استفاده کنیم:

البته این امر موجب نمی‌شود که نتوانیم از matchDispatchToProps نیز بهره بگیریم.

سخن پایانی و سورس کد

در این مقاله در مورد شیوه استفاده همزمان از چند ابزار مدیریت حالت در مورد داده‌های رویداد وب سوکت صحبت کردیم. بدین ترتیب از قیمت‌های زنده بازار به عنوان یک نمونه از مواردی که به چنین تنظیماتی نیاز دارد بهره جستیم. این راه‌حل انعطاف‌پذیری است که نیازی به نصب ریداکس در اپلیکیشن ندارد و امکان استفاده از Context را در صورت عدم نیاز اپلیکیشن به ریداکس فراهم می‌سازد. پیاده‌سازی کامل </ SocketManager> را در سورس کد زیر می‌توانید مشاهده کنید:

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

telegram
twitter

میثم لطفی

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری همه علاقه‌مندی‌های خود در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و تولید محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *