MakeFirstStep· Frontend + ИИ

Урок 18 · 🏁 Мини-проект №2 + Контрольная №2

Мини-проект: приложение на публичном API

Второй проект для портфолио — уже «взрослый»: он ходит за данными в интернет, показывает загрузку, переживает ошибки сети, не заваливает сервер запросами при каждой букве и собирается в готовую сборку через Vite. Именно так устроены погодные виджеты, поиск товаров и ленты новостей. Сегодня вы защитите проект, проверите код друг друга и напишете Контрольную №2 без ИИ.

  • Выбрать вариант приложения на публичном API и разбить его на шаги вместе с ИИ
  • Показывать состояния loading / error / empty / success и не показывать устаревшие ответы
  • Ограничивать запросы через debounce и разносить код по модулям
  • Собрать проект через Vite (npm run build) и залить в репозиторий
  • Защитить проект за 5 минут и провести code review в паре
  • Написать Контрольную №2 за 30 минут: объекты, async/await, fetch, обработка ошибок
  • Честно разобрать, где ИИ сэкономил время, а где увёл в сторону
Как работать с этой страницей

Сегодня два блока. Первый — мини-проект: задание, критерии оценки (0–100), план с ИИ, две разминки по состояниям и запросам, стартовый шаблон в песочнице, ревью в парах и защита. Второй — Контрольная №2: в разделе 11 лежит полная пробная версия без подсказок. Для разминок мы не ходим в настоящую сеть: вместо fetch используем «поддельный сервер» из Promise и setTimeout — так результат всегда одинаковый и его можно проверить автоматически. Свой проект вы собираете у себя в VS Code.

01 Задание: выберите вариант

Проект собирается из того, что вы прошли на уроках 13–17: асинхронность, fetch, состояния загрузки, debounce, модули и сборка Vite. Выберите один из трёх вариантов. Все они оцениваются по одним и тем же критериям, а API у всех открытые и не требуют ключа.

Вариант А. Поиск товаров

Поле поиска, под ним — карточки товаров. Запрос уходит с задержкой (debounce), пока идёт загрузка — индикатор, если ничего не нашли — понятное сообщение.

API: dummyjson.com/products/search?q=phone&limit=10

Вариант Б. Погода в городах

Список городов (Душанбе, Худжанд, Бохтар, Куляб и свои). Выбираете город — видите температуру и ветер. Ошибку сети можно повторить кнопкой.

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). Плохой официант молчит и не отвечает вообще — и вы не знаете, ждать или уходить. Ваше приложение должно вести себя как хороший официант.

Что вы показываете в конце

  1. Работающее приложение — открывается без ошибок в консоли, при отключённой сети показывает аккуратную ошибку.
  2. Репозиторий на GitHub с кодом на Vite, README и скриншотом; команда npm run build проходит без ошибок.
  3. Защита на 5 минут — демонстрация, объяснение кода и ответы на вопросы.
  4. Короткий разбор «ИИ в проекте» — раздел 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, как записано в правилах курса. Оценка складывается из восьми критериев:

КритерийБаллыМаксимум ставим, если…Частично, если…
Функциональность MVP20приложение решает задачу выбранного варианта от начала до конца, нет красных ошибок в консолиосновной сценарий работает, но есть баги или нет части пунктов
fetch и async/await20реальный запрос на публичный API, await стоит где нужно, проверяется response.ok, ответ разбирается через await response.json(), ошибки ловит try/catchзапрос работает, но 404 не считается ошибкой или try/catch формальный
Состояния loading / error / empty15все три состояния есть, переключаются из одного объекта состояния, есть «Повторить», двойной запрос невозможенесть одно-два из трёх, или состояния «залипают» (после ошибки остаются старые данные)
Debounce и контроль запросов10запросы ограничены debounce, пустой запрос не отправляется, устаревший ответ не затирает свежийdebounce есть, но гонка ответов не решена
Модули и структура10ES-модули с понятными границами (api не трогает DOM, ui не ходит в сеть), короткие функции, нет дублей и мёртвого кодамодули есть, но «всё в main.js» или функции-монстры
Сборка и репозиторий5npm 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 часов)

№ШагВремяЧто работает после него
1Vite-проект, структура src/api.js, ui.js, main.js20 минnpm run dev открывает пустую страницу
2Разметка: выбор города, блок результата, блок статуса25 минстраница выглядит как приложение, но «мёртвая»
3api.js: getWeather(city) с fetch и проверкой ok35 минфункцию можно вызвать из консоли и увидеть данные
4Объект state и функция getView(state)25 минкаждое состояние даёт свой текст
5ui.js: render(state)30 минвручную подставленное состояние рисуется
6main.js: клик по городу → loading → success30 минпогода появляется на странице
7Ветка error + кнопка «Повторить»30 минв режиме Offline — ошибка, после «Повторить» всё работает
8Состояние empty (город без данных) и блокировка кнопок на время запроса25 миндвойной клик не создаёт два запроса
9Защита от устаревших ответов (requestId)25 минбыстрое переключение городов показывает последний
10Стили, мобильная версия, фокус35 минприятно выглядит на 360 px
11Ручное тестирование: Slow 3G, Offline, мусорные данные30 миннет ошибок в консоли
12npm run build, проверка npm run preview20 минсборка работает из папки dist
13README, скриншот, репозиторий30 минпроект доступен по ссылке

Что вы делаете с ответом ИИ — по шагам

  1. Вычеркните лишнее. «Подключить React», «настроить Redux», «написать свой сервер» — не про наш проект.
  2. Проверьте порядок. Нельзя рисовать состояние error раньше, чем есть сам state. Каждый шаг опирается на готовые предыдущие.
  3. Оцените время сами. ИИ не знает вашей скорости: умножайте его оценку на 1.5. Шаг длиннее 40 минут — разбейте.
  4. Каждый шаг даёт видимый результат. «Разобраться с API» — не шаг. «getWeather("Душанбе") возвращает температуру в консоли» — шаг.
  5. Добавьте шаги на «плохую погоду»: Offline, ответ 404, пустой результат. ИИ часто забывает о них — это ваша зона ответственности.
  6. Запишите план в 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. Исходный массив менять нельзя.

Можно редактировать · Ctrl + Enter
const plan = [
  { title: "Разметка", min: 20, risky: false },
  { title: "Первый запрос к API", min: 40, risky: true },
  { title: "Обработка ошибок сети", min: 35, risky: true },
  { title: "Стили", min: 25, risky: false },
  { title: "Отмена устаревших запросов", min: 30, risky: true },
];

// отфильтруйте, отсортируйте копию, выведите
Ожидаемый результат
Рискованно: Первый запрос к API (40 мин)
Рискованно: Обработка ошибок сети (35 мин)
Рискованно: Отмена устаревших запросов (30 мин)
Рискованных: 3
Подсказка 1

filter возвращает новый массив, поэтому sort после него не затронет исходный plan.

Подсказка 2

Сортировка по убыванию: .sort((a, b) => b.min - a.min). Затем forEach со строкой-шаблоном и .length для итога.

ЗАДАЧА 4.3

Минуты по этапам

средняя

Соберите объект «этап → сумма минут» и выведите его через JSON.stringify. Порядок ключей — по первому появлению этапа в массиве.

Можно редактировать · Ctrl + Enter
const plan = [
  { phase: "api", min: 40 },
  { phase: "ui", min: 30 },
  { phase: "api", min: 35 },
  { phase: "build", min: 20 },
  { phase: "ui", min: 25 },
];

// const byPhase = plan.reduce(...)
// console.log(JSON.stringify(byPhase));
Ожидаемый результат
{"api":75,"ui":55,"build":20}
Подсказка 1

Начальное значение reduce — пустой объект {}. Ключом служит item.phase.

Подсказка 2

acc[item.phase] = (acc[item.phase] || 0) + item.min; return acc;

05 Разминка 1. Состояния запроса и ошибки сети

Любой запрос в сеть живёт в одном из четырёх состояний: 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
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// «сервер» отвечает через 20 мс; на запрос "error" падает
async function fakeApi(query) {
  await wait(20);
  if (query === "error") throw new Error("Сеть недоступна");
  return [`${query} 1`, `${query} 2`];
}

async function main() {
  console.log("запрос...");
  const items = await fakeApi("плов");
  console.log("получили:", items.join(", "));
  try {
    await fakeApi("error");
  } catch (err) {
    console.log("ошибка:", err.message);
  }
}
main();

Настоящий fetch: что нужно проверить после ответа

fetch устроен хитро: он «падает» (отклоняет промис) только если вообще не удалось связаться с сервером — нет сети, не найден домен. А вот ответ 404 Not Found или 500 Server Error — это успешный с точки зрения fetch ответ: сервер ведь ответил! Поэтому после await fetch(...) всегда проверяйте response.ok.

Шаблон запроса — используйте его в проекте
async function getJson(url) {
  const response = await fetch(url);                       // 1. ждём ответ
  if (!response.ok) {                                      // 2. 404 / 500 — тоже ошибка
    throw new Error(`HTTP ${response.status}`);
  }
  return await response.json();                            // 3. ждём разбор JSON
}

async function load() {
  try {
    const data = await getJson("https://jsonplaceholder.typicode.com/users/1");
    console.log(data.name);
  } catch (err) {
    console.log("Ошибка:", err.message);                   // сеть упала ИЛИ статус не 2xx
  }
}

Попробуйте настоящий запрос. Это единственный блок на странице, который ходит в сеть, поэтому у него нет автоматической проверки: результат зависит от API.

Живой запрос (нужен интернет) · Ctrl + Enter
async function getJson(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return await response.json();
}

getJson("https://jsonplaceholder.typicode.com/users/1")
  .then((user) => console.log("Пользователь:", user.name))
  .catch((err) => console.log("Ошибка:", err.message));

getJson("https://jsonplaceholder.typicode.com/users/99999")   // такого нет → 404
  .then((user) => console.log("Пользователь:", user.name))
  .catch((err) => console.log("Ошибка:", err.message));
Аналогия из жизни

Вы отправили посылку в Худжанд. «Не доставили вообще» (нет сети) — понятная проблема. Но есть и второй случай: посылку принял курьер, но на пункте выдачи сказали «такого адреса нет» (404). Курьер сработал, а результата нет. response.ok — это проверка не «приехал ли курьер», а «что он привёз».

Три классические ошибки

1. Забыть await перед fetch или response.json() — вместо данных вы получаете Promise. 2. Не проверить response.ok — страница «успешно» показывает ответ ошибки. 3. Не обработать ошибку — приложение молча зависает на «Загрузка…».

Этот код выдаст ошибку · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const fakeFetch = async () => { await wait(10); return { ok: true, json: async () => ({ name: "Плов" }) }; };

async function load() {
  const response = fakeFetch();         // забыли await
  const data = await response.json();   // TypeError: response.json is not a function
  console.log(data.name);
}
load().catch((err) => console.log("Ошибка:", err.message));

Читать подробнее: learn.javascript.ru — Fetch · MDN — Response.ok · learn.javascript.ru — try…catch

Практика: закрепляем

ЗАДАЧА 5.1

Экран для каждого состояния

лёгкая

Допишите getView(state). Правила: idle → Введите запрос; loading → Загрузка...; error → Ошибка: <текст>; success с пустым списком → Ничего не найдено, иначе Найдено: N.

Можно редактировать · Ctrl + Enter
function getView(state) {
  // допишите
}

const states = [
  { status: "idle", items: [], error: "" },
  { status: "loading", items: [], error: "" },
  { status: "success", items: [], error: "" },
  { status: "success", items: [{ id: 1 }, { id: 2 }], error: "" },
  { status: "error", items: [], error: "Сеть недоступна" },
];
states.forEach((s) => console.log(getView(s)));
Ожидаемый результат
Введите запрос
Загрузка...
Ничего не найдено
Найдено: 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 — пустой массив). Наружу ошибка не вылетает.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const okApi = async (q) => { await wait(10); return [`${q}-1`, `${q}-2`]; };
const badApi = async () => { await wait(10); throw new Error("Сервер упал"); };

// async function loadResults(api, query, onChange) { ... }

const log = (s) => console.log(`[${s.status}] items=${s.items.length} error="${s.error}"`);

async function main() {
  await loadResults(okApi, "плов", log);
  await loadResults(badApi, "плов", log);
}
main();
Ожидаемый результат
[loading] items=0 error=""
[success] items=2 error=""
[loading] items=0 error=""
[error] items=0 error="Сервер упал"
Подсказка 1

Каркас: вызвали onChange с loading, затем try { const items = await api(query); … } catch (err) { … }.

Подсказка 2

В catch: onChange({ status: "error", items: [], error: err.message });. Не забудьте await перед api(query).

ЗАДАЧА 5.3

Найдите баг: fetch без await и без ok

средняя

Функция getTitle должна вернуть заголовок, а для адреса, который отвечает 404, — строку Ошибка: HTTP 404. Сейчас она делает не то. Найдите три ошибки.

Можно редактировать · Ctrl + Enter
const fakeFetch = (url) => new Promise((resolve) => setTimeout(() => {
  if (url.endsWith("/missing")) {
    resolve({ ok: false, status: 404, json: async () => ({ message: "Not Found" }) });
  } else {
    resolve({ ok: true, status: 200, json: async () => ({ title: "Плов" }) });
  }
}, 10));

async function getTitle(url) {
  try {
    const response = fakeFetch(url);
    const data = response.json();
    return data.title;
  } catch (err) {
    return "Ошибка: " + err.message;
  }
}

async function main() {
  console.log(await getTitle("/dishes/1"));
  console.log(await getTitle("/dishes/missing"));
}
main();
Ожидаемый результат
Плов
Ошибка: HTTP 404
Подсказка 1

Запустите как есть: оба вызова дают одну и ту же ошибку про 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) => new Promise((resolve) =>
  setTimeout(() => resolve(`результаты для "${q}"`), delays[q]));

const state = { text: "" };
let latestId = 0;

async function onInput(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
function debounce(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 async function searchProducts(query, signal) {
  const url = `https://dummyjson.com/products/search?q=${encodeURIComponent(query)}&limit=10`;
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`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.

Читать подробнее: learn.javascript.ru — модули · learn.javascript.ru — Promise API · MDN — AbortController

Практика: закрепляем

ЗАДАЧА 6.1

Напишите debounce

средняя

Напишите debounce(fn, ms): возвращает функцию, которая вызывает fn с последними аргументами только после паузы в ms миллисекунд.

Можно редактировать · Ctrl + Enter
// function debounce(fn, ms) { ... }

const calls = [];
const search = debounce((q) => { calls.push(q); console.log("запрос:", q); }, 30);

search("п");
search("пл");
search("плов");
console.log("сразу:", calls.length);

setTimeout(() => search("чой"), 80);
setTimeout(() => console.log("всего запросов:", calls.length), 150);
Ожидаемый результат
сразу: 0
запрос: плов
запрос: чой
всего запросов: 2
Подсказка 1

Таймер хранится в переменной снаружи возвращаемой функции (let timer;), чтобы он «помнился» между вызовами.

Подсказка 2

Внутри возвращаемой функции: clearTimeout(timer); timer = setTimeout(() => fn(...args), ms);

ЗАДАЧА 6.2

Кэш запросов

средняя

Напишите makeCached(api): возвращает асинхронную функцию, которая приводит запрос к виду trim().toLowerCase(), и если такой запрос уже был — отдаёт сохранённый ответ без обращения к api.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
let apiCalls = 0;
const api = async (q) => { apiCalls++; await wait(10); return q.toUpperCase(); };

// function makeCached(api) { ... }

const cached = makeCached(api);
async function main() {
  console.log(await cached("Плов"));
  console.log(await cached("  плов "));
  console.log(await cached("чой"));
  console.log("вызовов API:", apiCalls);
}
main();
Ожидаемый результат
ПЛОВ
ПЛОВ
ЧОЙ
вызовов API: 2
Подсказка 1

Заведите const cache = new Map(); снаружи возвращаемой функции. Ключ — нормализованный запрос.

Подсказка 2

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). Рейтинг — с одним знаком после запятой.

Можно редактировать · Ctrl + Enter
const apiData = {
  total: 4,
  products: [
    { id: 1, title: "Телефон", price: 2500, rating: 4.46, stock: 5 },
    { id: 2, title: "Наушники", price: 300, rating: 3.9, stock: 0 },
    { id: 3, title: "Чехол", price: 50, rating: 4.8, stock: 20 },
    { id: 4, title: "Зарядка", price: 120, rating: 4.1, stock: 7 },
  ],
};

// function toCards(data) { ... }

const cards = toCards(apiData);
cards.forEach((c) => console.log(c));
console.log(cards.length, apiData.products[0].title);
Ожидаемый результат
Чехол — 50 сом. (★ 4.8)
Зарядка — 120 сом. (★ 4.1)
Телефон — 2500 сом. (★ 4.5)
3 Телефон
Подсказка 1

Цепочка: data.products.filter(...) → .sort(...) → .map(...). filter уже делает копию, поэтому исходный массив sort не изменит.

Подсказка 2

Деструктуризация в map упрощает шаблон: ({ title, price, rating }) => `${title} — ${price} сом. (★ ${rating.toFixed(1)})`.

ЗАДАЧА 6.4

Таймаут запроса через Promise.race

сложная

Напишите withTimeout(promise, ms): если promise не успел за ms миллисекунд — результат отклоняется ошибкой Таймаут, иначе возвращается значение промиса. Используйте Promise.race.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const fast = () => wait(20).then(() => "быстро");
const slow = () => wait(200).then(() => "медленно");

// function withTimeout(promise, ms) { ... }

async function main() {
  for (const fn of [fast, slow]) {
    try {
      console.log(await withTimeout(fn(), 100));
    } catch (err) {
      console.log("Ошибка:", err.message);
    }
  }
}
main();
Ожидаемый результат
быстро
Ошибка: Таймаут
Подсказка 1

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.

Можно редактировать · Ctrl + Enter
const text = `{
  "name": "product-search",
  "private": true,
  "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" },
  "devDependencies": { "vite": "^7.0.0" }
}`;

// разберите JSON и выведите результат
Ожидаемый результат
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 и отрисовка → поиск → события.

  1. «Сервер». Данные и функции, которые возвращают промис: поддельная и настоящая.
  2. Состояние. Один объект { status, items, error } и счётчик запросов latestId.
  3. Отрисовка. render() строит интерфейс из state. Больше никто DOM не трогает.
  4. Поиск. search(query): loading → success или error, устаревшие ответы игнорируются.
  5. События. Поле ввода с debounce.

Нажмите ▶ Запустить: самопроверка внизу напечатает, что пока не работает. Ваша задача — четыре отметки TODO.

Практика: достроим стартовое приложение

ЗАДАЧА 8.1

Допишите TODO — и поиск заработает

сложная

Реализуйте в script.js четыре отметки: 1) debounce, 2) getStatusText, 3) search (пустой запрос не уходит на сервер; состояния; защита от устаревших ответов; ошибка не вылетает наружу), 4) обработчик input с debounce. Блок самопроверки внизу сам «печатает» в поле и выводит результат в консоль. Когда всё готово, в консоли должно быть ровно то, что в «Ожидаемом результате». Блок самопроверки в свой проект не копируйте.

Стартовый поиск товаров · Ctrl + Enter
<main class="app">
  <h1>Поиск товаров</h1>
  <input id="q" placeholder="Например: телефон" autocomplete="off">
  <p id="status" class="status"></p>
  <ul id="results"></ul>
</main>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
старт: Введите запрос
сразу после ввода: Введите запрос
через 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);.

Как перенести шаблон в свой проект

  1. Создайте проект на Vite (раздел 06) и разнесите код по модулям: fakeSearch/realSearch — в api.js, debounce — в utils.js, getStatusText и render — в ui.js, состояние и события — в main.js.
  2. Каждому файлу добавьте export/import. render теперь принимает state аргументом — глобальные переменные между модулями не ходят.
  3. Замените fakeSearch на realSearch и убедитесь во вкладке Network, что запросы настоящие.
  4. Удалите блок самопроверки.
  5. Если у вас уже есть собственный проект с уроков 15–17 — не выбрасывайте его, а сравните со схемой «состояние → render → search → события».

09 Практика на уроке: ревью, защиты, контрольная

Сегодня занятие устроено иначе, чем обычно: проекты уже почти готовы, поэтому время уходит на проверку, защиту и контрольную. Действуем по плану.

  1. 0–10 мин · Последняя проверка. Чек-лист из раздела 07: npm run build, preview, Offline, Slow 3G. Преподаватель утверждает порядок защит.
  2. 10–30 мин · Code review в парах. По 10 минут на проект. Ревьюер идёт по чек-листу ниже и записывает замечания, автор слушает. Затем меняетесь.
  3. 30–60 мин · Защиты. Каждая — 5 минут плюс минута на смену. Остальные слушают и задают вопросы. Не успели сегодня — защищаетесь в начале следующего занятия.
  4. 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, гонка ответов, границы модулей, имена. Только замечания, без переписывания кода». Принимайте не все советы: каждую правку вы должны уметь обосновать.

Читать подробнее: learn.javascript.ru — отладка в браузере

Практика: ревью в миниатюре

ЗАДАЧА 9.1

Замечание ревьюера: ошибка не очищает данные

средняя

Ревьюер написал: «После ошибки на экране остаются старые результаты, а после успеха — старый текст ошибки». Исправьте applyResult: при error — items становится пустым массивом, при success — error становится пустой строкой.

Можно редактировать · Ctrl + Enter
function applyResult(state, result) {
  if (result.error) {
    return { ...state, status: "error", error: result.error };
  }
  return { ...state, status: "success", items: result.items };
}

let s = { status: "idle", items: [], error: "" };
s = applyResult(s, { items: ["плов", "чой"] });
console.log(s.status, s.items.length, JSON.stringify(s.error));
s = applyResult(s, { error: "Сеть недоступна" });
console.log(s.status, s.items.length, JSON.stringify(s.error));
s = applyResult(s, { items: ["самбуса"] });
console.log(s.status, s.items.length, JSON.stringify(s.error));
Ожидаемый результат
success 2 ""
error 0 "Сеть недоступна"
success 1 ""
Подсказка 1

Каждое состояние должно быть полным: при смене статуса сбрасывайте поля, которые к новому статусу не относятся.

Подсказка 2

В ветку ошибки добавьте items: [], а в ветку успеха — error: "".

ЗАДАЧА 9.2

Замечание ревьюера: опечатка в статусе

лёгкая

Статус «Загрузка» не показывается — ревьюер подозревает опечатку. Исправьте её, а заодно вынесите статусы в константу STATUS (IDLE, LOADING, SUCCESS, ERROR) и используйте её везде: так опечатка станет ошибкой, которую заметит редактор.

Можно редактировать · Ctrl + Enter
function badge(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.
  • Откройте нужные файлы заранее, увеличьте шрифт: группа должна видеть код.
  • Уберите лишнее: console.log, закомментированный код.
  • Не оправдывайтесь («не успел»). Лучше: «этого нет, потому что… вот как бы я добавил».

Практика: репетиция

ЗАДАЧА 10.1

Подготовьте ответы на вопросы защиты

лёгкая

Ответьте на пять вопросов выше про свой проект, своими словами, одним-двумя предложениями. Запишите ответы в объект (это заметки — автоматической проверки нет), затем отрепетируйте вслух перед соседом. Когда ответите — отметьте задачу вручную.

Можно редактировать · Ctrl + Enter
const defence = {
  ok: "Проверяю response.ok, потому что ...",
  states: "",
  debounce: "",
  race: "",
  ai: "",
};

console.log(Object.keys(defence).length + " ответов подготовлено");
Подсказка 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 отсутствует) — выведите не указан. Используйте деструктуризацию.

Можно редактировать · Ctrl + Enter
const students = [
  { name: "Алишер", age: 22, address: { city: "Душанбе" } },
  { name: "Мадина", age: 19, address: {} },
  { name: "Фаррух", age: 25, address: { city: "Худжанд" } },
];

// ваш код
Ожидаемый результат
Алишер (22) — Душанбе
Мадина (19) — не указан
Фаррух (25) — Худжанд
ЗАДАЧА 11.2

Прайс чайханы (Object.entries)

лёгкая

Дан объект цен. Выведите сумму всех цен в формате Сумма: 36 и самое дорогое блюдо в формате Дороже всего: плов (25).

Можно редактировать · Ctrl + Enter
const prices = { плов: 25, чой: 5, самбуса: 6 };

// ваш код
Ожидаемый результат
Сумма: 36
Дороже всего: плов (25)
ЗАДАЧА 11.3

Выручка по городам (reduce в объект)

средняя

Соберите объект «город → сумма заказов» и выведите его через JSON.stringify, затем количество городов в формате Городов: 2.

Можно редактировать · Ctrl + Enter
const orders = [
  { city: "Душанбе", sum: 100 },
  { city: "Худжанд", sum: 80 },
  { city: "Душанбе", sum: 50 },
];

// ваш код
Ожидаемый результат
{"Душанбе":150,"Худжанд":80}
Городов: 2
ЗАДАЧА 11.4

Найдите баг: забытый await

средняя

Ожидаем вывод Привет, Алишер! и Длина имени: 6. Сейчас код выводит чепуху. Исправьте.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const getName = () => wait(10).then(() => "Алишер");

async function greet() {
  const name = getName();
  console.log(`Привет, ${name}!`);
  return name.length;
}

greet().then((len) => console.log("Длина имени:", len));
Ожидаемый результат
Привет, Алишер!
Длина имени: 6
ЗАДАЧА 11.5

Два зависимых запроса

средняя

Напишите async function showProfile(id): сначала получите пользователя (api.user(id)), затем его команду по user.teamId (api.team(...)) и выведите Мадина — Frontend-17 (12 чел.). После завершения вызова выводится готово.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const api = {
  user: async (id) => { await wait(10); return { id, name: "Мадина", teamId: 7 }; },
  team: async (teamId) => { await wait(10); return { id: teamId, title: "Frontend-17", members: 12 }; },
};

// async function showProfile(id) { ... }

showProfile(1).then(() => console.log("готово"));
Ожидаемый результат
Мадина — Frontend-17 (12 чел.)
готово
ЗАДАЧА 11.6

Параллельные запросы (Promise.all)

средняя

Напишите loadAll(ids): запросите все города одновременно и верните массив названий в порядке ids (а не в порядке прихода ответов). Последовательные запросы дадут другой порядок строк пришёл … и не пройдут проверку.

Можно редактировать · Ctrl + Enter
const delays = { 1: 30, 2: 5, 3: 15 };
const cities = { 1: "Душанбе", 2: "Худжанд", 3: "Бохтар" };
const getCity = (id) => new Promise((resolve) => setTimeout(() => {
  console.log("пришёл", id);
  resolve(cities[id]);
}, delays[id]));

// function loadAll(ids) { ... }

loadAll([1, 2, 3]).then((names) => console.log(names.join(", ")));
Ожидаемый результат
пришёл 2
пришёл 3
пришёл 1
Душанбе, Худжанд, Бохтар
ЗАДАЧА 11.7

getJson с проверкой ok

средняя

Напишите getJson(url) с помощью fakeFetch: если response.ok ложно — бросьте ошибку с текстом HTTP <статус>, иначе верните разобранный JSON. Ошибки из цикла внизу обрабатываются уже готовым кодом.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const fakeFetch = async (url) => {
  await wait(5);
  if (url === "/users") return { ok: true, status: 200, json: async () => [{ name: "Алишер" }] };
  if (url === "/nope") return { ok: false, status: 404, json: async () => ({}) };
  return { ok: false, status: 500, json: async () => ({}) };
};

// async function getJson(url) { ... }

async function main() {
  for (const url of ["/users", "/nope", "/crash"]) {
    try {
      const data = await getJson(url);
      console.log("OK:", data.length);
    } catch (err) {
      console.log("Ошибка:", err.message);
    }
  }
}
main();
Ожидаемый результат
OK: 1
Ошибка: HTTP 404
Ошибка: HTTP 500
ЗАДАЧА 11.8

safeLoad: результат вместо исключения

сложная

Напишите safeLoad(task): она вызывает асинхронную функцию task и возвращает { ok: true, data }; при ошибке — { ok: false, name: err.name, error: err.message }. Счётчик finished должен увеличиваться при любом исходе (используйте finally).

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
let finished = 0;

// async function safeLoad(task) { ... }

const tasks = [
  async () => { await wait(5); return [1, 2, 3]; },
  async () => { await wait(5); throw new Error("Сервер недоступен"); },
  async () => { await wait(5); throw new TypeError("Неверный формат"); },
];

async function main() {
  for (const task of tasks) {
    const result = await safeLoad(task);
    console.log(result.ok ? `данные: ${result.data.length}` : `${result.name}: ${result.error}`);
  }
  console.log("завершено:", finished);
}
main();
Ожидаемый результат
данные: 3
Error: Сервер недоступен
TypeError: Неверный формат
завершено: 3
ЗАДАЧА 11.9

retry: повторяем при сбое

сложная

Напишите retry(fn, times): до times попыток вызывает fn; перед каждой выводит Попытка N (с единицы). Если попытка удалась — возвращает результат. Если все неудачны — бросает ошибку последней попытки.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
let attempt = 0;
const flaky = async () => {
  attempt++;
  await wait(5);
  if (attempt < 3) throw new Error(`сбой ${attempt}`);
  return "данные получены";
};
const alwaysBad = async () => { throw new Error("навсегда"); };

// async function retry(fn, times) { ... }

async function main() {
  console.log(await retry(flaky, 5));
  try {
    await retry(alwaysBad, 2);
  } catch (err) {
    console.log("Не вышло:", err.message);
  }
}
main();
Ожидаемый результат
Попытка 1
Попытка 2
Попытка 3
данные получены
Попытка 1
Попытка 2
Не вышло: навсегда
ЗАДАЧА 11.10

Всё вместе: load с состояниями и таймаутом

сложная

Напишите load(api, onChange). Сначала onChange("loading"). Затем запрос с таймаутом 100 мс (готовая withTimeout). Итог: непустой массив — onChange("success:N"), пустой — onChange("empty"), любая ошибка (в том числе таймаут) — onChange("error:" + сообщение). Исключения наружу не вылетают.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const withTimeout = (promise, ms) => Promise.race([
  promise,
  new Promise((_, reject) => setTimeout(() => reject(new Error("Таймаут")), ms)),
]);

const apis = {
  ok: async () => { await wait(10); return [1, 2, 3]; },
  empty: async () => { await wait(10); return []; },
  slow: async () => { await wait(300); return [1]; },
  bad: async () => { await wait(10); throw new Error("Сеть недоступна"); },
};

// async function load(api, onChange) { ... }

async function main() {
  for (const name of ["ok", "empty", "slow", "bad"]) {
    const log = [];
    await load(apis[name], (status) => log.push(status));
    console.log(name + ": " + log.join(" > "));
  }
}
main();
Ожидаемый результат
ok: loading > success:3
empty: loading > empty
slow: loading > error:Таймаут
bad: loading > 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: …

## Итог
- Где ИИ реально сэкономил время: …
- Где он увёл в сторону и почему я не заметил сразу: …
- Что я теперь делаю иначе: …

Три вопроса для самопроверки

  1. Могу ли я объяснить этот код построчно? Если нет — это долг: вы взяли чужое и не поняли. Либо разберите, либо перепишите.
  2. Как я узнал, что ответ ИИ правильный? «Заработало» — слабая проверка. Хорошая: сломал нарочно (Offline, 404, пустой запрос) и убедился, что поведение осмысленное.
  3. Что бы я сделал без ИИ — и сколько бы это стоило? Честная оценка помогает понять, где выгода реальна, а где — самообман.

Типичные «увели в сторону» в проектах с 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]

Читать подробнее: MDN — отладка JavaScript

Практика: считаем цену ИИ

ЗАДАЧА 12.1

Итоги разбора эпизодов

средняя

Эпизоды разбора лежат в массиве. Для каждого указано, сколько минут ИИ сэкономил (saved) и сколько потерял (lost: поиск ошибки в чужом коде, переписывание). Посчитайте суммы и итог со знаком (+20 или -5) и найдите эпизод с наибольшей потерей. Выведите четыре строки по образцу ниже.

Можно редактировать · Ctrl + Enter
const episodes = [
  { task: "разметка карточек", saved: 25, lost: 0 },
  { task: "запрос к API", saved: 15, lost: 10 },
  { task: "debounce", saved: 10, lost: 0 },
  { task: "обработка ошибок", saved: 5, lost: 40 },
  { task: "README", saved: 15, lost: 0 },
];

// посчитайте и выведите четыре строки
Ожидаемый результат
Сэкономил: 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 — проверка остаётся на вас.