Vue.js: الفتحات

آخر تحديث: 2026-08-26

تُعد «الفتحات» جزءًا من واجهة برمجة تطبيقات توزيع المحتوى في Vue — حيث يمكن للمكونات الأصلية إدراج أي محتوى في «الفتحات» الخاصة بالمكون الفرعي. وتجعل «الفتحات» المكونات مرنةً بقدر «القوالب»: فالمكونات الأصلية تتحكم في ما يتم عرضه، بينما تتحكم المكونات الفرعية في مكان عرضه.

يقدم Vue 3 ثلاثة أنواع من الفتحات: الفتحات الافتراضية، والفتحات المسماة، والفتحات ذات النطاق. سيساعدك هذا الدرس على إتقان استخدام هذه الأنواع الثلاثة وأفضل الممارسات المتعلقة بها.

1. ما ستتعلمه



2. التحدي المتمثل في عدم القدرة على تخصيص محتوى مكون البطاقة

(1) المشكلة: 5 أنماط للبطاقات، تتطلب كتابة 5 مكونات

احتاج مسؤول التجارة الإلكترونية لدى أليس إلى 5 أنواع من البطاقات:

VUE
<!-- ❌ 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)، مع محتوى مقدم من المكون الأصلي

VUE
<!-- 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>
VUE
<!-- 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) الإيرادات

بعد استخدام ماكينات القمار:



3. الفتحة الافتراضية

(1) الاستخدام الأساسي

VUE
<!-- BaseCard.vue Child component -->
<template>
  <div class="card">
    <!-- Slot: The parent component can contain any content -->
    <slot />
  </div>
</template>
VUE
<!-- App.vue Parent Component -->
<template>
  <BaseCard>
    <h3>Hello</h3>
    <p>This is the card content</p>
  </BaseCard>
</template>

نتائج العرض:

HTML
<div class="card">
  <h3>Hello</h3>
  <p>This is the card content</p>
</div>

(2) المحتوى الافتراضي (الاحتياطي)

VUE
<!-- 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>
VUE
<!-- 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) تعريف الفتحات المسماة

VUE
<!-- 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 صيغ)

VUE
<!-- 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) أسماء الخانات الديناميكية

VUE
<!-- 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) التحقق من وجود فتحة

VUE
<!-- 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>
VUE
<!-- Parent Component -->
<BaseCard>
  <template #header>...</template>  <!-- Sent header Slot, Display -->
</BaseCard>

<BaseCard>
  <!-- No header slot passed, Do not display card-header -->
</BaseCard>


5. فتحات النطاق

(1) نقطة أساسية: تقوم المكونات الفرعية بتمرير البيانات إلى فتحات المكون الرئيسي

VUE
<!-- 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>
VUE
<!-- 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>

نتائج العرض:

HTML
<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) مثال كامل: قائمة قابلة للتخصيص

VUE
<!-- 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>
▶ جرّب الكود
VUE
<!-- 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 الوصول إلى الفتحات

VUE
<!-- 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 (واجهة برمجة التطبيقات للتكوين)

VUE
<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 (الافتراضي + الفتحات المسماة)

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

VUE
<!-- 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. مثال كامل على فتحات النطاق

VUE
<!-- 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>
▶ جرّب الكود
VUE
<!-- 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

VUE
<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>
▶ جرّب الكود

❓ أسئلة شائعة

س ما الفرق بين «السلوتات» و«البروب»؟
ج تقوم «البروب» بتمرير البيانات (أي قيمة في JavaScript)، بينما تقوم «السلوتات» بتمرير المحتوى (أجزاء من HTML أو المكونات). وتُعد «البروب» مناسبة لخيارات التهيئة، في حين تُعد «السلوتات» مناسبة لتخصيص المحتوى.
س متى ينبغي استخدام فتحات النطاق؟
ج عندما يحتاج مكون أب إلى تخصيص طريقة عرضه استنادًا إلى البيانات الواردة من مكون تابع. على سبيل المثال: عرض أعمدة الجدول، أو عرض عناصر القائمة، أو تخطيط محتوى البطاقات.
س ما الفرق بين الاختصار v-slot و#؟
ج إنهما متطابقان تمامًا؛ #header is a shorthand for v-slot:header. نوصي باستخدام الاختصار (لأنه أكثر إيجازًا).
س هل يمكن تسمية الخانة الافتراضية؟
ج لا يلزم تسمية الخانة الافتراضية؛ <slot /> هي القيمة الافتراضية. إذا كان لا بد من تسميتها، فاستخدم <slot name="default" />؛ ويمكن للمكون الأصلي استخدام <template #default> أو ببساطة تمرير المحتوى مباشرةً.
س هل يمكن تمرير الخانات كمعلمات؟
ج لا يمكنك تمرير خانة مباشرةً كمعلمة. ومع ذلك، يمكنك تمرير VNode أو دالة عرض كمعلمة (استخدام متقدم). في معظم الحالات، ما عليك سوى استخدام <slot>.
س كيف يمكنني تحديد فتحة مكون تابع في مكون رئيسي؟
ج استخدم $slots (قالب) أو useSlots() (JS). يقوم المكون التابع بإتاحة الفتحة باستخدام <slot>، بينما يقوم المكون الرئيسي بملئها باستخدام <template #name>.
س هل يمكن أن تحتوي الخانات المسماة على محتوى افتراضي؟
ج نعم. <slot name="header"><h3>Default Header</h3></slot>. يتم عرض هذا المحتوى عندما لا يقوم المكون الأصلي بتمرير رأس.

📖 ملخص


📝 تمارين

  1. أسئلة أساسية (مستوى الصعوبة: ⭐)

    تنفيذ مكون «زر» بسيط:

    • BaseButton.vue: الفتحة الافتراضية + 3 أشكال (أساسي/نجاح/خطر)
    • props: variant (سلسلة)
    • المكون الأصلي: اختبار 3 متغيرات + محتوى مخصص للفتحة
  2. مسائل متقدمة (مستوى الصعوبة: ⭐⭐)

    تنفيذ مكون BaseLayout:

    • المكونات الفرعية: 3 خانات مسماة (رأس الصفحة / الافتراضي / تذييل الصفحة)
    • المكون الأصلي: استخدم «BaseLayout» لتنفيذ صفحة لوحة التحكم
    • استخدم $slots للتحقق من وجود الفتحة
    • إضافة محتوى افتراضي (احتياطي)
  3. مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)

    تنفيذ نظام مكونات DataTable كامل:

    1. DataTable.vue: يستقبل الأعمدة والبيانات، ويقوم بعرض الجدول
    2. خانة النطاق: #cell-{key} تتيح للمكونات الأصلية تخصيص الخلايا
    3. الخانة الافتراضية: تعرض القيمة الأصلية عندما لا يوفر المكون الأصلي قيمةً ما
    4. مثال على المكون الأصلي: جدول المستخدمين + جدول الطلبات — طريقتان مختلفتان لعرض الأعمدة
    5. الأداء: يقوم v-memo بتخزين الصفوف مؤقتًا (يتخطى إعادة الرسم عندما تظل البيانات دون تغيير)
    6. التعيين القوي للأنواع في TypeScript
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%