Web Components مجموعهای از تکنولوژیهایی میباشند که امکان ساخت المانهای سفارشی با قابلیت استفادهی مجدد و به همراه کپسولهسازی ساختار، استایل و عاملیت (Functionality) متناظر با المان ایجاد شده را در اختیار شما قرار میدهد.
در این سری چند قسمتی، ابتدا روش ساخت Web Components را بدون استفاده از ابزار خاصی بررسی کرده و در ادامه با معرفی Stenciljs، چند کامپوننت سفارشی را طراحی خواهیم کرد.
سه تکنولوژی اصلی مورد استفاده برای ساخت Web Components عبارتند از:
- Custom Elements
- Shadow DOM
- HTML Templates
Custom Elements
مجموعهای از APIهای جاوااسکریپتی هستند که امکان تعریف یکسری المان معتبر HTML را با قالب، رفتار و نام سفارشی، فراهم میکنند. علاوه بر اینکه میتوان یک المان سفارشی مستقل (Autonomous custom elements) را تعریف کرد، امکان سفارشیسازی و گسترش المانهای موجود (Customized built-in elements) را نیز خواهیم داشت.
المانهای سفارشی تعریف شده را مانند کامپوننتهای Angular و یا React تصور کنید؛ با این تفاوت که هیچ وابستگی به Angular و یا React ندارند. همچنین امکان استفاده از آنها در انوع و اقسام فریمورکهای فرانت-اند وجود دارد.
شکل سادهی یک Custom Element، متشکل است از کلاسی که کلاس HTMLElement را گسترش میدهد و یک نام یکتا که باید حتما دارای یک «-» باشد (kebab-case). برای مثال:
//app.component.js class AppComponent extends HTMLElement { static is = 'app-root' connectedCallback(){ this.innerHTML=`<h1>Hello World!</h1>` } } customElements.define(AppComponent.is, AppComponent)
//index.html <app-root></app-root>
در تکه کد بالا، از متد connectedCallback به عنوان یکی از متدهای چرخهی حیات یک المان سفارشی، برای تنظیم innerHTML آن استفاده شده است. سپس با استفاده متد define مربوط به CustomElementRegistry که از طریق window.customeElements قابل دسترسی میباشد و با مشخص کردن نام و کلاس مرتبط، المان مورد نظر را ثبت و معرفی کردیم.
برای سفارشیسازی یک المان موجود، کار با ارثبری از کلاس متناظر با آن المان شروع میشود. به عنوان مثال در اینجا قصد داریم با کلیک بر روی لینکهای موجود در صفحه و قبل از هدایت به آدرس مقصد، یک تأییدیه را از کاربر بگیریم:
//confirm-link.component.js class ConfirmLinkComponent extends HTMLAnchorElement { static is = "confim-link"; connectedCallback() { this.addEventListener("click", e => { if (!confirm("Are you sure?")) { e.preventDefault(); } }); } } customElements.define(ConfirmLinkComponent.is, ConfirmLinkComponent, { extends: "a" });
<a href="http://google.com" is='confirm-link'> google.com </a>
Shadow DOM
جنبهی بسیار مهم Web Components، کپسولهسازی میباشد که امکان مخفی کردن ساختار، استایل و رفتار متناظر با المان سفارشی را مهیا میکند تا با سایر قسمتهای کد تداخلی نداشته باشد. در این میان Shadow DOM نقش اصلی را برای رسیدن به این سطح از کپسولهسازی ایفا خواهد کرد. Shadow DOM به عنوان یک نسخهی کپسوله شدهی DOM میباشد که امکان کپسولهسازی Markup & Styling و همچنین کاهش پیچیدگی استایلدهی را مهیا خواهد کرد. میتوان Shadow DOM را همانند iframe تصور کرد که امکان نشتی استایلها و سلکتورها از Light DOM (همان DOM اصلی) به داخل و از داخل به Light DOM وجود ندارد؛ ولی برخلاف iframe همچنان Shadow Rootهای (المان ریشه Shadow DOM) متناظر، در همان کانتکست DOM اصلی قرار دارند و همچنین در اینجا برای دسترسی به مقادیر المانهای موجود در Shadow DOM، میتوان یک API مناسب را در سطح المان سفارشی در نظر گرفت، ولی در مقابل برای همین کار در یک iframe نیاز است تا DOM متناظر با آن را Traverse کنیم که البته به عمد به این صورت طراحی شده است؛ چرا که اهداف دیگری را نیز دنبال میکند.
به تصویر زیر توجه نمائید:
برای مشاهدهی Shadow DOM متناظر با یکسری المان توکار مانند input.range یا input.date در مرورگر کروم، لازم است به قسمت Developer tools آن مراجعه کرده و سپس با فشردن دکمهی F1 به قسمت تنظیمات آن مراجعه کرده و در قسمت Preferences آن و بخش Elements گزینه "Show user agent shadow DOM" را انتخاب کنید. در اینجا input به عنوان المانی که Shadow DOM به آن متصل شده (attach) نقش Shadow Host را ایفا میکند.
برای اتصال یک Shadow DOM به یک المان، به شکل زیر میتوان عمل کرد:
var div = document.createElement('div'); var shadowRoot = div.attachShadow({mode: 'open'}); //or mode: 'closed' shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
نکته: 'user-agent' نیز حالتی است که برای المانهای توکار مانند input.range و ... مورد استفاده قرار میگیرد و رفتاری شبیه به حالت 'closed' را دارد.
نکته: امکان اتصال Shadow DOM به المانهای خاصی از جمله div و یا المانهای سفارشی که کلاس HTMLElement را گسترش میدهند (این اتصال در زمان پیادهسازی آنها انجام خواهد شد)، وجود دارد.
در زمان استفاده از متد attachShadow، علاوه بر امکان تعیین حالت آن، امکان تنظیم delegatesFocus برای برطرف کردن موضوعات مرتبط با focus در زمان توسعهی المانهای سفارشی مورد استفاده قرار میگیرد. به این صورت که اگر در قسمتی از المان سفارشی که خاصیت و قابلیت focus را ندارد، کلیک شود، focus به اولین المان با قابلیت focus داخل المان سفارشی خواهد رسید و از این پس امکان استایلدهی با استفاده از سلکتور custom-element:focus را خواهیم داشت.
مفهوم دیگری وجود دارد تحت عنوان Shadow Boundary که تعیین کنندهی مرز بین Light DOM و Shadow DOM و همان لایهی مهیا کنندهی کپسولهسازی Styling و Markup میباشد. در مطالب آتی خواهیم دید که به چه شکلی رخدادهای سفارشی ما قابلیت عبور از این لایه را خواهند داشت.
HTML Templates
تا قبل از اینکه المان template معرفی شود، از یکی از روشهای زیر برای استفادهی مجدد از یک قالب HTML عمل میکردیم:
1- استفاده از یک المان خاص با ویژگی hidden یا استایل display:none
- استفاده از DOM و آگاه بودن مرورگر از وجود آن، عملیات clone را ساده خواهد کرد.
- محتوای داخل آن رندر نخواهد شد.
- اگرچه محتوای آن مخفی میباشد، با این حال درخواستهای مرتبط با تصاویر یا اسکریپتها انجام خواهد شد.
<div id="template" hidden> <img src="logo.png"> <div class="comment"></div> </div>
2- استفاده از تگ script با یک type سفارشی
<script id="template" type="text/x-template"> <img src="logo.png"> <div class="comment"></div> </script> <script id="template" type="text/x-kendo-template"> <ul> # for (var i = 0; i < data.length; i++) { # <li>#= data[i] #</li> # } # </ul> </script>
- از آنجا که تگ script به صورت پیشفرض دارای استایل display:none میباشد، محتوای داخل آن رندر نخواهد شد.
- به دلیل عدم مقداردهی ویژگی type آن با "text/javascript"، مرورگر محتوای آن را به عنوان کد جاوااسکریپت parse نخواهد کرد.
- استفاده از خصوصیت innerHTML مشکل امنیتی XSS را بدنبال خواهد داشت .
HTML Template ویژگیهای مثبت هر دو روش قبل را دارد. از طریق کد امکان clone و رندر کردن آن وجود دارد و همچنین کوئری المانهای داخل آن نیز ممکن نیست:
<template id="template"> <img src="logo.png"> <div class="comment"></div> </template>
var template = document.querySelector("template"); var clonedNode = template.content.cloneNode(true); //deep:true document.body.appendChild(clonedNode);
نکته: امکان تعریف قالبهای تودرتو نیز وجود دارد که در این صورت به شکل جداگانهای باید عملیات فعالسازی هر کدام از آنها انجام گیرد.