Web Components
Web Components nima, uchta ustuni (Custom Elements, Shadow DOM, HTML Templates), lifecycle callback'lar (constructor, connectedCallback, disconnectedCallback, attributeChangedCallback), observedAttributes, <template> va named slot'lar, custom event'lar orqali composed: true, customElements API metodlari va React/Vue bilan taqqoslash.
89-dars
Web Components
Oldingi mavzuda Shadow DOM'ni ko'rdik — bu izolyatsiya vositasi. Lekin bitta savol qoladi: qanday qilib butunlay yangi, qayta ishlatiladigan HTML teg yarataman — masalan, <my-button> yoki <user-card>, xuddi <video> yoki <select> kabi o'zining ichki mantig'i, holati va uslubi bilan?
React/Vue buni framework darajasida hal qiladi (<Button /> komponenti) — lekin bu faqat shu framework ichida ishlaydi. Web Components — bu brauzerning o'zida, native shu imkoniyatni beruvchi standart, hech qanday framework/build tool kerak emas.
Uchta ustun — birga ishlaydigan API'lar
1. Custom Elements → yangi HTML teg RO'YXATDAN o'tkazish
2. Shadow DOM → ichki tuzilmani IZOLYATSIYA qilish (o'tgan mavzu)
3. HTML Templates → qayta ishlatiladigan HTML SHABLON (<template>, <slot>)
1. Custom Elements — yangi teg yaratish
class UserCard extends HTMLElement {
// konstruktor — element YARATILGANDA chaqiriladi, lekin hali DOM'ga ULANMAGAN
constructor() {
super(); // HTMLElement konstruktorini chaqirish MAJBURIY
console.log('constructor: hali DOM\'da emas');
}
// element HAQIQATAN sahifaga qo'shilganda chaqiriladi
connectedCallback() {
console.log('connectedCallback: endi DOM\'da');
this.innerHTML = `<div>Salom, foydalanuvchi!</div>`;
}
// element sahifadan olib tashlanganda
disconnectedCallback() {
console.log('disconnectedCallback: DOM\'dan chiqarildi');
}
}
customElements.define('user-card', UserCard);
<user-card></user-card>
Bu yerda customElements.define('user-card', UserCard) — brauzerga aytadi: "har qachon HTML'da <user-card> tegini ko'rsang, uni oddiy HTMLUnknownElement emas, balki UserCard klassining instansiyasi sifatida ishla".
Nega constructorda emas, connectedCallbackda ishlash kerak
Bu — eng ko'p xato qilinadigan joy. constructor chaqirilganda, element hali DOM daraxtiga ulanmagan bo'lishi mumkin:
const card = document.createElement('user-card'); // constructor SHU YERDA ishlaydi
// lekin card hali sahifada YO'Q!
document.body.appendChild(card); // FAQAT SHU YERDA connectedCallback ishga tushadi
Agar siz constructorda this.innerHTML = ... yozsangiz, ba'zi holatlarda (masalan, atributlar hali o'rnatilmagan bo'lsa) noto'g'ri natija olishingiz mumkin. Shuning uchun qat'iy qoida: DOM bilan ishlash — faqat connectedCallbackda.
Lifecycle callback'lar — to'liq jadval
| Callback | Qachon chaqiriladi |
|---|---|
constructor() |
Element yaratilganda (hali DOM'da emas) |
connectedCallback() |
Element DOM'ga qo'shilganda (har safar, hatto qayta qo'shilsa ham) |
disconnectedCallback() |
Element DOM'dan olib tashlanganda |
attributeChangedCallback(name, oldVal, newVal) |
Kuzatilayotgan atribut o'zgarganda |
adoptedCallback() |
Element boshqa document'ga ko'chirilganda (kamdan-kam, masalan iframe orasida) |
Atributlarni kuzatish — observedAttributes
class RatingStars extends HTMLElement {
static get observedAttributes() {
return ['value']; // faqat shu atributlar KUZATILADI
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'value') {
this.render();
}
}
connectedCallback() {
this.render();
}
render() {
const value = Number(this.getAttribute('value')) || 0;
this.innerHTML = '★'.repeat(value) + '☆'.repeat(5 - value);
}
}
customElements.define('rating-stars', RatingStars);
<rating-stars value="3"></rating-stars>
Muhim: attributeChangedCallback faqat static get observedAttributes() ro'yxatida ko'rsatilgan atributlar uchun ishlaydi — bu performance uchun ataylab shunday: agar brauzer har bir atribut o'zgarishini kuzatsa, bu keraksiz overhead bo'lardi. Siz aniq qaysi atributlar muhimligini ro'yxat qilib berasiz.
2. HTML Template — samarali shablon yaratish
<template id="card-template">
<style>
.card { border: 1px solid #ccc; padding: 16px; border-radius: 8px; }
</style>
<div class="card">
<slot name="title">Sarlavha yo'q</slot>
<slot name="body"></slot>
</div>
</template>
<template> tegining muhim xususiyati: uning ichidagi HTML parse qilinadi (brauzer uni tushunadi, DOM tuzilmasiga aylantiradi), lekin hech qachon render qilinmaydi va bajarilmaydi — ichidagi <img> yuklanmaydi, <script> ishga tushmaydi, u shunchaki "inert" (harakatsiz) DOM bo'lagi sifatida xotirada yotadi, faqat siz JS orqali klonlaganingizda jonlanadi:
class MyCard extends HTMLElement {
connectedCallback() {
const template = document.getElementById('card-template');
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(template.content.cloneNode(true)); // true = CHUQUR klonlash
}
}
customElements.define('my-card', MyCard);
template.content — bu maxsus DocumentFragment obyekti. cloneNode(true) — butun ichki tuzilmani nusxalab oladi, shu bilan bir vaqtda bir nechta <my-card> elementi bitta shablondan, bir-biridan mustaqil nusxalar yaratishi mumkin.
Named Slots — bir nechta "teshik"
<my-card>
<span slot="title">Mening kartam</span>
<p slot="body">Bu — karta matni.</p>
</my-card>
slot="title" atributi orqali, tashqi element aniq qaysi <slot name="..."> joyiga tushishini belgilaydi — bu shablon ichida bir nechta joylashtiriladigan joy borligini bildiradi (oldingi mavzudagi oddiy <slot></slot> esa faqat standart, nomsiz joy edi).
3. To'liq amaliy misol — hammasi birga
class CounterButton extends HTMLElement {
#count = 0; // private field (haqiqiy JS private, Shadow DOM'dan mustaqil)
static get observedAttributes() {
return ['start'];
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'start') this.#count = Number(newVal) || 0;
}
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button { padding: 8px 16px; font-size: 16px; }
</style>
<button>Bosildi: ${this.#count}</button>
`;
shadow.querySelector('button').addEventListener('click', () => {
this.#count++;
shadow.querySelector('button').textContent = `Bosildi: ${this.#count}`;
// tashqariga CUSTOM EVENT chiqarish
this.dispatchEvent(new CustomEvent('count-changed', {
detail: { count: this.#count },
bubbles: true,
composed: true // Shadow chegarasidan o'tishi uchun MAJBURIY
}));
});
}
}
customElements.define('counter-button', CounterButton);
<counter-button start="5"></counter-button>
<script>
document.querySelector('counter-button')
.addEventListener('count-changed', (e) => {
console.log('Yangi qiymat:', e.detail.count);
});
</script>
Nega composed: true bu yerda majburiy
CustomEvent yaratilganda, standart holatda u Shadow chegarasidan tashqariga chiqmaydi (composed: false — default). Agar siz tashqi kod (light DOM'dagi) bu eventni ushlashini xohlasangiz, aniq composed: true berishingiz kerak — bu Shadow DOM'ning izolyatsiya printsipining tabiiy davomi: hatto event'lar ham, ataylab ruxsat berilmaguncha, chegaradan o'tmaydi.
4. customElements obyektining boshqa metodlari
| Metod | Vazifasi |
|---|---|
customElements.define(name, class, options?) |
Yangi elementni ro'yxatdan o'tkazadi |
customElements.get(name) |
Berilgan nom bo'yicha klassni qaytaradi (agar ro'yxatdan o'tgan bo'lsa) |
customElements.whenDefined(name) |
Element ro'yxatdan o'tguncha kutadigan Promise qaytaradi |
customElements.upgrade(node) |
Mavjud DOM elementini majburan "yangilaydi" |
whenDefined — foydali holat: agar <user-card> HTML'da allaqachon yozilgan, lekin uning JS klassi hali yuklanmagan bo'lsa (masalan, script tag pastda), brauzer uni vaqtincha HTMLElementning oddiy, "jonlanmagan" versiyasi sifatida ko'rsatadi — :not(:defined) CSS selektori orqali buni hatto stilize qilish mumkin (masalan, "yuklanmoqda" effekti):
user-card:not(:defined) {
opacity: 0;
}
5. Nega bu React/Vue'dan farq qiladi — muhim tushuncha
| React komponenti | Web Component | |
|---|---|---|
| Ishlash muhiti | Faqat React ilovasi ichida | Har qanday HTML sahifada (Vue, Angular, oddiy HTML — hammasida) |
| Qanday render qiladi | Virtual DOM orqali, qayta-qayta taqqoslab (diffing) | To'g'ridan-to'g'ri, native DOM API orqali |
| Holat (state) boshqaruvi | useState, virtual re-render |
Oddiy JS class field, siz o'zingiz DOM'ni yangilaysiz |
| Framework kerakmi | Ha, React runtime kerak | Yo'q — bu browser'ning o'z standarti |
Amalda, katta kompaniyalar (masalan, YouTube'ning ba'zi qismlari, GitHub'ning <relative-time>, <clipboard-copy> kabi elementlari) Web Components'dan framework'dan mustaqil, uzoq muddat barqaror komponentlar yaratish uchun foydalanadi — chunki bu brauzer standarti, React versiyasi o'zgarib ketishidan qat'i nazar, u doim ishlayveradi.
6. V8 va Blink darajasida — qisqa xulosa
customElements.define() chaqirilganda, Blink (rendering engine) ichida maxsus registryga yozuv qo'shiladi: "teg nomi → JS klassi" moslashuvi. Har safar HTML parser (yoki innerHTML, yoki createElement) shu teg nomiga duch kelganda, u oddiy elementni emas, balki ro'yxatdan o'tgan JS klassning yangi instansiyasini yaratadi, va lifecycle callback'larni aynan to'g'ri tartibda (constructor → connectedCallback) V8'ga signal beradi.
Bu — Shadow DOM'da ko'rgan C++ binding orqali JS'ga signal berish mexanizmining xuddi shu davomi: haqiqiy DOM daraxti boshqaruvi Blink'da, JS mantiq esa V8'da, ular doimiy, tez chaqiruvlar orqali sinxron ishlaydi.