Vue.js: الفتحات
آخر تحديث: 2026-08-26
تُعد «الفتحات» جزءًا من واجهة برمجة تطبيقات توزيع المحتوى في Vue — حيث يمكن للمكونات الأصلية إدراج أي محتوى في «الفتحات» الخاصة بالمكون الفرعي. وتجعل «الفتحات» المكونات مرنةً بقدر «القوالب»: فالمكونات الأصلية تتحكم في ما يتم عرضه، بينما تتحكم المكونات الفرعية في مكان عرضه.
يقدم Vue 3 ثلاثة أنواع من الفتحات: الفتحات الافتراضية، والفتحات المسماة، والفتحات ذات النطاق. سيساعدك هذا الدرس على إتقان استخدام هذه الأنواع الثلاثة وأفضل الممارسات المتعلقة بها.
1. ما ستتعلمه
- الاستخدام الأساسي للفتحة الافتراضية
<slot /> - الاختصارات الخاصة بالفتحات المسماة
<slot name="header" />وv-slot:header - فتحات النطاق: تقوم المكونات الفرعية بتمرير البيانات إلى فتحات المكون الرئيسي
<template v-slot>قواعد الصياغة (Vue 2.6+)- أسماء الخانات الديناميكية وقواعد كتابة الاختصارات
- محتوى الفتحة الافتراضي (المحتوى البديل)
$slots/useSlotsوصلة
2. التحدي المتمثل في عدم القدرة على تخصيص محتوى مكون البطاقة
(1) المشكلة: 5 أنماط للبطاقات، تتطلب كتابة 5 مكونات
احتاج مسؤول التجارة الإلكترونية لدى أليس إلى 5 أنواع من البطاقات:
<!-- ❌ The "Broken" Version:5 component -->
<!-- ProductCard.vue -->
<div class="card">{{ product.name }}</div>
<!-- UserCard.vue -->
<div class="card">{{ user.name }}</div>
<!-- OrderCard.vue -->
<div class="card">Order #{{ order.id }}</div>
<!-- 5 component,90% The code is the same,The only difference is the content -->
مدير المنتج تشارلي:
«أليس، أحتاج إلى 5 أنواع إضافية من البطاقات الأسبوع المقبل. لا يمكننا الاستمرار في إضافة مكونات جديدة. نحن بحاجة إلى بطاقة مرنة تقبل أي محتوى.»
(2) حل «الفتحات» في Vue: بطاقة أساسية واحدة (BaseCard)، مع محتوى مقدم من المكون الأصلي
<!-- BaseCard.vue - General-Purpose Card,The slot accepts any content -->
<template>
<div classe="card">
<div v-if="$slots.header" classe="card-header">
<slot name="header" />
</div>
<div classe="card-body">
<slot /> <!-- Default Slot -->
</div>
<div v-if="$slots.footer" classe="card-footer">
<slot name="footer" />
</div>
</div>
</template>
<!-- ProductCard Usage BaseCard -->
<BaseCard>
<template #header>
<h3>Product</h3>
</template>
<p>iPhone 15 Pro</p> <!-- Default Slot -->
<template #footer>
<button>Add to Cart</button>
</template>
</BaseCard>
1 BaseCard.vue → مجموعة لا حصر لها من البطاقات. في كل مرة تضيف فيها بطاقة جديدة، ما عليك سوى كتابة المحتوى — دون الحاجة إلى تعديل المكون.
(3) الإيرادات
بعد استخدام ماكينات القمار:
- عدد المكونات: 5 → 1 (-80%)
- نوع البطاقة الجديد: سطر واحد من كود «موديلو»
- تكاليف الصيانة: بمجرد تغيير نمط واحد، يتم تحديث أكثر من 5 بطاقات في آن واحد
- إمكانية إعادة الاستخدام: يمكن استخدام BaseCard في جميع الحالات المتعلقة بالبطاقات
3. الفتحة الافتراضية
(1) الاستخدام الأساسي
<!-- BaseCard.vue Child component -->
<template>
<div class="card">
<!-- Slot: The parent component can contain any content -->
<slot />
</div>
</template>
<!-- App.vue Parent Component -->
<template>
<BaseCard>
<h3>Hello</h3>
<p>This is the card content</p>
</BaseCard>
</template>
نتائج العرض:
<div class="card">
<h3>Hello</h3>
<p>This is the card content</p>
</div>
(2) المحتوى الافتراضي (الاحتياطي)
<!-- Child component -->
<template>
<div class="card">
<slot>
<!-- Default Content:Display when the parent component does not pass data -->
<p>No content provided</p>
</slot>
</div>
</template>
<!-- The parent component does not pass content -->
<BaseCard />
<!-- Rendering:<p>No content provided</p> -->
<!-- Passing Content from the Parent Component -->
<BaseCard>
<p>Custom content</p>
</BaseCard>
<!-- Rendering:<p>Custom content</p>(Override Default)-->
(3) 5 حالات استخدام رئيسية
| السيناريو | الاستخدام |
|---|---|
| محتوى البطاقة | <slot /> |
| نص الزر | <slot /> |
| عنصر القائمة | <slot :item="item" /> |
| حقل النموذج | <slot /> |
| نص مربع منبثق | <slot /> |
4. الفتحات المسماة
(1) تعريف الفتحات المسماة
<!-- BaseLayout.vue Child component -->
<template>
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- Default Slot(Optional name="default") -->
</main>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
(2) استخدام المكونات الأصلية (3 صيغ)
<!-- App.vue Parent Component -->
<!-- Writing Style 1:v-slot:name(Most Comprehensive) -->
<BaseLayout>
<template v-slot:header>
<h1>My App</h1>
</template>
<template v-slot:default>
<p>Main content</p>
</template>
<template v-slot:footer>
<p>Footer</p>
</template>
</BaseLayout>
<!-- Writing Style 2:Abbreviation #name(Recommendations) -->
<BaseLayout>
<template #header>
<h1>My App</h1>
</template>
<p>Main content</p> <!-- The default slot can be omitted template -->
<template #footer>
<p>Footer</p>
</template>
</BaseLayout>
<!-- Writing Style 3:v-slot Receive multiple slot objects(Vue 3 Recommendations) -->
<BaseLayout>
<template #header>
<h1>My App</h1>
</template>
<template #default="{ user }"> <!-- Deconstructing Scope Slots -->
<p>Hello, {{ user.name }}</p>
</template>
<template #footer>
<button>Logout</button>
</template>
</BaseLayout>
(3) أسماء الخانات الديناميكية
<!-- Child component -->
<template>
<div>
<slot :name="dynamicSlotName" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const dynamicSlotName = ref('header')
</script>
<!-- Parent Component:Dynamic Slot Binding -->
<BaseLayout>
<template #[dynamicSlotName]>
<p>Dynamic content</p>
</template>
</BaseLayout>
(4) التحقق من وجود فتحة
<!-- Child component:Check whether data is being received through the slot -->
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<slot />
</div>
</template>
<!-- Parent Component -->
<BaseCard>
<template #header>...</template> <!-- Sent header Slot, Display -->
</BaseCard>
<BaseCard>
<!-- No header slot passed, Do not display card-header -->
</BaseCard>
5. فتحات النطاق
(1) نقطة أساسية: تقوم المكونات الفرعية بتمرير البيانات إلى فتحات المكون الرئيسي
<!-- Child component:UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<!-- Pass user to the parent component's slot -->
<slot :user="user" :index="index" />
</li>
</ul>
</template>
<script setup>
defineProps({ users: Array })
</script>
<!-- Parent Component:App.vue -->
<template>
<UserList :users="users">
<!-- Receive data passed from child components -->
<template #default="{ user, index }">
<p>{{ index + 1 }}. {{ user.name }} ({{ user.email }})</p>
</template>
</UserList>
</template>
نتائج العرض:
<ul>
<li><p>1. Alice (alice@example.com)</p></li>
<li><p>2. Bob (bob@example.com)</p></li>
<li><p>3. Charlie (charlie@example.com)</p></li>
</ul>
(2) 5 أنماط لفتحات المنظار
| النمط | تنفيذ المكون الأصلي | الغرض |
|---|---|---|
| يقبل جميع العناصر | <template #default="slotProps"> |
يقبل الكائن بأكمله |
| التفكيك | <template #default="{ user }"> |
استخدم الحقول التي تحتاجها فقط |
| إعادة التسمية | <template #default="{ user: u }"> |
تجنب تعارض المتغيرات |
| القيمة الافتراضية | <template #default="{ user = defaultUser }"> |
إدخال قيمة افتراضية أثناء عملية التفكيك |
| لا يوجد قالب | {{ slotProps.user.name }} |
سيناريو بسيط (الفتحة الافتراضية) |
(3) مثال كامل: قائمة قابلة للتخصيص
<!-- UserList.vue Child component -->
<template>
<ul class="user-list">
<li v-for="(user, index) in users" :key="user.id">
<slot :user="user" :index="index" :isAdmin="user.role === 'admin'" />
</li>
</ul>
</template>
<script setup>
defineProps({ users: Array })
</script>
<!-- Using Parent Components(3 One way) -->
<template>
<!-- Method 1:A Brief Overview -->
<UserList :users="users">
<template #default="{ user }">
{{ user.name }}
</template>
</UserList>
<!-- Method 2:Table Display -->
<UserList :users="users">
<template #default="{ user, index, isAdmin }">
<tr>
<td>{{ index + 1 }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<span v-if="isAdmin" class="badge admin">Admin</span>
<span v-else class="badge user">User</span>
</td>
</tr>
</template>
</UserList>
<!-- Method 3:Card Display -->
<UserList :users="users">
<template #default="{ user }">
<UserCard :user="user" />
</template>
</UserList>
</template>
6. $slots و useSlots
(1) $slots الوصول إلى الفتحات
<!-- Child component -->
<template>
<div>
<!-- List all incoming slot names -->
<div v-for="(_, name) in $slots" :key="name">
<slot :name="name" />
</div>
</div>
</template>
(2) useSlots (واجهة برمجة التطبيقات للتكوين)
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
// Check if the slot exists
if (slots.header) {
console.log('Header slot exists')
}
// Dynamic Rendering
if (slots.default) {
console.log('Default slot exists')
}
</script>
(3) $slots مقابل useSlots
| البعد | $slots |
useSlots() |
|---|---|---|
| نوع واجهة برمجة التطبيقات (API) | تُستخدم مباشرةً في القوالب | تُستخدم في JavaScript (واجهة برمجة التطبيقات للتركيب) |
| العودة | كائن الفتحة | كائن الفتحة (تنسيق المراجع) |
| المشهد | داخل القالب | داخل <script setup> |
7. مثال كامل: مكون البطاقة القابل للتخصيص
▶ مثال: 1. BaseCard.vue (الافتراضي + الفتحات المسماة)
<!-- src/components/BaseCard.vue -->
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot>
<!-- Default Content:Display when the parent component does not pass data -->
<p>No content</p>
</slot>
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
<style scoped>
.card {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.card-header, .card-footer {
background: #f9fafb;
padding: 1rem;
}
.card-body {
padding: 1rem;
}
</style>
▶ مثال: 2. 5 طرق لاستخدام بطاقة BaseCard
<!-- 1. The simplest(Transmit Only body) -->
<BaseCard>
<p>Simple content</p>
</BaseCard>
<!-- 2. With header -->
<BaseCard>
<template #header>
<h3>Product Name</h3>
</template>
<p>Product description</p>
</BaseCard>
<!-- 3. Complete header + body + footer -->
<BaseCard>
<template #header>
<h3>Order #12345</h3>
</template>
<p>Total: $99.99</p>
<template #footer>
<button>View Details</button>
</template>
</BaseCard>
<!-- 4. Complete Syntax(v-slot) -->
<BaseCard>
<template v-slot:header>
<h3>Header</h3>
</template>
<template v-slot:default>
<p>Body</p>
</template>
<template v-slot:footer>
<p>Footer</p>
</template>
</BaseCard>
<!-- 5. Scope Slot(Receive Data) -->
<UserCard :user="user">
<template #actions="{ user }">
<button @click="edit(user)">Edit</button>
<button @click="remove(user)">Delete</button>
</template>
</UserCard>
▶ مثال: 3. مثال كامل على فتحات النطاق
<!-- Child component:DataTable.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<!-- Scope Slot:Allow the parent component to customize cell rendering -->
<slot :name="`cell-${col.key}`" :row="row" :index="index" :value="row[col.key]">
<!-- Default:Show Original Values -->
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({
columns: { type: Array, required: true },
data: { type: Array, required: true }
})
</script>
<!-- Using Parent Components DataTable -->
<DataTable :columns="columns" :data="users">
<!-- Custom status column -->
<template #cell-status="{ row }">
<span :class="['badge', row.status]">{{ row.status }}</span>
</template>
<!-- Custom actions column -->
<template #cell-actions="{ row }">
<button @click="edit(row)">Edit</button>
</template>
</DataTable>
▶ مثال: 4. مرجع سريع لـ 5 أخطاء شائعة
| الخطأ | الأعراض | الحل |
|---|---|---|
| اسم الخانة مكتوب بشكل خاطئ | المحتوى غير معروض | تحقق من تطابق <slot name="..."> و#name |
| وجود عناصر جذرية متعددة في المكون الأصلي | تحذير | المكون الأصلي محاط بtag <template #name> |
| v-slot (الصيغة القديمة) | متوافق مع Vue 2 | استخدم v-slot:header أو #header بدلاً من ذلك |
| لم يتم تمرير الخانة الافتراضية | لم يتم عرض المكون الأصلي | تحقق من محتوى <Component /> في المكون الأصلي |
| لا يتم تفكيك فتحات النطاق | القوالب مطولة | التفكيك باستخدام { user, index } |
▶ مثال: 5. 5 مقارنات الأداء
| بناء الجملة | الترجمة | الأداء | التوصيات |
|---|---|---|---|
<slot /> |
ثابت | ⭐⭐⭐⭐⭐ | افتراضي |
<slot name="x" /> |
ثابت | ⭐⭐⭐⭐⭐ | مسمى |
<slot :x="x" /> |
ثابت | ⭐⭐⭐⭐ | النطاق |
v-if="$slots.x" |
الstatus | ⭐⭐⭐⭐ | تم الكشف عنها |
useSlots() |
وقت التشغيل | ⭐⭐⭐ | يُستخدم في JS |
▶ مثال: 6. 5 استخدامات رئيسية لـ $slots
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
<template>
<div class="wrapper">
<!-- 1. Check if the slot exists -->
<div v-if="slots.header">Has header</div>
<!-- 2. List all slot names -->
<div v-for="name in Object.keys(slots)" :key="name">
<slot :name="name" />
</div>
<!-- 3. Passthrough attribute -->
<input v-bind="attrs" />
<!-- 4. Conditional Rendering -->
<template v-if="slots.default">
<div class="content">
<slot />
</div>
</template>
<!-- 5. Default Slot Packaging -->
<div class="default-wrapper">
<slot>
<p>Default fallback</p>
</slot>
</div>
</div>
</template>
❓ أسئلة شائعة
v-slot و#؟#header is a shorthand for v-slot:header. نوصي باستخدام الاختصار (لأنه أكثر إيجازًا).<slot /> هي القيمة الافتراضية. إذا كان لا بد من تسميتها، فاستخدم <slot name="default" />؛ ويمكن للمكون الأصلي استخدام <template #default> أو ببساطة تمرير المحتوى مباشرةً.<slot>.$slots (قالب) أو useSlots() (JS). يقوم المكون التابع بإتاحة الفتحة باستخدام <slot>، بينما يقوم المكون الرئيسي بملئها باستخدام <template #name>.<slot name="header"><h3>Default Header</h3></slot>. يتم عرض هذا المحتوى عندما لا يقوم المكون الأصلي بتمرير رأس.📖 ملخص
- «السلوتات» هي واجهة برمجة تطبيقات توزيع المحتوى في Vue: حيث تتحكم المكونات الأم في المحتوى، بينما تتحكم المكونات الفرعية في موضع العرض
- 3 أنواع من الفتحات: الفتحة الافتراضية (
<slot />)، والفتحة المسماة (name="x")، والفتحة ذات النطاق المحدد (:x="x") - يقوم المكون الأصلي بتمرير فتحة مسماة باستخدام
<template #name>أو الاختصار#name - تتيح فتحات النطاق للمكونات الفرعية تمرير البيانات إلى المكونات الأصلية (خصائص الفتحة)
<slot>المحتوى الافتراضي (الاحتياطي) الذي يتم عرضه عندما لا يمرر المكون الأصلي أي محتوى$slots/useSlots()التحقق من وجود الفتحة- التعامل مع «السلوتات» و«العناصر المساعدة»: تقوم «العناصر المساعدة» بتمرير الإعدادات، بينما تقوم «السلوتات» بتمرير المحتوى
📝 تمارين
-
أسئلة أساسية (مستوى الصعوبة: ⭐)
تنفيذ مكون «زر» بسيط:
- BaseButton.vue: الفتحة الافتراضية + 3 أشكال (أساسي/نجاح/خطر)
- props:
variant(سلسلة) - المكون الأصلي: اختبار 3 متغيرات + محتوى مخصص للفتحة
-
مسائل متقدمة (مستوى الصعوبة: ⭐⭐)
تنفيذ مكون BaseLayout:
- المكونات الفرعية: 3 خانات مسماة (رأس الصفحة / الافتراضي / تذييل الصفحة)
- المكون الأصلي: استخدم «BaseLayout» لتنفيذ صفحة لوحة التحكم
- استخدم
$slotsللتحقق من وجود الفتحة - إضافة محتوى افتراضي (احتياطي)
-
مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)
تنفيذ نظام مكونات DataTable كامل:
- DataTable.vue: يستقبل الأعمدة والبيانات، ويقوم بعرض الجدول
- خانة النطاق:
#cell-{key}تتيح للمكونات الأصلية تخصيص الخلايا - الخانة الافتراضية: تعرض القيمة الأصلية عندما لا يوفر المكون الأصلي قيمةً ما
- مثال على المكون الأصلي: جدول المستخدمين + جدول الطلبات — طريقتان مختلفتان لعرض الأعمدة
- الأداء: يقوم v-memo بتخزين الصفوف مؤقتًا (يتخطى إعادة الرسم عندما تظل البيانات دون تغيير)
- التعيين القوي للأنواع في TypeScript