سایه DOM 101

معرفی

Web Components مجموعه ای از استانداردهای پیشرفته است که:

  1. امکان ساخت ویجت ها را فراهم کنید
  2. ... که می تواند به طور قابل اعتماد دوباره استفاده شود
  3. ... و اگر نسخه بعدی مؤلفه جزئیات پیاده سازی داخلی را تغییر دهد، صفحات را شکسته نمی شود.

آیا این بدان معناست که شما باید تصمیم بگیرید که چه زمانی از HTML/JavaScript استفاده کنید و چه زمانی از Web Components استفاده کنید؟ نه! HTML و جاوا اسکریپت می توانند چیزهای بصری تعاملی ایجاد کنند. ویجت ها چیزهای بصری تعاملی هستند. استفاده از مهارت های HTML و جاوا اسکریپت در هنگام توسعه یک ویجت منطقی است. استانداردهای Web Components برای کمک به شما در انجام این کار طراحی شده اند.

اما یک مشکل اساسی وجود دارد که استفاده از ویجت‌های ساخته شده از HTML و جاوا اسکریپت را سخت می‌کند: درخت DOM درون یک ویجت از بقیه صفحه کپسوله نشده است. این عدم کپسوله‌سازی به این معنی است که شیوه نامه سند شما ممکن است به طور تصادفی روی قسمت‌های داخل ویجت اعمال شود. جاوا اسکریپت شما ممکن است به طور تصادفی قسمت های داخل ویجت را تغییر دهد. شناسه های شما ممکن است با شناسه های داخل ویجت همپوشانی داشته باشند. و غیره

اجزای وب از سه بخش تشکیل شده است:

  1. قالب ها
  2. سایه DOM
  3. عناصر سفارشی

Shadow DOM به مشکل کپسوله سازی درخت DOM می پردازد. چهار بخش Web Components به گونه ای طراحی شده اند که با هم کار کنند، اما شما همچنین می توانید انتخاب کنید که کدام بخش از Web Components استفاده کنید. این آموزش نحوه استفاده از Shadow DOM را به شما نشان می دهد.

سلام دنیای سایه

با 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>

نه تنها این، اگر جاوا اسکریپت در صفحه بپرسد که textContent دکمه چیست، "こんにちは、影の世界!"، "سلام، دنیا!" زیرا زیردرخت 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>

از آنجایی که درخت DOM فاقد کپسوله سازی است، کل ساختار تگ نام در معرض سند قرار می گیرد. اگر عناصر دیگر در صفحه به طور تصادفی از نام‌های کلاس یکسان برای استایل یا اسکریپت استفاده کنند، روزگار بدی خواهیم داشت.

ما می توانیم از گذراندن اوقات بد جلوگیری کنیم.

مرحله 1: پنهان کردن جزئیات ارائه

از نظر معنایی ما احتمالا فقط به این اهمیت می دهیم:

  • این یک برچسب نام است.
  • نام "باب" است.

ابتدا، نشانه گذاری می نویسیم که به معنای واقعی مورد نظر ما نزدیک تر است:

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

سپس تمام استایل ها و div های مورد استفاده برای ارائه را در یک عنصر <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> منتقل کردیم، آنها رندر نمی شوند، اما می توان از جاوا اسکریپت به آنها دسترسی داشت. اکنون این کار را برای پر کردن ریشه سایه انجام می دهیم:

<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> cherry تمام محتوا را از میزبان سایه انتخاب می کند. با استفاده از ویژگی 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"> به مهمانی زرد، و هیچ کس برای آمدن به مهمانی آبی در دسترس نیست. (شاید به همین دلیل است که آنقدر آبی است، اگرچه بدبختی عاشق شرکت است، بنابراین هرگز نمی دانید.)

اگر چیزی به هیچ مهمانی دعوت نشود، اصلا رندر نمی شود. این همان چیزی است که برای متن "سلام، جهان" در اولین مثال اتفاق افتاد. این زمانی مفید است که می‌خواهید به یک رندر کاملاً متفاوت دست یابید: مدل معنایی را در سند بنویسید، که همان چیزی است که برای اسکریپت‌های صفحه قابل دسترسی است، اما آن را برای اهداف رندر پنهان کنید و آن را به یک مدل رندر واقعا متفاوت در Shadow DOM متصل کنید. با استفاده از جاوا اسکریپت

برای مثال، 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 101 را پاس می کنید! با Shadow DOM می‌توانید کارهای بیشتری انجام دهید، به عنوان مثال، می‌توانید از چندین سایه در یک میزبان سایه یا سایه‌های تودرتو برای کپسوله‌سازی استفاده کنید، یا صفحه خود را با استفاده از Model-Driven Views (MDV) و Shadow DOM معماری کنید. و اجزای وب چیزی بیش از Shadow DOM هستند.

در پست های بعدی به توضیح آن ها می پردازیم.