جلسه دهم: کامپوننت‌ها در Vue.js

Components in Vue.js

10 مرداد 1400
درسنامه درس 10 از سری آموزش مقدماتی Vuejs
10-vuejsmain

در طی ۹ جلسه‌ی گذشته آموزشهای مقدماتی و متوسط فریم‌ورک قدرتمند جاوا اسکریپت Vue.js در اختیار شما قرار گرفت. در این بخش با آخرین جلسه‌ی آموزشی Vue.js‌ در خدمت شما هستیم. البته ناگفته نماند که این آخرین نوشته ما درباره Vue.js‌ نیست بلکه جلسات متعدد دیگری در اختیار شما قرار می‌گیرد تا بتوانید پروژه‌های کاربردی‌تر و اصولی‌تر را با این فریم ورک زیبا و بی‌نظیر پیاده‌سازی کنید.

کامپوننت vue.js‌ها چی هستند؟

کامپوننت vue.js‌ها از بزرگترین توانایی‌های فریم‌ورک Vue.js هستند. آنها به شما کمک می‌کنند تا المان‌های HTML را داخل یک بسته‌بندی مناسب و قابل استفاده، توسعه دهید. در سطوح بالاتر، کامپوننت vue.js‌ها المانهای تولیدی خواهند بود که کامپایلر Vue رفتارهای این المان‌ها را به آن متصل می‌کند. در برخی موارد، می‌توان از کامپوننت vue.js‌ها به عنوان یک مجموعه‌ی HTML کاملا مجزا استفاده کرد که با استفاده از صفت is قابل توسعه است.

ثبت‌ با استفاده از کامپوننت vue.js‌ها

شما در جلسه‌های گذشته آموختید که می‌توان یک نمونه جدید از Vue را ایجاد کرد. بنابراین داریم:

new Vue({
	el: "#some-element",
	// options
})

برای ثبت یک کامپوننت گلوبال، باید از دستور Vue.component(tagName, options) استفاده کرد. به عنوان مثال:

Vue.component('roxo-component',{
	// options
})

توجه داشته باشید که Vue قوانین W3C را برای تگ‌های دلخواه کامپوننت‌ها اجرا نمی‌کند. بر اساس قانون اجرایی این فریم ورک معمولا نام‌های کامپوننت‌ها با حروف کوچک و فاصله‌ی بین کلمات با خط تیره (-) مشخص می‌شود.

پس از ثبت نام، کامپوننت می‌تواند به عنوان یک نمونه قالب با المان‌های دلخواه مورد استفاده قرار بگیرد. مثلا به صورت قالب <roxo-component></roxo-component>. درنظر داشته باشید که کامپوننت‌هاحتما باید قبل از مقداردهی نمونه اصلی Vue، تعریف و ثبت شوند. در اینجا یک مثال کامل برای روشن‌تر شدن مفهوم کامپوننت‌ها شرح می‌دهیم:

<div id="roxoApp">
	<roxo-component></roxo-component>
</div>

// ثبت و تعریف کامپوننت
Vue.component('roxo-component', {
	template: '<div> یک کامپوننت دلخواه تعریف کردم </div>'
})

// ساخت یک نمونه اصلی از Vue
new Vue({
	el: '#roxoApp'
})

در نهایت خروجی شما به صورت کد زیر خواهد بود:

<div id="roxoApp">
	<div> یک کامپوننت دلخواه تعریف کردم </div>
</div>

و چیزی که شما در صفحه خروجی خودتان می‌بینید:

یک کامپوننت دلخواه تعریف کردم

تعریف محلی کامپوننت

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

var Child = {
	template: '<div> یک کامپوننت دلخواه تعریف میکنم</div>'
}

new Vue({
	//...
	components: {
		'roxo-component' : Child
	}
})

همانطوری که مشاهده می‌کنید کامپوننت roxo-component تنها برای قالب‌های والدش در دسترس است.

محدودیت‌های پردازش قالب DOM

هنگامیکه از DOM برای قالب خود استفاده می‌کنید ( به عبارت دیگر از گزینه el برای فعال‌سازی المان‌ها و محتوای درون آنها استفاده می‌کنید)،  شما می‌توانید محدودیهایی که به صورت ذاتی در کارایی HTML وجود دارند را نشانه‌گذاری کنید. زیرا Vue تنها می‌تواند محتوای قالب را پس از پردازش و بهینه‌سازی توسط مرورگر، بازیابی کند. توجه کنید، برخی از المان‌ها مانند <ul>, <ol> یا <table> و <select> محدودیتهایی را برای محتوایی که درون آنها قرار می‌گیرند، دارند یا بعضی از المان‌ها مانند <option> حتما باید درون یک المان دیگری قرار بگیرند.

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

<table>
	<roxo-row> ... </roxo-row>
</table>

در این مثال کامپوننت دلخواه roxo-row درون یک المان قرار داده شده که محتوای غلط را تولید می‌کند. یک راه حل استفاده از صف ویژه is درون این المان‌هاست که به صورت زیر می‌باشد:

<table>
	<tr is="roxo-row"></tr>
</table>

data باید یک تابع باشد

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

Vue.component('roxo-component', {
	template: <span>{{ message }}</span>,
	data:{
		message: 'hello'
	}
})

قطعا کنسول به شما اخطار می‌دهد و برنامه‌ی شما متوقف می‌شود و پیامی که برای شما صادر خواهد شد: data برای نمونه‌های کامپوننت باید یک تابع باشد. چه بهتر است که بدانید دلیل این امر چیست؟ بنابراین اجازه دهید یک کد را اجرا کنیم:

<div id="roxoApp">
	<roxo-component></roxo-component>
	<roxo-component></roxo-component>
	<roxo-component></roxo-component>
</div>


var data = { counter: 0 }

Vue.component('roxo-component', {
	template: '<button v-on:click="counter +=1">{{ counter }}</button>',
	
	data: function(){
		return data
	}
})

new Vue({
	el: '#roxoApp'
})

خروجی کد بالا را اینجا مشاهده کنید.

همانگونه که مشاهده می‌کنید هر سه نمونه‌ی کامپوننت یک مقدار مشابه برای شیء data درنظر گرفته اند که به اشتراک گذاشته است. با افزایش هر بلوک تمام بلوک ها افزایش پیدا می‌کنند. برای حل این مشکل باید هر بار یک داده‌ی جدید را برگرداند. به کد زیر دقت کنید:

<div id="roxoApp">
	<roxo-component></roxo-component>
	<roxo-component></roxo-component>
	<roxo-component></roxo-component>
</div>


var data = { counter: 0 }

Vue.component('roxo-component', {
	template: '<button v-on:click="counter +=1">{{ counter }}</button>',
	
	data: function(){
		return {
			counter: 0
		}
	}
})

new Vue({
	el: '#roxoApp'
})

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

نوشتن کامپوننت‌های مرتبط

کامپوننت‌ها اغلب موارد باهمدیگر مورد استفاده قرار می‌گیرند که معمول‌ترین آنها رابطه والد-فرزند است. مثلا کامپوننت A ممکن است از کامپوننت B در المان template خود استفاده کند. بنابراین هر دو آنها نیاز دارند که به ناچار با هم در ارتباط باشند. کامپوننت والد ممکن است نیاز داشته باشد تا data را به طبقه پایین‌تر (فرزند) ارسال کند و همچنین کامپوننت فرزند ممکن است نیاز داشته باشد تا تغییرات و اتفاق‌های رخ داده درون خودش را به طبقه بالاتر (والد) اطلاع دهد.

در فریم‌ورک Vue.js، کامپوننت والد-فرزند (parent-child component) به صورت props down و events up خلاصه‌نویسی می‌شوند. کامپوننت والد data را از طریق props به کامپوننت فرزند ارسال می‌کند و کامپوننت فرزند پیام‌ها را از طریق events به کامپوننت والد انتقال می‌دهد. توجه داشته باشید در اینجا منظور از کامپوننت والد یک کامپوننت مجزا نیست بلکه یک دستور از بالا صادر شده و به پایین ارسال می‌شود و در نهایت از والد (بالا) به فرزند (پایین) داده‌ها تحت عنوان props ارسال شده و نتیجه از فرزند (پایین) به والد (بالا) تحت عنوان events ارسال می‌شود. برای فهم بیشتر این موضوع به تصویر ذیل دقت کنید:

کامپوننت parent-child در Vue.js

ارسال Data توسط Props

هر نمونه‌ی (Instance) کامپوننت یک محدوده و دامنه‌ی محدود به خود را دارد. این بدین معنی است که شما نمی‌توانید (و نباید) به صورت مستقیم به داده‌های والد در یک template کامپوننت ارجاع بدهید. داده می‌تواند تنها با استفاده از props از کامپوننت والد به کامپوننت فرزند انتقال پیدا کند.

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

Vue.component('child',{
	
	//تعریف props برای دریافت اطلاعات
	props=['message'],
	
	// دقیقا مشابه data
	// props تنها داخل تمپلت ها استفاده میشوند
	// و همواره درون vm تحت عنوان this.message در دسترس هستند
	
	template: '<span>{{ message }}</span>'
})

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

<child message="hello!"></child>

بنابراین خروجی کد بالا به صورت ذیل خواهد بود:

hello!

ساختار camelCase یا kebab-case

صفات HTML به بزرگ و کوچک بودن حروف حساس هستند، بنابراین هنگامیکه از ساختار غیررشته‌ای templateها استفاده می‌کنید، ساختار نوشتاری camelcase برای اسامی propها باید به صورت kebab-case از کامپوننت‌ها ارسال شوند. درست شبیه به مثال ذیل:

<child my-message="hello!"></child>

Vue.component('child',{
	
	//camelCase in JavaScript
	props=['myMessage'],
	template: '<span>{{ myMessage }}</span>'
})

Propهای پویا

مشابه مبحث اتصال و ارسال داده‌ها (Binding)، می‌توان از v-bind برای ارسال داده‌های props به صورت کاملا پویا و دینامیکی به کامپوننت والد استفاده کرد. منطق به این صورت است که هرگاه داده‌ای در کامپوننت والد آپدیت و بروزرسانی شود، آن داده در کامپوننت فرزند نیز بروزرسانی خواهد شد:

<div>
	<input v-model="parentMessage">
	<br>
	<child v-bind:my-message="parentMessage"></child>
</div>

دستور ساده‌تر برای نمایش این روش به صورت زیر می‌باشد:

<child :my-message="parentMessage"></child>

Literal (ثابتی‌ست که اسم آن همان مقدار آن باشد) یا Dynamic

اشتباه رایجی که در افراد مبتدی وجود دارد، تمایل به ارسال اطلاعات با سختار لیترال است، مانند مثال زیر:

// ارسال یک رشته خالص با مقدار "1" به پایین
<comp some-prop="1"><comp>

بنابراین دستور بالا یک prop ثابت است اما مقداری که به کامپوننت فرزند ارسال می‌شود عدد واقعی 1 نمی‌باشد بلکه مقدار رشته‌ی "1" است. درصورتیکه نیاز داشتید یک عدد جاوا اسکریپتی واقعی را به کامپوننت فرزند ارسال کنید، بهتر است از دستور v-bind استفاده نمایید. زیرا این دستور یک عبارت جاوا اسکریپتی را ارسال می‌کند. بنابراین کد زیر یک روش صحیح برای ارسال اعداد به طبقه پایین‌تر (فرزند) است:

// ارسال یک عدد واقعی به پایین تر
<comp v-bind:some-prop="1"></comp>

تنها یک راه برای انتقال داده‌ها

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

علاوه بر این، هر وقت کامپوننت والد آپدیت شود، همه‌ی propهای کامپوننت‌های فرزند نیز متناسب با آخرین وضعیت مقادیر و داده‌های خود، بروزرسانی و نوسازی (refresh) می‌شوند. این بدین معنی‌ست که شما نباید props‌های موجود در کامپوننت فرزند را تغییر بدهید. اگر شما اینکار را انجام بدهید، کنسول Vue به شما اخطار خواهد داد.

هم اکنون دو وضعیت وجود دارد که شمارا برای تغییر prop وسوسه می‌کند:

۱) یک Prop تنها برای ارسال یک مقدار اولیه استفاده می‌شود، کامپوننت فرزند به سادگی می‌خواهد بعدا از این Prop برای ویژگی‌های داده‌ها به صورت محلی استفاده کند.

۲) یک Prop به عنوان یک مقدار خام ارسال شود.

در این حالت پاسخ به دو وضعیت بالا به صورت زیر خواهد بود:

۱) تعریف یک ویژگی داده محلی که مقدار اولیه prop در آن ذخیره می‌شود:

props: ['initialCounter'],
data: function(){
	return { counter: this.initialCounter }
}

۲) تغریف یک ویژگی Computed که وظیفه محاسبات مقادیر دریافتی از prop را به عهده داشته باشد:

props: ['size'],
computed:{
	nomalizedSize: function(){
		return this.size.trim().toLowerCase()
	}
}

تصدیق Prop

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

به جای تعریف Propها به عنوان رشته‌ای از آرایه‌ها، می‌توان از یک شیء با بررسی تصدیق آن استفاده کرد:

Vue.component('example', {
	props: {
		// مقدار برابر عدد
		propA: Number,
		
		// تعریف چندین نوع برای پراپ ها
		propB: [String, Number],
		
		//اجباری کردن یک رشته
		propC: {
			type: String,
			required: true
		},
		
		// قراردادن مقدار پیش فرض برای یک عدد
		propD:{
			type: Number,
			default: 100
		},
		
		//شیء و آرایه به صورت پیش فرض توسط یک تابع بازگردانده میشود
		propE{
			type: Object,
			default: function(){
				return { message: 'hello' }
			}
		},
		
		// اجرای یک تصدیق کننده خودکار
		propF{
			validator: function(value){
				return value > 10
			}
		}
	}
})

type هایی که می‌توان داخل یک prop استفاده کرد به شرح زیر می‌باشد:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array

تا به این جای کار باید آموخته باشید که اطلاعات توسط والد به پایین‌تر (فرزند) ارسال می‌شود که این ارسال با استفاده از Propها صورت می‌پذیرد، اما اگر اتفاقی در فرزند (پایین) به وقوع بپیوندد ما چگونه با والد در ارتباط باشیم؟ پاسخ به این سوال استفاده از سیستم رویداد دلخواه (Custom Event System)، می‌باشد.

استفاده v-on به همراه رویدادهای دلخواه

هر نمونه Vue تحت عنوان یک رابط رویداد اجرا و پیاده‌سازی می‌شود، بدین معنی:

  • ارسال هر رویداد با استفاده از $on (eventName)
  • رها سازی هر رویداد با استفاده از $emit (eventName)

علاوه‌بر این، کامپوننت والد می‌تواند ارسال اطلاعات متناسب با رویدادهایی را انچام بدهد که توسط کامپوننت فرزند حذف شده‌اند. این کار با استفاده از دستور v-on در template های فرزند، امکان پذیر است. به مثال زیر توجه کنید:

<div id="counter-event-example">
	<p>{{ total }} </p>
	<button-counter v-on:increment="incrementTotal"></button-counter>
	<button-counter v-on:increment="incrementTotal"></button-counter>
</div>



Vue.component('button-counter', {
	template: '<button v-on:click="increment">{{ counter }}</button>',
	data: function(){
		return{
			counter: 0
		}
	},
	methods: {
		increment: function(){
			this.counter += 1
			this.$emit('increment')
		}
	}
})

new Vue({
	el: '#counter-event-example',
	data: {
		total: 0
	},
	methods: {
		incrementTotal: function(){
			this.total +=1
		}
	}
})

خروجی کد بالا را اینجا مشاهده کنید.

جامع ترین دوره آموزشی Vue.js 2.0 در ایران را می توانید از طریق اینجا مشاهده کنید.

آموزش ویدیویی Vuejs فارسی

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش مقدماتی Vuejs توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما (4 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مینا
05 اردیبهشت 1399
سلام من دوبار ثبت نام کردم تو سایتتون ولیکن اجازه ورود نمیده متاسفانه

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

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

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

abbas
23 اردیبهشت 1397
سلام ممنون بابت اموزش تیتر زدید ارسال Data توسط Props من ندوستم کدوم میشه والد کدوم فرزند ؟ تگه child میشه والد؟ تگ span هم میشه چیلد؟

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

روکسو
23 اردیبهشت 1397
Vue.component('child',{ //تعریف props برای دریافت اطلاعات props=['message'], // دقیقا مشابه data // props تنها داخل تمپلت ها استفاده میشوند // و همواره درون vm تحت عنوان this.message در دسترس هستند template: '{{ message }}' }) این دقیقا کدی هست که توی کامپوننت والد نوشته میشه در واقع شما دارید با این کد یک پروپرتی از نوع message رو به کامپوننت child یا فرزند ارسال می کنید.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مرتضی
11 دی 1395
سلام واقعا عالی... فقط یه سوال: نمیشه template ها رو از یک فایل دیگه بخونه بجای اینکه بصورت اینلاین بنویسیم خودمون..

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

روکسو
28 بهمن 1395
با سلام شما می توانید فایل قالب موردنظر خود را ایجاد کرده و با مسیر دهی آن را درون کامپوننت خود فراخوانی کنید.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.