Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Браузерні API й мережа»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

3 питання

Усі три зберігають дані в браузері, але з різним терміном життя, обсягом і, головне, - різною поведінкою щодо сервера.

localStorage sessionStorage cookies
живе доки не видалять доки відкрита вкладка до Expires/Max-Age або до закриття браузера
видимість усі вкладки домену лише поточна вкладка усі вкладки домену
обсяг ~5 МБ на домен ~5 МБ ~4 КБ на cookie
іде на сервер ні ні з кожним запитом
доступ з JS так так так, якщо не HttpOnly

Web Storage (localStorage, sessionStorage):

localStorage.setItem('theme', 'dark');
localStorage.getItem('theme');              // 'dark'
localStorage.removeItem('theme');

// лише рядки - об'єкти через JSON
localStorage.setItem('filters', JSON.stringify({ city: 'Київ' }));
const filters = JSON.parse(localStorage.getItem('filters') ?? '{}');
  • зберігаються лише рядки: setItem('count', 5) збереже '5';
  • API синхронний - великі записи блокують головний потік;
  • зміни в одній вкладці викликають подію storage в інших вкладках того самого домену - спосіб синхронізувати вкладки;
  • у приватному режимі, при заповненому сховищі чи заблокованих cookies доступ може кидати виняток - звертання варто обгортати в try.

Cookies створені для сервера: браузер автоматично додає їх до кожного запиту на домен. Тому в них зберігають ідентифікатор сесії, а не налаштування інтерфейсу - інакше кожен запит, включно з картинками, нестиме зайві байти.

Безпека - головне:

  • не зберігайте токени автентифікації в localStorage. Будь-який скрипт на сторінці (включно з XSS чи скомпрометованою бібліотекою) прочитає їх і відправить зловмиснику;
  • сесійна cookie з прапорцем HttpOnly недоступна JavaScript взагалі - XSS не зможе її вкрасти. Так працює автентифікація Laravel (і Sanctum для SPA на тому ж домені);
  • Secure - лише через HTTPS, SameSite - захист від підробки запитів з інших сайтів.

Що куди класти:

  • тема, згорнуті панелі, чернетка форми - localStorage;
  • стан майстра, що не повинен «перетікати» в іншу вкладку, - sessionStorage;
  • сесія, автентифікація - HttpOnly-cookie від сервера;
  • великі обсяги чи бінарні дані - IndexedDB (асинхронна база в браузері).

Докладніше в документації: Window.localStorage

FormData збирає дані форми у формат, який браузер відправляє так само, як звичайна HTML-форма, - зокрема з файлами.

const form = document.querySelector('#profile-form');

form.addEventListener('submit', async (event) => {
  event.preventDefault();

  const data = new FormData(form);          // усі поля з атрибутом name, включно з файлами
  data.append('source', 'web');             // додаткове поле

  const response = await fetch(form.action, {
    method: 'POST',
    body: data,
    headers: { Accept: 'application/json' },
  });
});

Найчастіша помилка - вручну задати Content-Type:

headers: { 'Content-Type': 'multipart/form-data' }   // зламає запит

Для multipart/form-data заголовок обов'язково містить межу (boundary), що розділяє частини: multipart/form-data; boundary=----WebKitFormBoundary.... Браузер генерує її сам, якщо тіло - FormData. Вказаний вручну заголовок межі не має - сервер не зможе розібрати запит.

Корисні методи:

data.get('email');                    // значення поля
data.getAll('tags[]');                // усі значення поля з однаковою назвою
data.set('email', 'new@example.com'); // замінити
data.delete('password_confirmation');
Object.fromEntries(data);             // в об'єкт (для полів без повторів)

Файли: <input type="file" name="avatar"> потрапляє у FormData автоматично. Додати файл чи Blob вручну - data.append('avatar', file, 'avatar.png').

Особливості з Laravel:

  • PUT/PATCH з файлами: PHP історично розбирає multipart/form-data лише для POST. Для оновлення з файлами відправляють POST з підміною методу: data.append('_method', 'PUT');
  • CSRF: для форм, що відправляються fetch-ем, токен з поля _token потрапить у FormData сам, якщо в Blade-формі є @csrf;
  • Accept: application/json - щоб Laravel при помилці валідації повернув JSON 422 з полем errors, а не редирект назад;
  • масиви називають як у HTML: tags[], items[0][qty] - Laravel розбере їх у вкладені масиви.

Коли не FormData: якщо файлів немає, а API приймає JSON, простіше body: JSON.stringify(payload) з Content-Type: application/json.

Прогрес завантаження великого файлу fetch наразі не показує. Для смуги прогресу використовують XMLHttpRequest (подія upload.onprogress) або бібліотеку на кшталт axios, що використовує його всередині.

Докладніше в документації: FormData

Збирати адреси склеюванням рядків - джерело помилок: забуті ? і &, неекрановані пробіли, кирилиця й спецсимволи. Для цього є вбудовані класи.

URL - розбір і побудова адреси:

const url = new URL('https://laravelukraine.com/jobs?page=2#list');

url.hostname;     // 'laravelukraine.com'
url.pathname;     // '/jobs'
url.search;       // '?page=2'
url.hash;         // '#list'

// відносна адреса відносно базової
new URL('/api/users', window.location.origin).href;

URLSearchParams - параметри запиту:

const params = new URLSearchParams(window.location.search);
params.get('page');          // '2' - завжди рядок або null
params.has('q');
params.getAll('tags[]');     // для повторюваних параметрів

params.set('page', 3);
params.append('tags[]', 'php');
params.delete('page');
params.toString();           // 'tags%5B%5D=php'

Найзручніше - разом:

const url = new URL('/api/vacancies', location.origin);
url.searchParams.set('q', 'вакансії php');
url.searchParams.set('remote', '1');

await fetch(url);   // '/api/vacancies?q=%D0%B2%D0%B0...&remote=1'

Екранування робиться автоматично: кирилиця, пробіли, & і = у значеннях не зламають адресу.

Побудова з об'єкта:

const query = new URLSearchParams({ q: 'laravel ukraine', tag: 'php&js' });
query.toString();   // 'q=laravel+ukraine&tag=php%26js'

Пробіл кодується як + (формат форм), а не %20 - сервери розуміють обидва варіанти.

Корисні прийоми:

  • оновити адресу без перезавантаження (фільтри в каталозі):
const url = new URL(location.href);
url.searchParams.set('sort', 'price');
history.replaceState(null, '', url);
  • перевірка коректності: URL.canParse(value) - без try, бо конструктор new URL на некоректному рядку кидає TypeError;
  • безпека: перед переходом за адресою з даних користувача варто перевірити url.protocol - щоб не перейти на javascript:....

Пастки:

  • params.get повертає рядок: params.get('page') + 1 дасть '21'. Перетворюйте явно: Number(params.get('page') ?? 1);
  • URLSearchParams з об'єкта з масивом ({ ids: [1, 2] }) дасть ids=1%2C2 - для масивів потрібні окремі append.

Laravel читає повторювані параметри як масив лише з дужками (tags[]=php&tags[]=js) - без них збережеться лише останнє значення.

Докладніше в документації: URLSearchParams