Shadow DOM 101

مقدمة

مكوّنات الويب هي مجموعة من المعايير المتطوّرة التي:

  1. إتاحة إنشاء التطبيقات المصغّرة
  2. …التي يمكن إعادة استخدامها بشكل موثوق
  3. …وألا يؤدي إلى تعطُّل الصفحات إذا غيّر الإصدار التالي من المكوّن تفاصيل التنفيذ الداخلية.

هل يعني ذلك أنّ عليك تحديد حالات استخدام HTML/JavaScript، وحالات استخدام Web Components؟ لا يمكن استخدام HTML وJavaScript لإنشاء مواد مرئية تفاعلية. التطبيقات المصغّرة هي عناصر مرئية تفاعلية. من المنطقي الاستفادة من مهاراتك في HTML وJavaScript عند تطوير أداة. تم تصميم معايير Web Components لمساعدتك في تنفيذ ذلك.

ولكن هناك مشكلة أساسية تجعل من الصعب استخدام التطبيقات المصمّمة باستخدام HTML وJavaScript: لا يتم تغليف شجرة نموذج DOM داخل التطبيق عن بقية الصفحة. ويؤدي هذا النقص في الحزمة إلى أن يتم تطبيق ملف أسلوب المستند عن طريق الخطأ على أجزاء داخل التطبيق المصغّر، وقد يُعدِّل JavaScript أجزاءً داخل التطبيق المصغّر عن طريق الخطأ، وقد تتداخل أرقام التعريف الخاصة بك مع أرقام التعريف داخل التطبيق المصغّر، وهكذا.

تتألف Web Components من ثلاثة أجزاء:

  1. النماذج
  2. Shadow DOM
  3. العناصر المخصّصة

يعالج Shadow DOM مشكلة "تجميع شجرة نموذج DOM". تم تصميم الأجزاء الأربعة من Web Components للعمل معًا، ولكن يمكنك أيضًا اختيار أجزاء Web Components التي تريد استخدامها. يوضِّح لك هذا الدليل التمهيدي كيفية استخدام Shadow DOM.

مرحبًا، Shadow World

باستخدام Shadow DOM، يمكن للعناصر الحصول على نوع جديد من العقد المرتبطة بها. يُطلق على هذا النوع الجديد من العقد اسم جذر الظل. يُطلق على العنصر الذي يحتوي على جذر ظل مرتبط به اسم مضيف الظل. لا يتم عرض محتوى مضيف الظل، بل يتم عرض محتوى جذر الظل بدلاً من ذلك.

على سبيل المثال، إذا كان لديك علامات مثل هذا:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

ثم بدلاً من

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

شكل صفحتك

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

ليس هذا فقط، إذا طلبت JavaScript في الصفحة معرفة قيمة textContent للزر، لن تظهر القيمة “こんにちは、影の世界!”، بل "Hello, world!" لأنّ شجرة DOM الفرعية تحت جذر الظل تكون مُضمَّنة.

فصل المحتوى عن العرض التقديمي

سننظر الآن في استخدام Shadow DOM لفصل المحتوى عن طريقة العرض. لنفترض أنّ لدينا علامة الاسم هذه:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

في ما يلي الترميز. هذا هو ما ستكتبه اليوم. لا يتم استخدام Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

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

يمكننا تجنُّب حدوث أي مشكلة.

الخطوة 1: إخفاء تفاصيل العرض التقديمي

من الناحية الدلالية، لا يهمّنا سوى ما يلي:

  • إنها علامة اسم.
  • اسمي "بوب".

أولاً، نكتب علامات أقرب إلى الدلالات الحقيقية التي نريدها:

<div id="nameTag">Bob</div>

بعد ذلك، نضع جميع الأنماط وdivs المستخدَمة للعرض في عنصر <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

في هذه المرحلة، يتم عرض "بوبي" فقط. بما أنّنا نقلنا عناصر DOM للعرض التقديمي داخل عنصر <template>، لا يتم عرضها، ولكن يمكن الوصول إليها من JavaScript. نفعل ذلك الآن لتمتَلء جذر النسخة الاحتياطية:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

بعد أن أعددنا جذرًا للظل، يتم عرض علامة الاسم مجددًا. إذا نقرت بزر الماوس الأيمن على علامة الاسم وفحصت العنصر ، ستلاحظ أنّه تنسيق دلالي رائع:

<div id="nameTag">Bob</div>

يوضّح ذلك أنّنا أخفينا تفاصيل عرض علامة الاسم من المستند باستخدام Shadow DOM. يتم تضمين تفاصيل العرض في Shadow DOM.

الخطوة 2: فصل المحتوى عن العرض التقديمي

تخفي علامة الاسم الآن تفاصيل العرض من الصفحة، ولكنها لا تفصل العرض عن المحتوى، لأنّه على الرغم من أنّه يتوفر المحتوى (الاسم "عماد") في الصفحة، إلا أنّ الاسم الذي يتم عرضه هو الاسم الذي نسخناه إلى جذر الصورة الرمزية. إذا أردنا تغيير الاسم في علامة الاسم، علينا إجراء ذلك في مكانَين، وقد يؤدي ذلك إلى عدم تطابق الاسمَين.

عناصر HTML هي عناصر تركيبية، فيمكنك مثلاً وضع زر داخل جدول. نحتاج إلى تكوين: يجب أن تكون علامة الاسم مكوّنة من الخلفية الحمراء ونص "مرحبًا" والمحتوى المعروض على علامة الاسم.

يمكنك أنت، مؤلف المكوّن، تحديد كيفية عمل التركيب مع أداة العرض باستخدام عنصر جديد يُسمى <content>. يؤدي ذلك إلى إنشاء نقطة إدراج في عرض التطبيق المصغّر، وتعمل نقطة الإدراج على اختيار المحتوى من المضيف الظلّ لعرضه في تلك النقطة.

إذا غيّرنا علامات الترميز في Shadow DOM إلى ما يلي:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

عند عرض علامة الاسم، يتم إسقاط محتوى المضيف الظلّ في المكان الذي يظهر فيه عنصر<content>.

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

document.querySelector('#nameTag').textContent = 'Shellie';

هذا كل ما في الأمر. يعدّل المتصفّح تلقائيًا عرض علامة الاسم، لأنّنا نعرض محتوى علامة الاسم في مكانه باستخدام <content>.

<div id="ex2b">

لقد نجحنا الآن في فصل المحتوى عن العرض. يكون المحتوى في المستند، والعرض التقديمي في Shadow DOM. ويحافظ المتصفّح على مزامنتها تلقائيًا عند حلول وقت عرض محتوى معيّن.

الخطوة 3: تحقيق الربح

من خلال فصل المحتوى عن طريقة العرض، يمكننا تبسيط الرمز البرمجي الذي يعالج المحتوى. في مثال علامة الاسم، يحتاج الرمز البرمجي إلى التعامل فقط مع بنية بسيطة تحتوي على <div> واحد بدلاً من عدة علامات.

إذا غيّرنا العرض التقديمي، لن نحتاج إلى تغيير أي من الرمز البرمجي.

على سبيل المثال، لنفترض أنّنا نريد ترجمة علامة الاسم. ولا تزال هذه العلامة علامة اسم، لذا لا يتغيّر المحتوى الدلالي في المستند:

<div id="nameTag">Bob</div>

يظلّ رمز إعداد الجذر الظلّ كما هو. تغيّر المحتوى الذي يتم وضعه في جذر الصورة الرمزية:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

وهذا تحسين كبير مقارنةً بالوضع الحالي على الويب، لأنّه يمكن أن يعتمد رمز تعديل الاسم على بنية المكوّن البسيطة والمتسقة. لا يحتاج رمز تعديل اسمك إلى معرفة البنية المستخدَمة في عملية العرض. إذا أخذنا في الاعتبار ما يتم عرضه، يظهر الاسم ثانيًا باللغة الإنجليزية (بعد "مرحبًا. اسمي هو")، ولكن أولاً باللغة اليابانية (قبل "と申します"). هذا التمييز ليس له معنى من الناحية الدلالية من وجهة نظر تعديل الاسم المعروض، لذلك ليس من الضروري أن يعرف رمز تعديل الاسم هذه التفاصيل.

النقاط الإضافية: التوقّع المتقدّم

في المثال أعلاه، يختار العنصر <content> بعناية كل المحتوى من مضيف الظل. باستخدام سمة select، يمكنك التحكّم في نوع المحتوى الذي يعرضه عنصر المحتوى. يمكنك أيضًا استخدام عناصر محتوًى متعدّدة.

على سبيل المثال، إذا كان لديك مستند يحتوي على ما يلي:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

وجذر ظلّ يستخدم محددات CSS لاختيار محتوى معيّن:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

يتطابق العنصر <div class="email"> مع كل من العنصرَين <content select="div"> و<content select=".email">. كم مرة يظهر عنوان البريد الإلكتروني لـ "بدر"، وبأي ألوان؟

الإجابة هي أنّ عنوان البريد الإلكتروني لـ "بدر" يظهر مرة واحدة، وهو أصفر اللون.

والسبب هو أنّه، كما يعرف المتخصّصون في اختراق Shadow DOM، فإنّ إنشاء شجرة لما يتم عرضه فعليًا على الشاشة يشبه حفلة كبيرة. عنصر المحتوى هو الدعوة التي تسمح بنقل المحتوى من المستند إلى الطرف المعني بعرض Shadow DOM. يتم إرسال هذه الدعوات بالترتيب، ويعتمد مَن يتلقّى دعوة على الجهة المُرسَلة إليها (أي سمة select). بعد دعوة المحتوى، يقبل الدعوة دائمًا (من ذا الذي سيرفضها؟) ويُعرض على الفور. إذا تم إرسال دعوة لاحقة إلى هذا العنوان مرة أخرى، لن يحضر أحد إلى حفلتك.

في المثال أعلاه، يتطابق العنصر<div class="email"> مع كل من أداة الاختيارdiv وأداة الاختيار.email، ولكن بما أنّ عنصر المحتوى الذي يتضمّن أداة الاختيارdiv يقع في وقت سابق من المستند، ينتقل<div class="email"> إلى المجموعة الصفراء، ولا يتوفّر أحد للحضور إلى المجموعة الزرقاء. (قد يكون هذا هو سبب شعورك بالضيق، ولكن قد يكون السبب هو الشعور بالوحدة، لذلك لا يمكنك أبدًا معرفة السبب.)

إذا تمّت دعوة عنصر إلى أي مجموعات، لن يتم عرض العنصر على الإطلاق. هذا ما حدث للنص "Hello, world" في المثال الأول. يكون ذلك مفيدًا عندما تريد تحقيق عرض مختلف تمامًا: اكتب النموذج الدلالي في المستند، وهو ما يمكن للنصوص البرمجية في الصفحة الوصول إليه، ولكن أخفِه لأغراض العرض واربطه بنموذج عرض مختلف تمامًا في Shadow DOM باستخدام JavaScript.

على سبيل المثال، يتضمّن HTML أداة اختيار تاريخ رائعة. إذا كتبت <input type="date">، ستظهر لك نافذة منبثقة أنيقة للتقويم. ولكن ماذا لو أردت السماح للمستخدم باختيار نطاق تواريخ لعطلته على الجزيرة (مثلاً، مع أرجوحة شبكية مصنوعة من الكروم الأحمر). يمكنك إعداد المستند على النحو التالي:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

ولكن يمكنك إنشاء Shadow DOM يستخدم جدولاً لإنشاء تقويم أنيق يبرز نطاق التواريخ وما إلى ذلك. عندما ينقر المستخدم على الأيام في التقويم، يعدّل المكوّن الحالة في الإدخالين startDate وendDate. وعندما يرسل المستخدم النموذج، يتم إرسال القيم من عناصر الإدخال هذه.

لماذا أدرجت التصنيفات في المستند إذا لم يكن سيتم عرضها؟ والسبب في ذلك هو أنّه إذا عرض المستخدم النموذج باستخدام متصفّح لا يتيح استخدام Shadow DOM، سيظل النموذج قابلاً للاستخدام، ولكن ليس بدرجة الجمال نفسها. سيرى المستخدم ما يلي:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

اجتياز اختبار Shadow DOM 101

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

سنوضّح ذلك في المشاركات اللاحقة.