حسن پودینه
حسن پودینه
خواندن ۱۲ دقیقه·۳ سال پیش

الگوهای برقراری ارتباط بین کامپوننت ها در Vue.js

مروری بر نوع ارتباط کامپوننت ها در Vue.js

در Vue.js دو نوع ارتباط عمده بین کامپوننت ها وجود داره:

  1. ارتباط مستقیم والد-فرزندی، که می تونه از والد به فرزند یا از فرزند به والد باشه.
  2. ارتباط بین کامپوننت های بدون نسبت، که تو این نوع ارتباط هر کامپوننت بدون در نظر گرفتن نسبت می تونه با کامپوننت های دیگه ارتباط داشته باشه.

در ادامه با چند تا مثال این دو نوع ارتباط رو با هم بررسی می کنیم :)


ارتباط مستقیم والد-فرزندی

مدل استاندارد ارتباط میان کامپوننت ها مدل والد-فرزندی هست که با استفاده از props و custom events انجام میشه. تو تصویر زیری، نحوه ی پیاده سازی این مدل در عمل رو با هم می بینیم:

همونطور که می بینید، یک والد تنها می تونه با فرزند خودش ارتباط مستقیم برقرار کنه، فرزند هم تنها ارتباط مستقیم رو با والد خودش داره. تو این نوع ارتباط امکان ارتباط غیر همسان یا همزاد (sibling) وجود نداره.

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

ارتباط والد - فرزندی

فرض کنید کامپوننت هایی که داریم هر کدوم بخشی از یک بازی هستن. بیشتر بازی ها امتیاز مسابقه رو تو یه جایی از صفحه نمایش، نشون میدن. حالا یک متغیر با نام score در کامپوننت Parent A تعریف می کنیم و می خوایم اون رو تو کامپوننت Child A نمایش بدیم. خب، به نظرتون چیکار باید بکنیم؟ :)

آفرین درسته :D ، برای انتقال data از والد به فرزند، Vue.js از props استفاده می کنه. برای انتقال یک data property به کامپوننت فرزند سه تا کار لازمه که باهم انجامشون میدیم:

  1. رجیستر کردن props در فرزند، مثل این: props: ["score"]
  2. استفاده از این props در تمپلیت html
  3. بایند کردن متغیر score در کامپوننت والد، مثل این: </child-a :score="score">

مثل تصویر پایینی:

لینک Codepen
لینک Codepen

اعتبار سنجی props

برای خلاصه سازی من 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 است.

آپدیت کردن یک prop در فرزند

خب تا اینجا موفق شدیم امتیاز بازی رو نمایش بدیم. گاهی اوقات نیاز به آپدیت این امتیاز هم داریم. پس بیاید امتحان کنیم:

در این مثال، یک متد با نام 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 با استفاده از local data property

روش اول اینه که prop رو به یک data property مثل localScore تبدیل کنیم و ازش تو متد ()changeScore و در template استفاده کنیم:

حالا، اگه دکمه ی Rerender Parent رو دوباره بزنیم، بعد از تغییر score، می بینیم که این بار score همون مقدار تغییر یافته باقی میمونه (تغییرمون سر جاش هست و با رفرش به مقدار اولیه بر نمی گرده).

آپدیت prop با استفاده از یک computed property

روش دوم استفاده از score در یک computed property هست، یعنی در واقع با استفاده ازش یک مقدار جدید می سازیم:

تو این مثال، ما یک computed property با نام ()doubleScore ساختیم که score والد رو با 2 جمع می کنه و نتیجه رو داخل template نمایش می ده. مشخصه که با زدن دکمه ی Rerender Parent، هیچگونه side effect ای نخواهیم داشت.

ارتباط فرزند-والدی

حالا ببینیم کامپوننت ها چگونه برعکس نوع قبلی با هم ارتباط برقرار می کنن.

تا اینجا با نحوه ی آپدیت کردن prop تو کامپوننت فرزند آشنا شدیم، ولی اگر بخوایم از این مقدار تو کامپوننت های مختلف استفاده کنیم چه کاری باید انجام بدیم؟ تو این جور مواقع، نیاز هست که prop رو تو کامپوننت والد نیز تغییر بدیم تا این تغییر به درستی در همه ی کامپوننت های دیگه که از این prop استفاده می کنن اعمال بشه. برای انجام این کار، ویو custom events رو معرفی می کنه.

اصول کار این روش این هست که ما هنگام تغییر prop باید کامپوننت والد رو از این موضوع با خبر کنیم، parent این تغییر رو اعمال میکنه و در نتیجه prop ای که از والد به کامپوننت های دیگه هم پاس داده شده تغییر می کنه. مراحل این روش رو باهم می بینیم:

  1. در کامپوننت فرزند، ما یک event رو صطلاحا emit میکنیم که خبر از یک تغییر در prop می ده، برای مثال: this.$emit("updatingScore", 200).
  2. در کامپوننت والد، ما یک event listener برای event ای که emit می شه اضافه میکنم، مثل این:
    "updatingScore="updateScore@
  3. زمانی که این event، امیت (emit) بشه، متدی که در اون وجود داره مقدار prop را آپدیت می کنه، مثل این: this.score = newValue.

برای درک بهتر مثال زیر رو ببینید:

ما از متد $emit که built-in هم هست، برای emit کردن یک event استفاده می کنیم. این متد 2 ورودی میگیره، یکی event ای که قراره emit بشه و اون یکی م مقدار جدیدی هست که برای prop در نظر گرفتیم.

کلمه ی کلیدی (modifier) .sync

ویو یک کلمه ی کلیدی (modifier) با نام .sync هم داره که کارکرد مشابه داره و ممکنه ما در مواردی بخوایم از این روش استفاده کنیم. در چنین مواردی، از emit$ به روشی متفاوت استفاده می کنیم. ما update:score رو به عنوان ورودی event، اینطوری اضافه می کنیم this.$emit("update:score", 200). زمانی که prop رو به کامپوننت بایند می کنیم، .sync رو هم این شکلی مینویسیم: <child-a :score.sync="score">. تو کامپوننت Parent A ما ()updateScore و "updatingScore="updateScore@ رو حذف می کنیم و دیگه نیازی بهشون نداریم. :)

چرا نباید از this.$children و this.$parent برای ارتباط مستقیم والد-فرزندی استفاده کنیم؟

ویو دو 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 ها خواهد شد. بنابراین تو پروژه های بزرگتر بایستی از الگوی ارتباط بین کامپوننت های بی نسبت استفاده کنیم.

تصویر رو ببینید:

همونطور که تو تصویر هم مشخصه، این نوع ارتباط می تونه بین هر کامپوننت با هر کامپوننت دیگه ای برقرار بشه، هر کامپوننتی می تونه اطلاعات رو به/از هر کامپوننت دیگه ارسال/دریافت کنه بدون اینکه نیاز به استفاده از کامپوننت های واسط یا مراحل پیش نیاز خاصی داشته باشه.

در ادامه، چند تا روش باحال و رایج برای این نوع ارتباط بین کامپوننت ها رو بررسی می کنیم.

Global Event Bus

هر event bus یک Vue instance هست که ما ازش برای emit کردن و event listening استفاده می کنیم. در ادامه مثالی رو با هم می بینیم:

برای استفاده از این روش سه مرحله زیر رو باید انجام بدیم:

  1. تعریف کردن event bus به عنوان یک Vue Instance، به این شکل: ()const eventBus = new Vue.
  2. اِمیت (emit) کردن event از کامپوننت مبدا به این شکل: eventBus.$emit("updateScore", 200).
  3. اضافه کردن eventListener در کامپوننت مقصد به این شکل: eventBus.$on("updatingScore", this.updateScore)

تو تصویر بالا، ما "updatingScore="updateScore@ رو از کامپوننت فرزند حذف کردیم و به جاش آن از لایف سایکل ()created استفاده کردیم تا از فراخوانی updatingScore با خبر بشیم. زمانی که event فراخوانی می شه، متد updateScore اجرا خواهد شد. می توانیم متد آپدیت را به عنوان یک anonymous function هم پاس بدیم.

این روش مشکل تعدد event ها تو کد ما رو تا حد خوبی برطرف می کنه ولی مشکلات دیگه ای هم با خودش داره. دیتای اپلیکیشن بدون هیچ رد و نشونی ممکنه از هر جای app تغییر کنه. این موضوع، دیباگ و تست اپلیکیشن رو سخت تر می کنه.

برای پروژه های پیچیده تر، که ممکنه خیلی چیز ها از کنترل خارج بشه، ما باید از یک الگوی state management مشخص مثل Vuex استفاده کنیم که امکان کنترل بیشتر، سازماندهی و ساختار کد بهتر، ردیابی تغییرات و دیباگ راحت تری را برای ما به همراه داره.

Vuex

این ابزار یک کتابخونه ی state management برای پروژه های با مقیاس بزرگ تو Vue.js هست. استفاده از این ابزار باعث خواناتر شدن کد ما میشه که در طولانی مدت نتیجه ی خوبی خواهد داشت. این state management از یک store متمرکز برای تمامی کامپوننت های اپلیکیشن استفاده می کنه و موجب سازماندهی، شفافیت، ردیابی و دیباگ راحت اپلیکیشن می شه. این store کاملا reactive هست، به این صورت که هر تغییری به صورت آنی در پروژه اعمال می شه.

تو این مقاله خیلی به این مبحث نمی پردازیم ولی با ذکر مثال یه تصویر کلی از Vuex رو در اختیار شما قرار می دیم ، برای درک عمیق تر می تونید این لینک رو هم ببینید.

تصویر رو ببینید:

همونطور که می بینید، Vuex از چهار بخش مجزا تشکیل شده:

  1. استیت (State) : که دیتای اپلیکیشن رو داخلش نگهداری میکنیم.
  2. گِتِر (Getters): متدهایی برای دسترسی به state و اضافه کردن اونها به کامپوننت ها.
  3. میتویشن (Mutations): تنها روش برای تغییر state ها.
  4. اکشن (Actions): متدهایی برای اجرای کدهای async و فراخوانی mutation ها.

در مثال زیر یک store ساده با هم می سازیم و نحوه ی پیاده سازی روش های بالا رو در عمل می بینیم:


در store ما موارد زیر رو داریم:

  • متغیر score در آبجکت state.
  • میوتیشن (mutation) ()incrementStore که مقدار score رو با مقدار داده شده جابجا می کنه.
  • یک getter برای score که به score دسترسی داره و اون رو به کامپوننت ها منتقل می کنه.
  • اکشن (action) ()incrementStoreAsync که از ()incrementStore برای تغییر score (بعد از یک مدت زمان تعریف شده) استفاده می کنه.

داخل یک Vue Instance به جز props، ما از computed ها برای گرفتن مقدار score از getter استفاده می کنیم. بعد برای تغییر score، در کامپوننت Child A از store.commit("incrementStore", 100) استفاده می کنیم. در Parent B هم از store.dispatch("incrementStoreAsync", 3000) استفاده می کنیم.

Dependency Injection

قبل از جمع بندی، بریم یه روش دیگه رو باهم بررسی کنیم. موارد استفاده این روش عمدتا مربوط به پلاگین ها یا کتابخانه های کامپوننت مشترک در پروژه هست ولی برای تکمیل موضوع بهتره یه کم راجع بهش بدونیم :)

تو این روش ما می تونیم با استفاده از پراپرتی provide یک سرویس رو تعریف کنیم که باید از نوع object یا function ای باشه که یک object رو return میکنه و اون رو علاوه بر فرزندهای یک کامپوننت، برای همه مشتقات کامپوننت هم در دسترس قرار بدیم. بعد از این کار، می تونیم با استفاده از پراپرتی inject از این سرویس استفاده کنیم، به همین خوشمزگی :)

بریم با هم یه مثال دیگه ببینیم:

خب خب ..

با استفاده از آپشن provide در کامپوننت Grand Parent، متغیر score رو در تمامی وابسته های این کامپوننت در دسترس قرار دادیم. هر یک از این کامپوننت ها با استفاده از پراپرتی inject:['score'] به این متغیر دسترسی دارن. و همونطور که می بینید، score تو همه ی کامپوننت ها قابل نمایشه.

توجه توجه: binding ها تو این روش reactive نیستن، یعنی برای تغییر متغیری که با این روش بهش دسترسی داریم بایستی object رو برابر با یک data property قرار بدیم و از اون object داخل سرویس استفاده کنیم.

چرا نباید از this.$root برای کامپوننت های بدون نسبت استفاده کنیم؟

دلیلش مشابه همون دلیلی هست که برای this.$children و this.$parent قبلا توضیح دادیم -- باعث میشه وابستگی زیادی تو پروژه ایجاد بشه. استفاده از هر کدوم از این روش ها برای برقراری ارتباط بین کامپوننت ها (ممنوع که نه) خطرناکه!!

خب، حالا چطوری بهترین الگو رو برای پیاده سازی ارتباط بین کامپوننت ها انتخاب کنیم؟

تا اینجای کار بعد از اینکه با همه ی این روش ها آشنا شدیم، حالا چطور باید تشخیص بدیم که کدوم روش بهتره؟

جواب این سوال بستگی به پروژه شما داره. به پیچیدگی و نوع پروژتون. ولی سناریوهای مشترکی هستن که می تونیم باهم ببینیم:

  • در پروژه های ساده، استفاده از props و events به تنهایی همه کار شما رو راه میندازه.
  • در پروژه های با مقیاس متوسط، روش های منعطف تری مثل eventBus و dependency injection میتونن کمک بیشتری بکنن.
  • در پروژه های پیچیده با مقیاس بزرگ، پیشنهاد میشه از Vuex به عنوان یک سیستم مدیریت state کامل و پر قدرت استفاده کنید.

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

نتیجه گیری

توی این مقاله ی نسبتا طولانی، با الگوهای برقراری ارتباط بین کامپوننت ها آشنا شدیم. تو چند تا مثال دیدیم که چطور میشه باهاشون کار کرد و روش درست تر کدومه. این موضوع به ما کمک می کنه که اطمینان پیدا کنیم app ما بهترین نوع ارتباط بین کامپوننت ها رو تو خودش داره که باعث کارکرد بی نقص، قابلیت نگهداری، تست راحت و مقیاس پذیری پروژه ی ما میشه.

امیدوارم که تونسته باشم مطلب مفیدی رو ارائه بدم :)

ویوvuevue jscomponents
علاقه مند به برنامه نویسی
شاید از این پست‌ها خوشتان بیاید