Второй проект для портфолио — уже «взрослый»: он ходит за данными в интернет, показывает загрузку, переживает ошибки сети, не заваливает сервер запросами при каждой букве и собирается в готовую сборку через Vite. Именно так устроены погодные виджеты, поиск товаров и ленты новостей. Сегодня вы защитите проект, проверите код друг друга и напишете Контрольную №2 без ИИ.
Выбрать вариант приложения на публичном API и разбить его на шаги вместе с ИИ
Показывать состояния loading / error / empty / success и не показывать устаревшие ответы
Ограничивать запросы через debounce и разносить код по модулям
Собрать проект через Vite (npm run build) и залить в репозиторий
Защитить проект за 5 минут и провести code review в паре
Честно разобрать, где ИИ сэкономил время, а где увёл в сторону
Как работать с этой страницей
Сегодня два блока. Первый — мини-проект: задание, критерии оценки (0–100), план с ИИ, две разминки по состояниям и запросам, стартовый шаблон в песочнице, ревью в парах и защита. Второй — Контрольная №2: в разделе 11 лежит полная пробная версия без подсказок. Для разминок мы не ходим в настоящую сеть: вместо fetch используем «поддельный сервер» из Promise и setTimeout — так результат всегда одинаковый и его можно проверить автоматически. Свой проект вы собираете у себя в VS Code.
01 Задание: выберите вариант
Проект собирается из того, что вы прошли на уроках 13–17: асинхронность, fetch, состояния загрузки, debounce, модули и сборка Vite. Выберите один из трёх вариантов. Все они оцениваются по одним и тем же критериям, а API у всех открытые и не требуют ключа.
Вариант А. Поиск товаров
Поле поиска, под ним — карточки товаров. Запрос уходит с задержкой (debounce), пока идёт загрузка — индикатор, если ничего не нашли — понятное сообщение.
Список городов (Душанбе, Худжанд, Бохтар, Куляб и свои). Выбираете город — видите температуру и ветер. Ошибку сети можно повторить кнопкой.
API:api.open-meteo.com/v1/forecast с параметрами latitude, longitude, current=temperature_2m,wind_speed_10m
Вариант В. Люди и их посты
Поиск пользователя по имени, при выборе — его посты. Два зависимых запроса: сначала человек, потом его записи.
API:jsonplaceholder.typicode.com: /users и /posts?userId=1
Сложность примерно равная: у товаров больше логики поиска, у погоды — разбор вложенного ответа, у пользователей — зависимые запросы. Свою идею на другом открытом API можно предложить, если у него нет ключа, он работает из браузера (CORS) и объём такой же — согласуйте это с преподавателем до начала работы.
Аналогия из жизни
Вы заказываете плов в чайхоне через официанта. Пока готовится — вы видите «заказ принят» (loading). Если плова нет — официант честно говорит «закончился» (empty), а если на кухне случился сбой — «извините, попробуйте ещё раз» (error). Плохой официант молчит и не отвечает вообще — и вы не знаете, ждать или уходить. Ваше приложение должно вести себя как хороший официант.
Что вы показываете в конце
Работающее приложение — открывается без ошибок в консоли, при отключённой сети показывает аккуратную ошибку.
Репозиторий на GitHub с кодом на Vite, README и скриншотом; команда npm run build проходит без ошибок.
Защита на 5 минут — демонстрация, объяснение кода и ответы на вопросы.
Короткий разбор «ИИ в проекте» — раздел 12: где ИИ помог, где увёл в сторону.
Правила курса действуют и здесь
ИИ можно использовать, но: сначала пробуете сами 15 минут, не вставляете код, который не можете объяснить построчно, и на защите преподаватель вправе попросить объяснить любую строку. Не смогли — баллы за эту часть снимаются. А вот на Контрольной №2 ИИ выключен полностью.
02 Требования: MVP и бонусы
MVP — минимальная версия, которая уже работает от начала до конца. Сначала делаем её полностью, и только потом украшения. Проект без MVP не получит высокой оценки, сколько бы бонусов в нём ни было.
MVP — обязательно для всех вариантов
№
Требование
Как проверим
1
Данные приходят с публичного API через fetch и async/await
во вкладке Network виден запрос, в коде нет «пустых» данных-заглушек
2
Проверка response.ok: ответ 404 или 500 считается ошибкой, а не данными
в коде есть if (!response.ok) throw …
3
Состояние loading: пока идёт запрос — индикатор, поле или кнопка не дают отправить двойной запрос
включаем Slow 3G в DevTools
4
Состояние error: понятное сообщение и кнопка «Повторить»; приложение не падает
включаем Offline в DevTools
5
Состояние empty: «Ничего не найдено» вместо пустого экрана
вводим бессмысленный запрос
6
Поиск или выбор (по варианту) с debounce или явной кнопкой; пустой запрос не уходит на сервер
быстро печатаем: в Network не больше одного запроса
7
Устаревший ответ не затирает свежий (счётчик запросов или AbortController)
быстро меняем запрос — на экране последний, а не предпоследний
8
Код разбит на ES-модули: api.js, ui.js (или render.js), main.js
структура папки src/
9
Проект на Vite: npm run dev запускается, npm run build завершается без ошибок
запускаем на защите
Бонусы — после MVP
Скелетоны вместо слова «Загрузка…» (серые блоки под карточки).
Кэш последних запросов: тот же запрос второй раз не ходит в сеть.
Таймаут запроса через Promise.race или AbortController со своим сообщением.
История поиска или избранное в localStorage.
Автоповтор при ошибке: до трёх попыток с паузой.
Тёмная тема с сохранением выбора.
Пагинация или кнопка «Показать ещё» (limit и skip у dummyjson).
Совет
Бонус ценится, только если вы можете объяснить каждую его строку. Один простой кэш, который вы понимаете, лучше трёх «магических» кусков из чата с ИИ.
03 Как оценивается проект (0–100)
Мини-проект оценивается по шкале 0–100, как записано в правилах курса. Оценка складывается из восьми критериев:
Критерий
Баллы
Максимум ставим, если…
Частично, если…
Функциональность MVP
20
приложение решает задачу выбранного варианта от начала до конца, нет красных ошибок в консоли
основной сценарий работает, но есть баги или нет части пунктов
fetch и async/await
20
реальный запрос на публичный API, await стоит где нужно, проверяется response.ok, ответ разбирается через await response.json(), ошибки ловит try/catch
запрос работает, но 404 не считается ошибкой или try/catch формальный
Состояния loading / error / empty
15
все три состояния есть, переключаются из одного объекта состояния, есть «Повторить», двойной запрос невозможен
есть одно-два из трёх, или состояния «залипают» (после ошибки остаются старые данные)
Debounce и контроль запросов
10
запросы ограничены debounce, пустой запрос не отправляется, устаревший ответ не затирает свежий
debounce есть, но гонка ответов не решена
Модули и структура
10
ES-модули с понятными границами (api не трогает DOM, ui не ходит в сеть), короткие функции, нет дублей и мёртвого кода
модули есть, но «всё в main.js» или функции-монстры
Сборка и репозиторий
5
npm run build проходит, публичный репозиторий, README со скриншотом, ≥ 5 осмысленных коммитов
сборка работает, но README пустой или коммит «всё одним куском»
Интерфейс
5
аккуратно, читается на телефоне (320–400 px), есть :hover и :focus, кнопки блокируются на время запроса
работает, но выглядит как черновик
Защита
15
уложились в 5 минут, показали сценарий (в том числе ошибку и пустой результат), объяснили ключевые куски и ответили на вопросы
объясняете общими словами, поплыли на «объясни эту строку»
ИТОГО
100
Бонусы добавляют до +5 баллов, но итог не может быть больше 100.
Что снимает баллы
Не можете объяснить свой код — теряете баллы в том критерии, к которому относится непонятная часть.
Данные «захардкожены» вместо настоящего запроса — критерий «fetch и async/await» получает 0.
Не сдано в срок — до −10 баллов, по договорённости с преподавателем.
Код целиком скопирован у другого студента или из чата без понимания — 0 за проект. Помогать друг другу можно и нужно, но каждый пишет и понимает своё.
Как это влияет на итоговую оценку курса
Компонент курса
Вес
Как считается
Посещаемость
15%
доля посещённых уроков
Домашние задания
30%
средний балл за 24 ДЗ (0–10)
Мини-проекты (2 шт.)
25%
среднее за мини-проект 1 и 2 (0–100)
Финальный проект
30%
защита на уроке 21 (0–100)
Для сертификата нужно сдать оба мини-проекта. Контрольная №2 — отдельная проверка знаний: она показывает, что вы понимаете асинхронный код без ИИ, и разбирается с преподавателем (см. раздел 11).
Баллы
Оценка
90–100
Отлично
75–89
Хорошо
60–74
Удовлетворительно
менее 60
Не аттестован
04 Декомпозиция: разбиваем проект вместе с ИИ
Проект с API опаснее первого: появились сеть, ожидание и ошибки, которые не воспроизводятся «по заказу». Поэтому план особенно важен. Идея та же, что на уроке 12: сначала шаги, потом код, и у каждого шага — видимый результат. Но теперь в плане должны быть отдельные шаги на каждое состояние (loading, error, empty) и на «рискованные» места: первый запрос, гонка ответов, сборка.
Аналогия из жизни
Поездка маршруткой из Душанбе в Худжанд: нужно заранее знать остановки, время в пути и запасной вариант, если дорогу перекроют. «Поеду как-нибудь» — путь в никуда. План с запасным вариантом — это ваши состояния error и «Повторить».
ИИ-блок: большая задача — непонятно, с чего начать
ИИ хорошо предлагает список шагов, но порядок, лишнее и оценку времени проверяете вы. Составляйте запрос по формуле курса: контекст → задача → ограничения → формат ответа → проверка.
сделай приложение с погодой на js
Контекст: я студент курса JavaScript, делаю учебный проект «погода в городах Таджикистана» на чистом JS + Vite, без фреймворков. API: open-meteo (без ключа). Уже умею: fetch, async/await, try/catch, состояния loading/error/empty, debounce, ES-модули.
Задача: разбей проект на 10–14 шагов по 20–40 минут в порядке выполнения.
Ограничения: без библиотек; модули api.js, ui.js, main.js; в MVP — выбор города, запрос, три состояния, кнопка «Повторить», защита от устаревших ответов. Бонусы — отдельным списком в конце. Код не пиши.
Формат ответа: нумерованный список. У каждого шага: название, что должно работать после него, оценка в минутах.
Проверка: после списка назови 3 места, где новичок чаще всего ошибается в работе с fetch, и как их заметить.
Первый запрос даст случайный набор слов или сразу весь код, который вы не сможете объяснить. Второй — план, с которым можно работать. Но это только черновик. Вот пример плана после вашей правки:
Пример плана для варианта Б (≈ 5 часов)
№
Шаг
Время
Что работает после него
1
Vite-проект, структура src/api.js, ui.js, main.js
20 мин
npm run dev открывает пустую страницу
2
Разметка: выбор города, блок результата, блок статуса
25 мин
страница выглядит как приложение, но «мёртвая»
3
api.js: getWeather(city) с fetch и проверкой ok
35 мин
функцию можно вызвать из консоли и увидеть данные
4
Объект state и функция getView(state)
25 мин
каждое состояние даёт свой текст
5
ui.js: render(state)
30 мин
вручную подставленное состояние рисуется
6
main.js: клик по городу → loading → success
30 мин
погода появляется на странице
7
Ветка error + кнопка «Повторить»
30 мин
в режиме Offline — ошибка, после «Повторить» всё работает
8
Состояние empty (город без данных) и блокировка кнопок на время запроса
25 мин
двойной клик не создаёт два запроса
9
Защита от устаревших ответов (requestId)
25 мин
быстрое переключение городов показывает последний
10
Стили, мобильная версия, фокус
35 мин
приятно выглядит на 360 px
11
Ручное тестирование: Slow 3G, Offline, мусорные данные
30 мин
нет ошибок в консоли
12
npm run build, проверка npm run preview
20 мин
сборка работает из папки dist
13
README, скриншот, репозиторий
30 мин
проект доступен по ссылке
Что вы делаете с ответом ИИ — по шагам
Вычеркните лишнее. «Подключить React», «настроить Redux», «написать свой сервер» — не про наш проект.
Проверьте порядок. Нельзя рисовать состояние error раньше, чем есть сам state. Каждый шаг опирается на готовые предыдущие.
Оцените время сами. ИИ не знает вашей скорости: умножайте его оценку на 1.5. Шаг длиннее 40 минут — разбейте.
Каждый шаг даёт видимый результат. «Разобраться с API» — не шаг. «getWeather("Душанбе") возвращает температуру в консоли» — шаг.
Добавьте шаги на «плохую погоду»: Offline, ответ 404, пустой результат. ИИ часто забывает о них — это ваша зона ответственности.
Запишите план в PLAN.md и отмечайте выполненное.
Практика: считаем план средствами JavaScript
ЗАДАЧА 4.1
Сколько займёт проект
лёгкая
Дан массив шагов с оценкой в минутах. Посчитайте сумму и выведите строку вида Всего: 185 мин = 3.1 ч. Часы округлите до одного знака (toFixed(1)).
Можно редактировать · Ctrl + Enter
const plan = [
{ title: "Разметка и стили", min: 30 },
{ title: "Функция fetch и разбор ответа", min: 40 },
{ title: "Состояния loading / error / empty", min: 40 },
{ title: "Debounce для поиска", min: 25 },
{ title: "Разбивка на модули", min: 30 },
{ title: "Сборка Vite и проверка build", min: 20 },
];
// посчитайте сумму минут и выведите итоговую строку
Ожидаемый результат
Всего: 185 мин = 3.1 ч
Подсказка 1
Сумму полей массива объектов собирает reduce с начальным значением 0.
Подсказка 2
plan.reduce((sum, step) => sum + step.min, 0). Часы — это total / 60, а .toFixed(1) оставит один знак.
ЗАДАЧА 4.2
Самые рискованные шаги
средняя
У рискованных шагов стоит risky: true. Выведите их по убыванию времени в формате Рискованно: Название (N мин), а в конце — Рискованных: N. Исходный массив менять нельзя.
Любой запрос в сеть живёт в одном из четырёх состояний: idle (ещё не просили), loading (ждём), success (получили, и данные могут быть как с содержимым, так и пустыми) и error (не получилось). Главное правило: всё это лежит в одном объектеstate, а страница рисуется из него, как и в проекте №1.
Состояние запроса — один объект · Ctrl + Enter
const state = {
status: "loading", // "idle" | "loading" | "success" | "error"
items: [], // данные (при success)
error: "", // текст ошибки (при error)
};
// для каждого статуса — свой «экран»const screens = {
idle: "Введите запрос",
loading: "Загрузка...",
success: "Показываем данные",
error: "Что-то пошло не так",
};
console.log(screens[state.status]);
Чтобы тренироваться без интернета, мы будем подменять сервер своей функцией: она тоже возвращает Promise и отвечает с задержкой — точь-в-точь как fetch, но каждый раз одинаково.
Поддельный сервер: Promise + setTimeout · Ctrl + Enter
fetch устроен хитро: он «падает» (отклоняет промис) только если вообще не удалось связаться с сервером — нет сети, не найден домен. А вот ответ 404 Not Found или 500 Server Error — это успешный с точки зрения fetch ответ: сервер ведь ответил! Поэтому после await fetch(...) всегда проверяйте response.ok.
Шаблон запроса — используйте его в проекте
asyncfunctiongetJson(url) {
const response = awaitfetch(url); // 1. ждём ответif (!response.ok) { // 2. 404 / 500 — тоже ошибкаthrownewError(`HTTP ${response.status}`);
}
returnawait response.json(); // 3. ждём разбор JSON
}
asyncfunctionload() {
try {
const data = awaitgetJson("https://jsonplaceholder.typicode.com/users/1");
console.log(data.name);
} catch (err) {
console.log("Ошибка:", err.message); // сеть упала ИЛИ статус не 2xx
}
}
Попробуйте настоящий запрос. Это единственный блок на странице, который ходит в сеть, поэтому у него нет автоматической проверки: результат зависит от API.
Вы отправили посылку в Худжанд. «Не доставили вообще» (нет сети) — понятная проблема. Но есть и второй случай: посылку принял курьер, но на пункте выдачи сказали «такого адреса нет» (404). Курьер сработал, а результата нет. response.ok — это проверка не «приехал ли курьер», а «что он привёз».
Три классические ошибки
1. Забыть await перед fetch или response.json() — вместо данных вы получаете Promise. 2. Не проверить response.ok — страница «успешно» показывает ответ ошибки. 3. Не обработать ошибку — приложение молча зависает на «Загрузка…».
Допишите getView(state). Правила: idle → Введите запрос; loading → Загрузка...; error → Ошибка: <текст>; success с пустым списком → Ничего не найдено, иначе Найдено: N.
Введите запрос
Загрузка...
Ничего не найдено
Найдено: 2
Ошибка: Сеть недоступна
Подсказка 1
Удобно писать цепочку ранних return: if (state.status === "idle") return "…"; и так далее.
Подсказка 2
Для success проверьте state.items.length === 0. Для error используйте шаблонную строку с state.error.
ЗАДАЧА 5.2
loadResults: loading → success / error
средняя
Напишите async function loadResults(api, query, onChange). Сначала она вызывает onChange с состоянием { status: "loading", items: [], error: "" }. Затем ждёт api(query): при успехе вызывает onChange с success и полученными items, при ошибке — с error и текстом err.message (items — пустой массив). Наружу ошибка не вылетает.
Запустите как есть: оба вызова дают одну и ту же ошибку про json. Значит, response — это не ответ, а что-то другое. Что?
Подсказка 2
Нужны await перед fakeFetch(url) и перед response.json(), а между ними — if (!response.ok) throw new Error(`HTTP ${response.status}`);.
ЗАДАЧА 5.4
Гонка ответов: побеждает последний запрос
сложная
Пользователь быстро печатает «п», затем «пл». Ответ на «п» приходит медленнее (50 мс), чем на «пл» (10 мс), и затирает свежий результат. Допишите onInput(q) так, чтобы в state.text попадал ответ только на последний запрос. Идея: у каждого запроса свой номер; применяем ответ, только если номер не устарел.
Можно редактировать · Ctrl + Enter
const delays = { "п": 50, "пл": 10 };
const search = (q) =>newPromise((resolve) =>setTimeout(() =>resolve(`результаты для "${q}"`), delays[q]));
const state = { text: "" };
let latestId = 0;
asyncfunctiononInput(q) {
// допишите: номер запроса, await search(q), применить только свежий ответ// при применении выведите console.log("применён: " + q)
}
onInput("п");
onInput("пл");
setTimeout(() => console.log(state.text), 100);
Ожидаемый результат
применён: пл
результаты для "пл"
Подсказка 1
В начале функции: const myId = ++latestId;. Переменная myId запомнит номер именно этого вызова.
Подсказка 2
После const text = await search(q); проверьте if (myId !== latestId) return; — это устаревший ответ. Иначе запишите state.text = text и выведите строку.
06 Разминка 2. Debounce, кэш и таймаут
Если отправлять запрос на каждую нажатую букву, сервер получит десятки лишних обращений, а экран будет мигать. Debounce решает это: функция срабатывает только когда пользователь замолчал на заданное время. Каждый новый вызов сбрасывает таймер и запускает новый.
debounce: срабатывает после паузы · Ctrl + Enter
functiondebounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer); // отменили прошлый таймер
timer = setTimeout(() =>fn(...args), ms); // запустили новый
};
}
const search = debounce((q) => console.log("запрос:", q), 30);
search("п");
search("пл");
search("плов"); // сработает только этот вызов, через 30 мс
Аналогия из жизни
Лифт в офисе не уезжает, пока в него кто-то заходит: каждый вошедший «сбрасывает» таймер закрытия дверей. Двери закроются, только когда никто не заходил несколько секунд. Debounce — те же двери.
Модули и сборка: где что лежит
В проекте на Vite код живёт в нескольких файлах — ES-модулях. Каждый файл экспортирует то, что нужно другим (export), и импортирует чужое (import). Хорошее правило границ: api не знает про DOM, ui не знает про сеть, main их соединяет.
Структура проекта
product-search/
├─ index.html ← подключает <script type="module" src="/src/main.js">
├─ package.json ← скрипты dev / build / preview
├─ src/
│ ├─ api.js ← только fetch и разбор ответа
│ ├─ ui.js ← только DOM: render(state)
│ ├─ utils.js ← debounce и мелкие помощники
│ ├─ main.js ← состояние, события, связывает всё
│ └─ style.css
└─ dist/ ← появляется после npm run build (в Git не кладём)
src/api.js — только данные, без DOM
export asyncfunctionsearchProducts(query, signal) {
const url = `https://dummyjson.com/products/search?q=${encodeURIComponent(query)}&limit=10`;
const response = awaitfetch(url, { signal });
if (!response.ok) thrownewError(`HTTP ${response.status}`);
const data = await response.json();
return data.products;
}
src/main.js — соединяет модули
import { searchProducts } from "./api.js";
import { render } from "./ui.js";
import { debounce } from "./utils.js";
const state = { status: "idle", items: [], error: "" };
// … событие input → debounce(search, 400) → searchProducts → render(state)
Терминал
npm create vite@latest product-search -- --template vanilla
cd product-search
npm install
npm run dev # режим разработки, адрес вида http://localhost:5173
npm run build # собирает проект в папку dist
npm run preview # открывает собранную версию — проверьте её перед защитой
Частая ошибка
Проверять только npm run dev. Сборка строже: опечатка в пути импорта (./Api.js вместо ./api.js) на вашем компьютере может работать, а npm run build упадёт. Перед защитой всегда запускайте build и preview.
сразу: 0
запрос: плов
запрос: чой
всего запросов: 2
Подсказка 1
Таймер хранится в переменной снаружи возвращаемой функции (let timer;), чтобы он «помнился» между вызовами.
Подсказка 2
Внутри возвращаемой функции: clearTimeout(timer); timer = setTimeout(() => fn(...args), ms);
ЗАДАЧА 6.2
Кэш запросов
средняя
Напишите makeCached(api): возвращает асинхронную функцию, которая приводит запрос к виду trim().toLowerCase(), и если такой запрос уже был — отдаёт сохранённый ответ без обращения к api.
if (cache.has(key)) return cache.get(key); иначе const value = await api(key); cache.set(key, value); return value;
ЗАДАЧА 6.3
Ответ API → карточки
средняя
API вернул объект с товарами. Напишите toCards(data): оставьте только товары в наличии (stock > 0), отсортируйте по цене по возрастанию (исходный массив не менять), верните строки вида Чехол — 50 сом. (★ 4.8). Рейтинг — с одним знаком после запятой.
Напишите withTimeout(promise, ms): если promise не успел за ms миллисекунд — результат отклоняется ошибкой Таймаут, иначе возвращается значение промиса. Используйте Promise.race.
Promise.race([a, b]) завершается так же, как тот из промисов, который завершился первым. Вторым промисом сделайте «таймер-бомбу».
Подсказка 2
new Promise((_, reject) => setTimeout(() => reject(new Error("Таймаут")), ms)) — вот эта «бомба».
07 Чек перед защитой: package.json и сборка
Когда вы выполняете npm run build, Vite находит команду build в разделе scripts файла package.json, собирает все модули в несколько файлов и кладёт их в папку dist. Именно эту папку выкладывают на хостинг. В .gitignore должны быть node_modules и dist: их не нужно хранить в репозитории, они восстанавливаются командами npm install и npm run build.
Чек-лист «готов к защите»
☐ npm install на чистой папке проходит · ☐ npm run dev открывает приложение · ☐ npm run build без ошибок и предупреждений · ☐ npm run preview показывает то же самое · ☐ в консоли браузера нет красного · ☐ в Network запросы идут на настоящий API · ☐ в Offline показывается ошибка, а не белый экран.
Практика: закрепляем
ЗАДАЧА 7.1
Прочитайте package.json
лёгкая
Дан текст файла package.json. Разберите его через JSON.parse и выведите каждый скрипт в формате npm run имя → команда, затем Зависимостей: N — сколько всего пакетов в devDependencies.
npm run dev → vite
npm run build → vite build
npm run preview → vite preview
Зависимостей: 1
Подсказка 1
const pkg = JSON.parse(text); — теперь pkg.scripts обычный объект.
Подсказка 2
Пары «ключ — значение» даёт Object.entries(pkg.scripts). Количество пакетов — Object.keys(pkg.devDependencies).length.
08 Стартовый шаблон: поиск с состояниями
Ниже — рабочий скелет приложения с поиском в песочнице. Чтобы результат всегда был одинаковым, вместо настоящего сервера в нём «поддельный» (fakeSearch); а рядом лежит realSearch с настоящим fetch — в своём проекте вы подставите его одной строкой. Код разбит на блоки в том порядке, в каком его удобно объяснять на защите: «сервер» → состояние → DOM и отрисовка → поиск → события.
«Сервер». Данные и функции, которые возвращают промис: поддельная и настоящая.
Состояние. Один объект { status, items, error } и счётчик запросов latestId.
Отрисовка.render() строит интерфейс из state. Больше никто DOM не трогает.
Поиск.search(query): loading → success или error, устаревшие ответы игнорируются.
События. Поле ввода с debounce.
Нажмите ▶ Запустить: самопроверка внизу напечатает, что пока не работает. Ваша задача — четыре отметки TODO.
Практика: достроим стартовое приложение
ЗАДАЧА 8.1
Допишите TODO — и поиск заработает
сложная
Реализуйте в script.js четыре отметки: 1)debounce, 2)getStatusText, 3)search (пустой запрос не уходит на сервер; состояния; защита от устаревших ответов; ошибка не вылетает наружу), 4) обработчик input с debounce. Блок самопроверки внизу сам «печатает» в поле и выводит результат в консоль. Когда всё готово, в консоли должно быть ровно то, что в «Ожидаемом результате». Блок самопроверки в свой проект не копируйте.
// ===== 1. «Сервер» =====const PRODUCTS = [
{ id: 1, title: "Телефон Samsung", price: 2500 },
{ id: 2, title: "Телевизор LG", price: 4200 },
{ id: 3, title: "Ноутбук Acer", price: 5100 },
{ id: 4, title: "Наушники Sony", price: 600 },
{ id: 5, title: "Чехол для телефона", price: 50 },
{ id: 6, title: "Зарядка для телефона", price: 120 },
{ id: 7, title: "Клавиатура", price: 180 },
{ id: 8, title: "Мышь", price: 90 },
];
const wait = (ms) =>newPromise((resolve) =>setTimeout(resolve, ms));
// поддельный сервер: отвечает через 60 мс; на запрос "error" падаетasyncfunctionfakeSearch(query) {
awaitwait(60);
if (query ==="error") thrownewError("Сеть недоступна");
const q = query.trim().toLowerCase();
return PRODUCTS.filter((p) => p.title.toLowerCase().includes(q));
}
// настоящий сервер — в своём проекте поменяйте searchApi на негоasyncfunctionrealSearch(query) {
const url = `https://dummyjson.com/products/search?q=${encodeURIComponent(query)}&limit=10`;
const response = awaitfetch(url);
if (!response.ok) thrownewError(`HTTP ${response.status}`);
const data = await response.json();
return data.products;
}
const searchApi = fakeSearch;
// ===== 2. Состояние =====let state = { status: "idle", items: [], error: "" }; // "idle" | "loading" | "success" | "error"let latestId = 0; // номер последнего запросаconst DEBOUNCE_MS = 40;
functionsetState(patch) {
state = { ...state, ...patch };
render();
}
// TODO 1: debounce — вызывает fn только после паузы в ms миллисекундfunctiondebounce(fn, ms) {
return fn;
}
// TODO 2: текст статуса по состоянию:// idle → "Введите запрос", loading → "Загрузка...", error → "Ошибка: ТЕКСТ",// success: пусто → "Ничего не найдено", иначе → "Найдено: N"functiongetStatusText(s) {
return"";
}
// ===== 3. DOM и отрисовка =====const input = document.querySelector("#q");
const statusEl = document.querySelector("#status");
const list = document.querySelector("#results");
functioncreateItem(product) {
const li = document.createElement("li");
const name = document.createElement("span");
name.textContent = product.title;
const price = document.createElement("span");
price.className = "price";
price.textContent = product.price + " сом.";
li.append(name, price);
return li;
}
functionrender() {
statusEl.textContent = getStatusText(state);
statusEl.dataset.status = state.status;
list.replaceChildren(); // очистили список
state.items.forEach((product) => list.append(createItem(product)));
}
// ===== 4. Поиск =====// TODO 3: пустой запрос (после trim) → setState idle без обращения к серверу.// Иначе: свой номер запроса, status "loading", await searchApi, потом "success" или "error".// Если пока ждали, начался новый запрос (номер устарел) — результат выбросить.asyncfunctionsearch(query) {
}
// ===== 5. События =====// TODO 4: на каждый ввод вызывайте search(input.value), но через debounce(…, DEBOUNCE_MS)
input.addEventListener("input", () => {
});
render();
// ===== Самопроверка для задачи 8.1 (в свой проект не копируйте) =====
(async () => {
const type = (text) => {
input.value = text;
input.dispatchEvent(newEvent("input"));
};
console.log("старт:", statusEl.textContent);
type("т"); type("те"); type("тел");
console.log("сразу после ввода:", statusEl.textContent);
awaitwait(70);
console.log("через 70 мс:", statusEl.textContent);
awaitwait(100);
console.log("ответ:", statusEl.textContent, "|", list.children.length);
type("zzz");
awaitwait(200);
console.log("пусто:", statusEl.textContent);
type("error");
awaitwait(200);
console.log("ошибка:", statusEl.textContent);
type(" ");
awaitwait(100);
console.log("очистили:", statusEl.textContent);
})();
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
старт: Введите запрос
сразу после ввода: Введите запрос
через 70 мс: Загрузка...
ответ: Найдено: 4 | 4
пусто: Ничего не найдено
ошибка: Ошибка: Сеть недоступна
очистили: Введите запрос
Подсказка 1
Запустите шаблон как есть и сравните вывод с ожидаемым: строки, которые не совпадают, подсказывают, какой TODO решать следующим. Начните с getStatusText — это самый простой.
Подсказка 2
search: const myId = ++latestId;, проверка пустого запроса, setState({ status: "loading", ... }), затем try с await searchApi(query) и проверкой if (myId !== latestId) return; после await (и то же в catch). Обработчик: const debouncedSearch = debounce(search, DEBOUNCE_MS); вне слушателя, а внутри — debouncedSearch(input.value);.
Как перенести шаблон в свой проект
Создайте проект на Vite (раздел 06) и разнесите код по модулям: fakeSearch/realSearch — в api.js, debounce — в utils.js, getStatusText и render — в ui.js, состояние и события — в main.js.
Каждому файлу добавьте export/import. render теперь принимает state аргументом — глобальные переменные между модулями не ходят.
Замените fakeSearch на realSearch и убедитесь во вкладке Network, что запросы настоящие.
Удалите блок самопроверки.
Если у вас уже есть собственный проект с уроков 15–17 — не выбрасывайте его, а сравните со схемой «состояние → render → search → события».
09 Практика на уроке: ревью, защиты, контрольная
Сегодня занятие устроено иначе, чем обычно: проекты уже почти готовы, поэтому время уходит на проверку, защиту и контрольную. Действуем по плану.
0–10 мин · Последняя проверка. Чек-лист из раздела 07: npm run build, preview, Offline, Slow 3G. Преподаватель утверждает порядок защит.
10–30 мин · Code review в парах. По 10 минут на проект. Ревьюер идёт по чек-листу ниже и записывает замечания, автор слушает. Затем меняетесь.
30–60 мин · Защиты. Каждая — 5 минут плюс минута на смену. Остальные слушают и задают вопросы. Не успели сегодня — защищаетесь в начале следующего занятия.
60–90 мин · Контрольная №2. 30 минут, у всех одновременно, без ИИ. Подробности — в разделе 11. Разбор типичных ошибок — в конце.
Если проект ещё не готов
Не добавляйте бонусы. Откройте стартовый шаблон из раздела 08, закройте четыре TODO, проверьте build и добавьте README. Работающий простой проект лучше недоделанного сложного.
Code review: чек-лист ревьюера
Code review — это когда один разработчик читает код другого до того, как он «уйдёт в мир». Не экзамен и не поиск виноватых: свежий взгляд видит то, что автор уже перестал замечать. Формула замечания: где (файл и строка) → что вижу → почему важно → предложение (лучше вопросом).
Пробуем сломать (запуск)
☐ Режим Offline: приложение показывает ошибку и «Повторить», а не белый экран
☐ Slow 3G: виден индикатор, кнопки не дают отправить двойной запрос
☐ Бессмысленный запрос: сообщение «Ничего не найдено»
☐ Быстро печатаем: в Network не больше одного запроса на «паузу»
☐ Быстро меняем запрос: на экране ответ на последний, а не на предпоследний
☐ После ошибки и «Повторить» старые данные не «прилипают»
☐ Пустая строка не уходит на сервер
☐ Текст из API показывается как текст (textContent)
☐ На ширине 360 px всё читаемо; нет красного в консоли
Читаем код
☐ Один объект state со статусом, а не набор флагов isLoading, isError…
☐ После fetch стоит проверка response.ok
☐ Везде, где нужен, есть await; нет «забытого» промиса
☐ try/catch вокруг запроса и осмысленное сообщение, а не пустой catch
☐ Модули: api не трогает DOM, ui не ходит в сеть
☐ debounce написан один раз и переиспользуется
☐ Строки статусов вынесены в константы
☐ Нет закомментированного кода, console.log и мёртвых функций
☐ В репозитории нет node_modules и dist
ИИ как второй ревьюер — после людей, а не вместо них
Когда пара закончила, прогоните код через ИИ: «Проверь по чек-листу: обработка ошибок fetch, гонка ответов, границы модулей, имена. Только замечания, без переписывания кода». Принимайте не все советы: каждую правку вы должны уметь обосновать.
Ревьюер написал: «После ошибки на экране остаются старые результаты, а после успеха — старый текст ошибки». Исправьте applyResult: при error — items становится пустым массивом, при success — error становится пустой строкой.
Каждое состояние должно быть полным: при смене статуса сбрасывайте поля, которые к новому статусу не относятся.
Подсказка 2
В ветку ошибки добавьте items: [], а в ветку успеха — error: "".
ЗАДАЧА 9.2
Замечание ревьюера: опечатка в статусе
лёгкая
Статус «Загрузка» не показывается — ревьюер подозревает опечатку. Исправьте её, а заодно вынесите статусы в константу STATUS (IDLE, LOADING, SUCCESS, ERROR) и используйте её везде: так опечатка станет ошибкой, которую заметит редактор.
Можно редактировать · Ctrl + Enter
functionbadge(status) {
if (status ==="loadin") return"⏳ Загрузка";
if (status ==="error") return"⚠ Ошибка";
if (status ==="success") return"✓ Готово";
return"…";
}
console.log(badge("idle"));
console.log(badge("loading"));
console.log(badge("error"));
console.log(badge("success"));
Ожидаемый результат
…
⏳ Загрузка
⚠ Ошибка
✓ Готово
Подсказка 1
Сравните строку в условии с той, которую передают при вызове: они должны совпадать до буквы.
Подсказка 2
const STATUS = { IDLE: "idle", LOADING: "loading", SUCCESS: "success", ERROR: "error" }; — и везде STATUS.LOADING вместо строки.
10 Защита: 5 минут, которые решают 15 баллов
Защита — это короткий рассказ о том, что вы сделали и как это работает. Не презентация и не допрос: нужно показать, что проект ваш и вы его понимаете. Поэтому на неё отведено 15 баллов из 100. Здесь вы показываете не только «когда всё хорошо», но и «плохую погоду»: Offline и пустой результат.
Сценарий на 5 минут
Время
Что делаете
0:00–0:30
Представление. Одной фразой: что за приложение, какое API. «Это поиск товаров на dummyjson: пишешь слово — через полсекунды видишь карточки».
0:30–2:00
Демо. Поиск → пустой результат → Offline в DevTools → ошибка и «Повторить» → всё снова работает. Откройте preview собранной версии.
2:00–3:30
Код. Три места: getJson/api.js (fetch + ok), объект state и render, debounce. Объясняйте своими словами.
3:30–5:00
Вопросы. Не знаете — скажите честно и предположите, как бы искали ответ.
Типичные вопросы и о чём на них сказать
Что такое async/await и чем отличается от then?
Это тот же Promise, но код читается сверху вниз. await «ставит функцию на паузу» до ответа, не блокируя страницу.
Почему нужен response.ok? Разве fetch не упадёт на 404?
Нет: fetch отклоняется только при сетевой проблеме. Ответ 404/500 — «успешный», потому что сервер ответил. Статус нужно проверять самому.
Зачем await response.json(), это же уже ответ?
Заголовки пришли, а тело ответа читается отдельно и тоже асинхронно.
Что такое debounce и зачем он здесь?
Функция срабатывает после паузы. Без него каждая буква — запрос: лишняя нагрузка и «мигающий» экран.
Что такое гонка ответов и как ты её решил?
Ответ на старый запрос может прийти позже ответа на новый и затереть его. Решение: номер запроса (применяем только свежий) или AbortController.
Как у тебя устроены состояния?
Один объект state со status, items, error; render рисует из него. Нет набора флагов, которые могут противоречить друг другу.
Что в api.js, что в ui.js, и почему так?
В api.js только сеть, в ui.js только DOM, main.js связывает. Так каждую часть можно менять и проверять отдельно.
Что делает npm run build и что лежит в dist?
Vite собирает модули в готовые для хостинга файлы. В Git dist не кладут: его можно пересобрать.
Где ты использовал ИИ? Объясни эту строку.
Скажите честно, для чего (план, объяснение ошибки, ревью) и где он ошибся — раздел 12. Любую строку нужно объяснить своими словами.
Советы по подготовке
Прогоните демо вслух с секундомером. Offline включается в DevTools → Network → «No throttling» → Offline.
Откройте нужные файлы заранее, увеличьте шрифт: группа должна видеть код.
Не оправдывайтесь («не успел»). Лучше: «этого нет, потому что… вот как бы я добавил».
Практика: репетиция
ЗАДАЧА 10.1
Подготовьте ответы на вопросы защиты
лёгкая
Ответьте на пять вопросов выше про свой проект, своими словами, одним-двумя предложениями. Запишите ответы в объект (это заметки — автоматической проверки нет), затем отрепетируйте вслух перед соседом. Когда ответите — отметьте задачу вручную.
Отвечайте как другу, который ни разу не видел ваш код: коротко и с примером из проекта.
Подсказка 2
Хороший ответ содержит конкретику: «в api.js, строка 6, после fetch стоит if (!response.ok)», а не «ну там что-то проверяется».
11 Контрольная №2 · пробная версия
🚫 На контрольной ИИ выключен
Настоящая контрольная — 30 минут, без ИИ, без подсказок, без чата и без соседа. Ниже — полная пробная версия: 10 задач на темы объекты, async/await, fetch, обработка ошибок. В этом разделе нет подсказок (кнопок «Подсказка» вы не увидите) — так же, как на настоящей контрольной. Решайте с таймером на 30 минут, не выходя из страницы и не открывая чат с ИИ.
Как проходит пробная
Всё честно: сначала ставите таймер, потом решаете. Все «запросы» тут — поддельные функции из Promise и setTimeout, поэтому ответ каждой задачи проверяется автоматически и не зависит от интернета. Порядок строк в консоли тоже часть результата, так что смотрите на него внимательно.
Ориентир для самопроверки: 9–10 задач — отличный результат, 7–8 — хороший, 5–6 — удовлетворительно, меньше 5 — повторите уроки 13–16 и попробуйте ещё раз. Официальный порог настоящей контрольной определяет преподаватель.
Контрольная №2 · 10 задач · 30 минут
ЗАДАЧА 11.1
Профиль студента (деструктуризация)
лёгкая
Для каждого студента выведите строку Алишер (22) — Душанбе. Если города нет (address.city отсутствует) — выведите не указан. Используйте деструктуризацию.
Напишите async function showProfile(id): сначала получите пользователя (api.user(id)), затем его команду по user.teamId (api.team(...)) и выведите Мадина — Frontend-17 (12 чел.). После завершения вызова выводится готово.
Напишите loadAll(ids): запросите все города одновременно и верните массив названий в порядке ids (а не в порядке прихода ответов). Последовательные запросы дадут другой порядок строк пришёл … и не пройдут проверку.
пришёл 2
пришёл 3
пришёл 1
Душанбе, Худжанд, Бохтар
ЗАДАЧА 11.7
getJson с проверкой ok
средняя
Напишите getJson(url) с помощью fakeFetch: если response.ok ложно — бросьте ошибку с текстом HTTP <статус>, иначе верните разобранный JSON. Ошибки из цикла внизу обрабатываются уже готовым кодом.
Напишите safeLoad(task): она вызывает асинхронную функцию task и возвращает { ok: true, data }; при ошибке — { ok: false, name: err.name, error: err.message }. Счётчик finished должен увеличиваться при любом исходе (используйте finally).
Напишите retry(fn, times): до times попыток вызывает fn; перед каждой выводит Попытка N (с единицы). Если попытка удалась — возвращает результат. Если все неудачны — бросает ошибку последней попытки.
Попытка 1
Попытка 2
Попытка 3
данные получены
Попытка 1
Попытка 2
Не вышло: навсегда
ЗАДАЧА 11.10
Всё вместе: load с состояниями и таймаутом
сложная
Напишите load(api, onChange). Сначала onChange("loading"). Затем запрос с таймаутом 100 мс (готовая withTimeout). Итог: непустой массив — onChange("success:N"), пустой — onChange("empty"), любая ошибка (в том числе таймаут) — onChange("error:" + сообщение). Исключения наружу не вылетают.
Отметьте, какие задачи не получились, и ответьте на два вопроса: «какой урок мне нужно перечитать?» и «что я бы сделал, если бы на настоящей контрольной ИИ был включён — и почему это не помогло бы понять тему?». Разбор типичных ошибок преподаватель проводит в конце занятия. В домашнем задании вы перепишете нерешённые задачи.
12 Разбор: где ИИ сэкономил время, а где увёл в сторону
Это AI-блок урока: после двух проектов важно не просто «пользоваться ИИ», а знать его настоящую цену. Часто кажется, что ИИ ускорил работу, а потом вы час искали баг в чужом, непонятном коде. Бывает наоборот: ИИ за три минуты объяснил то, на что вы потратили бы вечер. Чтобы отличать одно от другого, нужны не ощущения, а записи.
Правила курса, которые мы сейчас проверяем
№1: сначала 15 минут сам, потом ИИ. №2: код, который не можешь объяснить построчно, не вставляем. №5: ИИ пишет черновик — ты проверяешь логику, имена и крайние случаи. Разбор покажет, где вы эти правила соблюдали, а где нет.
Аналогия из жизни
Навигатор в машине. На незнакомой трассе в Согдийскую область он экономит часы. Но если слепо ехать, куда он говорит, можно оказаться на закрытой дороге. Хороший водитель знает, куда ему нужно, и сверяет подсказки со знаками. ИИ — тоже навигатор: цель и проверка остаются на вас.
Структурированный разбор: шаблон
Выберите 3–5 эпизодов из проекта, где вы обращались к ИИ. Для каждого заполните одну строку таблицы. Пример (вариант Б, погода):
Эпизод
Что просил
Что получил
Итог
± минут
Как проверил
Разметка карточек погоды
«Сделай HTML и CSS для карточки: город, температура, ветер»
Рабочая вёрстка, пара лишних классов
✅ сэкономил
+25
Открыл в браузере, убрал лишнее
Запрос к API
«Как получить current-данные open-meteo?»
Правильный URL, но без проверки ok
⚖ частично
+15 / −10
Нашёл при тесте с ошибкой 404
Обработка ошибок
«Сделай так, чтобы не падало»
Пустой catch {}: ошибки молча глотались
❌ увёл в сторону
+5 / −40
Заметил в режиме Offline: экран не менялся
А теперь — шаблон для вашего README. Скопируйте его и заполните своими эпизодами:
Шаблон разбора — вставьте в README.md
## ИИ в проекте: где помог, где увёл в сторону
### Эпизод 1: <что делал>
- Что просил (коротко): …
- Что получил: …
- Итог: ✅ сэкономил / ⚖ частично / ❌ увёл в сторону
- Минуты: сэкономил … / потерял …
- Как проверил результат: …
- Вывод для себя (правило): …
### Эпизод 2: …
## Итог
- Где ИИ реально сэкономил время: …
- Где он увёл в сторону и почему я не заметил сразу: …
- Что я теперь делаю иначе: …
Три вопроса для самопроверки
Могу ли я объяснить этот код построчно? Если нет — это долг: вы взяли чужое и не поняли. Либо разберите, либо перепишите.
Как я узнал, что ответ ИИ правильный? «Заработало» — слабая проверка. Хорошая: сломал нарочно (Offline, 404, пустой запрос) и убедился, что поведение осмысленное.
Что бы я сделал без ИИ — и сколько бы это стоило? Честная оценка помогает понять, где выгода реальна, а где — самообман.
Типичные «увели в сторону» в проектах с API
Что ИИ делает плохо
Забывает response.ok — 404 «успешно» показывается как данные
Пишет пустой catch или только console.log вместо сообщения пользователю
Не думает про гонку ответов и двойной запрос
Придумывает несуществующие поля API (data.weather.temp) — их нужно сверять с настоящим ответом
Предлагает лишнее: библиотеки, фреймворки, сложный try/catch-каркас
Где ИИ реально помогает
Разбивка проекта на шаги (раздел 04)
Объяснение непонятного сообщения об ошибке — с вашим кодом и текстом ошибки
Черновик разметки и CSS, которые вы потом читаете и правите
Ревью по чек-листу — после людей
README и тексты — с обязательной правкой своими словами
оцени мой код
Контекст: учебный проект «поиск товаров», JS + Vite, модули api.js / ui.js / main.js. Умею: fetch, async/await, try/catch, debounce.
Задача: сделай ревью файла api.js — только замечания, без переписывания кода.
Ограничения: смотри на 4 вещи: проверка response.ok, обработка ошибок, имена, дублирование.
Формат ответа: список «строка → проблема → почему важно → как проверить исправление».
Проверка: в конце назови одно место, где ты не уверен, что я правильно понял API.
[вставьте код api.js]
Эпизоды разбора лежат в массиве. Для каждого указано, сколько минут ИИ сэкономил (saved) и сколько потерял (lost: поиск ошибки в чужом коде, переписывание). Посчитайте суммы и итог со знаком (+20 или -5) и найдите эпизод с наибольшей потерей. Выведите четыре строки по образцу ниже.
Сэкономил: 70 мин
Потерял: 50 мин
Итого: +20 мин
Главная ловушка: обработка ошибок
Подсказка 1
Две суммы собираются двумя reduce (или одним циклом). Итог — разность saved − lost.
Подсказка 2
Знак: net > 0 ? "+" + net : String(net). Худший эпизод — reduce, который оставляет элемент с большим lost.
13 Домашнее задание
Доработайте проект по замечаниям ревью и защиты, оформите разбор «ИИ в проекте» и сделайте работу над ошибками по контрольной.
Что нужно сделать
☐ Исправить замечания ревью и защиты, которые вы приняли.
☐ Проверить все пункты MVP из раздела 02 и пройти чек-лист ревьюера по своему проекту ещё раз; npm run build без ошибок.
☐ Убрать лишние console.log, закомментированный код и мёртвые функции.
☐ Дописать в README раздел «ИИ в проекте» по шаблону из раздела 12 (3–5 эпизодов и итог — своими словами).
☐ Обновить репозиторий: публичный, README со скриншотом, не менее 5 осмысленных коммитов, node_modules и dist не загружены.
☐ Работа над ошибками: каждую задачу пробной контрольной, которую вы не решили или решили с ошибкой, решите заново и объясните своими словами, в чём была ошибка (1–2 предложения в комментарии).
Что сдать
Ссылку на репозиторий в чат группы (Telegram). В сообщении напишите: вариант проекта (А, Б или В), какое API использовали, что добавили после ревью, какой бонус сделали, и список задач контрольной, которые переделывали.
Как оценивается ДЗ (0–10)
Что проверяем
Баллы
Ссылка открывается без входа, npm run build проходит на чистой копии
2
Замечания ревью исправлены (видно в коммитах)
2
README: описание, скриншот и раздел «ИИ в проекте» своими словами
3
Не менее 5 осмысленных коммитов с понятными сообщениями
1
Работа над ошибками по контрольной с объяснениями
2
Это оценка за домашнее задание урока (0–10). Оценка за мини-проект (0–100) выставляется по таблице из раздела 03 и учитывается отдельно.
✓ Шпаргалка урока
Проект: приложение на публичном API (товары, погода или пользователи) на Vite. Оценка 0–100: MVP 20, fetch и async/await 20, состояния 15, debounce и контроль запросов 10, модули 10, сборка и репозиторий 5, интерфейс 5, защита 15; бонус до +5.
fetch:await fetch(url) → if (!response.ok) throw … → await response.json(). fetch падает только при сбое сети, 404 и 500 — «успешные» ответы.
Состояния: один объект { status, items, error }, status: idle / loading / success / error; пустой success — это empty. Смена статуса сбрасывает лишние поля.
try/catch/finally:catch показывает пользователю понятное сообщение, а не молчит; finally выполняется всегда.
Параллельно или по очереди: независимые запросы — Promise.all, зависимые — await по очереди. Promise.race — для таймаута.
debounce: сбрасываем таймер при каждом вызове; срабатывает после паузы. Пустой запрос на сервер не отправляем.
Гонка ответов: номер запроса (myId !== latestId → игнор) или AbortController.
Модули:api.js — сеть, ui.js — DOM, main.js — связка; npm run build + preview перед защитой; dist и node_modules не в Git.
Ревью: запуск (Offline, Slow 3G, мусор) → чтение по чек-листу → замечание = где, что вижу, почему, предложение.
Контрольная №2: 30 минут, без ИИ, темы — объекты, async/await, fetch, ошибки; 10 задач с автопроверкой.
ИИ в проекте: записывайте эпизоды (что просил → что получил → итог → ± минут → как проверил). ИИ часто забывает ok, глотает ошибки и придумывает поля API — проверка остаётся на вас.