مروری بر نوع ارتباط کامپوننت ها در Vue.js
در Vue.js دو نوع ارتباط عمده بین کامپوننت ها وجود داره:
در ادامه با چند تا مثال این دو نوع ارتباط رو با هم بررسی می کنیم :)
مدل استاندارد ارتباط میان کامپوننت ها مدل والد-فرزندی هست که با استفاده از props و custom events انجام میشه. تو تصویر زیری، نحوه ی پیاده سازی این مدل در عمل رو با هم می بینیم:
همونطور که می بینید، یک والد تنها می تونه با فرزند خودش ارتباط مستقیم برقرار کنه، فرزند هم تنها ارتباط مستقیم رو با والد خودش داره. تو این نوع ارتباط امکان ارتباط غیر همسان یا همزاد (sibling) وجود نداره.
در ادامه با در نظر گرفتن کامپوننت های تصویر بالا، چند تا نمونه ی عملی رو باهم پیاده سازی می کنیم.
فرض کنید کامپوننت هایی که داریم هر کدوم بخشی از یک بازی هستن. بیشتر بازی ها امتیاز مسابقه رو تو یه جایی از صفحه نمایش، نشون میدن. حالا یک متغیر با نام score در کامپوننت Parent A تعریف می کنیم و می خوایم اون رو تو کامپوننت Child A نمایش بدیم. خب، به نظرتون چیکار باید بکنیم؟ :)
آفرین درسته :D ، برای انتقال data از والد به فرزند، Vue.js از props استفاده می کنه. برای انتقال یک data property به کامپوننت فرزند سه تا کار لازمه که باهم انجامشون میدیم:
مثل تصویر پایینی:
برای خلاصه سازی من props رو به خلاصه ترین روش ممکن رجیستر کردم. اما در واقعیت، پیشنهاد می شه که prop اعتبار سنجی داشته باشه. این اعتبار سنجی خیال شما رو راحت می کنه که props نوع و مقدار درستی رو دریافت کرده. برای مثال، متغیر score می تونه به روش زیر اعتبار سنجی بشه:
در استفاده از props باید تفاوت بین مقادیر literal و dynamic رو بدونیم. یک prop زمانی dynamic هست که به یک متغیر دیگر bind شده باشه (v-bind:score="score" || :score="score") و بنابراین، مقدار prop بسته به مقدار variable تغییر خواهد کرد. اگر بدون bind کردن یک مقدار پیشفرضی برای prop در نظر بگیریم، این مقدار به صورت literal و نتیجه هم ثابت خواهد بود. در مثال بالا، اگر بنویسیم "score="score، به جای 100، کلمه score رو خواهیم دید. چون pops مون از نوع literal props است.
خب تا اینجا موفق شدیم امتیاز بازی رو نمایش بدیم. گاهی اوقات نیاز به آپدیت این امتیاز هم داریم. پس بیاید امتحان کنیم:
در این مثال، یک متد با نام changeScore ساختیم که وظیفه ش آپدیت score بعد از فشردن دکمه ی Change Score هست. ممکنه ظاهرا آپدیت score به درستی انجام بشه ولی اگه کنسول رو باز کنید ارور زیر رم حتما می بینید:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "score"
پیام این ارور اینه که prop با re-render شدن کامپوننت parent، اصطلاحا overWrite میشه. این بار این کار رو با استفاده از متد ()forceUpdate$ تکرار می کنیم:
حالا می تونیم score رو تغییر دهیم و با فشار دادن دکمه ی Rerender Parent می بینیم که prop به مقدار اولیه ی خودش برمی گرده.
بنابراین، برای این کار دو روش داریم:
روش اول اینه که prop رو به یک data property مثل localScore تبدیل کنیم و ازش تو متد ()changeScore و در template استفاده کنیم:
حالا، اگه دکمه ی Rerender Parent رو دوباره بزنیم، بعد از تغییر score، می بینیم که این بار score همون مقدار تغییر یافته باقی میمونه (تغییرمون سر جاش هست و با رفرش به مقدار اولیه بر نمی گرده).
روش دوم استفاده از score در یک computed property هست، یعنی در واقع با استفاده ازش یک مقدار جدید می سازیم:
تو این مثال، ما یک computed property با نام ()doubleScore ساختیم که score والد رو با 2 جمع می کنه و نتیجه رو داخل template نمایش می ده. مشخصه که با زدن دکمه ی Rerender Parent، هیچگونه side effect ای نخواهیم داشت.
حالا ببینیم کامپوننت ها چگونه برعکس نوع قبلی با هم ارتباط برقرار می کنن.
تا اینجا با نحوه ی آپدیت کردن prop تو کامپوننت فرزند آشنا شدیم، ولی اگر بخوایم از این مقدار تو کامپوننت های مختلف استفاده کنیم چه کاری باید انجام بدیم؟ تو این جور مواقع، نیاز هست که prop رو تو کامپوننت والد نیز تغییر بدیم تا این تغییر به درستی در همه ی کامپوننت های دیگه که از این prop استفاده می کنن اعمال بشه. برای انجام این کار، ویو custom events رو معرفی می کنه.
اصول کار این روش این هست که ما هنگام تغییر prop باید کامپوننت والد رو از این موضوع با خبر کنیم، parent این تغییر رو اعمال میکنه و در نتیجه prop ای که از والد به کامپوننت های دیگه هم پاس داده شده تغییر می کنه. مراحل این روش رو باهم می بینیم:
برای درک بهتر مثال زیر رو ببینید:
ما از متد $emit که built-in هم هست، برای emit کردن یک event استفاده می کنیم. این متد 2 ورودی میگیره، یکی event ای که قراره emit بشه و اون یکی م مقدار جدیدی هست که برای prop در نظر گرفتیم.
ویو یک کلمه ی کلیدی (modifier) با نام .sync هم داره که کارکرد مشابه داره و ممکنه ما در مواردی بخوایم از این روش استفاده کنیم. در چنین مواردی، از emit$ به روشی متفاوت استفاده می کنیم. ما update:score رو به عنوان ورودی event، اینطوری اضافه می کنیم this.$emit("update:score", 200). زمانی که prop رو به کامپوننت بایند می کنیم، .sync رو هم این شکلی مینویسیم: <child-a :score.sync="score">. تو کامپوننت Parent A ما ()updateScore و "updatingScore="updateScore@ رو حذف می کنیم و دیگه نیازی بهشون نداریم. :)
ویو دو API method با نام های this.$children و this.$parent برای دسترسی مستقیم به کامپوننت های والد و فرزند داره که در نگاه اول استفاده ازشون وسوسه کننده ست ولی نباید ازشون استفاده کنیم. این روش ها اصطلاحا bad practice و anti pattern هستن به این دلیل که وابستگی شدیدی (tight coupling) بین کامپوننت های والد و فرزند ایجاد می کنن. دوم اینکه منجر به ایجاد کامپوننت هایی با ساختار های غیر منعطف و شکننده می شن که دیباگ کردن و درک اونها رو برای ما سخت می کنه. این متد ها به ندرت مورد استفاده قرار می گیرند و به عنوان یک قانون نانوشته باید بدونیم که استفاده ازشون با خطرات جانی و مالی زیادی همراهه...!!
تا اینجا مشخص شد که prop ها و event ها یک طرفه هستن. prop ها از بالا به پایین و event ها از پایین به بالا. اما با استفاده از prop و event کنار هم می توایم به طور قابل توجهی داخل درخت کامپوننت ها ارتباط برقرار کنیم و در نتیجه به two way data binding برسیم. این در وقع همون کاری هست که دایرکتیو v-model در عمل انجام می ده.
با رشد پروژه، الگوی ارتباط والد-فرزندی به تنهایی کارساز نیست. مشکلی که در سیستم prop-event وجود داره اینه که به ارتباط مستقیم بین کامپوننت ها محدوده. event هایی که در Vue.jsوجود هستن، بر خلاف event های خود جاوا اسکریپت، قابلیت bubbling ندارن و به همین دلیل هست که تا رسیدن به نتیجه نهایی (تغییر همه ی prop ها )، باید emit کردن رو ادامه بدیم. در نتیجه، پروژه ی ما پر از event listener ها و emit ها خواهد شد. بنابراین تو پروژه های بزرگتر بایستی از الگوی ارتباط بین کامپوننت های بی نسبت استفاده کنیم.
تصویر رو ببینید:
همونطور که تو تصویر هم مشخصه، این نوع ارتباط می تونه بین هر کامپوننت با هر کامپوننت دیگه ای برقرار بشه، هر کامپوننتی می تونه اطلاعات رو به/از هر کامپوننت دیگه ارسال/دریافت کنه بدون اینکه نیاز به استفاده از کامپوننت های واسط یا مراحل پیش نیاز خاصی داشته باشه.
در ادامه، چند تا روش باحال و رایج برای این نوع ارتباط بین کامپوننت ها رو بررسی می کنیم.
هر event bus یک Vue instance هست که ما ازش برای emit کردن و event listening استفاده می کنیم. در ادامه مثالی رو با هم می بینیم:
برای استفاده از این روش سه مرحله زیر رو باید انجام بدیم:
تو تصویر بالا، ما "updatingScore="updateScore@ رو از کامپوننت فرزند حذف کردیم و به جاش آن از لایف سایکل ()created استفاده کردیم تا از فراخوانی updatingScore با خبر بشیم. زمانی که event فراخوانی می شه، متد updateScore اجرا خواهد شد. می توانیم متد آپدیت را به عنوان یک anonymous function هم پاس بدیم.
این روش مشکل تعدد event ها تو کد ما رو تا حد خوبی برطرف می کنه ولی مشکلات دیگه ای هم با خودش داره. دیتای اپلیکیشن بدون هیچ رد و نشونی ممکنه از هر جای app تغییر کنه. این موضوع، دیباگ و تست اپلیکیشن رو سخت تر می کنه.
برای پروژه های پیچیده تر، که ممکنه خیلی چیز ها از کنترل خارج بشه، ما باید از یک الگوی state management مشخص مثل Vuex استفاده کنیم که امکان کنترل بیشتر، سازماندهی و ساختار کد بهتر، ردیابی تغییرات و دیباگ راحت تری را برای ما به همراه داره.
این ابزار یک کتابخونه ی state management برای پروژه های با مقیاس بزرگ تو Vue.js هست. استفاده از این ابزار باعث خواناتر شدن کد ما میشه که در طولانی مدت نتیجه ی خوبی خواهد داشت. این state management از یک store متمرکز برای تمامی کامپوننت های اپلیکیشن استفاده می کنه و موجب سازماندهی، شفافیت، ردیابی و دیباگ راحت اپلیکیشن می شه. این store کاملا reactive هست، به این صورت که هر تغییری به صورت آنی در پروژه اعمال می شه.
تو این مقاله خیلی به این مبحث نمی پردازیم ولی با ذکر مثال یه تصویر کلی از Vuex رو در اختیار شما قرار می دیم ، برای درک عمیق تر می تونید این لینک رو هم ببینید.
تصویر رو ببینید:
همونطور که می بینید، Vuex از چهار بخش مجزا تشکیل شده:
در مثال زیر یک store ساده با هم می سازیم و نحوه ی پیاده سازی روش های بالا رو در عمل می بینیم:
در store ما موارد زیر رو داریم:
داخل یک Vue Instance به جز props، ما از computed ها برای گرفتن مقدار score از getter استفاده می کنیم. بعد برای تغییر score، در کامپوننت Child A از store.commit("incrementStore", 100) استفاده می کنیم. در Parent B هم از store.dispatch("incrementStoreAsync", 3000) استفاده می کنیم.
قبل از جمع بندی، بریم یه روش دیگه رو باهم بررسی کنیم. موارد استفاده این روش عمدتا مربوط به پلاگین ها یا کتابخانه های کامپوننت مشترک در پروژه هست ولی برای تکمیل موضوع بهتره یه کم راجع بهش بدونیم :)
تو این روش ما می تونیم با استفاده از پراپرتی provide یک سرویس رو تعریف کنیم که باید از نوع object یا function ای باشه که یک object رو return میکنه و اون رو علاوه بر فرزندهای یک کامپوننت، برای همه مشتقات کامپوننت هم در دسترس قرار بدیم. بعد از این کار، می تونیم با استفاده از پراپرتی inject از این سرویس استفاده کنیم، به همین خوشمزگی :)
بریم با هم یه مثال دیگه ببینیم:
خب خب ..
با استفاده از آپشن provide در کامپوننت Grand Parent، متغیر score رو در تمامی وابسته های این کامپوننت در دسترس قرار دادیم. هر یک از این کامپوننت ها با استفاده از پراپرتی inject:['score'] به این متغیر دسترسی دارن. و همونطور که می بینید، score تو همه ی کامپوننت ها قابل نمایشه.
توجه توجه: binding ها تو این روش reactive نیستن، یعنی برای تغییر متغیری که با این روش بهش دسترسی داریم بایستی object رو برابر با یک data property قرار بدیم و از اون object داخل سرویس استفاده کنیم.
دلیلش مشابه همون دلیلی هست که برای this.$children و this.$parent قبلا توضیح دادیم -- باعث میشه وابستگی زیادی تو پروژه ایجاد بشه. استفاده از هر کدوم از این روش ها برای برقراری ارتباط بین کامپوننت ها (ممنوع که نه) خطرناکه!!
تا اینجای کار بعد از اینکه با همه ی این روش ها آشنا شدیم، حالا چطور باید تشخیص بدیم که کدوم روش بهتره؟
جواب این سوال بستگی به پروژه شما داره. به پیچیدگی و نوع پروژتون. ولی سناریوهای مشترکی هستن که می تونیم باهم ببینیم:
و به عنوان نکته ی آخر، اینکه کسی به شما بگه حتما باید از فلان ابزار استفاده کنید، دلیل نمیشه که حتما ازش استفاده کنید. برای انتخاب هر کدوم از این الگو ها کاملا مختارید، البته تا زمانی که مدیریت کدها و مقیاس پذیری و نگه داریشون رو به خوبی انجام بدین.
توی این مقاله ی نسبتا طولانی، با الگوهای برقراری ارتباط بین کامپوننت ها آشنا شدیم. تو چند تا مثال دیدیم که چطور میشه باهاشون کار کرد و روش درست تر کدومه. این موضوع به ما کمک می کنه که اطمینان پیدا کنیم app ما بهترین نوع ارتباط بین کامپوننت ها رو تو خودش داره که باعث کارکرد بی نقص، قابلیت نگهداری، تست راحت و مقیاس پذیری پروژه ی ما میشه.
امیدوارم که تونسته باشم مطلب مفیدی رو ارائه بدم :)