مبانی کامپوننتها
کامپوننتها به ما اجازه میدهند تا رابط کاربری را به قطعات مستقل و قابل استفاده مجدد تقسیم کنیم و در مورد هر قطعه به طور مجزا فکر کنیم. معمول است که یک برنامه به صورت درختی از کامپوننتهای تودرتو سازماندهی شود:
این بسیار شبیه به چگونگی تودرتو بودن عناصر HTML است، اما Vue به ما اجازه میدهد محتوا و منطق سفارشی را در هر کامپوننت بطور جدا محصور کنیم. Vue همچنین به خوبی با کامپوننتهای Web اصلی کار میکند. اگر در مورد رابطه بین کامپوننتهای Vue و کامپوننتهای Web کنجکاو هستید، اینجا بیشتر بخوانید.
تعریف کامپوننت
هنگامی که برنامه دارای مرحلهای برای build باشد، معمولاً هر کامپوننت Vue را در یک فایل اختصاصی با پسوند .vue
تعریف میکنیم - که به آن کامپوننت تک فایلی (Single-File Component (SFC)) میگویند:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
وقتی از مرحله build استفاده نمیکنیم، یک کامپوننت Vue میتواند به عنوان یک آبجکت جاوااسکریپت ساده که حاوی مقادیر معنا داری برای Vue است، تعریف شود:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// استفاده کرد in-DOM template همچنین میتوان از
// template: '#my-template-element'
}
تمپلیت یک رشته جاوااسکریپتی است که Vue آن را کامپایل میکند. همچنین میتوانید از یک سلکتور ID استفاده کنید که به عنصری اشاره میکند - Vue محتوای آن را به عنوان منبع تمپلیت استفاده خواهد کرد.
مثال بالا یک کامپوننت را تعریف میکند و آن را به عنوان export پیشفرض یک فایل export .js
میکند، اما میتوانید از export نامدار برای export کردن چند کامپوننت از یک فایل استفاده کنید.
استفاده از کامپوننت
نکته
ما برای بقیه این راهنما از سینتکس SFC استفاده خواهیم کرد - مفاهیم مربوط به کامپوننتها بدون توجه به اینکه آیا از مرحله build استفاده میکنید یا خیر، یکسان است. بخش Examples نحوه استفاده از کامپوننت را در هر دو سناریو نشان میدهد.
برای استفاده از یک کامپوننت فرزند، نیاز داریم آن را در کامپوننت والد import کنیم. فرض کنید کامپوننت شمارنده را داخل فایلی به نام ButtonCounter.vue
قرار دادهایم، این کامپوننت به عنوان export پیشفرض فایل در دسترس خواهد بود (نحوه استفاده در کامپوننت والد در پایین آمده):
vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
با استفاده از <script setup>
، کامپوننتهای import شده به طور خودکار در تمپلیت در دسترس قرار میگیرند.
همچنین میتوان یک کامپوننت را به طور سراسری ثبت کرد که آن را بدون نیاز به import کردن، در دسترس تمام کامپوننتهای یک برنامه داشته باشیم.
میتوانیم کامپوننتها را تا جایی که لازم است دوباره استفاده کنیم:
template
<h1>Here are many child components!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
توجه کنید که با کلیک روی دکمهها، هر کدام مقدار جداگانهای از count
را نگه میدارند. چون هر بار که از یک کامپوننت استفاده میکنید، نمونه جدیدی از آن ساخته میشود.
در SFCها، توصیه میشود برای نام تگ کامپوننتهای فرزند از فرمت PascalCase
استفاده شود تا از المانهای HTML متمایز شوند. اگرچه نامهای تگهای HTML بصورت case-insensitive هستند، اما SFC یک فرمت کامپایل شده است و میتوانیم از نامهای case-sensitive در آن استفاده کنیم. همچنین میتوانیم از />
برای بستن تگ استفاده کنیم.
اگر تمپلیتها را مستقیماً در DOM مینویسید (مثلاً به عنوان محتوای المان <template>
)، تمپلیت مطابق با رفتار پارسر HTML مرورگر عمل خواهد کرد. در چنین مواردی، نیاز دارید از نامهای kebab-case
و تگهای بستهشده صریح برای کامپوننتها استفاده کنید:
template
<!-- نوشته شده باشد DOM اگر این تمپلیت در -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
برای جزئیات بیشتر محدودیتهای تجزیه تمپلیت در DOM را مشاهده کنید.
پاس دادنِ props
اگر قرار است بلاگی بسازیم، احتمالاً نیاز به کامپوننتی داریم که نماینده پست بلاگ باشد. میخواهیم تمام پستهای بلاگ از طرح بصری یکسانی استفاده کنند، اما با محتوای متفاوت. چنین کامپوننتی بدون توانایی پاس دادن دادهها به آن، مانند عنوان و محتوای پست خاصی که میخواهیم نمایش دهیم، مفید نخواهد بود. در اینجاست که props (مانند یک قهرمان) وارد میشود.
props صفات سفارشی هستند که میتوانید روی یک کامپوننت ثبت کنید. برای پاس دادن عنوان به کامپوننت پست بلاگ، باید آن را در لیست props هایی که این کامپوننت قبول میکند، اعلام کنیم، با استفاده از ماکرو defineProps
:
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
defineProps
یک ماکرو زمان کامپایل است که تنها در <script setup>
در دسترس است و نیازی به import کردن صریح آن نیست. propsهای اعلام شده به طور خودکار در تمپلیت قابل دسترسی هستند. defineProps
همچنین یک آبجکت برمیگرداند که تمام propsهای پاس داده شده به کامپوننت را شامل میشود، به طوری که اگر لازم بود در کد جاوااسکریپت بتوانیم به آنها دسترسی داشته باشیم:
js
const props = defineProps(['title'])
console.log(props.title)
همچنین ببینید: Typing Component Props
اگر از <script setup>
استفاده نمیکنید، باید props
ها را با استفاده از گزینه props اعلام کنید، و آبجکت props به عنوان اولین آرگومان به setup()
پاس داده خواهد شد:
js
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
یک کامپوننت میتواند تعداد دلخواهی props داشته باشد، و به طور پیشفرض هر مقداری میتواند به هر prop پاس داده شود.
پس از ثبت یک prop، میتوانید داده را به عنوان یک صفت سفارشی شده به آن پاس دهید، مانند:
template
<BlogPost title="سفر من با ویو" />
<BlogPost title="بلاگنویسی با ویو" />
<BlogPost title="چرا ویو خیلی جالب است" />
اما در یک برنامه معمولی، احتمالا آرایهای از پستها در کامپوننت والد خواهید داشت:
js
const posts = ref([
{ id: 1, title: 'سفر من با ویو' },
{ id: 2, title: 'بلاگنویسی با ویو' },
{ id: 3, title: 'چرا ویو خیلی جالب است' }
])
سپس نیاز داریم برای هر کدام یک کامپوننت render کنیم، با استفاده از v-for
:
template
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
توجه کنید که چطور از سینتکس v-bind
در :title="post.title"
برای پاس دادن مقادیر پویای prop استفاده شده است. این ویژگی به خصوص زمانی مفید است که از قبل نمیدانید دقیقاً چه محتوایی قرار است render شود.
تنها چیزی که در حال حاضر درباره props نیاز دارید همین است، اما بعد از خواندن این صفحه و فهم محتوای آن، توصیه میکنیم بعداً برگردید و راهنمای کامل props را بخوانید.
گوش دادن به رویدادها
هنگام توسعه کامپوننت <BlogPost>
خود، ممکن است برخی ویژگیها نیازمند ارتباط بازگشتی با والد باشند. به عنوان مثال، ممکن است تصمیم بگیریم ویژگی برای بزرگ کردن متن پستهای بلاگ اضافه کنیم، در حالی که بقیه صفحه با اندازه پیشفرض باقی میماند.
در والد، میتوانیم از این ویژگی با اضافه کردن یک پراپرتی ref به نام postFontSize
پشتیبانی کنیم:
js
const posts = ref([
/* ... */
])
const postFontSize = ref(1)
که میتواند در تمپلیت برای کنترل اندازه فونت تمام پستهای بلاگ استفاده شود:
template
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>
حالا یک دکمه به تمپلیت کامپوننت <BlogPost>
اضافه میکنیم:
vue
<!-- BlogPost.vue, omitting <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button> بزرگ کردن متن </button>
</div>
</template>
دکمه هنوز کاری انجام نمیدهد - میخواهیم با کلیک روی دکمه به والد اطلاع دهیم که باید متن تمام پستها را بزرگ کند. برای حل این مسئله، کامپوننتها از یک سیستم رویدادهای سفارشی استفاده میکنند. والد میتواند انتخاب کند که به هر رویدادی روی نمونه کامپوننت فرزند با v-on
یا @
گوش دهد، دقیقاً مثل یک رویداد DOM:
template
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>
سپس کامپوننت فرزند میتواند با فراخوانی متد درونی $emit
و پاس دادن نام رویداد، روی خودش یک رویداد emit کند:
vue
<!-- BlogPost.vue, omitting <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')"> بزرگ کردن متن </button>
</div>
</template>
به خاطر listener @enlarge-text="postFontSize += 0.1"
, والد رویداد را دریافت خواهد کرد و مقدار postFontSize
را بهروزرسانی میکند.
میتوانیم رویدادهای emit شده را به طور اختیاری با استفاده از ماکرو defineEmits
اعلام کنیم:
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
این، همه رویدادهایی را که یک کامپوننت emit میکند مستندسازی میکند و اختیاراً آنها را اعتبارسنجی میکند. همچنین به Vue اجازه میدهد از اعمال ضمنی آنها به عنوان listener های ساختگی روی المان ریشه کامپوننت فرزند خودداری کند.
مشابه defineEmits
، defineProps
تنها در <script setup>
قابل استفاده است و نیازی به import کردن ندارد. این یک تابع emit
برمیگرداند که معادل متد $emit
است. میتوان از آن برای emit کردن رویدادها در بخش <script setup>
یک کامپوننت استفاده کرد، جایی که $emit
به طور مستقیم قابل دسترسی نیست:
vue
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>
همچنین ببینید: Typing Component Emits
اگر از <script setup>
استفاده نمیکنید، میتوانید رویدادهای emit شده را با استفاده از گزینه emits
اعلام کنید. میتوانید به تابع emit
به عنوان یک پراپرتی از context setup دسترسی داشته باشید (به عنوان آرگومان دوم به setup()
پاس داده میشود):
js
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
این همه چیزی است که در حال حاضر در مورد رویدادهای سفارشی کامپوننت نیاز دارید، اما بعد از خواندن این صفحه و فهم محتوای آن، توصیه میکنیم برگردید و راهنمای کامل Custom Events را بخوانید.
انتقال محتوا با slots
همانند المانهای HTML، اغلب مفید است بتوان محتوایی را به یک کامپوننت پاس داد، مانند:
template
<AlertBox>
.اتفاق بدی افتاده است
</AlertBox>
که ممکن است چیزی شبیه این را render کند:
این یک خطا برای اهداف نمایشی است
اتفاق بدی افتاده است.
این کار با استفاده از المان سفارشی <slot>
در Vue امکانپذیر است:
vue
<!-- AlertBox.vue -->
<template>
<div class="alert-box">
<strong>این یک خطا برای اهداف نمایشی است</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>
همانطور که بالا میبینید، از <slot>
به عنوان placeholder جایی که میخواهیم محتوا قرار بگیرد استفاده میکنیم - و همین! کارمان تمام شد!
این همهی چیزی است که در حال حاضر درباره slots نیاز دارید، امّا بعد از خواندن این صفحه و فهم محتوای آن، توصیه میکنیم بعداً برگردید و راهنمای کامل slots را بخوانید.
کامپوننتهای پویا
گاهی اوقات مفید است که به صورت پویا بین کامپوننتها سوئیچ کنیم، مثلا در یک رابط کاربری تَب بندی شده:
مورد بالا با استفاده از المان <component>
و ویژگی is
در Vue امکانپذیر است:
template
<!-- کامپوننت تغییر میکند currentTab هنگام تغییر -->
<component :is="tabs[currentTab]"></component>
در مثال بالا، مقدار پاس داده شده به :is
میتواند شامل یکی از موارد زیر باشد:
- نام یک کامپوننت ثبت شده به صورت یک رشته
- خود شی کامپوننت import شده
همچنین میتوانید از ویژگی is
برای ساخت المانهای HTML معمولی استفاده کنید.
هنگام سوئیچ کردن بین چند کامپوننت با <component :is="...">
، هنگامی که کامپوننت از آن جدا شود، آن کامپوننت unmount میشود. میتوانیم با استفاده از کامپوننت <KeepAlive>
، کامپوننتهای غیرفعال را وادار کنیم که «زنده» بمانند.
محدودیتهای تجزیه تمپلیت در DOM
اگر تمپلیتهای Vue را مستقیماً در DOM مینویسید، Vue باید string تمپلیت را از DOM بازیابی کند. این موضوع به دلیل رفتار پارسر HTML مرورگرها، منجر به چند محدودیت میشود.
نکته
لازم به ذکر است محدودیتهایی که در زیر این بخش میگوییم، فقط زمانی اعمال میشوند که تمپلیتها را مستقیماً در DOM مینویسید. آنها در موارد زیر اعمال نمیشوند:
- کامپوننت تک فایلی - Single-File Component (SFC)
- رشته تمپلیت تکخطی (مثلاً
template: '...'
) <script type="text/x-template">
عدم حساسیت بین حروف بزرگ و کوچک | Case Insensitivity
تگها و ویژگیهای HTML غیر حساس به بزرگی و کوچکی حروف هستند، بنابراین مرورگرها هر حرف بزرگی را به صورت حرف کوچک تفسیر میکنند. این بدان معناست که وقتی از تمپلیتهای درون DOM استفاده میکنید، نامهای کامپوننتهای PascalCase و نامهای propهای camelCase یا رویدادهای v-on
باید از معادلهای kebab-case (با - جدا شده) خود استفاده کنید:
js
// در جاوااسکریپت camelCase
const BlogPost = {
props: ['postTitle'],
emits: ['updatePost'],
template: `
<h3>{{ postTitle }}</h3>
`
}
template
<!-- HTML در kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>
تگهای تکی | Self Closing Tags
در نمونه کدهای قبلی از تگهای تکی (self-closing tags بصورت یک تگ نوشته میشوند) برای کامپوننتها استفاده کردهایم:
template
<MyComponent />
این به این دلیل است که پارسر تمپلیت Vue، عبارت />
را به عنوان نشانهای برای پایان هر نوع تگ، صرفنظر از نوع آن، رعایت میکند.
اما در تمپلیتهای درون DOM، همیشه باید از تگهای بستهشده بصورت صریح استفاده کنیم:
template
<my-component></my-component>
زیرا HTML فقط اجازه میدهد چند المان خاص تگهای بستهشده خود را حذف کنند که معمولترین آنها <input>
و <img>
هستند. برای تمام المانهای دیگر، اگر تگ بستهشده را حذف کنید، پارسر HTML تصور میکند شما هرگز تگ بازشده را نبسته اید.
template
<my-component /> <!-- قصد داریم تگ را اینجا ببندیم -->
<span>hello</span>
به این صورت پارس میشود:
template
<my-component>
<span>hello</span>
</my-component> <!-- اما مرورگر آن را اینجا میبندد -->
محدودیتهای قرارگیری المان | Element Placement Restrictions
برخی المانهای HTML مانند <ul>
، <ol>
، <table>
و <select>
محدودیتهایی در مورد المانهایی که میتوانند درون آنها قرار بگیرند دارند، و برخی المانها مانند <li>
، <tr>
و <option>
فقط میتوانند درون المانهای خاص دیگری قرار بگیرند.
این موضوع منجر به مشکلاتی هنگام استفاده از کامپوننتها با المانهایی که چنین محدودیتهایی دارند، میشود. به عنوان مثال:
template
<table>
<blog-post-row></blog-post-row>
</table>
کامپوننت سفارشی <blog-post-row>
به عنوان محتوای نامعتبر خارج خواهد شد که منجر به خطا در خروجی render شده نهایی میشود. میتوانیم از ویژگی is
به عنوان راهحل استفاده کنیم:
template
<table>
<tr is="vue:blog-post-row"></tr>
</table>
نکته
هنگام استفاده روی المانهای HTML ساختگی، مقدار is
باید با پیشوند vue:
شروع شود تا به عنوان یک کامپوننت Vue تفسیر شود. این کار برای اجتناب از ابهام با عناصر ساختگی سفارشی HTML لازم است.
این همه چیزی است که در حال حاضر در مورد محدودیتهای پارس تمپلیت در DOM نیاز دارید - و در واقع پایان بخش مبانی Vue. تبریک میگوییم! هنوز مطالب بیشتری برای یادگیری وجود دارد، اما ابتدا توصیه میکنیم متوقف شوید و با Vue کدنویسی کنید - چیز جالبی بسازید یا برخی از مثالها را بررسی کنید، اگر هنوز آنها را ندیدهاید.
وقتی احساس راحتی با دانشی که دریافت کردهاید میکنید، با عمق بیشتری به یادگیری در مورد کامپوننتها ادامه دهید.