Vanilla JS + Vite — real loyiha — SPA yaratish

~40 daq · JavaScript · Vanilla JS + Vite

Muammodan boshlaylik

Vazifalar ilovasi kerak: vazifa qo'shish, bajarildi deb belgilash, o'chirish, filtrlash va ma'lumotlarni saqlash. Hamma kodni bitta main.js ga yozsangiz — 500 qatorli "spagetti": DOM, mantiq va saqlash aralashib ketadi.

Bu darsda framework siz, lekin tartibli SPA (Single Page Application) quramiz: sahifa bir marta yuklanadi, keyin hamma narsa JavaScript orqali yangilanadi.


Tuzilma

src/
├── main.js              // ishga tushirish
├── store.js             // holat (state)
├── storage.js           // localStorage
├── components/
│   ├── VazifaForma.js
│   ├── VazifaRoyxati.js
│   └── Filtrlar.js
└── style.css

Har bir qatlamning o'z vazifasi:

  • storage — ma'lumotni saqlash va o'qish
  • store — holat va uni o'zgartirish qoidalari
  • components — holatni ko'rsatish va foydalanuvchi harakatlarini store ga yetkazish

storage.js

const KALIT = 'vazifalar:v1';

export function yukla() {
  try {
    return JSON.parse(localStorage.getItem(KALIT)) ?? [];
  } catch {
    return [];
  }
}

export function saqla(vazifalar) {
  localStorage.setItem(KALIT, JSON.stringify(vazifalar));
}

store.js

import { yukla, saqla } from './storage.js';

let holat = {
  vazifalar: yukla(),
  filtr: 'hammasi',   // 'hammasi' | 'faol' | 'bajarilgan'
};

const obunachilar = new Set();

export const getHolat = () => holat;

export function obuna(fn) {
  obunachilar.add(fn);
  return () => obunachilar.delete(fn);
}

function yangila(yangi) {
  holat = { ...holat, ...yangi };
  saqla(holat.vazifalar);
  obunachilar.forEach((fn) => fn(holat));
}

export function qosh(matn) {
  const vazifa = { id: crypto.randomUUID(), matn, bajarildi: false };
  yangila({ vazifalar: [...holat.vazifalar, vazifa] });
}

export function almashtir(id) {
  yangila({
    vazifalar: holat.vazifalar.map((v) =>
      v.id === id ? { ...v, bajarildi: !v.bajarildi } : v,
    ),
  });
}

export function ochir(id) {
  yangila({ vazifalar: holat.vazifalar.filter((v) => v.id !== id) });
}

export function filtrlash(filtr) {
  yangila({ filtr });
}

Holat faqat store funksiyalari orqali va immutable (67-dars) o'zgaradi. Har o'zgarishda obunachilar xabardor qilinadi. Bu pattern 79-darsda batafsil.


Komponentlar

VazifaForma.js

import { qosh } from '../store.js';

export function VazifaForma(ildiz) {
  ildiz.innerHTML = `
    <form class="forma">
      <label for="yangi" class="sr-only">Yangi vazifa</label>
      <input id="yangi" name="matn" placeholder="Nima qilish kerak?" required maxlength="200" />
      <button type="submit">Qo'shish</button>
    </form>
  `;

  const forma = ildiz.querySelector('form');
  forma.addEventListener('submit', (e) => {
    e.preventDefault();
    const matn = forma.matn.value.trim();
    if (!matn) return;
    qosh(matn);
    forma.reset();
    forma.matn.focus();
  });
}

VazifaRoyxati.js

import { getHolat, obuna, almashtir, ochir } from '../store.js';

function filtrla(vazifalar, filtr) {
  if (filtr === 'faol') return vazifalar.filter((v) => !v.bajarildi);
  if (filtr === 'bajarilgan') return vazifalar.filter((v) => v.bajarildi);
  return vazifalar;
}

export function VazifaRoyxati(ildiz) {
  const ul = document.createElement('ul');
  ul.className = 'royxat';
  ildiz.append(ul);

  function chiz({ vazifalar, filtr }) {
    const korinadigan = filtrla(vazifalar, filtr);

    if (korinadigan.length === 0) {
      ul.innerHTML = '<li class="bosh">Vazifalar yo\'q</li>';
      return;
    }

    ul.replaceChildren(
      ...korinadigan.map((v) => {
        const li = document.createElement('li');
        li.dataset.id = v.id;
        li.className = v.bajarildi ? 'bajarilgan' : '';

        const label = document.createElement('label');
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.checked = v.bajarildi;
        checkbox.dataset.amal = 'almashtir';
        label.append(checkbox, ` ${v.matn}`);

        const tugma = document.createElement('button');
        tugma.dataset.amal = 'ochir';
        tugma.setAttribute('aria-label', `"${v.matn}" ni o'chirish`);
        tugma.textContent = '×';

        li.append(label, tugma);
        return li;
      }),
    );
  }

  // event delegation — bitta listener butun ro'yxat uchun
  ul.addEventListener('click', (e) => {
    const amal = e.target.dataset.amal;
    const id = e.target.closest('li')?.dataset.id;
    if (!amal || !id) return;
    if (amal === 'almashtir') almashtir(id);
    if (amal === 'ochir') ochir(id);
  });

  chiz(getHolat());
  obuna(chiz);
}

[!WARNING] Foydalanuvchi kiritgan matnni innerHTML ga qo'ymang: <img src=x onerror=alert(1)> kabi matn kod sifatida bajariladi (XSS, 84-dars). Yuqorida v.matn faqat textContent va matn tugunlari orqali qo'shilgan — xavfsiz.

Filtrlar.js

import { getHolat, obuna, filtrlash } from '../store.js';

const FILTRLAR = [
  ['hammasi', 'Hammasi'],
  ['faol', 'Faol'],
  ['bajarilgan', 'Bajarilgan'],
];

export function Filtrlar(ildiz) {
  const nav = document.createElement('nav');
  nav.className = 'filtrlar';
  nav.innerHTML = FILTRLAR.map(
    ([qiymat, nom]) => `<button data-filtr="${qiymat}">${nom}</button>`,
  ).join('');
  ildiz.append(nav);

  nav.addEventListener('click', (e) => {
    const filtr = e.target.dataset.filtr;
    if (filtr) filtrlash(filtr);
  });

  function chiz({ filtr, vazifalar }) {
    nav.querySelectorAll('button').forEach((b) => {
      b.setAttribute('aria-pressed', b.dataset.filtr === filtr);
    });
    const qolgan = vazifalar.filter((v) => !v.bajarildi).length;
    nav.dataset.qolgan = `${qolgan} ta qoldi`;
  }

  chiz(getHolat());
  obuna(chiz);
}

main.js

import './style.css';
import { VazifaForma } from './components/VazifaForma.js';
import { VazifaRoyxati } from './components/VazifaRoyxati.js';
import { Filtrlar } from './components/Filtrlar.js';

const app = document.querySelector('#app');
app.innerHTML = `
  <h1>Vazifalar</h1>
  <section id="forma"></section>
  <section id="filtrlar"></section>
  <section id="royxat"></section>
`;

VazifaForma(app.querySelector('#forma'));
Filtrlar(app.querySelector('#filtrlar'));
VazifaRoyxati(app.querySelector('#royxat'));
/* style.css (qisqartirilgan) */
.bajarilgan label { text-decoration: line-through; opacity: 0.6; }
.filtrlar button[aria-pressed="true"] { font-weight: 700; }
.filtrlar::after { content: attr(data-qolgan); margin-left: 1rem; }
.sr-only {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

Ma'lumot oqimi

Foydalanuvchi ── click ──▶ Komponent ── qosh() ──▶ Store
                                                     │ holat yangilandi
                                                     │ saqla() → localStorage
                                                     ▼
              Komponentlar ◀── obunachilar(holat) ───┘
                  │
                  └── chiz() → DOM

Ma'lumot bir yo'nalishda oqadi: harakat → store → ko'rinish. Komponentlar bir-biri haqida bilmaydi — faqat store orqali bog'lanadi.

[!TIP] Har bir o'zgarishda butun ro'yxatni qayta chizish — oddiy va yuzlab elementlar uchun yetarlicha tez. Minglab elementlarda faqat o'zgargan elementlarni yangilash kerak bo'ladi — React kabi kutubxonalar aynan shu vazifani avtomatlashtiradi.


Nima uchun shunday?

Nima uchun kichik ilovani ham qatlamlarga bo'lish kerak?

Chunki ilovalar o'sadi. Bugun — vazifalar ro'yxati, ertaga — server bilan sinxronlash, bir nechta ro'yxat, muddatlar. Agar DOM kodi, mantiq va saqlash aralashgan bo'lsa, har bir yangi funksiya hamma narsani qayta yozishni talab qiladi. Qatlamlarga bo'lingan kodda esa localStorage ni server API ga almashtirish faqat storage.js ni o'zgartirish; yangi ko'rinish qo'shish — yangi komponent va obuna. Bu darsdagi tuzilma — React, Vue kabi framework lar ichida yashiringan g'oyalarning qo'lda yozilgan versiyasi: holat, komponentlar va bir yo'nalishli ma'lumot oqimi.


Xulosa

Eslab qoling:

  • SPA — bitta HTML, hamma narsa JavaScript orqali yangilanadi
  • Qatlamlar: storage (saqlash), store (holat), components (ko'rinish)
  • Holat faqat store funksiyalari orqali va immutable o'zgaradi; obunachilar xabardor qilinadi
  • Komponent — ildiz element oladi, chizadi va store ga obuna bo'ladi
  • Event delegation — ro'yxat uchun bitta listener
  • Foydalanuvchi matni — textContent, innerHTML emas

Keyingi dars

78-dars: Vanilla JS + Vite — router — history API bilan