مكونات HTML قابلة لإعادة الاستخدام: بناء رؤوس وتذييلات احترافية لمواقع الويب

دقائق القراءة: 16

نصمم مواقع الويب باستمرار، وغالبًا ما نصادف سيناريوهات تتطلب تكرار أجزاء معينة من الكود عبر صفحات متعددة. تخيل أنك تبني موقعًا لعميل لديه متجر صغير يتكون من صفحتين فقط. عند الانتهاء من الصفحة الرئيسية والبدء في صفحة “اتصل بنا”، قد تميل ببساطة إلى إنشاء ملف HTML جديد ونسخ كل الكود من الصفحة الأولى. يبدو الرأس (Header) والتذييل (Footer) رائعين بالفعل، وكل ما عليك فعله هو تغيير المحتوى المتبقي.

ولكن ماذا لو طلب عميلك 10 صفحات؟ أو 20؟ وطلب تعديلات طفيفة على الرأس والتذييل خلال عملية التطوير؟ فجأة، أي تغيير، مهما كان صغيرًا، يجب تكراره عبر جميع هذه الملفات. هذه إحدى المشكلات الرئيسية التي تحلها أطر العمل مثل React أو مكتبات القوالب مثل Handlebars.js: حيث يمكن كتابة أي كود، خاصة العناصر الهيكلية مثل الرأس أو التذييل، مرة واحدة وإعادة استخدامه بسهولة في جميع أنحاء المشروع.

حتى وقت قريب، لم يكن من الممكن استخدام المكونات (Components) في بيئة HTML و JavaScript النقيتين (Vanilla HTML and JavaScript). ولكن مع تقديم Web Components، أصبح من الممكن إنشاء مكونات قابلة لإعادة الاستخدام دون الحاجة إلى أطر عمل معقدة مثل React.

ما هي مكونات الويب (Web Components)؟

مكونات الويب ليست تقنية واحدة، بل هي مجموعة من التقنيات المختلفة التي تمكنك من إنشاء عناصر HTML مخصصة وقابلة لإعادة الاستخدام. هذه التقنيات هي:

  • قوالب HTML (HTML Templates)

    تسمح لك بإنشاء أجزاء من ترميز HTML باستخدام عناصر <template> التي لن يتم عرضها حتى يتم إلحاقها بالصفحة باستخدام JavaScript. هذا يعني أنه يمكنك تعريف كود HTML الذي سيتم استخدامه لاحقًا دون أن يؤثر على عرض الصفحة الأولية.

  • العناصر المخصصة (Custom Elements)

    واجهات برمجة تطبيقات (APIs) مدعومة على نطاق واسع في JavaScript تتيح لك إنشاء عناصر DOM جديدة. بمجرد إنشاء عنصر مخصص وتسجيله باستخدام هذه الواجهات، يمكنك استخدامه بشكل مشابه لمكونات React، مما يوفر طريقة قوية لتوسيع HTML.

  • نموذج الكائن الظلي (Shadow DOM)

    نموذج كائن المستند (DOM) صغير ومغلف ومعزول عن DOM الرئيسي ويتم عرضه بشكل منفصل. أي أنماط (Styles) أو نصوص برمجية (Scripts) تنشئها لمكوناتك المخصصة داخل Shadow DOM لن تؤثر على العناصر الأخرى في DOM الرئيسي، مما يضمن التغليف (Encapsulation) التام.

سنتعمق في كل من هذه التقنيات بشكل أكبر خلال هذا الدليل.

كيفية استخدام قوالب HTML (HTML Templates)

الخطوة الأولى في فهم مكونات الويب هي تعلم كيفية استخدام قوالب HTML لإنشاء ترميز HTML قابل لإعادة الاستخدام. لنلقِ نظرة على مثال بسيط لرسالة ترحيب:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="style.css" rel="stylesheet" type="text/css"/>
    <script src="index.js" type="text/javascript" defer></script>
</head>
<body>
    <template id="welcome-msg">
        <h1>مرحباً بالعالم!</h1>
        <p>وكل من يسكنه</p>
    </template>
</body>
</html>

إذا نظرت إلى الصفحة، فلن يتم عرض عنصري <h1> أو <p>. ولكن إذا فتحت وحدة تحكم المطور (Dev Console)، فسترى أنه تم تحليل كلا العنصرين:

لقطة شاشة لوحدة تحكم المطور تظهر تحليل عناصر HTML داخل وسم القالب

لعرض رسالة الترحيب فعليًا، ستحتاج إلى استخدام القليل من JavaScript:

const template = document.getElementById('welcome-msg');
document.body.appendChild(template.content);

لقطة شاشة لصفحة الويب تعرض رسالة الترحيب بعد إلحاقها باستخدام JavaScript

على الرغم من أن هذا مثال بسيط للغاية، إلا أنه يمكنك بالفعل رؤية كيف يسهل استخدام القوالب (Templates) إعادة استخدام الكود في جميع أنحاء الصفحة. المشكلة الرئيسية هي أن كود رسالة الترحيب، في المثال الحالي على الأقل، مختلط مع بقية محتوى الصفحة. إذا أردت تغيير رسالة الترحيب لاحقًا، فستحتاج إلى تغيير الكود عبر ملفات متعددة.

بدلاً من ذلك، يمكنك سحب قالب HTML إلى ملف JavaScript، بحيث تقوم أي صفحة تتضمن ملف JavaScript هذا بعرض رسالة الترحيب:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="style.css" rel="stylesheet" type="text/css"/>
    <script src="index.js" type="text/javascript" defer></script>
</head>
<body>
</body>
</html>
const template = document.createElement('template');
template.innerHTML = `
    <h1>مرحباً بالعالم!</h1>
    <p>وكل من يسكنه</p>
`;
document.body.appendChild(template.content);

الآن بعد أن أصبح كل شيء في ملف JavaScript، لا تحتاج إلى إنشاء عنصر <template> – يمكنك بسهولة إنشاء <div> أو <span>. ومع ذلك، يمكن إقران عناصر <template> بعنصر <slot>، مما يسمح لك بتغيير النص للعناصر داخل <template>. هذا خارج نطاق هذا الدليل قليلاً، لذا يمكنك قراءة المزيد عن عناصر <slot> على موقع MDN.

كيفية إنشاء عناصر مخصصة (Custom Elements)

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

كعنصر مخصص، قد تبدو رسالة الترحيب الخاصة بك كما يلي:

<welcome-message></welcome-message>

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

الإعداد (Setup)

بالنسبة لموقع عرض أعمال (Portfolio Site)، قد يكون لديك بعض الكود الأساسي الذي يبدو كالتالي:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="style.css" rel="stylesheet" type="text/css"/>
</head>
<body>
    <main>
        <!-- محتوى صفحتك -->
    </main>
</body>
</html>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html, body {
    height: 100%;
}
body {
    color: #333;
    font-family: sans-serif;
    display: flex;
    flex-direction: column;
}
main {
    flex: 1 0 auto;
}

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

تحديد عنصر مخصص (Define a Custom Element)

أولاً، أنشئ مجلدًا باسم components وداخله، أنشئ ملفًا جديدًا باسم header.js بالكود التالي:

class Header extends HTMLElement {
    constructor() {
        super();
    }
}

هذا مجرد Class بسيط من ES5 يعلن عن مكون Header المخصص الخاص بك، مع دالة البناء constructor() والكلمة المفتاحية الخاصة super(). يمكنك قراءة المزيد عن هذه المفاهيم على MDN. من خلال توسيع الفئة العامة HTMLElement، يمكنك إنشاء أي نوع من العناصر التي تريدها. من الممكن أيضًا توسيع عناصر محددة مثل HTMLParagraphElement.

تسجيل عنصرك المخصص (Register Your Custom Element)

قبل أن تتمكن من البدء في استخدام عنصرك المخصص، ستحتاج إلى تسجيله باستخدام دالة customElements.define():

class Header extends HTMLElement {
    constructor() {
        super();
    }
}

customElements.define('header-component', Header);

تأخذ هذه الدالة وسيطتين على الأقل. الأولى هي سلسلة DOMString ستستخدمها عند إضافة المكون إلى الصفحة، في هذه الحالة، <header-component></header-component>. الثانية هي فئة المكون التي أنشأتها سابقًا، هنا، فئة Header. الوسيطة الثالثة الاختيارية تصف أي عنصر HTML موجود يرث منه عنصرك المخصص خصائصه، على سبيل المثال، {extends: 'p'}. لكننا لن نستخدم هذه الميزة في هذا الدليل.

استخدام دوال رد النداء لدورة الحياة (Lifecycle Callbacks) لإضافة الرأس إلى الصفحة

هناك أربع دوال رد نداء خاصة بدورة الحياة للعناصر المخصصة يمكننا استخدامها لإلحاق ترميز الرأس بالصفحة: connectedCallback، attributeChangeCallback، disconnectedCallback، و adoptedCallback. من بين هذه الدوال، connectedCallback هي واحدة من الأكثر استخدامًا. تعمل دالة connectedCallback في كل مرة يتم فيها إدراج عنصرك المخصص في نموذج كائن المستند (DOM). يمكنك قراءة المزيد عن دوال رد النداء الأخرى هنا.

بالنسبة لمثالنا البسيط، connectedCallback كافية لإضافة رأس إلى الصفحة:

class Header extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this.innerHTML = `
            <style>
                nav {
                    height: 40px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    background-color: #0a0a23;
                }
                ul {
                    padding: 0;
                }
                a {
                    font-weight: 700;
                    margin: 0 25px;
                    color: #fff;
                    text-decoration: none;
                }
                a:hover {
                    padding-bottom: 5px;
                    box-shadow: inset 0 -2px 0 0 #fff;
                }
            </style>
            <header>
                <nav>
                    <ul>
                        <li><a href="about.html">حول</a></li>
                        <li><a href="work.html">أعمال</a></li>
                        <li><a href="contact.html">اتصل بنا</a></li>
                    </ul>
                </nav>
            </header>
        `;
    }
}

customElements.define('header-component', Header);

ثم في ملف index.html، أضف السكريبت components/header.js والعنصر <header-component></header-component> فوق عنصر <main> مباشرة:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="style.css" rel="stylesheet" type="text/css"/>
    <script src="components/header.js" type="text/javascript" defer></script>
</head>
<body>
    <header-component></header-component>
    <main>
        <!-- محتوى صفحتك -->
    </main>
</body>
</html>

وهكذا، يجب أن يتم عرض مكون الرأس القابل لإعادة الاستخدام في الصفحة:

لقطة شاشة لصفحة ويب تعرض مكون رأس مخصص

الآن أصبح إضافة رأس إلى الصفحة سهلاً مثل إضافة وسم <script> يشير إلى components/header.js، وإضافة <header-component></header-component> أينما تريد.

لاحظ أنه نظرًا لأن الرأس وتنسيقه يتم إدراجهما مباشرة في DOM الرئيسي، فمن الممكن تنسيقه في ملف style.css. ولكن إذا نظرت إلى أنماط الرأس المضمنة في connectedCallback، فهي عامة جدًا، ويمكن أن تؤثر على التنسيقات الأخرى في الصفحة. على سبيل المثال، إذا أضفنا Font Awesome ومكون تذييل إلى index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA==" crossorigin="anonymous"/>
    <link href="style.css" rel="stylesheet" type="text/css"/>
    <script src="components/header.js" type="text/javascript" defer></script>
    <script src="components/footer.js" type="text/javascript" defer></script>
</head>
<body>
    <header-component></header-component>
    <main>
        <!-- محتوى صفحتك -->
    </main>
    <footer-component></footer-component>
</body>
</html>
class Footer extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this.innerHTML = `
            <style>
                footer {
                    height: 60px;
                    padding: 0 10px;
                    list-style: none;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    background-color: #dfdfe2;
                }
                ul li {
                    list-style: none;
                    display: inline;
                }
                a {
                    margin: 0 15px;
                    color: inherit;
                    text-decoration: none;
                }
                a:hover {
                    padding-bottom: 5px;
                    box-shadow: inset 0 -2px 0 0 #333;
                }
                .social-row {
                    font-size: 20px;
                }
                .social-row li a {
                    margin: 0 15px;
                }
            </style>
            <footer>
                <ul>
                    <li><a href="about.html">حول</a></li>
                    <li><a href="work.html">أعمال</a></li>
                    <li><a href="contact.html">اتصل بنا</a></li>
                </ul>
                <ul class="social-row">
                    <li><a href="https://github.com/my-github-profile"><i class="fab fa-github"></i></a></li>
                    <li><a href="https://twitter.com/my-twitter-profile"><i class="fab fa-twitter"></i></a></li>
                    <li><a href="https://www.linkedin.com/in/my-linkedin-profile"><i class="fab fa-linkedin"></i></a></li>
                </ul>
            </footer>
        `;
    }
}

customElements.define('footer-component', Footer);

إليك كيف ستبدو الصفحة:

لقطة شاشة لصفحة ويب تعرض مكون رأس وتذييل مخصصين مع تضارب في الأنماط

تتجاوز الأنماط من مكون التذييل أنماط الرأس، مما يغير لون الروابط. هذا سلوك متوقع لـ CSS، ولكن سيكون من الجيد لو كانت أنماط كل مكون مقتصرة على هذا المكون، ولا تؤثر على الأشياء الأخرى في الصفحة. حسنًا، هذا هو بالضبط حيث يتألق Shadow DOM.

كيفية استخدام نموذج الكائن الظلي (Shadow DOM) مع العناصر المخصصة

يعمل Shadow DOM كنسخة منفصلة وأصغر من DOM الرئيسي. بدلاً من أن يكون مجرد نسخة من DOM الرئيسي، يشبه Shadow DOM شجرة فرعية (subtree) خاصة بعنصرك المخصص فقط. أي شيء يتم إضافته إلى Shadow DOM، خاصة الأنماط، يكون مقتصرًا على ذلك العنصر المخصص المحدد. بطريقة ما، يشبه استخدام const و let بدلاً من var في JavaScript.

لنبدأ بإعادة هيكلة مكون الرأس (Header component):

const headerTemplate = document.createElement('template');
headerTemplate.innerHTML = `
    <style>
        nav {
            height: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
            background-color: #0a0a23;
        }
        ul {
            padding: 0;
        }
        ul li {
            list-style: none;
            display: inline;
        }
        a {
            font-weight: 700;
            margin: 0 25px;
            color: #fff;
            text-decoration: none;
        }
        a:hover {
            padding-bottom: 5px;
            box-shadow: inset 0 -2px 0 0 #fff;
        }
    </style>
    <header>
        <nav>
            <ul>
                <li><a href="about.html">حول</a></li>
                <li><a href="work.html">أعمال</a></li>
                <li><a href="contact.html">اتصل بنا</a></li>
            </ul>
        </nav>
    </header>
`;

class Header extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        // سيتم إضافة الكود هنا لاحقًا
    }
}

customElements.define('header-component', Header);

أول شيء تحتاج إلى فعله هو استخدام دالة .attachShadow() لإلحاق جذر ظلي (shadow root) بعنصر مكون الرأس المخصص الخاص بك. في دالة connectedCallback()، أضف الكود التالي:

class Header extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'closed' });
    }
}

customElements.define('header-component', Header);

لاحظ أننا نمرر كائنًا إلى دالة .attachShadow() مع خيار، mode: 'closed'. هذا يعني ببساطة أن Shadow DOM لمكون الرأس غير قابل للوصول من JavaScript الخارجي. إذا كنت ترغب في التلاعب بـ Shadow DOM لمكون الرأس لاحقًا باستخدام JavaScript خارج ملف components/header.js، فما عليك سوى تغيير الخيار إلى mode: 'open'.

أخيرًا، ألحق shadowRoot بالصفحة باستخدام دالة .appendChild():

class Header extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'closed' });
        shadowRoot.appendChild(headerTemplate.content);
    }
}

customElements.define('header-component', Header);

والآن، نظرًا لأن أنماط مكون الرأس مغلفة (encapsulated) في Shadow DOM الخاص به، يجب أن تبدو الصفحة كما يلي:

لقطة شاشة لصفحة ويب تعرض مكون رأس مع أنماط معزولة باستخدام Shadow DOM

وإليك مكون التذييل (Footer component) بعد إعادة هيكلته لاستخدام Shadow DOM:

const footerTemplate = document.createElement('template');
footerTemplate.innerHTML = `
    <style>
        footer {
            height: 60px;
            padding: 0 10px;
            list-style: none;
            display: flex;
            flex-shrink: 0;
            justify-content: space-between;
            align-items: center;
            background-color: #dfdfe2;
        }
        ul {
            padding: 0;
        }
        ul li {
            list-style: none;
            display: inline;
        }
        a {
            margin: 0 15px;
            color: inherit;
            text-decoration: none;
        }
        a:hover {
            padding-bottom: 5px;
            box-shadow: inset 0 -2px 0 0 #333;
        }
        .social-row {
            font-size: 20px;
        }
        .social-row li a {
            margin: 0 15px;
        }
    </style>
    <footer>
        <ul>
            <li><a href="about.html">حول</a></li>
            <li><a href="work.html">أعمال</a></li>
            <li><a href="contact.html">اتصل بنا</a></li>
        </ul>
        <ul class="social-row">
            <li><a href="https://github.com/my-github-profile"><i class="fab fa-github"></i></a></li>
            <li><a href="https://twitter.com/my-twitter-profile"><i class="fab fa-twitter"></i></a></li>
            <li><a href="https://www.linkedin.com/in/my-linkedin-profile"><i class="fab fa-linkedin"></i></a></li>
        </ul>
    </footer>
`;

class Footer extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'closed' });
        shadowRoot.appendChild(footerTemplate.content);
    }
}

customElements.define('footer-component', Footer);

ولكن إذا تحققت من الصفحة، ستلاحظ أن أيقونات Font Awesome مفقودة الآن:

لقطة شاشة لصفحة ويب تعرض مكون التذييل مع أيقونات Font Awesome مفقودة

الآن بعد أن أصبح مكون التذييل مغلفًا داخل Shadow DOM الخاص به، لم يعد لديه وصول إلى رابط CDN الخاص بـ Font Awesome في ملف index.html. دعنا نلقي نظرة سريعة على سبب ذلك، وكيفية إعادة تشغيل Font Awesome.

التغليف و Shadow DOM

بينما يمنع Shadow DOM أنماط مكوناتك من التأثير على بقية الصفحة، إلا أن بعض الأنماط العامة (Global Styles) يمكن أن تتسرب (leak through) إلى مكوناتك. في الأمثلة أعلاه، كانت هذه ميزة مفيدة. على سبيل المثال، يرث مكون التذييل تعريف color: #333 الذي تم تعيينه في ملف style.css. هذا لأن color هي إحدى الخصائص القابلة للتوريث (inheritable properties) القليلة، جنبًا إلى جنب مع font، font-family، direction، والمزيد.

إذا كنت ترغب في منع هذا السلوك، وتنسيق كل مكون من الصفر تمامًا، يمكنك القيام بذلك ببضعة أسطر من CSS:

:host {
    all: initial;
    display: block;
}

:host هو محدد زائف (pseudo-selector) يحدد العنصر الذي يستضيف Shadow DOM. في هذه الحالة، هو مكونك المخصص. ثم يعيد تعريف all: initial جميع خصائص CSS إلى قيمتها الأولية. ويقوم display: block بنفس الشيء لخاصية display، ويعيدها إلى القيمة الافتراضية للمتصفح، وهي block. للحصول على قائمة كاملة بخصائص CSS القابلة للتوريث، راجع هذه الإجابة على Stack Overflow.

كيفية استخدام Font Awesome مع Shadow DOM

قد تفكر الآن، إذا كانت خصائص CSS المتعلقة بالخطوط مثل font و font-family هي خصائص قابلة للتوريث، فلماذا لا يتم تحميل Font Awesome الآن بعد أن أصبح مكون التذييل يستخدم Shadow DOM؟

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

ملاحظة: لا تزال جميع هذه الطرق تتطلب تضمين Font Awesome في ملف index.html باستخدام عنصر <link> كما هو الحال في مقتطفات الكود أعلاه.

1. الربط بـ Font Awesome داخل مكونك

الطريقة الأكثر وضوحًا لجعل Font Awesome يعمل في مكون Shadow DOM الخاص بك هي تضمين رابط إليه داخل المكون نفسه:

const footerTemplate = document.createElement('template');
footerTemplate.innerHTML = `
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA==" crossorigin="anonymous"/>
    <style>
        footer {
            height: 60px;
            padding: 0 10px;
            list-style: none;
            /* ... بقية الأنماط ... */
        }
        /* ... بقية الأنماط ... */
    </style>
    <footer>
        <ul>
            <li><a href="about.html">حول</a></li>
            <li><a href="work.html">أعمال</a></li>
            <li><a href="contact.html">اتصل بنا</a></li>
        </ul>
        <ul class="social-row">
            <li><a href="https://github.com/my-github-profile"><i class="fab fa-github"></i></a></li>
            <li><a href="https://twitter.com/my-twitter-profile"><i class="fab fa-twitter"></i></a></li>
            <li><a href="https://www.linkedin.com/in/my-linkedin-profile"><i class="fab fa-linkedin"></i></a></li>
        </ul>
    </footer>
`;

شيء واحد يجب ملاحظته هو أنه بينما يبدو أنك تتسبب في تحميل المتصفح لـ Font Awesome مرتين (مرة واحدة لـ DOM الرئيسي ومرة أخرى للمكون)، فإن المتصفحات ذكية بما يكفي لعدم جلب نفس المورد مرة أخرى. إليك علامة تبويب الشبكة (Network tab) التي تظهر أن Chrome يجلب Font Awesome مرة واحدة فقط:

لقطة شاشة لعلامة تبويب الشبكة في أدوات المطور تظهر تحميل Font Awesome مرة واحدة فقط

2. استيراد Font Awesome داخل مكونك

بعد ذلك، يمكنك استخدام @import و url() لتحميل Font Awesome إلى مكونك:

const footerTemplate = document.createElement('template');
footerTemplate.innerHTML = `
    <style>
        @import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css");
        footer {
            height: 60px;
            padding: 0 10px;
            list-style: none;
            /* ... بقية الأنماط ... */
        }
        /* ... بقية الأنماط ... */
    </style>
    <footer>
        <ul>
            <li><a href="about.html">حول</a></li>
            <li><a href="work.html">أعمال</a></li>
            <li><a href="contact.html">اتصل بنا</a></li>
        </ul>
        <ul class="social-row">
            <li><a href="https://github.com/my-github-profile"><i class="fab fa-github"></i></a></li>
            <li><a href="https://twitter.com/my-twitter-profile"><i class="fab fa-twitter"></i></a></li>
            <li><a href="https://www.linkedin.com/in/my-linkedin-profile"><i class="fab fa-linkedin"></i></a></li>
        </ul>
    </footer>
`;

لاحظ أن عنوان URL يجب أن يكون هو نفسه الذي تستخدمه في ملف index.html.

3. استخدام JavaScript لتحميل Font Awesome ديناميكيًا إلى مكونك

أخيرًا، الطريقة الأكثر جفافًا (DRYest) لتحميل Font Awesome داخل مكونك هي استخدام القليل من JavaScript:

class Footer extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        // البحث عن Font Awesome في DOM الرئيسي
        const fontAwesome = document.querySelector('link[href*="font-awesome"]');
        const shadowRoot = this.attachShadow({ mode: 'closed' });

        // تحميل Font Awesome إلى المكون بشكل شرطي
        if (fontAwesome) {
            shadowRoot.appendChild(fontAwesome.cloneNode());
        }
        shadowRoot.appendChild(footerTemplate.content);
    }
}

customElements.define('footer-component', Footer);

تعتمد هذه الطريقة على هذه الإجابة على Stack Overflow، وتعمل ببساطة شديدة. عندما يتم تحميل المكون، إذا كان عنصر <link> يشير إلى Font Awesome موجودًا، فإنه يتم نسخه وإلحاقه بـ Shadow DOM للمكون:

لقطة شاشة لصفحة ويب تعرض أيقونات Font Awesome تعمل بشكل صحيح بعد تحميلها ديناميكيًا

الكود النهائي

إليك كيف يبدو الكود النهائي عبر جميع الملفات، باستخدام الطريقة الثالثة لتحميل Font Awesome في مكون التذييل:

ملف index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA==" crossorigin="anonymous"/>
    <link href="style.css" rel="stylesheet" type="text/css"/>
    <script src="components/header.js" type="text/javascript" defer></script>
    <script src="components/footer.js" type="text/javascript" defer></script>
</head>
<body>
    <header-component></header-component>
    <main>
        <!-- محتوى صفحتك -->
    </main>
    <footer-component></footer-component>
</body>
</html>

ملف style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html, body {
    height: 100%;
}
body {
    color: #333;
    font-family: sans-serif;
    display: flex;
    flex-direction: column;
}
main {
    flex: 1 0 auto;
}

ملف components/header.js

const headerTemplate = document.createElement('template');
headerTemplate.innerHTML = `
    <style>
        nav {
            height: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
            background-color: #0a0a23;
        }
        ul {
            padding: 0;
        }
        ul li {
            list-style: none;
            display: inline;
        }
        a {
            font-weight: 700;
            margin: 0 25px;
            color: #fff;
            text-decoration: none;
        }
        a:hover {
            padding-bottom: 5px;
            box-shadow: inset 0 -2px 0 0 #fff;
        }
    </style>
    <header>
        <nav>
            <ul>
                <li><a href="about.html">حول</a></li>
                <li><a href="work.html">أعمال</a></li>
                <li><a href="contact.html">اتصل بنا</a></li>
            </ul>
        </nav>
    </header>
`;

class Header extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'closed' });
        shadowRoot.appendChild(headerTemplate.content);
    }
}

customElements.define('header-component', Header);

ملف components/footer.js

const footerTemplate = document.createElement('template');
footerTemplate.innerHTML = `
    <style>
        footer {
            height: 60px;
            padding: 0 10px;
            list-style: none;
            display: flex;
            flex-shrink: 0;
            justify-content: space-between;
            align-items: center;
            background-color: #dfdfe2;
        }
        ul {
            padding: 0;
        }
        ul li {
            list-style: none;
            display: inline;
        }
        a {
            margin: 0 15px;
            color: inherit;
            text-decoration: none;
        }
        a:hover {
            padding-bottom: 5px;
            box-shadow: inset 0 -2px 0 0 #333;
        }
        .social-row {
            font-size: 20px;
        }
        .social-row li a {
            margin: 0 15px;
        }
    </style>
    <footer>
        <ul>
            <li><a href="about.html">حول</a></li>
            <li><a href="work.html">أعمال</a></li>
            <li><a href="contact.html">اتصل بنا</a></li>
        </ul>
        <ul class="social-row">
            <li><a href="https://github.com/my-github-profile"><i class="fab fa-github"></i></a></li>
            <li><a href="https://twitter.com/my-twitter-profile"><i class="fab fa-twitter"></i></a></li>
            <li><a href="https://www.linkedin.com/in/my-linkedin-profile"><i class="fab fa-linkedin"></i></a></li>
        </ul>
    </footer>
`;

class Footer extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        const fontAwesome = document.querySelector('link[href*="font-awesome"]');
        const shadowRoot = this.attachShadow({ mode: 'closed' });
        if (fontAwesome) {
            shadowRoot.appendChild(fontAwesome.cloneNode());
        }
        shadowRoot.appendChild(footerTemplate.content);
    }
}

customElements.define('footer-component', Footer);

في الختام

لقد غطينا الكثير في هذا الدليل، وقد تكون قد قررت بالفعل استخدام React أو Handlebars.js بدلاً من ذلك. وكلاهما خيارات رائعة! ومع ذلك، بالنسبة للمشاريع الأصغر التي ستحتاج فيها فقط إلى عدد قليل من المكونات القابلة لإعادة الاستخدام، قد تكون مكتبة كاملة أو لغة قوالب مبالغة. نأمل أن يكون لديك الآن الثقة لإنشاء مكونات HTML القابلة لإعادة الاستخدام الخاصة بك. انطلق الآن وأنشئ شيئًا رائعًا (وقابل لإعادة الاستخدام)!

الخلاصة التقنية

تُقدم مكونات الويب (Web Components) حلاً قويًا ومرنًا لتحدي إعادة استخدام الكود في تطوير الويب الحديث، خاصةً للمشاريع التي لا تتطلب تعقيد أطر عمل JavaScript الكبيرة. من خلال دمج قوالب HTML والعناصر المخصصة و Shadow DOM، يمكن للمطورين إنشاء مكونات معزولة ومغلفة تمامًا، مما يضمن أن الأنماط والسلوكيات الخاصة بالمكون لا تتسرب أو تتضارب مع بقية DOM. هذا النهج يعزز قابلية الصيانة، ويقلل من الأخطاء المحتملة، ويحسن كفاءة التطوير بشكل كبير. على الرغم من أن التعامل مع الأصول الخارجية مثل Font Awesome داخل Shadow DOM يتطلب بعض الاعتبارات الإضافية، إلا أن الحلول المقدمة بسيطة وفعالة، مما يجعل Web Components خيارًا ممتازًا لبناء واجهات مستخدم معيارية وقابلة للتطوير.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *