Բոլոր դասերը
FrontendՍկսնակ14 րոպե ընթերցումԻմ ստեկում

HTML, CSS և JavaScript

Յուրաքանչյուր կայքի հիմքը՝ կառուցվածք, ոճ և վարքագիծ, ճիշտ արված։

  • Semantic HTML
  • Flexbox & Grid
  • DOM
  • Events
  • fetch
Քայլ 1/5
HTML-ը կառուցվածքն է՝ պատեր, տանիք, դուռ ու պատուհաններ։ Այն ասում է, թե ինչ է ամեն մի մասը, ոչ թե ինչպես է երևում։

30 վայրկյանում

Յուրաքանչյուր վեբ էջ կառուցված է երեք շերտից։ HTML-ը նկարագրում է, թե ինչ է կոնտենտը (վերնագիր, կոճակ, ձև), CSS-ը որոշում է, թե ինչպես է այն երևում և ինչպես է հարմարվում յուրաքանչյուր էկրանին, իսկ JavaScript-ը ավելացնում է վարքագիծ, օրինակ՝ click-երին արձագանքել կամ տվյալներ բեռնել։ React-ի և Next.js-ի պես framework-ները նստած են այս երեքի վրա. դրանք լավ հասկանալը քեզ ավելի արագ է դարձնում, իսկ քո կայքերը՝ ավելի հասանելի ցանկացած framework-ում։

Ինչու եմ օգտագործում

  • Յուրաքանչյուր ինտերֆեյս, անկախ framework-ից, կառուցում եմ semantic HTML-ի և մաքուր CSS-ի սկզբունքների վրա, որովհետև հենց դա են իրականում հասկանում բրաուզերները, որոնողական համակարգերը և screen reader-ները։
  • Նկատում եմ, որ «framework-ի բագերի» մեծ մասը, որոնք debug եմ անում, իրականում HTML-ի, CSS-ի կամ DOM-ի հարցեր են՝ բացակայող label, collapse եղող margin, event, որը bubble է անում այնտեղ, որտեղ ոչ ոք չէր սպասում։
  • Layout-ները նախագծում եմ mobile-first՝ Flexbox-ով, Grid-ով և design token-ներով, և ստուգում եմ դրանք հեռախոսի, պլանշետի և desktop-ի լայնություններում՝ նախքան էջն ավարտված համարելը։
  • Accessibility-ն համարում եմ որակի մաս, ոչ թե հավելում. semantic markup-ը ոչինչ չարժե և օգնում է յուրաքանչյուր օգտատիրոջ։

Ինչպես է աշխատում

01Semantic HTML և accessibility-ի հիմունքներ

HTML տարրերը իմաստ են կրում։ <button>-ը focus է ստանում, աշխատում է Enter-ով և Space-ով և screen reader-ների կողմից հայտարարվում է որպես կոճակ. click handler-ով <div>-ը սրանցից ոչ մեկը չէ։ Օգտագործիր landmark-ներ (<header>, <nav>, <main>, <footer>), մեկ էջում մեկ <h1>՝ հերթականությամբ վերնագրերով, <a> նավիգացիայի և <button> գործողությունների համար, իսկ ցուցակների համար՝ ցուցակներ։ Ձևի յուրաքանչյուր դաշտի պետք է տեսանելի <label>՝ կապված for-ով և id-ով։ Նկարներին պետք է alt տեքստ, որը նկարագրում է դրանց նպատակը, կամ alt="", եթե դրանք դեկորատիվ են։ Համոզվիր, որ ամեն ինչ աշխատում է միայն ստեղնաշարով, և focus outline-ը տեսանելի է։ Նպատակ դիր WCAG 2.2 AA մակարդակը, որը ներառում է սովորական տեքստի համար առնվազն 4.5:1 կոնտրաստ։ ARIA-ն օգտագործիր միայն երբ ոչ մի native տարր չի համապատասխանում. ARIA-ի առաջին կանոնը native HTML-ը նախընտրելն է։

html
<header>
  <nav aria-label="Main">
    <a href="/">Home</a>
    <a href="/shop" aria-current="page">Shop</a>
  </nav>
</header>
<main>
  <h1>Running shoes</h1>
  <img src="/img/shoe.webp" alt="Blue running shoe, side view" width="640" height="480" />
  <form action="/subscribe" method="post">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    <button type="submit">Subscribe</button>
  </form>
</main>

02Box model և cascade

Յուրաքանչյուր տարր ուղղանկյուն box է չորս շերտով՝ content, padding (տարածություն border-ի ներսում), border և margin (տարածություն դրսում)։ Լռելյայն width-ը կիրառվում է միայն content-ի վրա, ուստի padding ավելացնելը box-ը դարձնում է ավելի լայն, քան խնդրել ես։ Ամեն ինչի վրա box-sizing: border-box դնելը ստիպում է width-ին ներառել padding-ը և border-ը, ինչը շատ ավելի ինտուիտիվ է։ Block-երի միջև ուղղահայաց margin-ները կարող են collapse լինել մեկի մեջ, ինչը դասական անակնկալ է. Flexbox-ում և Grid-ում gap-ը դրանից խուսափում է։ Երբ մի քանի կանոն թիրախավորում է նույն տարրը, cascade-ը որոշում է, թե ով է հաղթում՝ importance, հետո specificity (id-ն հաղթում է class-ին, class-ը՝ element-ին), հետո սորսի հերթականությունը։ Specificity-ն պահիր ցածր և հարթ, հիմնականում մեկ class-ով, և խուսափիր !important-ից։ Ժամանակակից CSS-ը նաև առաջարկում է custom property-ներ (փոփոխականներ) design token-ների համար, cascade layer-ներ (@layer)՝ ոճերի ամբողջական խմբեր դասավորելու համար, և native nesting։

css
:root {
  --space-2: 0.5rem;
  --space-4: 1rem;
  --radius: 12px;
  --color-surface: #ffffff;
}
*, *::before, *::after {
  box-sizing: border-box;
}
.card {
  padding: var(--space-4);
  border: 1px solid #dddddd;
  border-radius: var(--radius);
  background: var(--color-surface);
}
 
.card:focus-visible {
  outline: 2px solid #4f46e5;
  outline-offset: 2px;
}

03Flexbox, թե Grid

Երկուսն էլ layout համակարգեր են, և ժամանակակից կայքերն օգտագործում են երկուսն էլ։ Flexbox-ը միաչափ է. այն տարրերը դասավորում է մեկ տողում կամ սյունակում և գերազանց է տարածություն բաշխելու և հավասարեցնելու մեջ, օրինակ՝ navbar, պատկերակով կոճակ կամ tag-երի տող, որը wrap է անում։ Մտածում ես content-first. տարրերն իրենք են որոշում իրենց չափը, իսկ Flexbox-ը բաշխում է մնացած տարածությունը։ Grid-ը երկչափ է. սահմանում ես տողեր և սյունակներ, և տարրերը տեղադրվում են այդ կառուցվածքում, ինչը հարմար է էջերի layout-ների, քարտերի պատկերասրահների և dashboard-ների համար։ Մտածում ես layout-first։ Grid-ի օգտակար pattern է repeat(auto-fill, minmax(240px, 1fr))-ը, որը ստեղծում է այնքան սյունակ, որքան տեղավորվում է, ընդհանրապես առանց media query-ների։ Երկուսն էլ աջակցում են gap՝ հետևողական տարածությունների համար։ Գործնական կանոն. Grid՝ ընդհանուր կառուցվածքի համար, Flexbox՝ դրա ներսի կոմպոնենտների համար։

css
/* Flexbox: a toolbar with items on both ends */
.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
 
/* Grid: responsive cards without media queries */
.products {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1.5rem;
}

04Responsive դիզայն

Responsive կայքն աշխատում է ցանկացած էկրանի վրա՝ 360 պիքսելանոց հեռախոսից մինչև լայն մոնիտոր։ Սկսիր viewport meta tag-ից, այլապես մոբայլ բրաուզերները կցուցադրեն փոքրացված desktop էջ։ Նախագծիր mobile-first. հիմնական ոճերը գրիր փոքր էկրանների համար և ավելացրու media query-ներ մեծերի համար՝ breakpoint-ներն ընտրելով այնտեղ, որտեղ քո կոնտենտն է «կոտրվում», ոչ թե կոնկրետ սարքերի համար։ Օգտագործիր հարաբերական միավորներ (rem՝ տառատեսակի և տարածությունների համար, % և fr՝ լայնությունների համար) և clamp()՝ սահուն տառաչափերի համար։ Container query-ները, որոնք այժմ աջակցվում են բոլոր ժամանակակից բրաուզերներում, թույլ են տալիս կոմպոնենտին հարմարվել իր ծնողի լայնությանը՝ ամբողջ պատուհանի փոխարեն, ինչը իդեալական է վերաօգտագործվող քարտերի համար։ Նկարների համար օգտագործիր srcset և sizes, որպեսզի հեռախոսները փոքր ֆայլեր ներբեռնեն, դիր width և height՝ layout shift-ը կանխելու համար, և lazy-load արա fold-ից ներքև գտնվող նկարները։ Թեստավորիր իրական սարքերի վրա և բրաուզերի device toolbar-ով։

html
<meta name="viewport" content="width=device-width, initial-scale=1" />
 
<style>
  h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }
 
  .layout { display: grid; gap: 1rem; }
  @media (min-width: 768px) {
    .layout { grid-template-columns: 240px 1fr; }
  }
 
  .card-wrap { container-type: inline-size; }
  @container (min-width: 420px) {
    .card { display: flex; gap: 1rem; }
  }
</style>
 
<img src="/img/hero-800.webp" srcset="/img/hero-400.webp 400w, /img/hero-800.webp 800w, /img/hero-1600.webp 1600w"
     sizes="(min-width: 768px) 50vw, 100vw" width="800" height="450" alt="Product on a desk" loading="lazy" />

05DOM և event-ներ

Երբ բրաուզերը parse է անում HTML-ը, կառուցում է DOM-ը (Document Object Model)՝ օբյեկտների ծառ, որը JavaScript-ը կարող է կարդալ և փոխել։ Տարրերը գտնում ես querySelector-ով, փոխում ես textContent-ի, classList-ի և dataset-ի պես property-ների միջոցով, և օգտատիրոջն արձագանքում ես addEventListener-ով։ Event-ները ճանապարհորդում են. click-ը սկսվում է թիրախ տարրից և bubble է անում վեր՝ նրա նախնիների միջով։ Սա հնարավոր է դարձնում event delegation. մեկ listener ծնողի վրա, որը մշակում է իր բոլոր երեխաների click-երը, այդ թվում հետո ավելացվածների։ event.preventDefault()-ը կանգնեցնում է բրաուզերի լռելյայն գործողությունը, օրինակ՝ ձևի submit-ը. event.target-ը ասում է, թե իրականում ինչի վրա են սեղմել։ Օգտատիրոջ տվյալների համար օգտագործիր textContent, ոչ թե innerHTML՝ cross-site scripting-ից (XSS) խուսափելու համար։ Script-երը բեռնիր defer-ով կամ type="module"-ով, որպեսզի դրանք չարգելափակեն էջի rendering-ը։

html
<ul id="cart">
  <li>Coffee beans <button data-id="101" class="remove">Remove</button></li>
  <li>Filter papers <button data-id="102" class="remove">Remove</button></li>
</ul>
<p id="status" role="status"></p>
 
<script type="module">
  const cart = document.querySelector("#cart");
  const status = document.querySelector("#status");
 
  // One listener for every current and future button (delegation)
  cart.addEventListener("click", (event) => {
    const button = event.target.closest("button.remove");
    if (!button) return;
    button.closest("li").remove();
    status.textContent = `Item ${button.dataset.id} removed`; // safe: no innerHTML
  });
</script>

06Տվյալների բեռնում fetch-ով

fetch()-ը HTTP հարցումներ անելու բրաուզերի ներկառուցված միջոցն է, և այն հասանելի է նաև Node.js-ում։ Այն վերադարձնում է Response-ի promise։ Երկու մանրուք մարդկանց հաճախ սայթաքեցնում է։ Առաջին՝ fetch-ը reject է անում միայն ցանցի ձախողման դեպքում. 404-ը կամ 500-ը դեռ resolve են լինում, ուստի միշտ ստուգիր response.ok-ը կամ response.status-ը։ Երկրորդ՝ body-ն կարդացվում է առանձին՝ response.json()-ով կամ response.text()-ով, որը նույնպես ասինխրոն է։ JSON ուղարկիր method-ով, Content-Type header-ով և body-ի համար JSON.stringify-ով։ Օգտագործիր AbortController՝ հարցումը չեղարկելու համար, երբ օգտատերը հեռանում է էջից կամ նոր որոնում է մուտքագրում, և AbortSignal.timeout()՝ անվերջ սպասելուց խուսափելու համար։ UI-ում ցույց տուր loading և error վիճակները և անջատիր submit կոճակը, քանի դեռ հարցումն ընթացքի մեջ է, որպեսզի օգտատերերը չկարողանան այն երկու անգամ ուղարկել։ Cross-origin հարցումները կարգավորվում են CORS-ով, որը պետք է թույլատրի սերվերը, ոչ թե բրաուզերի կոդը։

js
async function searchProducts(query, signal) {
  const res = await fetch(
    `https://api.example.com/products?q=${encodeURIComponent(query)}`,
    { headers: { Accept: "application/json" }, signal }
  );
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  return res.json();
}
 
let controller;
document.querySelector("#search").addEventListener("input", async (e) => {
  controller?.abort(); // cancel the previous, now outdated request
  controller = new AbortController();
  try {
    const results = await searchProducts(e.target.value, controller.signal);
    console.log(results);
  } catch (err) {
    if (err.name !== "AbortError") console.error(err);
  }
});

Օգտագործեք, երբ

  • Միշտ. յուրաքանչյուր վեբ framework վերջում render է անում HTML, CSS և JavaScript։
  • Պարզ կայքեր, landing էջեր և email-ի նման կոնտենտ, որտեղ մաքուր HTML-ն ու CSS-ը արագ են և build քայլ չեն պահանջում։
  • Progressive enhancement. ձև, որն աշխատում է առանց JavaScript-ի և դրանով ավելի հաճելի է դառնում։
  • Framework հավելվածների debug, որտեղ բրաուզերի Elements, Network և Console պանելները ցույց են տալիս, թե իրականում ինչ է կատարվում։

Մի՛ օգտագործեք, երբ

  • Մեծ ինտերակտիվ հավելվածներ՝ շատ ընդհանուր վիճակով, որտեղ ձեռքով գրված DOM կոդը դժվար է սպասարկել, և կոմպոնենտային framework-ը (React, Vue, Svelte) արդարացնում է իրեն։
  • Բարդ widget-ները (date picker-ներ, combobox-ներ, modal-ներ) զրոյից վերագրելը, երբ արդեն կա լավ թեստավորված, հասանելի կոմպոնենտ։

Այլընտրանքներ

  • Component frameworks (React, Vue, Svelte)

    UI-ը նկարագրում են որպես վիճակով կոմպոնենտներ. դրանք միևնույն է արտադրում են HTML, CSS և DOM թարմացումներ։

  • Utility CSS (Tailwind)

    Ոճավորում փոքր class-ներով հենց markup-ում՝ առանձին stylesheet-ների փոխարեն. արագ է, բայց միևնույն է պետք է հասկանաս box model-ը, Flexbox-ը և Grid-ը։

  • htmx and server-rendered HTML

    HTML ուղարկիր սերվերից և փոխարինիր fragment-ներ՝ շատ քիչ JavaScript-ով. լավ է ադմին և CRUD հավելվածների համար։

Լավագույն պրակտիկաներ

  1. Նախ օգտագործիր native տարրեր՝ <button> գործողությունների համար, <a> նավիգացիայի համար, <label> յուրաքանչյուր input-ի համար։
  2. Գրիր mobile-first CSS design token-ներով (custom property-ներ)՝ hard-code արված գույների և չափերի փոխարեն։
  3. Օգտագործիր Grid էջի կառուցվածքի համար, Flexbox՝ կոմպոնենտների համար, և gap՝ տարածությունների համար։
  4. Նկարներին տուր width, height, alt տեքստ և responsive srcset. fold-ից ներքև նկարները lazy-load արա։
  5. Focus-ը պահիր տեսանելի, թեստավորիր միայն ստեղնաշարով և ստուգիր գույների կոնտրաստը WCAG 2.2 AA-ի դեմ։
  6. Յուրաքանչյուր fetch-ից հետո ստուգիր response.ok-ը, ցույց տուր loading և error վիճակները և չեղարկիր հնացած հարցումները։
  7. Անիմացիաների համար հարգիր prefers-reduced-motion-ը։

Տարածված սխալներ

  • Սեղմվող <div>-եր կոճակների փոխարեն, որոնցից ստեղնաշարի և screen reader-ի օգտատերերը չեն կարող օգտվել։
  • Input-ներ առանց label-ների՝ հույս դնելով placeholder տեքստի վրա, որն անհետանում է մուտքագրելիս։
  • Viewport meta tag-ը մոռանալը, ինչի պատճառով մոբայլ կայքը ցուցադրվում է փոքրացված։
  • Ենթադրելը, որ fetch-ը HTTP սխալների դեպքում exception է նետում, և 500 պատասխանից հետո «հաջողություն» ցույց տալը։
  • Օգտատիրոջ մուտքն innerHTML-ով տեղադրելը և XSS-ի համար դուռ բացելը։

Խորը վերլուծություն

07Rendering pipeline-ը և Core Web Vitals-ը

Էջը նկարելու համար բրաուզերը HTML-ը parse է անում DOM-ի, CSS-ը՝ CSSOM-ի, դրանք միավորում է render tree-ի մեջ, հաշվարկում է layout-ը (յուրաքանչյուր box-ի չափն ու դիրքը), պիքսելները նկարում է շերտերում (paint) և վերջում այդ շերտերը GPU-ի վրա համադրում է (composite)։ head-ում CSS-ն արգելափակում է rendering-ը, իսկ առանց defer-ի դասական script-ները արգելափակում են parsing-ը, ահա թե ինչու կրիտիկական CSS-ը պետք է փոքր լինի, իսկ script-ները՝ defer արված։ width-ի կամ top-ի նման layout property-ի փոփոխությունը նորից layout է պարտադրում, երբեմն ամբողջ էջի համար. transform-ի և opacity-ի անիմացիան սովորաբար պահանջում է միայն compositing, որը շատ ավելի էժան է։ Style-եր գրելուց անմիջապես հետո offsetHeight-ի նման layout արժեք կարդալը սինխրոն layout է պարտադրում, իսկ դա ցիկլում անելը (layout thrashing) սառեցնում է էջը։ Google-ի Core Web Vitals-ը սա վերածում է չափելի թիրախների՝ իրական օգտատերերի 75-րդ percentile-ում. Largest Contentful Paint-ը՝ 2.5 վայրկյանից պակաս, Interaction to Next Paint-ը (որը 2024-ի մարտին փոխարինեց First Input Delay-ին)՝ 200 միլիվայրկյանից պակաս, իսկ Cumulative Layout Shift-ը՝ 0.1-ից պակաս։

js
// Layout thrashing: read, write, read, write... forces layout each time
for (const card of cards) {
  card.style.height = card.offsetWidth * 0.75 + "px";
}
 
// Better: batch all reads, then all writes
const widths = cards.map((card) => card.offsetWidth);
cards.forEach((card, i) => {
  card.style.height = widths[i] * 0.75 + "px";
});
 
// Best for this case: let CSS do it, no JavaScript at all
// .card { aspect-ratio: 4 / 3; }
 
// Yield to the browser during long work so input stays responsive (INP)
await new Promise((resolve) => setTimeout(resolve, 0));

08Ժամանակակից CSS ճարտարապետություն՝ layer-ներ, :where(), :has() և specificity-ի վերահսկում

Մեծ stylesheet-երը ձախողվում են specificity-ի պատերազմների պատճառով. ամեն ուղղում պահանջում է նախորդից ուժեղ selector, մինչև !important-ը հայտնվում է ամենուր։ Ժամանակակից CSS-ը գործիքներ է տալիս սա գիտակցաբար վերահսկելու համար։ Cascade layer-ները (@layer reset, base, components, utilities) առաջնահերթությունը որոշում են ըստ layer-ների հերթականության՝ նախքան specificity-ն ընդհանրապես համեմատվի, այնպես որ ավելի ուշ layer-ում պարզ utility class-ը հաղթում է ավելի վաղ layer-ի բարդ component selector-ին։ :where()-ը համընկնում է :is()-ի պես, բայց զրո specificity է ավելացնում, ինչը կատարյալ է reset-ների և գրադարանային style-երի համար, որոնք պետք է հեշտ override լինեն։ :has()-ը երկար սպասված parent selector-ն է. style տուր նկար պարունակող card-ին կամ ձևի դաշտին, որի input-ը անվավեր է՝ առանց JavaScript-ի։ Custom property-ները ժառանգվում են ծառի միջով և կարող են փոխվել runtime-ում, այնպես որ theme-ը պարզապես արժեքների այլ հավաքածու է parent էլեմենտի վրա։ Միասին դրանք թույլ են տալիս գրեթե ամեն selector պահել մեկ class-ի մակարդակում և միևնույն ժամանակ ունենալ կանխատեսելի override-ներ։

css
@layer reset, base, components, utilities;
 
@layer reset {
  :where(ul, ol) { margin: 0; padding: 0; list-style: none; }
}
@layer components {
  .card { padding: var(--space-4); border-radius: var(--radius); }
  .card:has(> img) { padding-top: 0; }               /* parent selector */
  .field:has(input:user-invalid) { --border: #b91c1c; }
}
@layer utilities {
  .p-0 { padding: 0; } /* wins over .card because of layer order */
}
 
[data-theme="dark"] { --color-surface: #111318; --color-text: #e8e8ea; }
 
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition: none !important; }
}

09Պլատֆորմի native UI՝ dialog, popover, inert և focus-ի կառավարում

Modal-ները, մենյուները և tooltip-ները հասանելի դարձնելու համար նախկինում հարյուրավոր տող JavaScript էր պետք։ Հիմա պլատֆորմը դրա մեծ մասն ինքն է անում։ showModal()-ով բացված <dialog> էլեմենտը պահում է focus-ը իր ներսում, էջի մնացած մասը դարձնում է inert, փակվում է Escape-ով և նկարվում է ամեն ինչից վեր՝ top layer-ում։ Popover API-ը (popover ատրիբուտը՝ կոճակի վրա popovertarget-ով, Baseline 2024-ից) ստեղծում է light-dismiss popup-ներ, օրինակ՝ մենյուներ, առանց որևէ script-ի։ inert ատրիբուտը ամբողջ ենթածառը հանում է focus-ից և accessibility tree-ից, ինչն օգտակար է off-canvas նավիգացիայի համար։ Focus-ի կառավարումը դեռ քո պատասխանատվությունն է. երբ dialog-ը փակվում է, focus-ը պետք է վերադառնա այն կոճակին, որը բացել էր այն (dialog-ը դա անում է քո փոխարեն), իսկ client-side նավիգացիայից հետո focus-ը տեղափոխիր նոր էջի վերնագրին, որպեսզի screen reader-ի օգտատերերն իմանան, որ բովանդակությունը փոխվել է։ Ասինխրոն արդյունքները հայտարարիր live region-ի միջոցով (role="status" կամ aria-live)՝ միայն տեսողական փոփոխություններին հույս դնելու փոխարեն։

html
<button popovertarget="account-menu">Account</button>
<div id="account-menu" popover>
  <a href="/orders">Orders</a>
  <a href="/settings">Settings</a>
</div>
 
<button id="open-delete">Delete address</button>
<dialog id="confirm-delete" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this address?</h2>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Delete</button>
  </form>
</dialog>
<script type="module">
  const dialog = document.querySelector("#confirm-delete");
  document.querySelector("#open-delete").addEventListener("click", () => dialog.showModal());
  dialog.addEventListener("close", () => console.log(dialog.returnValue));
</script>

10Front-end-ի անվտանգություն՝ XSS, CSP և երրորդ կողմի ապահով script-ներ

Cross-site scripting-ը (XSS) տեղի է ունենում, երբ հարձակվողի վերահսկած տեքստը մեկնաբանվում է որպես կոդ. տեղադրված innerHTML-ով, href-ում դրված որպես javascript:, կամ կցված inline event handler-ին։ Լռելյայն escaping-ը (textContent, framework-ի templating) կանխում է դրա մեծ մասը. երբ իսկապես պետք է օգտատերերից եկած HTML նկարել, օրինակ՝ rich-text մեկնաբանություններ, sanitize արա այն DOMPurify-ի նման սպասարկվող գրադարանով։ Content Security Policy-ն պաշտպանության երկրորդ գիծն է. HTTP header, որը բրաուզերին ասում է, թե script-ների որ աղբյուրներն են թույլատրված, այնպես որ ներարկված script-ները չեն աշխատում։ Խիստ policy-ն օգտագործում է ամեն request-ի համար nonce կամ hash-եր՝ ամբողջ domain-ներ whitelist անելու փոխարեն։ Trusted Types-ը, որտեղ աջակցվում է, ավելի հեռու է գնում՝ ստիպելով բոլոր վտանգավոր DOM sink-երին անցնել review արված policy-ի միջով։ Քո ավելացրած յուրաքանչյուր երրորդ կողմի script աշխատում է քո էջի լիարժեք հասանելիությամբ, ներառյալ ձևերի դաշտերը, ուստի բեռնիր հնարավորինս քիչ, ֆիքսիր տարբերակները, CDN-ներից ստատիկ ֆայլերի համար օգտագործիր Subresource Integrity և երբեք analytics մի դիր վճարման էջերում։

http
# HTTP response header (one line in practice), nonce generated per request
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m123' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data: https://cdn.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
 
<!-- Static library from a CDN, verified by hash -->
<script src="https://cdn.example.com/[email protected]/lib.min.js"
        integrity="sha384-BASE64_HASH_OF_THE_FILE" crossorigin="anonymous" defer></script>

Իրական իրավիճակներ

01

Էջը բեռնվելիս ցատկում է, և օգտատերերը սխալ կոճակն են սեղմում

Խնդիր

Մոբայլում ապրանքի էջը բեռնվելիս մի քանի անգամ տեղաշարժվում է. հայտնվում է banner, font-երը փոխվում են, իսկ նկարները բովանդակությունը ներքև են հրում։ Օգտատերերը բողոքում են պատահական սեղմումներից, և field data-ում CLS-ը 0.32 է։

Ինչպես լուծել

Նախ գտիր տեղաշարժվող էլեմենտները. Chrome DevTools-ի Performance panel-ը նշում է layout shift-երը, իսկ Web Vitals extension-ը ցույց է տալիս, թե որ node-երն են շարժվել։ Հետո տեղ պահիր այն ամենի համար, ինչ ուշ է գալիս։ Յուրաքանչյուր նկարի տուր width և height կամ CSS-ում aspect-ratio։ Banner-ի slot-ի համար պահիր ֆիքսված min-height կամ նկարիր այն fold-ից ներքև։ Օգտագործիր font-display: swap՝ չափով ճշգրտված fallback font-ով (size-adjust կամ framework-ի font loader), որպեսզի տեքստը reflow չանի։ Երբեք բովանդակություն մի ավելացրու առկա բովանդակությունից վեր, եթե դա օգտատիրոջ գործողության պատասխան չէ։ Deploy արա, հետո հետևիր 75-րդ percentile-ի CLS-ին իրական օգտատերերի տվյալներում, որովհետև lab թեստերը բաց են թողնում գովազդից և դանդաղ ցանցերից առաջացած shift-երը։

02

Accessibility audit-ը տապալում է checkout-ը

Խնդիր

Հաճախորդի audit-ը հաղորդում է, որ checkout-ը հնարավոր չէ ավարտել ստեղնաշարով, սխալի հաղորդագրությունները չեն հայտարարվում, և մի քանի կոճակի կոնտրաստը չի անցնում։ Իրավական deadline-ը մեկ ամսից է։

Ինչպես լուծել

Ուղղիր ըստ ազդեցության՝ սկսելով արգելափակողներից։ Սեղմվող div-երը փոխարինիր իրական button-ներով և link-երով, համոզվիր, որ ամեն քայլ աշխատում է Tab-ով, Enter-ով, Space-ով և Escape-ով, և պահիր տեսանելի :focus-visible outline։ Յուրաքանչյուր input կապիր label-ի հետ, սխալները նշիր aria-invalid-ով և հաղորդագրությունը միացրու aria-describedby-ով. submit-ի ժամանակ focus-ը տեղափոխիր սխալների ամփոփագրին՝ դաշտերի link-երով։ «Card declined»-ի նման ասինխրոն արդյունքները հայտարարիր role="alert" region-ում։ Գույների token-ները ուղղիր մեկ անգամ, որպեսզի յուրաքանչյուր կոճակ անցնի 4.5:1-ը։ Ստուգիր axe DevTools-ով, հետո ձեռքով թեստավորիր screen reader-ով, օրինակ՝ VoiceOver-ով կամ NVDA-ով, որովհետև ավտոմատ գործիքները բռնում են խնդիրների միայն մի մասը։

03

Մարքեթինգային script-ը դանդաղեցնում է յուրաքանչյուր ինտերակցիա

Խնդիր

Chat-ի, heatmap-ի և երկու tracking script-ի ավելացումից հետո կայքը «ծանր» է զգացվում։ Field data-ում INP-ն բարձրացել է մինչև 450 ms, իսկ main thread-ը բեռնումից հետո վայրկյաններով զբաղված է։

Ինչպես լուծել

Profile արա Chrome-ի Performance panel-ում CPU throttling-ով և ակտիվությունը խմբավորիր ըստ երրորդ կողմի՝ տեսնելու, թե որ script-ին են պատկանում long task-երը։ Հեռացրու script-ները, որոնք ոչ ոք չի օգտագործում. սովորաբար այդպիսին մեկը կա։ Մնացածը բեռնիր defer-ով կամ load event-ից հետո, իսկ chat widget-ը բեռնիր միայն այն ժամանակ, երբ օգտատերը սեղմում է «Chat» կոճակը (facade)։ Tag manager-ի tag-երը տեղափոխիր consent-ի հետևում, ինչը, միևնույն է, պարտադիր է։ Քո կոդում երկար handler-ները բաժանիր փոքր մասերի, դրանց միջև main thread-ին հերթ զիջիր (yield) և ծանր աշխատանքը, օրինակ՝ մեծ ցուցակների ֆիլտրումը, հեռու պահիր input handler-ներից։ Մի քանի օր անց նորից ստուգիր INP-ն real-user monitoring-ում, որովհետև այն field metric է։

Արագ ուղեցույց

Modal՝ focus trap-ով և Escape-ով
dialog.showModal() // <form method="dialog"> closes it
Մենյու առանց JavaScript-ի
<button popovertarget="menu">Menu</button> <div id="menu" popover>...</div>
Թաքցնել հատվածը focus-ից և screen reader-ներից
<aside inert>...</aside>
Cascade-ի հերթականության սահմանում
@layer reset, base, components, utilities;
Zero-specificity reset
:where(ul, ol) { list-style: none; }
Parent-ին style տալ ըստ իր child-ի
.card:has(> img) { padding-top: 0; }
Տեղ պահել մեդիայի համար
img, video { aspect-ratio: 16 / 9; width: 100%; height: auto; }
Հայտարարել ասինխրոն թարմացումները
<p role="status" aria-live="polite"></p>
Կրճատված շարժում
@media (prefers-reduced-motion: reduce) { ... }
Հիմնական CSP header
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'

Իմ իրական նախագծերում

  • Գործակալության մարքեթինգային կայքը վերակառուցեցի զրոյից՝ ներառյալ ինտերակտիվ էֆեկտներ, dark mode, sitemap և SEO redirect-ներ, հենց այս հիմունքների վրա։
  • Կենդանիների ապրանքների խանութում ամբողջ թիմի համար գրի առա design-system-ի կանոնները. միայն design token-ներ, ոչ մի raw գույն կամ պիքսելային արժեք և յոթ սահմանված breakpoint։
  • Էջերը ստուգում եմ հեռախոսի, պլանշետի և desktop-ի լայնություններում (390, 768 և 1920 պիքսել)՝ ավտոմատ browser-verification քայլով, նախքան աշխատանքն ընդունվելը։
  • Մեր ներքին փաթեթները ներառում են design inspector, որը դիզայներներին թույլ է տալիս CSS փոփոխականները կարգավորել live բրաուզերում և պահպանել դրանք հետ՝ սորսում։

Հարցեր և պատասխաններ

Q1Ի՞նչ է semantic HTML-ը և ինչո՞ւ է այն կարևոր։

Semantic HTML նշանակում է տարրերն ընտրել իրենց իմաստով, ոչ թե տեսքով՝ <nav> նավիգացիայի համար, <button> գործողությունների համար, <h2> բաժնի վերնագրի համար, <table> աղյուսակային տվյալների համար։ Բրաուզերը և assistive տեխնոլոգիաները օգտագործում են այդ իմաստը։ Screen reader-ը կարող է թվարկել վերնագրերն ու landmark-ները, որպեսզի օգտատերերը թռչեն էջի մեջ. իսկական կոճակը ստանում է ստեղնաշարի focus և ճիշտ է հայտարարվում առանց լրացուցիչ կոդի. որոնողական համակարգերը հասկանում են կառուցվածքը և հիմնական կոնտենտը։ Այն նաև նվազեցնում է կոդը, որովհետև վարքագիծը ստանում ես անվճար՝ JavaScript-ով և ARIA-ով այն վերակառուցելու փոխարեն։ Ոճավորումն առանձին է, ուստի semantic տարրը կարող է ունենալ ցանկացած տեսք։

Q2Ե՞րբ ես ընտրում Flexbox, և ե՞րբ՝ Grid։

Flexbox-ը մեկ չափման համար է. տարրերի տող կամ սյունակ, որոնց չափը հիմնականում գալիս է իրենց կոնտենտից, օրինակ՝ նավիգացիոն բարեր, toolbar-ներ, կոճակների խմբեր կամ wrap անող tag-երի ցուցակներ։ Grid-ը երկու չափման համար է. երբ ուզում ես նախ սահմանել տողեր և սյունակներ և տարրերը տեղադրել դրանց մեջ, օրինակ՝ էջերի layout-ներ, ապրանքների պատկերասրահներ և dashboard-ներ։ Grid-ը նաև տալիս է responsive սյունակներ առանց media query-ների՝ repeat(auto-fill, minmax(...))-ի միջոցով։ Գործնականում յուրաքանչյուր էջում օգտագործում եմ երկուսն էլ. Grid՝ ընդհանուր կառուցվածքի և քարտերի ցուցակների համար, Flexbox՝ յուրաքանչյուր կոմպոնենտի ներսում հավասարեցման համար։ Երկուսն էլ աջակցում են gap, որը նախընտրում եմ տարրերի միջև margin-ներից։

Q3Բացատրիր event bubbling-ը և event delegation-ը։

Երբ սեղմում ես տարրի վրա, event-ը նախ իջնում է document-ից մինչև թիրախ (capture փուլ), հետո bubble է անում հետ՝ յուրաքանչյուր նախնու միջով։ Հետևաբար ծնողների վրա listener-ները «լսում» են իրենց երեխաների click-երը։ Event delegation-ը օգտագործում է սա. երկար ցուցակի յուրաքանչյուր տողին listener կցելու փոխարեն մեկ listener ես կցում ցուցակին և ստուգում event.target-ը, սովորաբար closest()-ով՝ համապատասխան երեխային գտնելու համար։ Այն ավելի քիչ հիշողություն է օգտագործում և ավտոմատ աշխատում է հետո ավելացված տարրերի համար։ Propagation-ը կարող ես կանգնեցնել stopPropagation()-ով, բայց օգտագործիր այն խնայողաբար, որովհետև այն կոտրում է այլ կոդ, օրինակ՝ անալիտիկան կամ outside-click handler-ները, որոնք հենվում են bubbling-ի վրա։

Q4Ի՞նչ է փոխում box-sizing: border-box-ը։

Լռելյայն (content-box) width-ը և height-ը սահմանում են միայն content-ի տարածքը. padding-ը և border-ը ավելանում են վրան։ 300 պիքսել լայնությամբ box-ը 20 պիքսել padding-ով և 1 պիքսել border-ով իրականում 342 պիքսել լայն է, ինչը layout-ները կոտրում է շփոթեցնող ձևերով։ border-box-ով width-ը ներառում է padding-ը և border-ը, ուստի box-ը ճիշտ 300 պիքսել է, իսկ content-ը փոքրանում է՝ տեղավորվելու համար։ Սա համապատասխանում է դիզայներների մտածելակերպին և տոկոսային layout-ները դարձնում կանխատեսելի։ Գրեթե յուրաքանչյուր ժամանակակից reset կիրառում է border-box բոլոր տարրերի և pseudo-element-ների վրա, և ես նույնն եմ անում յուրաքանչյուր նախագծում։

Q5Ինչո՞ւ fetch-ը սխալ չի նետում 404 կամ 500 պատասխանի դեպքում։

Որովհետև fetch-ի տեսանկյունից հարցումը հաջողվել է. այն հասել է սերվերին և ստացել վավեր HTTP պատասխան։ fetch-ը reject է անում միայն այն ժամանակ, երբ հարցումն ընդհանրապես չի կարողացել ավարտվել, օրինակ՝ երբ ցանցն անջատված է, DNS-ը ձախողվում է, CORS-ն արգելափակում է այն կամ այն abort է արվել։ HTTP սխալի status-ները նորմալ պատասխաններ են, որոնք պետք է ինքդ ստուգես response.ok-ով, որը true է 200-299-ի համար, կամ response.status-ով։ fetch-ը փաթաթում եմ փոքր helper-ի մեջ, որը ստուգում է ok-ը, parse է անում JSON-ը, նետում է տիպավորված սխալ՝ status-ով և body-ով, և աջակցում է AbortSignal և timeout, այնպես որ մնացած կոդը երբեք չի մոռանում այս ստուգումները։

Սենիոր մակարդակի հարցեր

S1Ինչպե՞ս կկառուցես design system, որն աշխատի մի քանի պրոդուկտների և framework-ների միջև։

Այն բաժանում եմ տարբեր կյանքի տևողություն ունեցող շերտերի։ Հիմքը design token-ներն են՝ գույներ, spacing, տառատեսակ, radius-ներ և շարժում, պահված որպես տվյալներ և export արված որպես CSS custom property-ներ, այնպես որ ցանկացած framework կամ նույնիսկ պարզ HTML կարող է դրանք օգտագործել, իսկ theme-երը պարզապես տարբեր արժեքներ են։ Դրանց վրա գալիս են CSS-ի պայմանավորվածությունները՝ cascade layer-ներ, ցածր specificity-ով class-ների անուններ և փաստաթղթավորված breakpoint-ներ։ Կոմպոնենտները գալիս են վերջում և ամենաշատն են կապված framework-ի հետ՝ կառուցված native էլեմենտների վրա accessibility-ի համար։ Փոխզիջումը ճկունության և հետևողականության միջև է. խիստ կոմպոնենտները պրոդուկտները միատեսակ են պահում, բայց հիասթափեցնում են հատուկ կարիքներ ունեցող թիմերին, ուստի ես թույլ եմ տալիս composition և փաստաթղթավորված «փախուստի ելքեր»՝ անվերջ props-ների փոխարեն։ Versioning-ը նույնպես կարևոր է. token-ները փոխվում են հազվադեպ և ապահով, իսկ կոմպոնենտների API-ներին պետք են semver և միգրացիայի նշումներ, ուստի հնարավորության դեպքում դրանք release եմ անում առանձին։

S2Էջն ունի վատ INP։ Ինչպե՞ս ես ախտորոշում և ուղղում այն։

INP-ն չափում է ինտերակցիայից մինչև հաջորդ frame ընկած ուշացումը, ուստի պատճառը միշտ զբաղված main thread-ն է՝ handler-ի աշխատելուց առաջ (input delay), հենց handler-ում (processing) կամ դրանից հետո rendering-ում (presentation delay)։ Սկսում եմ field data-ից՝ real-user monitoring-ի կամ web-vitals գրադարանի attribution-ով, գտնելու համար, թե որ ինտերակցիաներն ու էլեմենտներն են դանդաղ, որովհետև lab թեստերը հազվադեպ են դրանք վերարտադրում։ Հետո վերարտադրում եմ Performance panel-ում CPU throttling-ով։ Ուղղումները կախված են փուլից. հեռացրու կամ հետաձգիր երրորդ կողմի script-ները, որոնք long task-եր են ստեղծում, երկար handler-ները բաժանիր և մասերի միջև yield արա, նախ թարմացրու UI-ը, իսկ ծանր աշխատանքը արա հետո, խուսափիր layout thrashing-ից, փոքրացրու DOM-ի չափը և էկրանից դուրս երկար հատվածների համար օգտագործիր content-visibility։ React հավելվածներում transition-ները և re-render-ի ավելի փոքր scope-երը օգնում են նույն կերպ։

S3Ե՞րբ է progressive enhancement-ը 2026-ին արժե լրացուցիչ ջանքերին։

Այն արժե ամենուր, որտեղ ձախողումը թանկ է, և JavaScript-ը կարող է ձախողվել՝ checkout, գրանցում, որոնման և կոնտակտային ձևեր, ինչպես նաև բովանդակություն, որը պետք է ինդեքսավորվի։ JavaScript-ն ավելի հաճախ է ձախողվում, քան թիմերը կարծում են. դանդաղ ցանցերը timeout են տալիս, extension-ները կոտրում են script-ները, հին սարքերի հիշողությունը վերջանում է, և մեկ runtime սխալը կարող է կանգնեցնել ամբողջ bundle-ը։ Ձևը, որը submit է լինում որպես պարզ HTML և բարելավվում client-side վալիդացիայով ու ակնթարթային արձագանքով, շարունակում է աշխատել այդ բոլոր դեպքերում։ Ժամանակակից framework-ները սա ավելի էժան են դարձնում, քան նախկինում էր, որովհետև server action-ները և native form submission-ը աշխատում են առանց hydration-ի։ Փոխզիջումն այն է, որ բարձր ինտերակտիվ գործիքները, օրինակ՝ էդիտորները, քարտեզները կամ dashboard-ները, քիչ բան են շահում առանց JavaScript-ի fallback-ից, ուստի progressive enhancement-ը կիրառում եմ կրիտիկական ուղիների վրա, ոչ թե որպես համընդհանուր կանոն։

Բառարան

Render tree
DOM-ի և CSSOM-ի համադրությունը, որի layout-ը բրաուզերը հաշվարկում և նկարում է. թաքնված էլեմենտները բացառվում են։
Reflow (layout)
Box-երի չափերի և դիրքերի վերահաշվարկ փոփոխությունից հետո. թանկ է, երբ ազդում է էջի մեծ մասերի վրա։
Compositing
Նկարված շերտերի համադրում GPU-ի վրա. transform-ի և opacity-ի անիմացիան սովորաբար պահանջում է միայն այս քայլը։
LCP
Largest Contentful Paint. երբ է ավարտվում ամենամեծ տեսանելի էլեմենտի rendering-ը։ Լավ է 2.5 վայրկյանից պակասը։
INP
Interaction to Next Paint. որքան արագ է էջը տեսանելիորեն արձագանքում click-երին, tap-երին և ստեղներին։ Լավ է 200 ms-ից պակասը։
CLS
Cumulative Layout Shift. որքան է տեսանելի բովանդակությունն անսպասելիորեն շարժվում էջի օգտագործման ընթացքում։ Լավ է 0.1-ից պակասը։
Cascade layer
Style-երի անվանված խումբ. layer-ների հերթականությունը որոշում է առաջնահերթությունը՝ նախքան selector-ների specificity-ն համեմատվի։
Top layer
Բրաուզերի կառավարած շերտ ամբողջ բովանդակությունից վեր, որտեղ նկարվում են modal dialog-ները և popover-ները՝ անկախ z-index-ից։
CSP
Content Security Policy. response header, որը թվարկում է, թե script-ների, style-երի և frame-երի որ աղբյուրներից կարող է բրաուզերը բեռնել։
Live region
Էլեմենտ, որի բովանդակության փոփոխությունները հայտարարում են screen reader-ները. նշվում է aria-live-ով կամ role="status"-ով։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։