کار با عناصر سفارشی

مقدمه

وب به شدت فاقد بیان است. برای اینکه متوجه منظور من شوید، نگاهی به یک برنامه وب "مدرن" مانند GMail بیندازید:

جیمیل

هیچ چیز مدرنی در مورد سوپ <div> وجود ندارد. و با این حال، این روشی است که ما برنامه های وب می سازیم. غم انگیز است. آیا ما نباید از پلتفرم خود مطالبه بیشتری داشته باشیم؟

نشانه گذاری سکسی بیایید آن را تبدیل به یک چیز کنیم

HTML یک ابزار عالی برای ساختار یک سند به ما می دهد، اما واژگان آن محدود به عناصری است که استاندارد HTML تعریف می کند.

اگر نشانه گذاری برای GMail بی رحمانه نبود چه؟ اگه قشنگ بود چی :|

<hangout-module>
    <hangout-chat from="Paul, Addy">
    <hangout-discussion>
        <hangout-message from="Paul" profile="profile.png"
            profile="118075919496626375791" datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.
        <p>Heard of it?
        </hangout-message>
    </hangout-discussion>
    </hangout-chat>
    <hangout-chat>...</hangout-chat>
</hangout-module>

چقدر با طراوت! این برنامه کاملاً منطقی است. معنی دار ، آسان برای درک ، و بهتر از همه، قابل نگهداری است. من آینده/ شما دقیقاً با بررسی ستون فقرات اعلامی آن خواهید دانست.

شروع کردن

عناصر سفارشی به توسعه دهندگان وب اجازه می دهد تا انواع جدیدی از عناصر HTML را تعریف کنند . این مشخصات یکی از چندین API ابتدایی جدید است که در زیر چتر Web Components فرود می‌آیند، اما احتمالاً مهمترین آنهاست. اجزای وب بدون ویژگی های باز شده توسط عناصر سفارشی وجود ندارند:

  1. عناصر جدید HTML/DOM را تعریف کنید
  2. عناصری را ایجاد کنید که از عناصر دیگر گسترش یابد
  3. به طور منطقی عملکردهای سفارشی را در یک برچسب واحد با هم ترکیب کنید
  4. API عناصر DOM موجود را گسترش دهید

ثبت عناصر جدید

عناصر سفارشی با استفاده از document.registerElement() ایجاد می شوند:

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

اولین آرگومان برای document.registerElement() نام تگ عنصر است. نام باید دارای خط تیره (-) باشد . بنابراین برای مثال، <x-tags> ، <my-element> و <my-awesome-app> همه نام‌های معتبر هستند، در حالی که <tabs> و <foo_bar> اینگونه نیستند. این محدودیت به تجزیه کننده اجازه می دهد تا عناصر سفارشی را از عناصر معمولی متمایز کند، اما همچنین هنگام اضافه شدن برچسب های جدید به HTML، سازگاری رو به جلو را تضمین می کند.

آرگومان دوم یک شی (اختیاری) است که prototype عنصر را توصیف می کند. اینجا مکانی برای افزودن قابلیت های سفارشی (مثلاً ویژگی ها و روش های عمومی) به عناصر شما است. بیشتر در مورد آن بعدا.

به طور پیش فرض، عناصر سفارشی از HTMLElement ارث می برند. بنابراین، مثال قبلی معادل است با:

var XFoo = document.registerElement('x-foo', {
    prototype: Object.create(HTMLElement.prototype)
});

فراخوانی به document.registerElement('x-foo') به مرورگر در مورد عنصر جدید آموزش می دهد و سازنده ای را برمی گرداند که می توانید از آن برای ایجاد نمونه های <x-foo> استفاده کنید. یا اگر نمی‌خواهید از سازنده استفاده کنید، می‌توانید از تکنیک‌های دیگر نمونه‌سازی عناصر استفاده کنید.

عناصر گسترش دهنده

عناصر سفارشی به شما امکان می دهد عناصر HTML موجود (بومی) و همچنین سایر عناصر سفارشی را گسترش دهید. برای گسترش یک عنصر، باید نام registerElement() و prototype عنصر را برای ارث بردن از آن ارسال کنید.

گسترش عناصر بومی

بگویید از <button> Regular Joe راضی نیستید. شما می‌خواهید قابلیت‌های آن را به یک «دکمه مگا» اضافه کنید. برای گسترش عنصر <button> ، یک عنصر جدید ایجاد کنید که prototype HTMLButtonElement را به ارث برده و نام عنصر را extends . در این مورد، "دکمه":

var MegaButton = document.registerElement('mega-button', {
    prototype: Object.create(HTMLButtonElement.prototype),
    extends: 'button'
});

عناصر سفارشی که از عناصر بومی به ارث می برند، عناصر سفارشی پسوند نوع نامیده می شوند. آنها از یک نسخه تخصصی HTMLElement به عنوان راهی برای گفتن "عنصر X یک Y است" به ارث می برند.

مثال:

<button is="mega-button">

گسترش یک عنصر سفارشی

برای ایجاد یک عنصر <x-foo-extended> که عنصر سفارشی <x-foo> را گسترش می‌دهد، به سادگی نمونه اولیه آن را به ارث برده و بگویید از چه برچسبی به ارث می‌برید:

var XFooProto = Object.create(HTMLElement.prototype);
...

var XFooExtended = document.registerElement('x-foo-extended', {
    prototype: XFooProto,
    extends: 'x-foo'
});

برای اطلاعات بیشتر در مورد ایجاد نمونه های اولیه عنصر، به افزودن ویژگی ها و روش های JS در زیر مراجعه کنید.

نحوه ارتقاء عناصر

آیا تا به حال به این فکر کرده اید که چرا تجزیه کننده HTML تگ های غیر استاندارد را مناسب نمی کند؟ برای مثال، اگر <randomtag> در صفحه اعلام کنیم، بسیار خوشحال کننده است. با توجه به مشخصات HTML :

متاسفم <randomtag> ! شما غیر استاندارد هستید و از HTMLUnknownElement به ارث برده اید.

همین امر برای عناصر سفارشی صادق نیست. عناصر با نام عناصر سفارشی معتبر از HTMLElement به ارث می برند. می‌توانید این واقعیت را با روشن کردن کنسول تأیید کنید: Ctrl + Shift + J (یا Cmd + Opt + J در مک)، و در خطوط کد زیر قرار دهید. آنها true هستند:

// "tabs" is not a valid custom element name
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype

// "x-tabs" is a valid custom element name
document.createElement('x-tabs').__proto__ == HTMLElement.prototype

عناصر حل نشده

از آنجایی که عناصر سفارشی توسط اسکریپت با استفاده از document.registerElement() ثبت می‌شوند، می‌توان آن‌ها را قبل از ثبت تعریف آنها توسط مرورگر اعلام یا ایجاد کرد . به عنوان مثال، می‌توانید <x-tabs> در صفحه اعلام کنید، اما در نهایت بعداً document.registerElement('x-tabs') را فراخوانی می‌کنید.

قبل از اینکه عناصر به تعریف خود ارتقا داده شوند، عناصر حل نشده نامیده می شوند. اینها عناصر HTML هستند که نام عنصر سفارشی معتبری دارند اما ثبت نشده اند.

این جدول ممکن است به ثابت نگه داشتن موارد کمک کند:

نام ارث می برد از نمونه ها
عنصر حل نشده HTMLElement <x-tabs> ، <my-element>
عنصر ناشناخته HTMLUnknownElement <tabs> ، <foo_bar>

عناصر نمونه سازی

تکنیک های متداول ایجاد عناصر هنوز هم برای عناصر سفارشی اعمال می شود. مانند هر عنصر استاندارد، آنها را می توان در HTML اعلام کرد یا در DOM با استفاده از جاوا اسکریپت ایجاد کرد.

نمونه سازی برچسب های سفارشی

آنها را اعلام کنید :

<x-foo></x-foo>

ایجاد DOM در JS:

var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
    alert('Thanks!');
});

از اپراتور new استفاده کنید:

var xFoo = new XFoo();
document.body.appendChild(xFoo);

نمونه سازی عناصر پسوند نوع

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

آنها را اعلام کنید :

<!-- <button> "is a" mega button -->
<button is="mega-button">

ایجاد DOM در JS:

var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true

همانطور که می بینید، اکنون یک نسخه اضافه بار از document.createElement() وجود دارد که ویژگی is="" را به عنوان پارامتر دوم خود می گیرد.

از اپراتور new استفاده کنید:

var megaButton = new MegaButton();
document.body.appendChild(megaButton);

تا کنون، ما یاد گرفته ایم که چگونه از document.registerElement() برای اطلاع رسانی به مرورگر در مورد یک تگ جدید استفاده کنیم، اما کار زیادی انجام نمی دهد. بیایید خواص و روش ها را اضافه کنیم.

افزودن خواص و متدهای JS

نکته قدرتمند در مورد عناصر سفارشی این است که می‌توانید با تعریف ویژگی‌ها و روش‌ها در تعریف عنصر، عملکردهای متناسب را با این عنصر ترکیب کنید. به این به عنوان راهی برای ایجاد یک API عمومی برای عنصر خود فکر کنید.

این یک مثال کامل است:

var XFooProto = Object.create(HTMLElement.prototype);

// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
    alert('foo() called');
};

// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});

// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');

// 5. Add it to the page.
document.body.appendChild(xfoo);

البته هزاران راه برای ساختن یک prototype وجود دارد. اگر طرفدار ایجاد نمونه های اولیه مانند این نیستید، در اینجا یک نسخه فشرده تر از همان چیز وجود دارد:

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function () {
        return 5;
      }
    },
    foo: {
      value: function () {
        alert('foo() called');
      }
    }
  })
});

فرمت اول امکان استفاده از ES5 Object.defineProperty را می دهد. دومی امکان استفاده از get/set را می دهد.

روش های برگشت به تماس چرخه حیات

عناصر می توانند روش های خاصی را برای بهره برداری از زمان های جالب وجود خود تعریف کنند. این روش‌ها به‌طور مناسب به callbacks چرخه حیات نامیده می‌شوند. هر کدام نام و هدف خاصی دارند:

نام برگشت به تماس کی زنگ زد
ایجاد تماس برگشتی نمونه ای از عنصر ایجاد می شود
پاسخ تماس پیوست شده است یک نمونه در سند درج شد
پاسخ تماس جدا شده یک نمونه از سند حذف شد
attributeChangedCallback(attrName، oldVal، newVal) یک ویژگی اضافه، حذف یا به روز شد

مثال: تعریف createdCallback() و attachedCallback() در <x-foo> :

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {prototype: proto});

همه تماس‌های چرخه حیات اختیاری هستند ، اما اگر/زمانی که منطقی است آنها را تعریف کنید. به عنوان مثال، فرض کنید عنصر شما به اندازه کافی پیچیده است و یک اتصال به IndexedDB در createdCallback() باز می کند. قبل از اینکه از DOM حذف شود، کار پاکسازی لازم را در detachedCallback() انجام دهید. توجه: به عنوان مثال، اگر کاربر برگه را ببندد، نباید به این تکیه کنید، بلکه آن را به عنوان یک قلاب بهینه سازی احتمالی در نظر بگیرید.

یکی دیگر از تماس‌های چرخه حیات مورد استفاده برای تنظیم شنونده‌های رویداد پیش‌فرض روی عنصر است:

proto.createdCallback = function() {
  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });
};

اضافه کردن نشانه گذاری

ما <x-foo> ایجاد کرده ایم، به آن یک API جاوا اسکریپت داده ایم، اما خالی است! آیا به آن مقداری HTML بدهیم تا رندر شود؟

تماس‌های چرخه حیات در اینجا مفید هستند. به ویژه، ما می‌توانیم از createdCallback() برای اعطای یک عنصر با مقداری HTML پیش‌فرض استفاده کنیم:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    this.innerHTML = "**I'm an x-foo-with-markup!**";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});

نمونه‌برداری از این تگ و بررسی در DevTools (با کلیک راست، گزینه Inspect Element) را نشان می‌دهد:

▾<x-foo-with-markup>
  **I'm an x-foo-with-markup!**
</x-foo-with-markup>

کپسوله سازی داخلی ها در Shadow DOM

Shadow DOM به خودی خود یک ابزار قدرتمند برای کپسوله کردن محتوا است. از آن در ارتباط با عناصر سفارشی استفاده کنید و چیزها جادویی می شوند!

Shadow DOM عناصر سفارشی را می دهد:

  1. راهی برای مخفی کردن دل و جرات خود، در نتیجه محافظت از کاربران از جزئیات اجرای بد.
  2. کپسوله کردن سبک … رایگان.

ایجاد یک عنصر از Shadow DOM مانند ایجاد عنصری است که نشانه گذاری اولیه را ارائه می دهد. تفاوت در createdCallback() است:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    // 1. Attach a shadow root on the element.
    var shadow = this.createShadowRoot();

    // 2. Fill it with markup goodness.
    shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

به جای تنظیم .innerHTML عنصر، یک Shadow Root برای <x-foo-shadowdom> ایجاد کردم و سپس آن را با نشانه گذاری پر کردم. با فعال بودن تنظیم «Show Shadow DOM» در DevTools، یک #shadow-root را مشاهده خواهید کرد که می‌توان آن را گسترش داد:

<x-foo-shadowdom>
  ▾#shadow-root
    **I'm in the element's Shadow DOM!**
</x-foo-shadowdom>

این همان ریشه سایه است!

ایجاد عناصر از یک الگو

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

مثال: ثبت عنصر ایجاد شده از <template> و Shadow DOM:

<template id="sdtemplate">
  <style>
    p { color: orange; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<script>
  var proto = Object.create(HTMLElement.prototype, {
    createdCallback: {
      value: function() {
        var t = document.querySelector('#sdtemplate');
        var clone = document.importNode(t.content, true);
        this.createShadowRoot().appendChild(clone);
      }
    }
  });
  document.registerElement('x-foo-from-template', {prototype: proto});
</script>

<template id="sdtemplate">
  <style>:host p { color: orange; }</style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<div class="demoarea">
  <x-foo-from-template></x-foo-from-template>
</div>

این چند خط کد تاثیر زیادی دارد. بیایید همه چیزهایی را که اتفاق می افتد درک کنیم:

  1. ما یک عنصر جدید در HTML ثبت کرده ایم: <x-foo-from-template>
  2. DOM عنصر از یک <template> ایجاد شده است
  3. جزئیات ترسناک عنصر با استفاده از Shadow DOM پنهان می شود
  4. Shadow DOM به سبک عنصر کپسوله‌سازی می‌کند (مثلاً p {color: orange;} کل صفحه را نارنجی نمی‌کند)

خیلی خوب!

یک ظاهر طراحی شده عناصر سفارشی

مانند هر تگ HTML، کاربران تگ سفارشی شما می توانند با انتخابگرها به آن استایل بدهند:

<style>
  app-panel {
    display: flex;
  }
  [is="x-item"] {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  [is="x-item"]:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > [is="x-item"] {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-panel>
    <li is="x-item">Do</li>
    <li is="x-item">Re</li>
    <li is="x-item">Mi</li>
</app-panel>

عناصر استایلی که از Shadow DOM استفاده می کنند

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

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

یک ظاهر طراحی Shadow DOM یک موضوع بزرگ است! اگر می خواهید در مورد آن بیشتر بدانید، من چند مقاله دیگر خود را توصیه می کنم:

پیشگیری از FOUC با استفاده از: حل نشده

برای کاهش FOUC ، عناصر سفارشی یک کلاس شبه CSS جدید، :unresolved را مشخص می کنند. از آن برای هدف قرار دادن عناصر حل نشده استفاده کنید، درست تا زمانی که مرورگر createdCallback() شما را فراخوانی کند (به روش‌های چرخه عمر مراجعه کنید). هنگامی که این اتفاق می افتد، عنصر دیگر یک عنصر حل نشده نیست. فرآیند ارتقا کامل شده است و عنصر به تعریف خود تبدیل شده است.

مثال : وقتی تگ‌های "x-foo" ثبت می‌شوند، محو می‌شوند:

<style>
  x-foo {
    opacity: 1;
    transition: opacity 300ms;
  }
  x-foo:unresolved {
    opacity: 0;
  }
</style>

به خاطر داشته باشید که :unresolved فقط برای عناصر حل نشده اعمال می شود، نه برای عناصری که از HTMLUnknownElement به ارث می برند ( به نحوه ارتقاء عناصر مراجعه کنید).

<style>
  /* apply a dashed border to all unresolved elements */
  :unresolved {
    border: 1px dashed red;
    display: inline-block;
  }
  /* x-panel's that are unresolved are red */
  x-panel:unresolved {
    color: red;
  }
  /* once the definition of x-panel is registered, it becomes green */
  x-panel {
    color: green;
    display: block;
    padding: 5px;
    display: block;
  }
</style>

<panel>
    I'm black because :unresolved doesn't apply to "panel".
    It's not a valid custom element name.
</panel>

<x-panel>I'm red because I match x-panel:unresolved.</x-panel>

پشتیبانی از تاریخچه و مرورگر

تشخیص ویژگی

تشخیص ویژگی به بررسی وجود document.registerElement() بستگی دارد:

function supportsCustomElements() {
    return 'registerElement' in document;
}

if (supportsCustomElements()) {
    // Good to go!
} else {
    // Use other libraries to create components.
}

پشتیبانی از مرورگر

document.registerElement() برای اولین بار در کروم 27 و فایرفاکس ~23 در پشت پرچم قرار گرفت. با این حال، مشخصات از آن زمان تا حد زیادی تکامل یافته است. کروم 31 اولین نسخه ای است که از مشخصات به روز شده پشتیبانی واقعی دارد.

تا زمانی که پشتیبانی از مرورگر بسیار خوب باشد، یک polyfill وجود دارد که توسط Google's Polymer و Mozilla's X-Tag استفاده می شود.

چه اتفاقی برای HTMLElementElement افتاد؟

برای کسانی که کار استانداردسازی را دنبال کرده‌اند، می‌دانید زمانی <element> وجود داشت. زانوی زنبورها بود. شما می توانید از آن برای ثبت عناصر جدید به صورت اعلامی استفاده کنید:

<element name="my-element">
    ...
</element>

متأسفانه، مشکلات زمان‌بندی بسیار زیادی در فرآیند ارتقا ، موارد گوشه‌ای و سناریوهای شبیه به آرماگدون وجود داشت که نمی‌توان آن را حل کرد. <element> باید قفسه بندی می شد. در آگوست 2013، دیمیتری گلازکوف با انتشار پستی در وب‌اپ‌های عمومی ، حذف آن را حداقل در حال حاضر اعلام کرد.

شایان ذکر است که Polymer یک فرم اعلامی ثبت عنصر را با <polymer-element> پیاده سازی می کند. چگونه؟ از document.registerElement('polymer-element') و تکنیک هایی که در ایجاد عناصر از یک الگو توضیح دادم استفاده می کند.

نتیجه گیری

عناصر سفارشی ابزاری را در اختیار ما قرار می دهند تا واژگان HTML را گسترش دهیم، ترفندهای جدید را به آن آموزش دهیم و از کرم چاله های پلت فرم وب پرش کنیم. آنها را با دیگر پلتفرم های اولیه اولیه مانند Shadow DOM و <template> ترکیب کنید، و ما شروع به درک تصویر Web Components می کنیم. نشانه گذاری می تواند دوباره سکسی باشد!

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