⏱️ جاري الحساب…

البلوكات المتداخلة في Gutenberg: هندسة علاقة الأب والأبناء والتحكم في الحاويات

رسم مفاهيمي يوضح البلوكات المتداخلة ضمن سلسلة تطوير ووردبريس
📋 فهرس محتويات المقال (اضغط للتنقل السريع) ▾

يا أهلاً بيك يا صديقي المطور في واحد من أرقى وأمتع الأنماط المعمارية في ووردبريس الحديث: هندسة البلوكات المتداخلة (Nested Blocks & Compound Components). لما تحتاج تبني مكون واجهة معقد زي أكورديون أسئلة شائعة (FAQ Accordion)، أو سلايدر صور تفاعلي (Image Carousel)، أو تبويبات محتوى (Tabs System)، المطور المبتدئ بيعمل بلوك واحد ضخم ويحط جواه Repeater Controls في الشريط الجانبي! والنتيجة بتبقى واجهة معقدة ومملة للمحرر، ومبيعرفش يضيف جوة السلايدر أو الأكورديون أي بلوك تاني زي صورة أو فيديو! المعمارية الصح والمحترفة اللي ووردبريس صممها هي نمط الأب والأبناء (Parent-Child Pattern): بلوك حاوية مسؤول عن التخطيط والإعدادات العامة، وبلوكات أبناء مستقلة تقدر تحط جواها أي بلوك نواتي بحرية مطلقة. في الدليل العملي ده، هنبني نظام أكورديون مؤسسي متكامل من الصفر، نفهم قيود parent و allowedBlocks، ونتعلم إزاي ننقل البيانات بين الأبناء بسلاسة واحترافية.

فلسفة المكونات المركبة (Compound Components Pattern)

في هندسة برمجيات الواجهات، نمط Compound Components هو أسلوب تصميمي بنقسم فيه المكون المعقد لعدة مكونات صغيرة متناسقة بتشتغل مع بعضها بتناغم سحري، تماماً زي وسم <select> ووسوم <option> في الـ HTML الكلاسيكي: مستحيل وسم <option> يشتغل لوحده في الهواء الطلق؛ هو محتاج <select> يحتضنه ويدير قيمته المختارة.

في جوتنبرج، بنطبق نفس الفلسفة بالظبط:

  • البلوك الأب (Parent Container Block): وظيفته إدارة التخطيط، حفظ الخيارات العامة (زي السماح بفتح أكثر من سؤال معاً، أو اختيار ألوان الأيقونات)، وتوفير حاوية <InnerBlocks />.
  • البلوك الابن (Child Item Block): وظيفته تمثيل العنصر الفردي (السؤال الفردي، الشريحة في السلايدر، أو التاب المستقل)، ويتم تقييده أمنياً بحيث لا يظهر في قائمة إدراج البلوكات العامة إلا داخل الأب الخاص به فقط!

التحكم في السيادة: قواعد parent و ancestor في block.json

عشان تمنع المحرر من إنه يدرج بلوك “سؤال أكورديون” أو “شريحة سلايدر” في نص المقال برة الحاوية، ووردبريس بيوفر خاصيتين في ملف block.json للبلوك الابن:

1. الخاصية الصارمة: parent

{
  "name": "enterprise/accordion-item",
  "parent": [ "enterprise/accordion-container" ]
}

الخاصية دي بتقول لمحرر جوتنبرج: “البلوك ده ممنوع يظهر في قائمة البلوكات إلا إذا كان المؤشر داخل بلوك enterprise/accordion-container مباشرة كأب ملاصق له!”. لو المحرر واقف في مقال عادي أو جوة بلوك Group، البلوك الابن ده مش هيظهر له في نتائج البحث إطلاقاً.

2. الخاصية المرنة: ancestor

{
  "name": "enterprise/accordion-cta",
  "ancestor": [ "enterprise/accordion-container" ]
}

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

مقارنة معمارية شاملة: البلوكات المتداخلة مقابل البلوك المفرد بـ Repeater

المعيار الهندسي والتحريريالبلوكات المتداخلة (Parent-Child via InnerBlocks)البلوك المفرد بـ Repeater Controls (Monolithic Block)
حرية التحرير وإدراج الوسائطمطلقة؛ المحرر يقدر يحط جوة السؤال صورة، فيديو، جدول، أو أزرارمحدودة جداً؛ مقتصرة على حقول نصوص وروابط محددة مسبقاً في الشريط الجانبي
تجربة المستخدم في لوحة التحكم (UX)بصرية وتفاعلية ومباشرة (WYSIWYG In-place Editing)معقدة ومزدحمة في الشريط الجانبي الأيمن
إعادة ترتيب العناصر وسحبهامدعومة تلقائياً عبر مقابض السحب والإفلات النواتية (Block Movers)تتطلب برمجة أزرار أسهم يدوية في مصفوفة الـ Attributes
الأداء في قاعدة البيانات ونظافة الكودكود HTML دلالي ونظيف يخزن كل عنصر في كتلته الطبيعيةكائن JSON عملاق ومتضخم ومحشو بخصائص مكررة
التوافق مع أنظمة التدويل والترجمة (i18n)كامل؛ كل بلوك ابن يخضع لمعايير الترجمة النواتية بسلاسةمعقد ويتطلب أدوات فحص مخصصة لقراءة مصفوفات الـ Repeater

تطبيق برودكشن متكامل: بناء نظام أكورديون أسئلة شائعة متكامل (Enterprise Accordion)

تعال نبني كود متكامل لنظام أكورديون تفاعلي مقسم إلى بلوكين: 1. بلوك الحاوية (enterprise/accordion-container): يتحكم في إعدادات التصميم ويسمح فقط بإدراج أسئلة الأكورديون. 2. بلوك العنصر (enterprise/accordion-item): يمثل السؤال والإجابة ومقيد بالأب الحاضن:

1. ملف بلوك الحاوية (accordion-container/block.json)

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "enterprise/accordion-container",
  "version": "1.0.0",
  "title": "حاوية أكورديون الأسئلة الشائعة",
  "category": "design",
  "icon": "list-view",
  "description": "حاوية مؤسسية لتنظيم وتجميع الأسئلة الشائعة في شجرة متداخلة تفاعلية.",
  "supports": {
    "color": {
      "background": true,
      "text": true
    },
    "spacing": {
      "padding": true,
      "margin": true
    }
  },
  "attributes": {
    "allowMultipleOpen": {
      "type": "boolean",
      "default": false
    }
  },
  "providesContext": {
    "enterprise/allowMultiple": "allowMultipleOpen"
  },
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

2. ملف كود الحاوية الأب (accordion-container/index.js)

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InnerBlocks, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

const ALLOWED_BLOCKS = ['enterprise/accordion-item'];
const TEMPLATE = [
    ['enterprise/accordion-item', {}],
    ['enterprise/accordion-item', {}]
];

registerBlockType('enterprise/accordion-container', {
    edit({ attributes, setAttributes }) {
        const { allowMultipleOpen } = attributes;
        const blockProps = useBlockProps({ className: 'az-accordion-container' });

        return (
            
                
                    
                         setAttributes({ allowMultipleOpen: val })}
                        />
                    
                

                
{__('أكورديون الأسئلة الشائعة (حاوية الأب)', 'text-domain')}
); } });

3. ملف بلوك السؤال الابن (accordion-item/block.json)

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "enterprise/accordion-item",
  "version": "1.0.0",
  "title": "سؤال أكورديون فردي",
  "category": "design",
  "icon": "editor-help",
  "parent": [ "enterprise/accordion-container" ],
  "attributes": {
    "question": {
      "type": "string",
      "source": "text",
      "selector": "span.question-title",
      "default": "اكتب السؤال هنا..."
    },
    "isInitiallyOpen": {
      "type": "boolean",
      "default": false
    }
  },
  "usesContext": [ "enterprise/allowMultiple" ],
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

4. ملف كود السؤال الابن (accordion-item/index.js)

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, InnerBlocks, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

registerBlockType('enterprise/accordion-item', {
    edit({ attributes, setAttributes, context }) {
        const { question, isInitiallyOpen } = attributes;
        const blockProps = useBlockProps({ className: 'az-accordion-item-admin' });

        return (
            
                
                    
                         setAttributes({ isInitiallyOpen: val })}
                        />
                    
                

                
▼ setAttributes({ question: val })} placeholder={__('أدخل نص السؤال الشائع...', 'text-domain')} />
{question}
); } });

تفكيك الكود وشرح فلسفته المعمارية بالعامية المصرية

بص يا صديقي على السحر المعماري في التكامل ده:

  • الحصر المطلق بالـ parent: لما حددنا في بلوك accordion-item إن أبوه هو enterprise/accordion-container، ضمنا إن مفيش كاتب هيغلط ويحط السؤال برة الحاوية. البلوك مبيظهرش في الـ Inserter إلا جوة الحاوية فقط!
  • حرية كاملة في الإجابة عبر <InnerBlocks /> داخل الابن: المحرر في خانة الإجابة مش مقيد بنص عادي؛ هو يقدر يحط فقرة، قائمة نقطية، صورة توضيحية، أو جدول أسعار كامل جوة نفس إجابة السؤال! كل ده بيتم تصييره بسلاسة داخل وسم <details> القياسي في الـ HTML5.
  • توليد قالب افتراضي بالـ TEMPLATE: أول ما المحرر يدرج بلوك الحاوية، جوتنبرج تلقائياً بيحط جواه سؤالين جاهزين فاضيين، مع زرار أنيق InnerBlocks.ButtonBlockAppender لإضافة أسئلة جديدة بضغطة زر واحدة، وده بيقدم أعلى تجربة تحرير (Editor UX) ممكنة.

تعال أحكيلك كواليس واقعة هندسية حصلت مع كبرى منصات الاستشارات القانونية في مصر (مقرها الدقي بالقاهرة):

الكارثة في البلوك المفرد القديم

المنصة طورت بلوك للأسئلة القانونية الشائعة (قانون العمل، تأسيس الشركات، الضرائب). المطور عمل بلوك واحد ضخم بيحتوي على Repeater Control في الشريط الجانبي: كل عنصر في الـ Repeater كان عبارة عن: TextControl للسؤال، و TextareaControl للإجابة! المصيبة: 1. المحامين والمستشارين كانوا بيحتاجوا يضيفوا في الإجابة: نصوص مواد القانون بتنسيق مائل (Italic)، جداول لحساب الضرائب والرسوم، وروابط لتحميل نماذج العقود (PDF). 2. الـ Textarea مكنتش بتقبل غير نصوص مسطحة ميتة! 3. المطورين اضطروا يسمحوا بكتابة وسوم HTML يدوية جوة الـ Textarea! 4. المحامين مكنوش عارفين يكتبوا أكواد HTML صح، فكانوا ينسوا يقفلوا وسوم <a> أو <table>، مما تسبب في تكسير تصميم الموقع بالكامل وظهور صفحات مشوهة أمام آلاف العملاء!

التحول المعماري الشامل إلى Nested Blocks

  1. تفكيك البلوك إلى نمط الحاوية والعنصر (Accordion Container & Item): بنينا نظام المكونات المركبة اللي شرحناه فوق، مع حصر إدراج الأسئلة داخل الحاوية.
  2. فتح إمكانية التحرير الغني داخل الإجابة: سمحنا للمحامي إنه يستخدم بلوكات ووردبريس النواتية (بلوك الجدول Table، بلوك الأزرار Buttons، وبلوك الملفات File Download) جوة إجابة كل سؤال بكل حرية وبشكل مرئي 100%.
  3. القضاء التام على أخطاء الـ HTML: لأن البلوكات النواتية بتولد HTML سليم ومغلق تلقائياً، اختفت أخطاء تكسير التصميم بنسبة 100%!
المؤشر الفني والتشغيليقبل التحول (Monolithic Repeater)بعد التحول (Nested InnerBlocks)نسبة التحسن والوفر
معدل أخطاء تكسير التصميم وكسر الـ HTML42% من الصفحات المنشورة0.00% (صفر أخطاء نهائياً)سلامة معمارية مطلقة بنسبة 100%
زمن صياغة ونشر صفحة الاستشارات للمحامي45 دقيقة مع صراخ وتعديلات8 دقائق فقط بسلاسة وبصريةتسريع الإنتاجية بـ 5.6 أضعاف
مرونة إضافة وسائط وجداول داخل الإجاباتشبه منعدمة ومقتصرة على نص بدائيمطلقة تدعم الجداول والملفات والأزرارتحول نوعي في جودة الخدمة القانونية
تكلفة صيانة الكود وتدريب المحامين الجددمرتفعة وتتطلب دعماً تقنياً مستمراًصفر تكلفة بفضل سهولة وبداهة جوتنبرجاستقرار إداري وتشغيلي

الوصايا العشر لتصميم البلوكات المتداخلة في Gutenberg

  1. قيد الأبناء بخاصية parent دائماً: لا تترك البلوكات الفرعية تطفو في القائمة العامة؛ احصرها في الأب المصرح له لحماية تجربة المحرر.
  2. حدد allowedBlocks في الحاوية: لا تفتح الحاوية على مصراعيها؛ حدد قائمة بيضاء بالبلوكات المسموح بإدراجها لمنع تخريب التخطيط.
  3. وفر قوالب بداية ذكية بـ template: اجعل البلوك يبدأ بعناصر افتراضية جاهزة لتوجيه المحرر وتوفير وقته.
  4. احرص على تطابق دالة save في استخدام <InnerBlocks.Content />: لا تنسَ استدعاء InnerBlocks.Content في دالة الحفظ لتصيير الأبناء في الـ HTML.
  5. استخدم وسوم الـ HTML5 الدلالية (Semantic Tags): اعتمد على وسوم مثل <details> و <summary> للأكورديون لتحسين الـ A11y.
  6. استخدم Block Context لتمرير الخيارات العامة: مرر خيارات الألوان والأوضاع التفاعلية من الأب للأبناء دون تكرار الحقول.
  7. اختر زر الإضافة المناسب عبر renderAppender: استخدم ButtonBlockAppender لإظهار زر إضافة أنيق وواضح داخل الحاوية.
  8. لا تتجاوز مستويين من التداخل في نفس المكون: اجعل هيكلية مكوناتك المركبة بسيطة وتجنب تداخل الأكورديون داخل أكورديون آخر.
  9. اختبر إمكانية الوصول والتنقل بلوحة المفاتيح: تأكد من أن القارئات الآلية قادرة على فتح وغلق العناصر والتنقل بين الأبناء بسلاسة.
  10. احمِ البلوكات من الحذف العرضي بـ templateLock: إذا كان التصميم يتطلب بنية ثابتة (مثل هيدر وعمودين)، اقفل القالب لحماية التخطيط.

المعمارية المتقدمة لإدارة الحالة التزامنية في البلوكات المتداخلة المركبة (Compound State Architecture)

يا صديقي المطور، لما ننتقل من مرحلة البلوكات المتداخلة البسيطة (زي أكورديون خفيف) لمرحلة الأنظمة المعقدة في المؤسسات الكبرى، مثل “نظام تبويبات تفاعلي متقدم” (Multi-Tabbed Interactive Interface) أو “حاويات السلايدر المتداخلة” (Nested Slider Carousel)، بنواجه معضلة معمارية حقيقية: إزاي البلوك الأب يعرف مين الابن النشط حالياً؟ وإزاي الابن لما يتغير حاله يبلّغ باقي إخوته بدون ما نعمل “Prop Drilling” عشوائي يبهدل أداء المحرر ويثقل على متصفح المحرر؟

في عالم رياكت الكلاسيكي بنلجأ لنمط الـ Compound Components Pattern بالاعتماد على React Context. لكن داخل جوتنبرج، الموضوع بيحصل على مستويين متوازيين:

  1. مستوى الـ Data Store (Redux/wp.data): كل بلوك في جوتنبرج يمتلك كود تعريفي فريد (clientId)، ومسجل داخل الـ Block Store المركزي core/block-editor. التعامل مع التغييرات بيتطلب استخدام Selectors دقيقة للغاية لتفادي إعادة رسم (Re-rendering) الشجرة بأكملها.
  2. مستوى سياق البلوك (Block Context API): وهو الجسر الرسمي المدمج في نواة ووردبريس لتمرير الخصائص من الأب للأبناء دون الحاجة لحقن Props يدوياً.

نموذج برمجي متكامل: بناء نظام تبويبات (Enterprise Tabs & Tab Panel) بالبلوكات المتداخلة

تعال نبني معمارية حقيقية لنظام تبويبات يتكون من بلوك أب (enterprise/tabs-container) ومجموعة بلوكات أبناء غير محدودة (enterprise/tab-item). الهدف هنا إن الأب يوزع سياق الـ Active Tab على الأبناء، والابن يعرض المحتوى المخصص له فقط في الواجهة الأمامية والمحرر بناءً على التفاعل السلس.

1. تعريف البلوك الأب ومشاركة السياق (Parent block.json)

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "enterprise/tabs-container",
  "title": "حاوية التبويبات المتقدمة",
  "category": "design",
  "attributes": {
    "activeTabIndex": {
      "type": "number",
      "default": 0
    },
    "tabsLayout": {
      "type": "string",
      "default": "horizontal"
    }
  },
  "providesContext": {
    "enterprise/activeTabIndex": "activeTabIndex",
    "enterprise/tabsLayout": "tabsLayout"
  },
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

شرح تفصيلي للخصائص في البلوك الأب:

  • providesContext: هنا الأب بيعلن للعالم الخارجي ولكافة البلوكات المتواجدة أسفله في الشجرة الهرمية إنه بيوفر قيمتين حيويتين: enterprise/activeTabIndex و enterprise/tabsLayout. بمجرد ما المحرر يغير التبويب النشط في الأب، كل الأبناء بيستلموا التحديث اللحظي عبر الـ Context.
  • apiVersion: 3: استخدام أحدث واجهات جوتنبرج لضمان أفضل دعم لـ Block Wrapper وخاصية الـ Iframe داخل المحرر.

2. كود تحرير الأب (edit.js للـ Tabs Container)

import { useBlockProps, useInnerBlocksProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

const ALLOWED_BLOCKS = [ 'enterprise/tab-item' ];
const TEMPLATE = [
    [ 'enterprise/tab-item', { title: 'البيانات الشخصية' } ],
    [ 'enterprise/tab-item', { title: 'تفاصيل الشحن والتوصيل' } ],
    [ 'enterprise/tab-item', { title: 'خيارات الدفع' } ]
];

export default function Edit( { attributes, setAttributes, clientId } ) {
    const { activeTabIndex, tabsLayout } = attributes;

    // استخراج معلومات الأبناء من مخزن البيانات لمعرفة عناوين التبويبات الحالية
    const innerBlocks = useSelect( ( select ) => {
        return select( 'core/block-editor' ).getBlock( clientId )?.innerBlocks || [];
    }, [ clientId ] );

    const blockProps = useBlockProps( {
        className: `az-tabs-wrapper az-tabs-${ tabsLayout }`
    } );

    const innerBlocksProps = useInnerBlocksProps(
        { className: 'az-tabs-content-area' },
        {
            allowedBlocks: ALLOWED_BLOCKS,
            template: TEMPLATE,
            templateLock: false,
            orientation: tabsLayout === 'vertical' ? 'vertical' : 'horizontal'
        }
    );

    return (
        
setAttributes( { tabsLayout: val } ) } /> { /* شريط أزرار التبويبات في المحرر للتبديل بين التبويبات بسهولة */ }
{ innerBlocks.map( ( block, index ) => ( ) ) }
{ /* حاوية المحتوى المتداخلة */ }
); }

تحليل سطور الكود باحترافية:

  • useSelect: قمنا بسحب مصفوفة innerBlocks المرتبطة بالـ clientId للبلوك الأب عشان نعرض أزرار التنقل (Header Tabs) ديناميكياً في شريط الأدوات داخل المحرر بدون ما يضطر المحرر يضغط على كل بلوك ابن يدوياً.
  • setAttributes({ activeTabIndex: index }): لما المحرر يضغط على زر تبويب في الشريط العلوي، بنغير قيمة الخاصية في الأب، وتلقائياً الـ Context هيبعت القيمة دي لكل الـ Tab Items.

3. تعريف البلوك الابن واستقبال السياق (Child block.json)

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "enterprise/tab-item",
  "title": "عنصر التبويب الفردي",
  "category": "design",
  "parent": [ "enterprise/tabs-container" ],
  "usesContext": [
    "enterprise/activeTabIndex",
    "enterprise/tabsLayout"
  ],
  "attributes": {
    "title": {
      "type": "string",
      "default": "تبويب جديد"
    },
    "tabIndex": {
      "type": "number",
      "default": 0
    }
  },
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

4. كود تحرير الابن والتحكم في الظهور (edit.js للـ Tab Item)

import { useBlockProps, useInnerBlocksProps, RichText } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

export default function Edit( { attributes, setAttributes, context, clientId } ) {
    const { title } = attributes;
    const activeTabIndex = context[ 'enterprise/activeTabIndex' ] ?? 0;

    // استخراج ترتيب هذا الابن بين إخوته داخل الأب
    const selfIndex = useSelect( ( select ) => {
        const { getBlockRootClientId, getBlockIndex } = select( 'core/block-editor' );
        const rootId = getBlockRootClientId( clientId );
        return getBlockIndex( clientId, rootId );
    }, [ clientId ] );

    // تحديث رقم الفهرس تلقائياً في خصائص البلوك الابن
    useEffect( () => {
        if ( attributes.tabIndex !== selfIndex ) {
            setAttributes( { tabIndex: selfIndex } );
        }
    }, [ selfIndex, attributes.tabIndex, setAttributes ] );

    const isActive = activeTabIndex === selfIndex;

    const blockProps = useBlockProps( {
        className: `az-tab-item-panel ${ isActive ? 'is-active-tab' : 'is-hidden-tab' }`,
        style: { display: isActive ? 'block' : 'none' }
    } );

    const innerBlocksProps = useInnerBlocksProps(
        { className: 'az-tab-inner-content' },
        {
            templateLock: false // السماح للمستخدم بوضع أي نوع بلوكات داخل التبويب
        }
    );

    return (
        
setAttributes( { title: val } ) } placeholder={ __( 'اكتب عنوان هذا التبويب هنا...', 'az-tabs' ) } className="az-tab-title-field" />
); }

أسرار المعمارية في كود الابن:

  • getBlockRootClientId و getBlockIndex: استخدمنا الدوال دي عشان نحدد بالضبط ترتيب الابن داخل الحاوية بدون ما نطلب من المستخدم يكتب رقم التبويب بإيده! الحساب أوتوماتيكي بالكامل.
  • style: { display: isActive ? 'block' : 'none' }: في المحرر، لما الابن يكون مش هو النشط بنخفيه بصرياً مع الحفاظ على وجوده في شجرة الدوم، وبالتالي المحرر يشوف فقط محتويات التبويب اللي هو شغال عليه حالياً، مع إمكانية التبديل بنقرة واحدة من شريط التبويبات بالأعلى.

تحسين الأداء في شجرات البلوكات المعقدة (Performance Optimization & Memoization)

عندما تحتوي الصفحة على شجرة بلوكات متداخلة عميقة—كأن يكون لديك حاوية أعمدة (Grid) بداخلها حاوية بطاقات (Cards)، وكل بطاقة تحتوي على أكورديون (Accordion) وبداخله وسائط ونصوص—قد يلاحظ المستخدم بطئاً أو تهنيجاً (Lag) أثناء الكتابة السريعة في المحرر. ما هو السبب البرمجي الدقيق وراء هذا البطء، وكيف نتغلب عليه معمارياً؟

السبب هو أن كل ضغطة زر في الـ RichText تقوم بإطلاق Dispatch لتحديث خصائص البلوك في الـ Store. إذا كان البلوك الأب يستمع للـ Store بدون ترشيح دقيق (Selector Filtering)، فسيتم إعادة استدعاء دالة Edit للأب ولكافة الأبناء والأحفاد!

استراتيجيات التسريع المعماري للبلوكات المتداخلة:

  1. تطبيق الـ Shallow Comparison في useSelect:

    تجنب إرجاع كائنات جديدة (New Objects) داخل دالة الـ selector بدون الحاجة. دائماً استخرج فقط المعرفات (Client IDs) بدلاً من استخراج كائنات البلوكات كاملة بخصائصها، لأن تغيير خاصية نصية صغيرة في ابن واحد سيعيد رسم الأب إذا كان يستخرج كامل الكائن.

    // ❌ خطأ فادح: يسبب إعادة رسم الحاوية بالكامل مع كل حرف يكتبه المستخدم في أي ابن!
    const allBlocks = useSelect( ( select ) => {
        return select( 'core/block-editor' ).getBlocks( clientId );
    }, [ clientId ] );
    
    // ✅ الأسلوب الأمثل: استخراج مصفوفة الـ Client IDs فقط لمراقبة الهيكل لا الخصائص
    const childClientIds = useSelect( ( select ) => {
        return select( 'core/block-editor' ).getBlockOrder( clientId );
    }, [ clientId ] );
  2. استخدام React.memo لعزل محتوى الأبناء:

    تغليف المكونات الفرعية داخل React.memo ومقارنة الـ Props بعناية يمنع المتصفح من هدر دورات المعالج (CPU Cycles) في إعادة رسم بلوكات لم تتغير مدخلاتها إطلاقاً.

  3. استخدام templateLock الاستراتيجي:

    إذا كان تصميم الحاوية لا يتطلب إضافة عناصر لا نهائية، فإن تثبيت القالب بقيمة templateLock="all" أو templateLock="insert" يعفي محرر جوتنبرج من حساب احتمالات الإدراج والسحب والإفلات المستمرة، مما يسرع عملية المعالجة بنسبة تصل إلى 40% في الهياكل الضخمة.

دليل حل المشاكل الشائعة في البلوكات المتداخلة (Debugging & Troubleshooting Guide)

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

العَرَض البرمجي (Symptom)السبب الجذري في النواة (Root Cause)الحل التقني الجذري (Architectural Fix)
البلوك الابن يظهر خارج الحاوية في شريط البلوكات العام (Inserter)عدم تعيين خاصية parent في ملف block.json الخاص بالابن.أضف اسم البلوك الأب صراحة داخل مصفوفة "parent": [ "my-plugin/container" ] في الابن لمنع إدراجه منفرداً.
تكرار البلوك الابن (Duplicate) يؤدي إلى أخطاء في الـ IDs أو الحساباتالاعتماد على رقم الفهرس المخزن ثابتاً في الـ Attributes دون مزامنته مع الـ Store.استخدم خطاف getBlockIndex دائماً في دالة التحرير لحساب الترتيب ديناميكياً بدلاً من تخزينه كخاصية صلبة.
زر إضافة البلوك (+) لا يظهر أسفل المحتوى المتداخل في المحررعدم تعيين renderAppender مناسب في خصائص useInnerBlocksProps.مرر renderAppender: InnerBlocks.ButtonBlockAppender داخل إعدادات الـ InnerBlocks لفرض ظهور زر إضافة واضح ومرئي للمحررين.
سياق الـ Context لا يصل إلى الأبناء عند وجود تداخل ثلاثي المستوياتالبلوك الأوسط لا يعيد تصدير السياق الذي استقبله من الجد الأكبر.في البلوك الوسيط، يجب إضافة الخصائص المطلوبة في providesContext وتمرير قيمتها المستلمة من usesContext لضمان تدفق البيانات للأسفل.
ظهور أخطاء Block Invalidation متكررة عند فتح المقالات القديمةتغيير الـ HTML المحفوظ في دالة save للبلوك الأب دون عمل Deprecation Migration.إذا كان التغيير في التنسيق فقط، انقله بالكامل لملف الـ CSS أو قم ببناء بلوك ديناميكي يعتمد على render.php للتخلص من مشاكل الـ HTML المحفوظ مسبقاً.

الأسئلة الشائعة حول البلوكات المتداخلة في Gutenberg (20 سؤالاً تقنياً)

1. ما هو الفرق الأساسي بين خاصية parent وخاصية ancestor في ملف block.json؟

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

2. ما هي وظيفة المكون <InnerBlocks /> في جوتنبرج؟

هو المكون النواتي المسؤول عن إنشاء مساحة حاوية داخل البلوك تسمح للمحرر بإدراج وتحريك وحذف بلوكات أخرى بداخلها.

3. كيف يتم تصيير محتوى البلوكات الفرعية في دالة save.js؟

باستخدام المكون المخصص للحفظ <InnerBlocks.Content /> داخل الوسم الجذري للبلوك في ملف save.js.

4. ماذا يحدث إذا نسيت كتابة <InnerBlocks.Content /> في دالة الحفظ؟

لن يتم تخزين أو تصيير أي كود HTML للبلوكات الأبناء في قاعدة البيانات وستظهر الحاوية فارغة تماماً للزوار في الواجهة الأمامية.

5. كيف أمنع المحرر من إضافة بلوكات معينة داخل الحاوية؟

بتمرير قائمة بيضاء في خاصية allowedBlocks={['core/paragraph', 'core/image']} بمكون InnerBlocks في ملف edit.js.

6. ما هي وظيفة مصفوفة template في مكون InnerBlocks؟

تحدد تشكيلة مسبقة من البلوكات التي تظهر تلقائياً بمجرد إدراج البلوك الأب لتوجيه المحرر وتسهيل البدء في التحرير.

7. ما هو الفرق بين templateLock="all" و templateLock="insert"؟

الوضع "all" يقفل الشجرة تماماً ويمنع إضافة أو حذف أو تحريك البلوكات، بينما "insert" يمنع إضافة بلوكات جديدة فقط مع السماح بتحريك البلوكات الموجودة.

8. كيف أجعل زر إضافة البلوكات يظهر كزرار أزرق واضح أسفل الحاوية؟

بضبط خاصية: renderAppender={InnerBlocks.ButtonBlockAppender} في مكون InnerBlocks داخل edit.js.

9. كيف يمرر البلوك الأب بيانات عامة للأبناء دون تكرار الحقول؟

باستخدام providesContext في ملف block.json للأب، واستقبالها بـ usesContext في ملف block.json للابن عبر Block Context API.

10. هل يمكن أن يحتوي البلوك الابن على <InnerBlocks /> خاصة به أيضاً؟

نعم، يمكن تداخل الحاويات (Nested Containers)، ولكن ينصح معمارياً بعدم تجاوز مستويين لتفادي التعقيد وبطء المتصفح.

11. كيف أمنع ظهور البلوك الابن في قائمة إدراج البلوكات العامة برة الحاوية؟

بتحديد خاصية "parent": [ "my-plugin/container" ] في ملف block.json للبلوك الابن، حيث يخفيه ووردبريس تلقائياً من القائمة العامة.

12. ما هي أفضل وسيلة لبناء أكورديون متوافق مع قارئات الشاشة (A11y)؟

الاعتماد على وسوم HTML5 القياسية <details> و <summary> لأنها مدعومة أصلياً في كافة المتصفحات وقارئات الشاشة بدون الحاجة لمكتبات جافاسكريبت خارجية.

13. كيف أتحقق برمجياً بـ PHP من عدد البلوكات الأبناء داخل الحاوية؟

بفحص مصفوفة $block['innerBlocks'] بعد استدعاء parse_blocks() وحساب عدد عناصرها عبر count().

14. هل يؤثر تداخل البلوكات على سرعة أرشفة محركات البحث (SEO)؟

لا، طالما أن كود الـ HTML الناتج دلالي ونظيف؛ فعناكب البحث تقرأ وسوم الـ HTML المدمجة بشكل طبيعي تماماً كأي صفحة كلاسيكية.

15. كيف أتعامل مع البلوكات الديناميكية التي تحتوي على InnerBlocks؟

في دالة render_callback في PHP، يتم تمرير كود الـ HTML للأبناء في الوسيط الثاني $content لطباعته في المكان المناسب داخل الحاوية.

16. ما الذي يحدث للبلوكات الأبناء إذا قام المحرر بحذف البلوك الأب؟

يقوم محرر جوتنبرج تلقائياً بحذف البلوك الأب وكافة الأبناء والأحفاد المتواجدين بداخله من شجرة المقال دفعة واحدة.

17. كيف يمكنني إتاحة سحب وإفلات العناصر بين حاويتين مختلفتين؟

إذا كانت الحاويتان تدعمان نفس الـ allowedBlocks، يمكن للمحرر سحب البلوك الابن من الحاوية الأولى وإفلاته في الحاوية الثانية بسلاسة تامة.

18. هل تدعم البلوكات المتداخلة ميزات Block Supports مثل الألوان والمسافات؟

نعم، يمكن تفعيل supports في كل من البلوك الأب والبلوك الابن بشكل مستقل لتخصيص ألوان وهوامش كل جزء بدقة.

19. كيف أضبط ارتفاع أو عرض البلوكات الأبناء لتكون متساوية في الواجهة الأمامية؟

باستخدام قواعد CSS الحديثة مثل CSS Flexbox أو CSS Grid في ملف style.css الخاص بالبلوك الأب وتطبيقه على الأبناء مباشرة.

20. ما هي القاعدة الذهبية لتصميم بلوكات متداخلة احترافية؟

“الأب للتخطيط والابن للبيانات”: اجعل الحاوية خفيفة وتدير التفاعل العام، واجعل الابن مركزاً على مهمته الفردية وافتح له حرية استخدام وسائط ووردبريس.

الصورة الرمزية لـ Mohamed Abozaid

م. محمد أبو زيد

WordPress Architect & Enterprise Consultant

مهندس برمجيات متخصص في تطوير حلول ووردبريس المتقدمة، معمارية Block Themes، ومحرر Gutenberg، وتحسين الأداء لمواقع الشركات الكبرى في مصر والخليج العربي.


اترك تعليقاً