Shadow DOM 301

مفاهیم پیشرفته و DOM API

این مقاله درباره کارهای شگفت‌انگیز بیشتری که می‌توانید با Shadow DOM انجام دهید بحث می‌کند! این مقاله براساس مفاهیم بحث‌شده در Shadow DOM 101 و Shadow DOM 201 نوشته شده است.

استفاده از چند ریشه سایه

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

بیایید ببینیم اگر بخواهیم چند ریشه سایه را به یک میزبان متصل کنیم چه اتفاقی می‌افتد:

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

آنچه ارائه می‌شود «Root 2 FTW» است، بااینکه قبلاً درخت سایه را پیوست کرده بودیم. دلیلش این است که آخرین درخت سایه اضافه‌شده به میزبان برنده می‌شود. این یک پشته LIFO است تا جایی که به رندر مربوط می‌شود. بررسی DevTools این رفتار را تأیید می‌کند.

So what's the point of using multiple shadows if only the last is invited to the rendering party? نقاط درج سایه را وارد کنید.

نقاط درج سایه

«نقاط درج سایه» (<shadow>) مشابه نقاط درج معمولی (<content>) هستند، زیرا جای‌بان هستند. اما به‌جای اینکه جای‌بانی برای محتوای میزبان باشند، میزبان درخت‌های سایه دیگر هستند. این Shadow DOM Inception است!

همان‌طور که احتمالاً می‌توانید تصور کنید، هرچه بیشتر در این موضوع عمیق شوید، مسائل پیچیده‌تر می‌شوند. به همین دلیل، مشخصات به‌وضوح توضیح می‌دهد که وقتی چندین عنصر <shadow> در بازی وجود دارد چه اتفاقی می‌افتد:

اگر به مثال اولیه‌مان برگردیم، اولین سایه root1 از فهرست دعوت حذف شد. افزودن نقطه درج <shadow> آن را برمی‌گرداند:

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

چند نکته جالب درباره این مثال وجود دارد:

  1. «Root 2 FTW» همچنان بالای «Root 1 FTW» ارائه می‌شود. دلیلش این است که نقطه درج <shadow> را کجا قرار داده‌ایم. اگر می‌خواهید برعکس شود، نقطه درج را حرکت دهید: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. توجه کنید که اکنون یک نقطه درج <content> در root1 وجود دارد. این کار باعث می‌شود گره نوشتاری «Light DOM» برای رندر کردن همراه شود.

چه چیزی در <shadow> ارائه می‌شود؟

گاهی اوقات دانستن درخت سایه قدیمی‌تر که در <shadow> ارائه می‌شود مفید است. می‌توانید ازطریق .olderShadowRoot به آن درخت ارجاع دهید:

**root2.olderShadowRoot** === root1 //true

دریافت ریشه سایه میزبان

اگر عنصری میزبان Shadow DOM باشد، می‌توانید بااستفاده از .shadowRoot به جدیدترین ریشه سایه آن دسترسی پیدا کنید:

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

اگر نگران هستید که افراد وارد سایه‌های شما شوند، .shadowRoot را به‌عنوان null بازتعریف کنید:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

کمی شبیه به هک است، اما کار می‌کند. درنهایت، مهم است به‌یاد داشته باشید که اگرچه Shadow DOM بسیار فوق‌العاده است، به‌عنوان یک ویژگی امنیتی طراحی نشده است. برای جداسازی کامل محتوا به آن اتکا نکنید.

ساختن Shadow DOM در JS

اگر ترجیح می‌دهید DOM را در JS بسازید، HTMLContentElement و HTMLShadowElement میاناهایی برای آن دارند.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

این مثال تقریباً مشابه مثال موجود در بخش قبلی است. تنها تفاوت این است که اکنون از select برای بیرون کشیدن <span> جدید اضافه‌شده استفاده می‌کنم.

کار کردن با نقاط درج

گره‌هایی که از عنصر میزبان انتخاب می‌شوند و در درخت سایه «توزیع» می‌شوند، گره‌های توزیع‌شده نامیده می‌شوند! آن‌ها مجازند از مرز سایه عبور کنند وقتی نقاط درج آن‌ها را به داخل دعوت می‌کنند.

آنچه از نظر مفهومی درباره نقاط درج عجیب است این است که آن‌ها DOM را به‌طور فیزیکی جابه‌جا نمی‌کنند. گره‌های میزبان دست‌نخورده باقی می‌مانند. نقاط درج صرفاً گره‌ها را از میزبان به درخت سایه بازطرح می‌کنند. این یک موضوع ارائه/پرداخت است: «این گره‌ها را به اینجا منتقل کنید» «این گره‌ها را در این مکان پرداخت کنید.»

برای مثال:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

بفرمایید! ‫h2 فرزند DOM سایه نیست. این ما را به نکته جالب دیگری می‌رساند:

Element.getDistributedNodes()

نمی‌توانیم به <content> برویم، اما میانای برنامه‌سازی کاربردی .getDistributedNodes() به ما اجازه می‌دهد گره‌های توزیع‌شده را در نقطه درج پُرسمان کنیم:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

مشابه .getDistributedNodes()، می‌توانید با فراخوانی .getDestinationInsertionPoints()، بررسی کنید که گره در چه نقاط درج توزیع شده است:

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

ابزار: Shadow DOM Visualizer

درک جادوی سیاه که همان Shadow DOM است دشوار است. یادم می‌آید که برای اولین‌بار سعی می‌کردم آن را درک کنم.

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

Shadow DOM Visualizer
راه‌اندازی «دیداری‌ساز DOM سایه»

آن را امتحان کنید و نظرتان را به من بگویید!

مدل رویداد

برخی رویدادها از مرز سایه عبور می‌کنند و برخی نه. در مواردی که رویدادها از مرز عبور می‌کنند، هدف رویداد تنظیم می‌شود تا کپسوله‌سازی‌ای که مرز بالایی ریشه سایه فراهم می‌کند حفظ شود. یعنی رویدادها دوباره هدف‌یابی می‌شوند تا به‌نظر برسد که از عنصر میزبان آمده‌اند نه از عناصر داخلی به Shadow DOM.

Play Action 1

  • این یکی جالب است. باید mouseout را از عنصر میزبان (<div data-host>) به گره آبی ببینید. اگرچه این یک گره توزیع‌شده است، اما هنوز در میزبان قرار دارد، نه در ShadowDOM. با حرکت دادن موشواره به پایین‌تر در بخش زرد، دوباره mouseout در گره آبی ایجاد می‌شود.

Play Action 2

  • یک mouseout وجود دارد که در میزبان (در انتهای آن) ظاهر می‌شود. معمولاً رویدادهای mouseout را می‌بینید که برای همه بلوک‌های زرد راه‌اندازی می‌شوند. اما در این مورد، این عناصر در Shadow DOM داخلی هستند و رویداد از مرز بالایی آن عبور نمی‌کند.

Play Action 3

  • توجه کنید که وقتی روی ورودی کلیک می‌کنید، focusin روی ورودی نشان داده نمی‌شود بلکه روی خود گره میزبان نشان داده می‌شود. هدف‌یابی مجدد شده است!

رویدادهایی که همیشه متوقف می‌شوند

رویدادهای زیر هرگز از مرز سایه عبور نمی‌کنند:

  • لغو کردن
  • خطا
  • انتخاب کردن
  • تغییر
  • بار
  • تنظیم مجدد
  • تغییر اندازه
  • حرکت
  • selectstart

نتیجه‌گیری

امیدوارم موافق باشید که Shadow DOM فوق‌العاده قدرتمند است. برای اولین‌بار، کپسوله‌سازی مناسبی بدون بار اضافی <iframe> یا سایر تکنیک‌های قدیمی‌تر داریم.

Shadow DOM is certainly complex beast, but it's a beast worth adding to the web platform. کمی وقت با آن بگذرانید. یاد بگیرید. سؤال بپرسید.

اگر می‌خواهید بیشتر بدانید، مقاله مقدماتی دومینیک Shadow DOM 101 و مقاله من Shadow DOM 201: CSS & Styling را ببینید.