Shadow DOM و پیدایش Web Component ها

Shadow DOM و پیدایش Web Component ها

شاید بارها پیش اومده باشه که مثلا از تگ video در html5 استفاده کرده باشیم و برامون سوال شده باشه که چطور این کد:

<video width="400" controls/>

باعث نمایش همچین المانی تو صفحه وب میشه

دکمه های play و pause، تایمر ویدیو، صدا، full screen و .... از کجا اومدن؟ functionality اونها کجا پیاده سازی شده و استایل مربوط به این المان کجا نوشته شده؟ حتی توی inspector مرورگرمون هم اثری از اینها نیست و صرفا همون تگ <video> رو خواهیم دید:

پاسخ کوتاه به این سؤال می تونه این باشه که مرورگر اونها رو توی shadow DOM تعبیه کرده. تو این پست سعی دارم در مورد shadow DOM توضیحی بدم و باهم یه شبه کامپوننت رو بدون استفاده از هیچ فریمورکی و در واقع فقط  با کد جاوااسکریپت معمولی و مفهوم shadow DOM پیاده سازی کنیم.

اصلاً DOM چی هست؟

طبق تعریف W3C(World Wide Web Consortium) در واقع DOM  یا Document Object Model یک پلتفرم و واسط language-neutral (مسقل از زبان برنامه نویسی) هست که برنامه ها و اسکریپتها می تونن از طریق اون به محتوا، ساختار و استایل داکیومنت بصورت داینامیک دسترسی داشته باشند و اونها رو آپدیت کنند.

به زبان ساده تر زمانیکه مرورگر در حال بارگزاری صفحه ی ماست، یک سری کارها انجام میده که یکی از اونها ایجاد یک data model از صفحه هست. به این معنی که از المانهای html  که در صفحه داریم درختی از object ها میسازه که جاوااسکریپت می تونه به تمام چیزی هایی که برای ایجاد یک صفحه ی داینامیک نیاز داره دسترسی داشته باشه. استاندارد DOM در W3C به سه بخش کلی تقسیم میشه:

  • Core DOM: که به همه ی انواع داکیومنت مربوط میشه
  • XML DOM: که مربوط به داکیومنت های XML هست
  • HTML DOM: که برای اسناد HTML ای هست و در این پست می خوام در مورد این داکیومنت ها صحبت کنم

بواسطه ی DOM، جاوااسکریپت میتونه تمام المانهای html صفحه، همه ی attribute های HTML و همه ی استایلهای CSS رو تغییر بده و با event های HTML کار کنه.

یک مثال ساده

کد زیر رو در نظر بگیرید:

<html>
    <head>
        <meta name="viewport">
    </head>
    <body>
        <p>Click on the "x" symbol to delete the contact.</p>
        <div class="chip">
            <img src="joy-pic.jpg" alt="joy">
                Joy
            <span class="closebtn" onclick="this.parentElement.style.display='none'">&times;</span>
        </div>
    </body>
</html>

مرورگر صفحه رو بصورت درختی از آبجکتها (node ها) در نظر میگیره و آبجکت DOM رو از روی همچین درختی میسازه:

tree-chart

به همین سادگی :) حالا فرض کنید می خواید چندین مورد از این contact chip ها رو که توی مثال بالا آوردیم، در رنگها و طرحهای مختلف داشته باشیم.

یه راه اینه که همینطور این پنج خط کد رو کپی کنیم و هرجای صفحه که می خوایم paste  کنیم و برای تغییر رنگشون هم مدام یا کلاس جدید بهشون اضافه کنیم یا اینکه با سلکتورهای CSS ایه دیگه انتخابشون کنیم و رنگ موردنظر رو براشون اعمال کنیم. یه راهم اینه که اون رو به صورت یه shadow DOM در بیاریم و هر چیزی که لازم داره براش هندل بشه رو توی خودش هندل کنیم.

قبل از شروع توجه داشته باشید که shadow DOM با استفاده از جاوااسکریپت به DOM معمولی اضافه میشه و نمیشه اون رو با کدهای HTML به وجود آورد. به همین دلیل برای اضافه کردن shadow DOM به DOM نیاز به المانی داریم که نقش اصطلاحاً shadow host  رو بازی کنه. تو این مثال ما از خوده المان body بعنوان shadow host استفاده می کنیم. اولین قدم بعد از انتخاب shadow host ایجاد کلاس ContactChips مونه.

می تونیم کدهای js مربوط به کلاسمون رو تو فایل جدا بنویسیم و بعد تو صفحه مون بهش آدرس دهی کنیم:

class ContactChips extends HTMLElement {
    constructor() {
         super();
         // Insert rest of the code here          			
    }     			
}

این کد کلاس ContactChips رو میسازه که از HTMLElement ارث بری کرده. الان جاییه که ما میتونیم حوزه ی shadow DOM خودمون رو تعریف کنیم. اولین و پایه ای ترین المان در shadow DOM، المان shadow root هستش.

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

این کد یک shadow root خالی ایجاد میکنه. shadow root در واقع شروع یک shadow DOM جدیده، همونطور که المان شروع یک DOM معمولی محسوب میشه. می تونیم shadow root رو با استفاده از متد ()attachShadow به هر المانی که بخوایم attach کنیم. اینجا this.attachShadow نشون میده که می خواهیم به انتهای body اضافه بشه

این متد آبجکتی رو به عنوان پارامتر ورودی می گیره که یکی از property های آن mode و property دومdelegatesFocus هست که مربوط به focus المان در زمان کلیک روی shadow DOM میشه و البته فقط در کروم ساپورت میشه و هنوز استاندارد سازی نشده. mode: open نشان دهنده اجازه دسترسی به shadow DOM هست.

یه نکته ی جالب: اینکه ما mode رو close انتخاب کنیم یا open دست خودمونه البته mode: close به هیچ وجه توصیه نمیشه چون از خارج کد جاوااسکریپت نمیشه به DOM کامپوننت تون دسترسی داشت . این در واقع همون mode ای هست که المانهای native مثل <video> و <textarea> باهاش  کار می کنند. جاوااسکریپت نمیتونه به shadow DOM مربوط به تگ <video> دسترسی داشته باشه و ما  نمی تونیم ساختار داخلی اون رو توی inspector مرورگرمون ببینیم چون مرورگر اون رو با استفاده از closed-mode برای shadow root پیاده سازی کرده. حالا می خوایم المان موردنظرمون  رو که  به این صورت بود :

<div class="chip">
    <img src="2.jpg" alt="joy">
        Joy
    <span class="closebtn" onclick="this.parentElement.style.display='none'">&times;</span>
</div>

درون shadow DOM تعریف کنیم .

var wrapper = document.createElement('div');

اینجا المان div رو می سازیم که بعنوان wrapper بقیه ی المانها رو شامل میشه. حالا میایم تگ img رو اضافه می کنیم:

var avatar = document.createElement('img');

می تونیم این قابلیت رو در نظر بگیریم که کاربر بتونه هر عکسی که خودش خواست رو به عنوان آواتار به المان ما پاس بده و اگر چیزی نفرستاد یه آواتار بصورت پیش فرض نمایش بدیم. بعد اونو به wrapper اضافه می کنیم:

var imgUrl;
if(this.hasAttribute('img')) {
    imgUrl = this.getAttribute('img');
} else {
    imgUrl = '1.jpg';
}
avatar.src = imgUrl;
wrapper.appendChild(avatar);

حالا میریم یه قابلیت دیگه هم به المانمون اضافه می کنیم و اون دکمه ی delete برای حذف کل المان هستش. اول تگ span رو اضافه می کنیم و بعد برای event مربوطه  که click هستش متد موردنظرمون رو می نویسیم و این تگ رو هم به wrapper اضافه میکنیم:

var closebtn = document.createElement('span');
closebtn.innerHTML = `&times;`;
closebtn.onclick = () => closebtn.parentElement.style.display = 'none';
wrapper.appendChild(closebtn);

خب تا اینجا همه تگهای مورد نیاز رو اضافه کردیم. وقتشه که از تگ استایل استفاده کنیم و همه ی استایلهای مورد نیاز در درون خوده المان رو بنویسیم. لازمه اینجا یه نکته ی جالب بگم: وب کامپوننتها در واقع مجموعه ای از سه تکنولوژی هستن که با هم استفاده می شن:

یکی از مهمترین ایده ها یی که shadow DOM برای وب کامپوننت داشت مطرح کردن بحث scoped style ها بود که به مفهوم encapsulation در کامپوننت هم کمک بسزایی کرد. بیاید ببینیم چطور shadow DOM استایل رو پیاده سازی میکنه. اول تگ استایل رو ایجاد می کنیم:

var style = document.createElement('style')

و بعد کلاسهای مورد نظر رو درونش تعریف می کنیم:

style.textContent = `
.chip {
    display: inline-block;
    padding: 0 25px;
    height: 50px;
    font-size: 18px;
    line-height: 50px;
    border-radius: 25px;
    background-color: #f1f1f1;
}
.chip img {
    float: left;
    margin: 0 10px 0 -25px;
    height: 50px;
    width: 50px;
    border-radius: 50%;
}
.closebtn {
    padding-left: 10px;
    color: #888;
    font-weight: bold;
    float: right;
    font-size: 20px;
    cursor: pointer;
}
.closebtn:hover {
    color: #000;
}`;

بعد از تعریف استایلها باید مشخص کنیم هر استایل مربوط به کدوم تگ هست.

wrapper.setAttribute('class', 'chip');
closebtn.setAttribute('class', 'closebtn');

تا اینجا ما فقط یه سری تگ ایجاد کردیم و یه shadow root. حالا باید تگ wrapper و style رو به shadow root وصل کنیم تا shadow DOM مورد نظرمون کامل ساخته شه.

shadowRoot.appendChild(style);
shadowRoot.appendChild(wrapper);

اوکی shadow DOM مون ساخته شده و با کد زیر در دسترس و آماده ی بهره برداریه:

customElements.define('contact-chips', ContactChips);

اینجا با استفاده از متد ()define المانی که ساختیم رو ثبت می کنیم. متد define از آبجکت customElements برای تعریف و register یک custom elementاستفاده می کنه. برمی گردیم به کد html مثالمون و قسمت body رو به این صورت تغییر میدیم:

<body>
    <p> Click on the "x" symbol to delete the contact. </p>
    <contact-chips img="2.jpg"/>
</body>

با اجرای صفحه و بازکردن DevTools  (اگر از کروم استفاده می کنید) می تونیم  shadow DOM ای که ساختیم رو با استفاده از shadow-root# در قسمت  elements  ببینیم:

صحبت آخر

shadow DOM در کل مفهوم جدیدی نیست و مرورگرها مدتهاست که برای encapsulate  کردن ساختار داخلی یک المان از اون استفاده می کنند و اون رو در یک درخت DOM کاملا جدا تولید میکنن که اسمش shadow tree هست. طبق نظریه های ارائه شده درواقع بهتره که به application بعنوان مجموعه ای از تکه های DOM نگاه کنید و نه صرفاً یک page بزرگ. shadow DOM با مخفی کردن خصوصیات و استایل یک المان از المانهای دیگه،  مفهوم  Web Component و scoped styles را به وب پلتفرم معرفی کرده و یک سری از مشکلات CSS و DOM رو برطرف کرده.