MakeFirstStep· Frontend + ИИ

Урок 20 · Инструменты и финал

Чистый код, рефакторинг и деплой

Код пишут один раз, а читают — десятки. Через месяц вы сами не поймёте, что имели в виду под x2, а работодатель на собеседовании открывает ваш репозиторий за 30 секунд. Сегодня учимся наводить в коде порядок так, чтобы он не ломался, а потом выкладываем проекты в интернет — чтобы по ссылке их мог открыть любой человек, от мамы до рекрутёра.

  • Давать имена переменным и функциям, которые не нужно расшифровывать
  • Дробить большие функции, убирать вложенность ранним return, не повторяться (DRY)
  • Выносить «магические числа» в константы и отделять расчёты от отображения
  • Проверять код по чек-листу и рефакторить так, чтобы поведение не менялось
  • Собрать проект командой npm run build и настроить переменные окружения без утечки секретов
  • Задеплоить проект на Vercel и GitHub Pages, а советы ИИ-ревью — разделять на полезные и вредные
Как работать с этой страницей

Код с кнопкой ▶ Запустить — живой: меняйте его и смотрите результат в консоли. Блоки с подписью «Терминал» и конфигурационные файлы запускать нельзя — их вы повторите у себя на компьютере. В задачах на рефакторинг поведение кода менять нельзя: проверка сравнивает вывод, поэтому в каждой такой задаче внизу уже есть строки-тесты. Не трогайте их — они ваша страховка. Подсказки спрятаны: сначала сами.

01 Имена: код читают чаще, чем пишут

Компьютеру всё равно, как назвать переменную: a, x2 или somoniForOnePlov. Но программу читает не только компьютер — её читаете вы через неделю, ваш напарник по проекту и ИИ-помощник. Хорошее имя избавляет от комментария: из него сразу понятно, что это и зачем.

Придётся расшифровывать
JavaScript
const d = 7;
const l = [5, 35, 15];
function proc(x) {
  return x * 11;
}
let flag = false;
Читается как текст
JavaScript
const daysInWeek = 7;
const dishPrices = [5, 35, 15];
function convertUsdToSomoni(usd) {
  return usd * 11;
}
let isPaid = false;
Аналогия из жизни

Представьте кухню в чайхоне, где все банки подписаны «Банка 1», «Банка 2», «Банка 3». Повар-новичок положит в плов не зиру, а красный перец. А теперь банки с надписями «Зира», «Соль», «Барбарис» — ошибиться почти невозможно. Имена переменных — это подписи на банках вашей программы.

Правила хороших имён

Что называемКакПример
Переменная с даннымисуществительное, описывает содержимоеstudentName, totalPrice
Массивсуществительное во множественном числеstudents, prices
Функцияглагол + что делаетcalcTotal, renderMenu, getUserName
Булево значениевопрос: is, has, canisPaid, hasTicket, canEnroll
КонстантаЗАГЛАВНЫМИ через подчёркиваниеMAX_TRIES, USD_RATE
  • Без сокращений, которые знаете только вы: cnt, usr, tmp. Лишние пять букв — дешёвая цена за понятность. Исключение — общепринятые: i в коротком цикле, id, url.
  • Без «мусорных» слов: data, info, temp, thing, list2. Спросите себя: «Какие именно данные?»
  • Одно слово — одно понятие. Если в одном месте getUser, в другом fetchOrder, а в третьем loadProduct — выберите один глагол для одного вида действий и держитесь его.
  • Длина имени зависит от «жизни» переменной. Переменная на три строки может быть короткой (item), а видная всему файлу — подробной (selectedDishId).
  • Только латиница, язык — английский. Комментарии и тексты для пользователя пишем по-русски.
Худшее, что бывает: имя врёт

Функция getUser, которая заодно удаляет пользователя. Переменная isEmpty, равная true, когда список не пуст. Такой код опаснее, чем с плохими именами: вы полагаетесь на название — и ошибаетесь. Если поменяли поведение — поменяйте и имя.

Имя врёт — и код читается наоборот · Ctrl + Enter
const items = [5, 35];
const isEmpty = items.length > 0;   // ✖ на самом деле «не пусто»

if (isEmpty) {
  console.log("Корзина пуста");     // ← а корзина-то полная!
}

Читать подробнее: learn.javascript.ru — Стиль кода, learn.javascript.ru — Ниндзя-код (как делать не надо)

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

Учимся замечать плохие имена и чинить их.

ЗАДАЧА 1.1

Хорошее имя или плохое

лёгкая

Ниже восемь фрагментов. Впишите в массив badNames номера тех, где имя плохое (непонятное, мусорное или не говорит, что внутри). Номера — по возрастанию. Выведите их через запятую.

Можно редактировать · Ctrl + Enter
// 1
const d = 7;
// 2
const daysInWeek = 7;
// 3
function proc(x) { return x * 2; }
// 4
const isPaid = true;
// 5
const flag = false;   // здесь хранится «оплачен ли заказ»
// 6
const students = ["Рустам", "Нилуфар"];
// 7
function getStudentCount(students) { return students.length; }
// 8
const data2 = [1, 2, 3];

const badNames = [/* номера плохих имён */];
console.log(badNames.join(", "));
Ожидаемый результат
1, 3, 5, 8
Подсказка 1

Для каждого имени спросите: «Если увижу его через месяц — пойму, что это?» Одна буква, proc, flag, data2 — не скажут ничего.

Подсказка 2

Хорошие имена здесь под номерами 2, 4, 6 и 7. Остальные четыре — ваш ответ.

ЗАДАЧА 1.2

Три буквы вместо смысла

лёгкая

Переменные p, n, t нужно переименовать: price (цена одной самбусы), count (сколько штук), total (сумма). Последняя строка уже использует новые имена — поэтому сейчас код падает. Поведение не меняется.

Можно редактировать · Ctrl + Enter
const p = 25;
const n = 4;
const t = p * n;

console.log(`${price} × ${count} = ${total}`);
Ожидаемый результат
25 × 4 = 100
Подсказка 1

Переименуйте каждую переменную везде, где она встречается, — и в объявлении, и в расчёте p * n.

Подсказка 2

В VS Code клавиша F2 на имени переменной переименует её сразу во всём файле. Но в этом упражнении наберите руками.

ЗАДАЧА 1.3

Говорящие функции

средняя

Три функции работают, но названы так, что ничего не понятно. Переименуйте их в isAdult, getTotal, formatPrice, а заодно дайте понятные имена параметрам и переменным внутри (age, prices, total, amount). Вызовы внизу уже используют новые имена. Результат не меняется.

Можно редактировать · Ctrl + Enter
function check(a) {
  return a >= 18;
}

function calc(arr) {
  let s = 0;
  for (const x of arr) {
    s += x;
  }
  return s;
}

function fmt(n) {
  return n + " сомони";
}

console.log(isAdult(20), isAdult(15));
console.log(getTotal([5, 35, 15]));
console.log(formatPrice(getTotal([5, 35, 15])));
Ожидаемый результат
true false
55
55 сомони
Подсказка 1

Имя функции — глагол: getTotal «получает итог», formatPrice «форматирует цену». Булева isAdult отвечает на вопрос «взрослый?».

Подсказка 2

В цикле for (const x of arr) замените x на price, а arr — на prices: так цикл читается почти как предложение.

02 Маленькие функции и ранний возврат

Правило номер один для функций: одна функция — одна задача. Если вы описываете её словом «и» («считает сумму и применяет скидку и печатает чек»), это три функции. Короткая функция проще читать, называть, проверять и использовать в другом месте.

  • Размер. Ориентир — помещается на один экран, 10–20 строк. Не закон, а сигнал: если пролистываете — пора делить.
  • Параметры. Два–три — нормально. Пять и больше — повод подумать, не стоит ли передать объект.
  • Один уровень детализации. Верхняя функция рассказывает «что происходит» (validate → calc → render), а подробности спрятаны в маленьких функциях.
Большая функция делает всё сразу · Ctrl + Enter
function processOrder(prices) {
  let sum = 0;
  for (const price of prices) {
    sum += price;
  }
  const discount = sum >= 100 ? sum / 10 : 0;
  console.log("К оплате: " + (sum - discount));
}

processOrder([40, 70, 20]);
Три маленькие функции — «главная» читается как план · Ctrl + Enter
function calcSum(prices) {
  let sum = 0;
  for (const price of prices) {
    sum += price;
  }
  return sum;
}

function calcDiscount(sum) {
  return sum >= 100 ? sum / 10 : 0;
}

function processOrder(prices) {
  const sum = calcSum(prices);
  const discount = calcDiscount(sum);
  console.log("К оплате: " + (sum - discount));
}

processOrder([40, 70, 20]);

Ранний возврат (guard clauses)

Когда проверки вкладываются друг в друга, получается «стрелка» из скобок: основная логика уехала вправо на три уровня, а в голове приходится держать все условия сразу. Лекарство — ранний возврат: сначала отсекаем плохие случаи и сразу выходим из функции, а основная логика остаётся на одном уровне.

Стрелка из вложенных if
JavaScript
function getFare(age, hasCard) {
  if (age >= 0) {
    if (hasCard) {
      if (age < 7) {
        return 0;
      } else {
        return 3;
      }
    } else {
      return 5;
    }
  } else {
    return -1;
  }
}
Ранний возврат — плоско
JavaScript
function getFare(age, hasCard) {
  if (age < 0) return -1;
  if (!hasCard) return 5;
  if (age < 7) return 0;
  return 3;
}

Обе функции возвращают одно и то же, но вторую читаешь сверху вниз как список правил. Заметили? Слово else пропало: после return оно не нужно, функция уже закончилась.

Аналогия из жизни

Контролёр на входе в метро проверяет билет сразу у турникета и не пускает без него. Представьте, что вас пустили внутрь, довезли до конечной — и только там сообщили, что билета не было. Ранний возврат — это турникет в начале функции.

Частая ошибка: забыли return в «охране»

Если в проверке написать только сообщение, а return забыть — функция продолжит работать с плохими данными.

Проверка есть, а выхода нет · Ctrl + Enter
function divide(a, b) {
  if (b === 0) {
    console.log("Делить на ноль нельзя");   // сообщили, но не вышли!
  }
  return a / b;                             // всё равно делим
}

console.log(divide(10, 0));   // → Infinity, а хотели остановиться

Читать подробнее: learn.javascript.ru — Функции, refactoring.guru — Рефакторинг

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

Учимся делить функции и убирать вложенность. Поведение функций остаётся прежним.

ЗАДАЧА 2.1

Охрана на входе в зал

средняя

Перепишите функцию с ранними возвратами: без единого else. Ответы для всех входов должны остаться такими же. Последняя строка проверяет, что слова else в тексте функции не осталось (в комментариях его тоже не пишите).

Можно редактировать · Ctrl + Enter
function getAccessMessage(age, hasTicket, isVip) {
  if (age >= 18) {
    if (hasTicket) {
      if (isVip) {
        return "Добро пожаловать в VIP-зал";
      } else {
        return "Добро пожаловать в зал";
      }
    } else {
      return "Купите билет";
    }
  } else {
    return "Только с 18 лет";
  }
}

console.log(getAccessMessage(20, true, true));
console.log(getAccessMessage(20, true, false));
console.log(getAccessMessage(20, false, true));
console.log(getAccessMessage(15, true, true));
console.log(getAccessMessage.toString().includes("else"));
Ожидаемый результат
Добро пожаловать в VIP-зал
Добро пожаловать в зал
Купите билет
Только с 18 лет
false
Подсказка 1

Начните с «плохих» случаев: if (age < 18) return "Только с 18 лет";. Проверки идут в том же порядке, что и вложенность в исходнике.

Подсказка 2

Остаётся три строки-охранника: возраст → билет → VIP. Последний return — для обычного посетителя «в зал».

ЗАДАЧА 2.2

Чек из кучи в три функции

средняя

Разделите printReceipt на calcSum(prices) (возвращает сумму), calcDiscount(sum) (скидка 10% от 100 сомони и выше, иначе 0) и саму printReceipt, которая только вызывает их и печатает. Вывод чека должен остаться прежним.

Можно редактировать · Ctrl + Enter
function printReceipt(prices) {
  let sum = 0;
  for (const price of prices) {
    sum += price;
  }
  let discount = 0;
  if (sum >= 100) {
    discount = sum * 0.1;
  }
  const total = sum - discount;
  console.log("Сумма: " + sum);
  console.log("Скидка: " + discount);
  console.log("Итого: " + total);
}

printReceipt([40, 70, 20]);
printReceipt([30, 20]);

console.log(calcSum([10, 20, 30]));
console.log(calcDiscount(200));
console.log(calcDiscount(50));
Ожидаемый результат
Сумма: 130
Скидка: 13
Итого: 117
Сумма: 50
Скидка: 0
Итого: 50
60
20
0
Подсказка 1

Вынесите цикл суммирования в calcSum с return sum. Условие со скидкой — в calcDiscount: она получает сумму и возвращает число.

Подсказка 2

Внутри printReceipt остаётся: два вызова (const sum = calcSum(prices), const discount = calcDiscount(sum)), расчёт total и три console.log.

ЗАДАЧА 2.3

Охранник без выхода

лёгкая

Функция divide должна при делении на ноль вывести сообщение и вернуть null, а не считать дальше. Найдите, чего не хватает.

Можно редактировать · Ctrl + Enter
function divide(a, b) {
  if (b === 0) {
    console.log("Делить на ноль нельзя");
  }
  return a / b;
}

console.log(divide(10, 0));
console.log(divide(10, 2));
Ожидаемый результат
Делить на ноль нельзя
null
5
Подсказка 1

Сообщение выведено, но функция не закончилась: следующая строка return a / b всё равно выполнится.

Подсказка 2

Добавьте внутрь блока if строку return null; после console.log.

ЗАДАЧА 2.4

Цена билета в маршрутке

средняя

Функция считает цену проезда в сомони: возраст вне диапазона 0–120 → -1; дети до 7 лет → 0; студенты → 2; в выходные → 4; иначе 5. Перепишите её без else и без переменной price — только ранние return. Результаты не меняются.

Можно редактировать · Ctrl + Enter
function getTicketPrice(age, isStudent, isWeekend) {
  let price;
  if (age < 0 || age > 120) {
    price = -1;
  } else {
    if (age < 7) {
      price = 0;
    } else {
      if (isStudent) {
        price = 2;
      } else {
        if (isWeekend) {
          price = 4;
        } else {
          price = 5;
        }
      }
    }
  }
  return price;
}

console.log(getTicketPrice(5, false, false));
console.log(getTicketPrice(20, true, false));
console.log(getTicketPrice(30, false, true));
console.log(getTicketPrice(30, false, false));
console.log(getTicketPrice(-3, false, false));
console.log(getTicketPrice(200, true, true));
console.log(getTicketPrice.toString().includes("else"));
Ожидаемый результат
0
2
4
5
-1
-1
false
Подсказка 1

Порядок проверок важен: сначала «невозможный возраст», потом дети, потом студенты, потом выходные. Каждая — одна строка if (...) return ...;.

Подсказка 2

Последняя строка функции — просто return 5; для всех оставшихся случаев.

03 DRY и магические числа

DRY — Don't Repeat Yourself, «не повторяйся». Скопировали кусок кода три раза — значит, при любой правке придётся найти и исправить все три копии. Одну забудете — получите баг, который проявляется «иногда». Решение то же, что в уроке про функции: одно место, где написано, и много мест, где вызвано.

Копипаста: поменять формат — править в трёх местах · Ctrl + Enter
console.log("Самбуса: " + 5 + " сомони");
console.log("Курутоб: " + 35 + " сомони");
console.log("Шакароб: " + 15 + " сомони");
DRY: формат описан один раз · Ctrl + Enter
function printPriceTag(name, price) {
  console.log(`${name}: ${price} сомони`);
}

const menu = [
  { name: "Самбуса", price: 5 },
  { name: "Курутоб", price: 35 },
  { name: "Шакароб", price: 15 },
];

for (const dish of menu) {
  printPriceTag(dish.name, dish.price);
}
Правило трёх

Повторилось второй раз — потерпите. Повторилось в третий — выносите в функцию или данные. Не абстрагируйтесь слишком рано: два похожих куска кода иногда похожи случайно и завтра будут отличаться. Если «общая» функция обрастает параметрами-флагами (isStudent, isSenior, isSpecial…), вы объединили то, что объединять не стоило.

Магические числа

Магическое число — число (или строка) в коде, смысл которого непонятен без объяснений. Что значит * 11? А >= 300? Через месяц не вспомните сами. Лекарство — именованная константа: вынести значение наверх файла и дать ему имя.

Магия
JavaScript
function deliveryCost(sum) {
  if (sum >= 300) return 0;
  return 25;
}
Константы объясняют смысл
JavaScript
const FREE_DELIVERY_FROM = 300; // сомони
const DELIVERY_PRICE = 25;      // сомони

function deliveryCost(sum) {
  if (sum >= FREE_DELIVERY_FROM) return 0;
  return DELIVERY_PRICE;
}
  • Константы пишем ЗАГЛАВНЫМИ_С_ПОДЧЁРКИВАНИЕМ и собираем в начале файла — так видно все «настройки» программы.
  • Единицу измерения — в имя или комментарий: TIMEOUT_MS, FREE_DELIVERY_FROM // сомони.
  • Нужно поменять курс или порог — меняете в одном месте.
  • Не магия: 0, 1, -1, 2 в очевидных местах (i % 2, list.length - 1). Выносить их — перебор.
  • Строки-статусы тоже магия: вместо "paid" в десяти местах заведите const STATUS_PAID = "paid" — опечатка в имени константы поймается сразу, в строке — никогда.
Аналогия из жизни

В бозоре табличка «Курс доллара — 11 сомони» висит одна. Если бы продавец каждому покупателю объявлял курс заново «из головы», то в какой-то момент один сказал бы 11, а другой — 10,5. Константа — это табличка, на которую смотрят все.

Читать подробнее: refactoring.guru — Замена магического числа константой

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

Выносим числа в константы и убираем копипасту. Результаты остаются прежними.

ЗАДАЧА 3.1

Курс и доставка

лёгкая

Вынесите числа в константы с точными именами: USD_RATE (курс), FREE_DELIVERY_FROM (порог бесплатной доставки), DELIVERY_PRICE (цена доставки). Объявите их над функциями. Последняя строка проверяет, что константы существуют.

Можно редактировать · Ctrl + Enter
function priceInSomoni(usd) {
  return usd * 11;
}

function deliveryCost(sum) {
  if (sum >= 300) return 0;
  return 25;
}

console.log(priceInSomoni(10));
console.log(deliveryCost(100));
console.log(deliveryCost(300));
console.log(typeof USD_RATE, typeof FREE_DELIVERY_FROM, typeof DELIVERY_PRICE);
Ожидаемый результат
110
25
0
number number number
Подсказка 1

Три строки const ... = число; в самом верху, до функций. Имена — ровно как в условии.

Подсказка 2

Внутри функций замените числа на имена: usd * USD_RATE, sum >= FREE_DELIVERY_FROM, return DELIVERY_PRICE.

ЗАДАЧА 3.2

Ценники без копипасты

средняя

Три блока вывода ценника с НДС отличаются только названием и ценой. Замените их функцией printPriceTag(name, price) и вызывайте её в цикле по массиву menu. Коэффициент НДС — константа VAT_RATE (1.1). Вывод остаётся прежним.

Можно редактировать · Ctrl + Enter
const menu = [
  { name: "Самбуса", price: 5 },
  { name: "Курутоб", price: 35 },
  { name: "Шакароб", price: 15 },
];

const samsaPrice = 5;
console.log("Самбуса: " + samsaPrice + " сомони, с НДС " + (samsaPrice * 1.1).toFixed(2));
const kurutobPrice = 35;
console.log("Курутоб: " + kurutobPrice + " сомони, с НДС " + (kurutobPrice * 1.1).toFixed(2));
const shakarobPrice = 15;
console.log("Шакароб: " + shakarobPrice + " сомони, с НДС " + (shakarobPrice * 1.1).toFixed(2));

console.log(printPriceTag.length, typeof VAT_RATE);
Ожидаемый результат
Самбуса: 5 сомони, с НДС 5.50
Курутоб: 35 сомони, с НДС 38.50
Шакароб: 15 сомони, с НДС 16.50
2 number
Подсказка 1

Функция принимает два параметра и выводит одну строку. Три отдельные переменные с ценами больше не нужны — данные уже лежат в menu.

Подсказка 2

Цикл: for (const dish of menu) { printPriceTag(dish.name, dish.price); }. Строку соберите шаблоном: `${name}: ${price} сомони, с НДС ${(price * VAT_RATE).toFixed(2)}`.

ЗАДАЧА 3.3

Три скидки — одна функция

сложная

Три функции скидок — копии друг друга. Сделайте одну общую applyDiscount(price, percent) (результат округляется Math.round). Три старые функции оставьте — но каждая теперь должна быть одной строкой с вызовом общей. Проценты вынесите в константы STUDENT_DISCOUNT (20), SENIOR_DISCOUNT (30), CHILD_DISCOUNT (50).

Можно редактировать · Ctrl + Enter
function studentPrice(price) {
  const result = price - price * 20 / 100;
  return Math.round(result);
}

function seniorPrice(price) {
  const result = price - price * 30 / 100;
  return Math.round(result);
}

function childPrice(price) {
  const result = price - price * 50 / 100;
  return Math.round(result);
}

console.log(studentPrice(55), seniorPrice(55), childPrice(55));
console.log(applyDiscount(200, 25));
console.log(typeof STUDENT_DISCOUNT, typeof SENIOR_DISCOUNT, typeof CHILD_DISCOUNT);
Ожидаемый результат
44 39 28
150
number number number
Подсказка 1

Общая часть у всех трёх: price - price * percent / 100 и округление. Меняется только процент — он становится параметром.

Подсказка 2

Итог: function studentPrice(price) { return applyDiscount(price, STUDENT_DISCOUNT); } — и так для остальных двух.

04 Логика отдельно от отображения. Комментарии

В каждой программе есть две разные работы: считать (логика: отфильтровать товары, посчитать средний балл) и показывать (отображение: собрать HTML, вывести в консоль). Когда они свалены в одну функцию, её нельзя проверить без браузера, нельзя использовать повторно и страшно менять: тронешь вёрстку — сломаешь математику.

Аналогия из жизни

Повар и официант. Повар готовит блюдо — это логика. Официант красиво подаёт — отображение. Если бы повар сам выходил в зал, а официант бегал на кухню помешивать плов, то смена оформления зала замедлила бы кухню. Пусть каждый занимается своим, а передают друг другу готовое блюдо — данные.

Схема: три роли

  1. Функция-расчёт (getAffordableDishes, calcStats): получает данные, возвращает данные. Не трогает DOM, не печатает, не меняет внешние переменные. Её легко проверить через console.log.
  2. Функция-отображение (renderMenu, formatStats): получает готовые данные, строит строку или элементы. Ничего не считает.
  3. «Склейка» (обработчик события, главная функция): вызывает первую, результат отдаёт второй.
Смесь: расчёт и вывод в одной функции · Ctrl + Enter
function showStats(grades) {
  let sum = 0;
  for (const grade of grades) sum += grade;
  console.log("Средний балл: " + sum / grades.length);
}

showStats([90, 55, 72]);
Раздельно: считаем, потом показываем · Ctrl + Enter
function calcAverage(grades) {
  let sum = 0;
  for (const grade of grades) sum += grade;
  return sum / grades.length;
}

function formatAverage(average) {
  return "Средний балл: " + average;
}

console.log(formatAverage(calcAverage([90, 55, 72])));
console.log(calcAverage([100, 50]));  // расчёт легко проверить отдельно

В уроках про DOM и события вы делали render() — это именно оно. Правило тянется дальше: в React (уроки 22–24) компонент получает данные и отображает их, а расчёты живут отдельно.

Комментарии: объясняйте «почему», а не «что»

Что делает код, видно из самого кода — если имена хорошие. А вот почему он так написан, из кода не видно никак. Это и есть место для комментария: бизнес-правило, обходной путь, неочевидное ограничение.

Комментарий повторяет код
JavaScript
// увеличиваем i на 1
i++;

// если цена больше 100
if (price > 100) { ... }

// return total * 0.9;
return total;
Комментарий даёт новое знание
JavaScript
// касса не принимает монеты меньше
// 1 сомони, поэтому округляем вверх
const payment = Math.ceil(total);

// TODO(Мадина, до пятницы): заменить
// на запрос к API, когда он будет готов
  • Закомментированный код — удалите. Он засоряет файл, а в Git (урок 19) вся история сохранена — вернуть можно в любой момент.
  • Устаревший комментарий хуже, чем его отсутствие — он врёт. Меняете код — проверьте комментарий над ним.
  • Хочется объяснить, что делает кусок кода? Сначала попробуйте вынести его в функцию с хорошим именем — имя станет «комментарием», который не устаревает.
  • TODO — допустимо, если с автором и сроком. Забытые «потом исправлю» гниют годами.

Читать подробнее: learn.javascript.ru — Комментарии

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

Разделяем расчёт и вывод, учимся отличать полезные комментарии от шума.

ЗАДАЧА 4.1

Шум или полезный комментарий

лёгкая

Восемь комментариев из реальных проектов. Впишите в массив noise номера тех, которые бесполезны (повторяют код, закомментированный код, пересказ имени функции). Номера по возрастанию.

Можно редактировать · Ctrl + Enter
/*
// 1
total = total + price; // прибавляем цену к итогу

// 2
const MAX_TRIES = 3; // после 3 неверных паролей блокируем вход — требование службы безопасности

// 3
// return total * 0.9;

// 4
users.sort(byName); // сортируем пользователей по имени

// 5
if (hour < 6) return "ночь"; // до 6 утра маршруты не ходят, поэтому тариф «ночной»

// 6
// TODO(Шахром, до пятницы): заменить на запрос к API

// 7
let x = 5; // x равно пяти

// 8
// Функция считает сумму
function calcTotal(prices) { ... }
*/

const noise = [/* номера */];
console.log(noise.join(", "));
Ожидаемый результат
1, 3, 4, 7, 8
Подсказка 1

Закройте код пальцем и прочитайте только комментарий: если после него вы узнали что-то, чего не сказал бы сам код, — комментарий полезный.

Подсказка 2

Полезные — 2 (причина лимита), 5 (причина тарифа), 6 (TODO с автором и сроком). Остальные пять — шум.

ЗАДАЧА 4.2

Статистика группы

средняя

showStats считает и печатает одновременно. Разделите: calcStats(grades) возвращает объект { average, best, passedCount } (проходной балл — константа PASS_SCORE = 56), formatStats(stats, total) возвращает массив из трёх строк для печати (total — сколько всего оценок), а showStats только соединяет их и печатает. Вывод не меняется.

Можно редактировать · Ctrl + Enter
function showStats(grades) {
  let sum = 0;
  let best = 0;
  let passed = 0;
  for (const grade of grades) {
    sum += grade;
    if (grade > best) best = grade;
    if (grade >= 56) passed++;
  }
  console.log("Средний балл: " + sum / grades.length);
  console.log("Лучший: " + best);
  console.log("Сдали: " + passed + " из " + grades.length);
}

showStats([90, 55, 72, 38, 85]);

console.log(JSON.stringify(calcStats([60, 40])));
console.log(typeof formatStats, typeof PASS_SCORE);
Ожидаемый результат
Средний балл: 68
Лучший: 90
Сдали: 3 из 5
{"average":50,"best":60,"passedCount":1}
function number
Подсказка 1

calcStats — весь цикл, в конце return { average: ..., best, passedCount: passed }. Внутри нет ни одного console.log.

Подсказка 2

showStats станет короткой: const lines = formatStats(calcStats(grades), grades.length); и цикл for (const line of lines) console.log(line);.

ЗАДАЧА 4.3

Меню чайхоны: фильтр и вёрстка отдельно

сложная

Сейчас фильтрация блюд и создание <li> перемешаны в одном цикле. Сделайте две функции: getAffordableDishes(dishes, maxPrice) возвращает массив блюд с ценой не выше maxPrice (ничего не трогает в DOM), renderMenu(list) рисует список в #menu. Затем вызовите renderMenu(getAffordableDishes(dishes, 30)). Нижние строки проверки не меняйте.

Меню чайхоны · Ctrl + Enter
<h3>Меню до 30 сомони</h3>
<ul id="menu"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
function function
3
2
Подсказка 1

getAffordableDishes — это dishes.filter(...) или цикл с push; она возвращает массив и не знает ни про menu, ни про document.

Подсказка 2

В renderMenu(list) оставьте цикл по list с созданием li. Старый цикл со всем сразу удалите — иначе в списке будут дубли и проверка покажет 6 вместо 3.

05 Рефакторинг: меняем форму, не поведение

Рефакторинг — это улучшение устройства кода без изменения того, что он делает. Для пользователя ничего не поменялось: те же кнопки, те же цифры. Для вас поменялось всё: код стал понятнее и дальше его легче развивать. Главное слово здесь — «без изменения»: если по пути вы нечаянно изменили поведение, это уже не рефакторинг, а новый баг.

Аналогия из жизни

Уборка на кухне чайхоны во время работы: ножи в один ящик, специи по алфавиту, чистые доски. Посетители продолжают получать плов, но повара стали готовить быстрее и реже ошибаться. Нельзя начать уборку и заодно изменить рецепт плова — иначе непонятно, что именно ухудшило вкус.

Безопасный рабочий цикл

  1. Зафиксируйте поведение. Прежде чем что-то менять, запустите код и запишите, что он выводит для нескольких входов — включая граничные (0, пусто, ровно порог). Это ваши «тесты».
  2. Один маленький шаг: переименовали → запустили → вывод тот же. Вынесли константу → запустили → тот же. Не делайте десять правок подряд.
  3. Порядок шагов от простого к сложному: имена → константы → удалить мусор и мёртвый код → ранний возврат → выделить функции → убрать повторы → разделить логику и вывод.
  4. Сломалось — откатитесь к последнему рабочему состоянию. В Git это одна команда; поэтому после каждого удачного шага делаем коммит (урок 19), например rename vars in cart.js.
  5. Две шляпы. Либо вы рефакторите, либо чините баг/добавляете функцию. Одновременно — нельзя. Нашли баг по пути — запишите и вернитесь к нему отдельным коммитом.

Мини-тесты в одну функцию

Для проверки рефакторинга не нужны специальные инструменты. Достаточно маленькой функции, которая сравнивает ответ с ожидаемым:

check — самодельный тест · Ctrl + Enter
function check(name, actual, expected) {
  console.log(`${actual === expected ? "OK  " : "FAIL"} ${name}`);
}

const isAdult = age => age >= 18;

check("18 — взрослый", isAdult(18), true);
check("17 — не взрослый", isAdult(17), false);
check("0 — не взрослый", isAdult(0), false);

Единый чек-лист качества кода

Чтобы «чистый» не означало «как мне нравится», у нас общий чек-лист из 12 пунктов — полная печатная версия в конце урока. Пункты 1–8 про код, 9–12 про проект. Сейчас нужны первые восемь:

№Пункт чек-листа
1Имена говорят, что это (нет a, data, temp; булевы — is/has/can)
2Размер: функция делает одну вещь и помещается на экран
3DRY: нет копипасты — повторяющееся вынесено
4Ранний возврат: вложенность не глубже 2–3 уровней, нет лишних else
5Константы: нет магических чисел и строк
6Логика ≠ отображение: расчёты не трогают DOM и консоль
7Комментарии объясняют «почему»; нет закомментированного кода
8Чисто: нет отладочных console.log, неиспользуемого кода, var

Читать подробнее: refactoring.guru — Что такое рефакторинг

Практика: оцениваем и рефакторим

Сначала учимся видеть проблемы — оцениваем код по чек-листу. Потом правим.

ЗАДАЧА 5.1

Оцените код по чек-листу

лёгкая

Прочитайте функцию (она в комментарии, запускать её не нужно) и впишите в массив violations номера нарушенных пунктов чек-листа (1–8, по возрастанию).

Можно редактировать · Ctrl + Enter
/*
function calc(l) {
  let t = 0;
  console.log("calc вызвана");
  for (let i = 0; i < l.length; i++) {
    t = t + l[i].p * l[i].q;
  }
  if (t > 500) {
    t = t - t * 0.05;
  }
  return t;
}
*/

const violations = [/* номера пунктов */];
console.log(violations.join(", "));
Ожидаемый результат
1, 5, 8
Подсказка 1

Имена calc, l, t, p, q — пункт 1. Числа 500 и 0.05 без имён — пункт 5. А что делает строка с console.log внутри расчёта?

Подсказка 2

Функция короткая, повторов нет, вложенность одна, комментариев нет — пункты 2, 3, 4, 6, 7 здесь не нарушены.

ЗАДАЧА 5.2

Что не так с карточкой профиля

средняя

Ещё одна функция. Найдите нарушения (пункты 1–8). Внимательно: одно нарушение может прятаться в обычном на вид числе.

Можно редактировать · Ctrl + Enter
/*
function showProfile(user) {
  const box = document.createElement("div");   // создаём div
  if (user) {
    if (user.name) {
      if (user.birthYear) {
        const age = 2026 - user.birthYear;
        box.textContent = user.name + ", возраст: " + age;
        document.body.append(box);
      }
    }
  }
}
*/

const violations = [/* номера пунктов */];
console.log(violations.join(", "));
Ожидаемый результат
4, 5, 6, 7
Подсказка 1

Вложенность из трёх if — это пункт 4. Комментарий «создаём div» повторяет код — пункт 7.

Подсказка 2

Число 2026 — год, зашитый в код: через год возраст посчитается неверно (пункт 5). И ещё: функция считает возраст и рисует на странице — какой пункт про это?

ЗАДАЧА 5.3

Корзина магазина: полный рефакторинг

сложная

Функция f считает сумму заказа: товары → промокод (NAVRUZ −15%, STUDENT −10%) → доставка 25 сомони, если сумма меньше 300 → округление. Приведите её в порядок по чек-листу и сохраните результаты:

  • функция называется calcOrderTotal(items, promoCode), у товаров поля price и quantity (в тестах они уже так названы);
  • константы FREE_DELIVERY_FROM, DELIVERY_PRICE и объект PROMO_DISCOUNTS (код → процент);
  • без else; расчёт подытога — отдельная функция calcSubtotal(items).
Можно редактировать · Ctrl + Enter
function f(a, c) {
  let r = 0;
  for (let i = 0; i < a.length; i++) {
    r += a[i].p * a[i].q;
  }
  if (c === "NAVRUZ") {
    r = r - r * 15 / 100;
  } else {
    if (c === "STUDENT") {
      r = r - r * 10 / 100;
    }
  }
  if (r < 300) {
    r = r + 25;
  }
  return Math.round(r);
}

const small = [{ price: 50, quantity: 2 }, { price: 120, quantity: 1 }];
const big = [{ price: 200, quantity: 3 }];

console.log(calcOrderTotal(small, ""));
console.log(calcOrderTotal(small, "NAVRUZ"));
console.log(calcOrderTotal(small, "STUDENT"));
console.log(calcOrderTotal(big, ""));
console.log(calcOrderTotal(big, "NAVRUZ"));
console.log(calcOrderTotal(big, "UNKNOWN"));
console.log(calcSubtotal(small));
console.log(typeof FREE_DELIVERY_FROM, typeof DELIVERY_PRICE, typeof PROMO_DISCOUNTS);
console.log(calcOrderTotal.toString().includes("else"));
Ожидаемый результат
245
212
223
600
510
600
220
number number object
false
Подсказка 1

План по шагам: 1) переименуйте, 2) вынесите константы, 3) calcSubtotal через цикл for...of или reduce, 4) промокод: const percent = PROMO_DISCOUNTS[promoCode] || 0; — вместо двух вложенных if. После каждого шага запускайте и сверяйте.

Подсказка 2

Скидка: const discounted = subtotal - subtotal * percent / 100;. Доставка: ранний return Math.round(discounted) для суммы от порога, иначе Math.round(discounted + DELIVERY_PRICE). Порог сравнивается с суммой после скидки — как в исходном коде.

ЗАДАЧА 5.4

Рефакторинг, который сломал код

средняя

Коллега «отрефакторил» функцию оценки: красиво, но поведение изменилось. Тесты (check) выводят FAIL там, где что-то сломано. Найдите причину и исправьте так, чтобы все тесты стали OK. Тесты не меняйте: правильные оценки — 86 и выше → 5, от 71 → 4, от 56 → 3, ниже → 2.

Можно редактировать · Ctrl + Enter
const PASS_SCORE = 56;

function getGrade(score) {
  if (score > 86) return 5;
  if (score >= 71) return 4;
  if (score > PASS_SCORE) return 3;
  return 2;
}

function check(name, actual, expected) {
  console.log(`${actual === expected ? "OK  " : "FAIL"} ${name}`);
}

check("90 → 5", getGrade(90), 5);
check("86 → 5", getGrade(86), 5);
check("71 → 4", getGrade(71), 4);
check("56 → 3", getGrade(56), 3);
check("55 → 2", getGrade(55), 2);
check("0 → 2", getGrade(0), 2);
Ожидаемый результат
OK 90 → 5
OK 86 → 5
OK 71 → 4
OK 56 → 3
OK 55 → 2
OK 0 → 2
Подсказка 1

FAIL бывает ровно на границах. Посмотрите на знаки сравнения: где вместо >= стоит >?

Подсказка 2

Ошибок две: в первой строке с 86 и в строке с PASS_SCORE. Урок: граничные значения обязательно входят в тесты.

ЗАДАЧА 5.5

Посещаемость: циклы → методы массивов

сложная

Функция report считает «отличников» (посетили 8+ занятий; имена — заглавными) и среднюю посещаемость. Разбейте на getExcellentNames(students) (через filter и map) и calcAverageVisits(students) (через reduce), порог вынесите в MIN_VISITS_FOR_EXCELLENT. В итоге ни в одной из трёх функций не должно быть цикла for (. Вывод прежний.

Можно редактировать · Ctrl + Enter
function report(students) {
  const names = [];
  for (let i = 0; i < students.length; i++) {
    if (students[i].visited >= 8) {
      names.push(students[i].name.toUpperCase());
    }
  }
  let total = 0;
  for (let i = 0; i < students.length; i++) {
    total = total + students[i].visited;
  }
  const avg = total / students.length;
  return names.join(", ") + " | среднее: " + avg;
}

const group = [
  { name: "Рустам", visited: 10 },
  { name: "Зарина", visited: 7 },
  { name: "Нилуфар", visited: 8 },
  { name: "Далер", visited: 4 },
];

console.log(report(group));
console.log(report([{ name: "Фарход", visited: 9 }]));
console.log(report([{ name: "Анора", visited: 3 }]));
console.log(typeof MIN_VISITS_FOR_EXCELLENT);
console.log([report, getExcellentNames, calcAverageVisits].some(fn => fn.toString().includes("for (")));
Ожидаемый результат
РУСТАМ, НИЛУФАР | среднее: 7.25
ФАРХОД | среднее: 9
| среднее: 3
number
false
Подсказка 1

getExcellentNames: сначала students.filter(s => s.visited >= MIN_VISITS_FOR_EXCELLENT), затем к результату .map(s => s.name.toUpperCase()).

Подсказка 2

calcAverageVisits: students.reduce((sum, s) => sum + s.visited, 0) / students.length. Тогда report — одна строка, которая склеивает результаты двух функций.

06 Сборка проекта: npm run build

В уроке 17 вы создали проект на Vite и запускали npm run dev. Это режим разработки: Vite отдаёт файлы «как есть», мгновенно обновляет страницу и добавляет подсказки для разработчика. Для настоящего сайта такой режим не годится — код не сжат, лишний, а сервер работает только у вас на ноутбуке. Для публикации нужна сборка (build).

Аналогия из жизни

Режим разработки — это кухня во время готовки: кастрюли, ножи, очистки на столе. Сборка — готовый обед, красиво упакованный в контейнер навынос. Клиенту нужен контейнер, а не вся кухня. Папка dist — это и есть упакованный «контейнер» вашего сайта.

Что делает сборка

  1. Собирает все ваши модули (import/export) в несколько файлов.
  2. Минифицирует: убирает пробелы, комментарии, сокращает имена переменных. Файл становится в несколько раз меньше — страница грузится быстрее.
  3. Выкидывает неиспользуемое (tree shaking): если вы подключили библиотеку, но пользуетесь одной функцией, лишнее не попадёт на сайт.
  4. Добавляет хеш в имена файлов (index-Ab12Cd34.js): изменили код — имя поменялось, и браузер скачает свежую версию, а не возьмёт старую из кэша.
  5. Кладёт результат в папку dist/ — её и отдают на хостинг.
Терминал — собираем и смотрим результат
npm install        # если ещё не ставили зависимости
npm run build      # запускает «vite build», результат — в папке dist/
npm run preview    # локальный сервер для готовой сборки: http://localhost:4173

Что лежит в dist/ после сборки небольшого проекта:

Структура dist/
dist/
├── index.html                  ← точка входа, ссылки на файлы из assets/
├── favicon.svg                 ← всё из папки public/ копируется как есть
└── assets/
    ├── index-Ab12Cd34.js       ← весь ваш JavaScript, сжатый
    └── index-Ef56Gh78.css      ← весь CSS, сжатый
Три частые ошибки

1. «В dev работает, а в build — нет». Сборка строже: она не прощает битые импорты и опечатки в путях. Всегда запускайте npm run build до деплоя.
2. Регистр букв в именах файлов. На Windows и macOS import "./Header.js" найдёт файл header.js. Но хостинги работают на Linux, где Header.js и header.js — разные файлы: на вашем компьютере всё хорошо, а на Vercel сборка падает. Пишите имена файлов и импорты в точности одинаково.
3. Открыть dist/index.html двойным кликом. Пути в нём начинаются с /assets/… и из файловой системы не находятся — страница будет пустой. Для проверки есть npm run preview.

Windows и macOS

Команды npm одинаковы везде. На Windows запускайте их в PowerShell, «Терминале» VS Code или Git Bash; на macOS — в «Терминале» или во встроенном терминале VS Code (Ctrl + `). Проверьте, что Node установлен: node -v. Если npm run build пишет «vite: command not found» — вы не в папке проекта или не выполнили npm install.

Папку dist в Git не коммитим

Это «производный» файл: его всегда можно получить командой build. В .gitignore (урок 19) должны быть строки node_modules и dist — в шаблоне Vite они уже есть. Хостинг сам запустит сборку из вашего кода.

Читать подробнее: vite.dev — Сборка для продакшена

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

ЗАДАЧА 6.1

Что верно про сборку

лёгкая

Шесть утверждений. Впишите в массив correct буквы верных (в алфавитном порядке, в кавычках).

Можно редактировать · Ctrl + Enter
// A. Папку dist нужно коммитить в репозиторий.
// B. В dist лежат минифицированные файлы JS и CSS.
// C. npm run preview показывает локально собранную версию сайта.
// D. Хеш в имени файла (index-Ab12Cd34.js) нужен, чтобы браузер брал свежую версию после обновления.
// E. Если в режиме dev всё работает, то и сборка точно пройдёт.
// F. dist/index.html надёжно открывается двойным кликом из папки.

const correct = [/* "A", "B" ... */];
console.log(correct.join(", "));
Ожидаемый результат
B, C, D
Подсказка 1

Три утверждения неверны: dist — результат сборки, а не исходник; в dev и build код проверяется по-разному; пути в index.html абсолютные.

Подсказка 2

Неверные: A, E, F. Верные — остальные три.

ЗАДАЧА 6.2

Охотник за регистром

средняя

Напишите findCaseMismatches(files, imports) — она возвращает те импорты, которые на Windows/macOS сработают, а на Linux-хостинге сломают сборку: файл с таким путём есть, но только если не различать регистр букв. Импорт несуществующего файла в любом регистре в ответ не попадает. Порядок — как в imports.

Можно редактировать · Ctrl + Enter
const files = ["src/Header.js", "src/footer.js", "src/utils/format.js", "public/Logo.png"];
const imports = ["src/header.js", "src/footer.js", "src/utils/Format.js", "public/Logo.png", "src/missing.js"];

function findCaseMismatches(files, imports) {
  // ваш код
}

console.log(JSON.stringify(findCaseMismatches(files, imports)));
Ожидаемый результат
["src/header.js","src/utils/Format.js"]
Подсказка 1

Импорт «сломан», если !files.includes(path), но найдётся файл, у которого file.toLowerCase() === path.toLowerCase().

Подсказка 2

return imports.filter(path => ...), где условие — это !files.includes(path) и files.some(...) со сравнением в нижнем регистре.

07 Переменные окружения и секреты

Адрес API на вашем компьютере — один, в «боевой» версии сайта — другой. Размер страницы, название приложения, ключ к сервису погоды — всё это настройки, которые не нужно зашивать в код. Их выносят в переменные окружения — отдельный файл, который читается при запуске и сборке.

Аналогия из жизни

Рецепт плова один, а количество порций зависит от случая: на семью — килограмм риса, на свадьбу — десять. Вы не переписываете рецепт, а меняете «настройку» в записке на двери кухни. Переменные окружения — такая записка: код один, а значения для каждой среды (ваш ноутбук, сайт в интернете) свои.

Как это работает в Vite

Создайте в корне проекта (рядом с package.json) файл .env:

.env
VITE_API_URL=https://dummyjson.com
VITE_PAGE_SIZE=12
VITE_APP_NAME=Лавка MakeFirstStep
DB_PASSWORD=не-попадёт-в-браузер

В коде читаем через import.meta.env:

src/config.js
export const API_URL = import.meta.env.VITE_API_URL;
export const PAGE_SIZE = Number(import.meta.env.VITE_PAGE_SIZE);   // все значения — строки!

console.log(import.meta.env.DB_PASSWORD);   // undefined: без префикса VITE_ переменная скрыта
console.log(import.meta.env.MODE);          // "development" при dev, "production" при build
console.log(import.meta.env.DEV);           // true только в режиме разработки
  • Префикс VITE_ — единственные переменные, которые Vite отдаёт в браузерный код. Остальные остаются невидимыми.
  • Значения — всегда строки. VITE_PAGE_SIZE=12 даёт "12", а VITE_DEBUG=false — строку "false", которая в условии считается истиной! Преобразуйте сами: Number(...), value === "true".
  • После правки .env перезапустите npm run dev — переменные читаются при старте.
  • Встроенные: import.meta.env.MODE, DEV, PROD, BASE_URL.

Какие бывают файлы .env

ФайлКогда загружаетсяВ Git?
.envвсегдаможно, если там нет секретов
.env.localвсегда, ваши личные значениянет (*.local в .gitignore)
.env.developmentтолько npm run devможно
.env.productionтолько npm run buildможно
.env.production.localтолько build, личные значениянет

Файл «своего режима» перекрывает общий .env. А переменные, уже заданные в системе или на хостинге, перекрывают всё.

Главное правило: в браузере нет секретов

Значения VITE_* при сборке подставляются прямо в JavaScript-файл — любой пользователь откроет DevTools → Sources и прочитает их. Поэтому VITE_ — для публичных настроек (адрес API, название, размер страницы). Платёжные ключи, пароли от баз данных, токены с правом записи в такие переменные класть нельзя: для них нужен собственный сервер или серверные функции — это тема следующего курса.

Терминал — убеждаемся сами: значение лежит в сборке
# macOS / Linux
grep -r "dummyjson" dist/assets

# Windows (PowerShell)
Select-String -Path dist\assets\*.js -Pattern "dummyjson"

Что делать, чтобы не было утечки

  1. Личные файлы в .gitignore: строки .env.local и *.local. Проверьте командой git status — файла в списке быть не должно.
  2. Заведите .env.example — копию .env с пустыми значениями. Он идёт в Git и подсказывает напарнику, какие переменные нужны: VITE_API_URL=
  3. В README напишите: «скопируйте .env.example в .env и заполните».
  4. На хостинге значения вводят в панели (Vercel — Settings → Environment Variables, GitHub — Settings → Secrets and variables), а не в репозитории.
  5. Если ключ случайно попал в коммит — считайте его украденным: немедленно отзовите и выпустите новый в сервисе. Удалить коммит недостаточно: история Git и боты, сканирующие GitHub, уже успели его забрать.
.gitignore
node_modules
dist
.env.local
*.local
Windows и macOS

Скопировать файл: macOS — cp .env.example .env, Windows PowerShell — copy .env.example .env. Имя файла начинается с точки — в Проводнике Windows и Finder на macOS такие файлы могут быть скрыты (Finder: Cmd + Shift + .); в VS Code они видны всегда.

Читать подробнее: vite.dev — Переменные окружения и режимы

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

import.meta.env работает только внутри проекта на Vite, поэтому здесь мы тренируем логику на обычных объектах и строках.

ЗАДАЧА 7.1

Что увидит браузер

лёгкая

Напишите getPublicEnv(env): она возвращает новый объект только с теми переменными, которые Vite отдал бы в браузер (имена начинаются с VITE_).

Можно редактировать · Ctrl + Enter
const env = {
  VITE_API_URL: "https://api.example.com",
  VITE_APP_NAME: "Мой магазин",
  DB_PASSWORD: "x1",
  SECRET_KEY: "y2",
  MODE: "production",
};

function getPublicEnv(env) {
  // ваш код
}

console.log(JSON.stringify(getPublicEnv(env)));
Ожидаемый результат
{"VITE_API_URL":"https://api.example.com","VITE_APP_NAME":"Мой магазин"}
Подсказка 1

Object.entries(env) превращает объект в массив пар [ключ, значение]. Оставьте пары, у которых ключ key.startsWith("VITE_").

Подсказка 2

Object.fromEntries( собирает объект обратно из пар: Object.entries(env).filter(...) внутри, return перед всем выражением.

ЗАДАЧА 7.2

Читаем .env руками

средняя

Напишите parseEnv(text): разбирает текст файла .env в объект. Правила: пустые строки и строки, начинающиеся с #, пропускаются; формат КЛЮЧ=значение; пробелы вокруг ключа и значения убираются; значение в кавычках — без кавычек; знак = внутри значения допустим (делим только по первому). Все значения остаются строками.

Можно редактировать · Ctrl + Enter
const text = `
# адрес API
VITE_API_URL=https://dummyjson.com

VITE_APP_NAME="Лавка MakeFirstStep"
VITE_PAGE_SIZE = 12
VITE_FILTER=sort=asc
`;

function parseEnv(text) {
  // ваш код
}

console.log(JSON.stringify(parseEnv(text)));
Ожидаемый результат
{"VITE_API_URL":"https://dummyjson.com","VITE_APP_NAME":"Лавка MakeFirstStep","VITE_PAGE_SIZE":"12","VITE_FILTER":"sort=asc"}
Подсказка 1

Идите по text.split("\n"). Для каждой строки: line.trim(); если пусто или начинается с # — continue.

Подсказка 2

Позиция первого равно: const index = line.indexOf("="). Ключ — line.slice(0, index).trim(), значение — line.slice(index + 1).trim(). Кавычки: если значение начинается и заканчивается на ", отрежьте по символу с каждой стороны: value.slice(1, -1).

ЗАДАЧА 7.3

Ловушка со строкой "false"

средняя

Сначала предскажите, что выведет первая строка Boolean("false"). Затем напишите readFlag(value) — true только для строки "true", иначе false — и readNumber(value, fallback) — число из строки, а если значения нет (undefined), оно пустое или не число, то fallback.

Можно редактировать · Ctrl + Enter
console.log(Boolean("false"));

function readFlag(value) {
  // ваш код
}

function readNumber(value, fallback) {
  // ваш код
}

console.log(readFlag("true"), readFlag("false"), readFlag(undefined));
console.log(readNumber("12", 6), readNumber(undefined, 6), readNumber("abc", 6), readNumber("", 6));
Ожидаемый результат
true
true false false
12 6 6 6
Подсказка 1

Любая непустая строка — истина, даже "false". Поэтому флаг сравниваем со строкой: value === "true".

Подсказка 2

Для числа: если value === undefined || value === "" — вернуть fallback. Иначе const number = Number(value); и Number.isNaN(number) ? fallback : number.

ЗАДАЧА 7.4

Охотник за секретами

сложная

Перед коммитом полезно просканировать код. Напишите findSecrets(lines): возвращает номера строк (считая с 1), где имени, содержащему key, token, secret или password (в любом регистре), присваивается строковый литерал длиной от 8 символов (через = или :). Чтение из import.meta.env, пустые и короткие строки секретом не считаются.

Можно редактировать · Ctrl + Enter
const lines = [
  'const API_URL = import.meta.env.VITE_API_URL;',
  'const apiKey = "sk_live_51Hh83jKd92hs";',
  'fetch(`${API_URL}/products`);',
  "const password = 'qwerty12345';",
  'const token = import.meta.env.VITE_TOKEN;',
  'const secret = "";',
  'const authToken = "Bearer eyJhbGciOiJI";',
  'const title = "My secret garden";',
];

function findSecrets(lines) {
  // ваш код
}

console.log(findSecrets(lines).join(", "));
Ожидаемый результат
2, 4, 7
Подсказка 1

Здесь поможет регулярное выражение. Скелет: /(key|token|secret|password)\w*\s*[:=]\s*["'`][^"'`]{8,}["'`]/i — разберите по частям, что значит каждая из них.

Подсказка 2

Пройдите по строкам с индексом и соберите в массив index + 1 для тех, где pattern.test(line) истинно. Строка 8 не подходит: слово secret стоит внутри текста, а не в имени переменной.

08 Деплой: Vercel и GitHub Pages

Деплой — публикация сайта в интернете, чтобы его открывал любой человек по ссылке. Пока проект лежит у вас на ноутбуке, это упражнение. Когда у него есть ссылка — это портфолио. Мы разберём два бесплатных способа для статических сайтов (а ваши проекты на Vite — именно такие).

Аналогия из жизни

Вы испекли самбусу дома — вкусно, но попробуют только родные. Деплой — открыть лоток на базаре: тот же продукт, но теперь у него есть адрес, и к нему могут прийти все. Хостинг — это место на базаре, а ссылка — номер вашего лотка.

Перед деплоем: короткий чек-лист

  • npm run build проходит без ошибок, npm run preview показывает рабочий сайт.
  • Проект лежит в репозитории на GitHub (урок 19), ветка с рабочим кодом отправлена: git push.
  • В репозитории нет .env.local и секретов; есть .env.example.
  • Регистр имён файлов и импортов совпадает.

Способ 1. Vercel

Vercel сам находит Vite, сам собирает проект и после каждого git push обновляет сайт. Для новичка — самый короткий путь.

  1. Откройте vercel.com и войдите через кнопку Continue with GitHub (тариф Hobby бесплатный для личных проектов).
  2. Нажмите Add New… → Project. Vercel покажет ваши репозитории: найдите нужный и нажмите Import (при первом разе разрешите доступ к репозиториям).
  3. На экране настройки проверьте: Framework Preset — Vite (определяется сам), Build Command — npm run build, Output Directory — dist.
  4. Раскройте Environment Variables и добавьте переменные из вашего .env (например, VITE_API_URL) — по одной: имя и значение. Файл .env Vercel из репозитория не читает.
  5. Нажмите Deploy. Через минуту появится адрес вида название-проекта.vercel.app. Это ваша живая ссылка.
  6. Дальше: каждый git push в основную ветку обновляет сайт, а каждая другая ветка получает отдельный адрес-превью — удобно показывать правки до слияния.
Терминал — обновление сайта = обычный push
git add .
git commit -m "fix price format"
git push
# Vercel сам запустит npm run build и обновит сайт
Если сайт открылся, но данные не грузятся

Откройте DevTools → Network (урок 19). Запрос уходит на undefined/products? Значит, переменная не добавлена в Vercel или у неё нет префикса VITE_. Добавили переменную после деплоя — этого мало: переменные подставляются при сборке, поэтому откройте Deployments → … → Redeploy.

Способ 2. GitHub Pages

Бесплатный хостинг прямо в репозитории (для публичных репозиториев). Адрес — https://ваш-логин.github.io/имя-репозитория/. Обратите внимание на последнюю часть: сайт живёт не в корне, а в подпапке. Поэтому Vite нужно сказать, откуда брать файлы — настройка base.

vite.config.js
import { defineConfig } from "vite";

export default defineConfig({
  base: "/my-shop/",   // ← точное имя репозитория между слэшами
});

Без base страница загрузится пустой: в консоли будет 404 на /assets/index-….js, потому что браузер ищет файл в корне github.io, а не в вашем репозитории. В коде ссылки на файлы из public/ строим через import.meta.env.BASE_URL, а не пишем "/logo.png" руками.

  1. Задайте base в vite.config.js, выполните npm run build и npm run preview (адрес превью будет включать /my-shop/).
  2. Отправьте код на GitHub: git push.
  3. В репозитории: Settings → Pages → в поле Source выберите GitHub Actions.
  4. Создайте файл .github/workflows/deploy.yml (ниже), закоммитьте и отправьте.
  5. Вкладка Actions: дождитесь зелёной галочки. Адрес сайта появится в Settings → Pages.
  6. Нужны переменные? Settings → Secrets and variables → Actions → Variables, а в workflow передайте их шагу сборки (см. env ниже).
.github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci
      - run: npm run build
        env:
          VITE_API_URL: ${{ vars.VITE_API_URL }}
      - uses: actions/configure-pages@v6
      - uses: actions/upload-pages-artifact@v5
        with:
          path: ./dist
      - id: deployment
        uses: actions/deploy-pages@v5

Номера версий действий (@v7, @v5…) со временем меняются — перед копированием сверьтесь с актуальным примером на vite.dev/guide/static-deploy. Это нормально: инструкции по деплою всегда стоит открывать свежие, а не доверять памяти (и памяти ИИ).

Что выбрать

VercelGitHub Pages
Адреспроект.vercel.appлогин.github.io/репозиторий/
Настройкамышкой, за 3 минутыфайл workflow + base
Переменные окруженияв панели проектаVariables в настройках репозитория
Превью ветокда, автоматическинет
Нужен baseнетда, /имя-репозитория/
Приватный репозиторийможнобесплатно — только публичный

Для курса: первый проект — на Vercel, второй — на GitHub Pages. Так вы попробуете оба подхода, а в портфолио будет две живые ссылки.

Windows и macOS

Команды Git и npm одинаковы. Различие в терминале: на Windows используйте PowerShell или Git Bash, на macOS — «Терминал». Если Git ругается на «LF will be replaced by CRLF» на Windows — это предупреждение о переводах строк, сборке оно не мешает. И главное для обеих систем: проверяйте сайт после деплоя — откройте ссылку в режиме инкогнито и с телефона.

Читать подробнее: vite.dev — Деплой статического сайта, Vercel — Vite

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

ЗАДАЧА 8.1

Путь к файлам на GitHub Pages

средняя

Напишите две функции. getBase(repoName) возвращает значение base для Vite: "/" + имя + "/", а для «пользовательского» сайта (имя заканчивается на .github.io) — просто "/". assetUrl(base, path) склеивает базу и путь к файлу так, чтобы между ними был ровно один слэш (в path слэш в начале может быть, а может и не быть).

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

function assetUrl(base, path) {
  // ваш код
}

console.log(getBase("my-shop"));
console.log(getBase("komilovsg.github.io"));
console.log(assetUrl("/my-shop/", "img/logo.png"));
console.log(assetUrl("/my-shop/", "/img/logo.png"));
console.log(assetUrl("/", "/favicon.svg"));
Ожидаемый результат
/my-shop/
/
/my-shop/img/logo.png
/my-shop/img/logo.png
/favicon.svg
Подсказка 1

Для getBase: if (repoName.endsWith(".github.io")) return "/"; — ранний возврат, затем return `/${repoName}/`;.

Подсказка 2

Для assetUrl уберите слэш в начале пути: const cleanPath = path.startsWith("/") ? path.slice(1) : path; — и склейте base + cleanPath (база всегда заканчивается слэшем).

ЗАДАЧА 8.2

Диагноз: что сломалось после деплоя

лёгкая

Четыре ситуации после деплоя и четыре причины. Сопоставьте: в массиве answers для каждой ситуации по порядку впишите букву причины.

Можно редактировать · Ctrl + Enter
// Ситуации:
// 1. Локально картинка logo.png видна, на Vercel — нет. В коде написано "./Logo.png".
// 2. Сайт открылся, но запросы идут на адрес "undefined/products".
// 3. GitHub Pages: страница пустая, в консоли 404 на /assets/index-xxx.js.
// 4. Переменную добавили в панель Vercel, но сайт показывает старое значение.
//
// Причины:
// A. Не задан base в vite.config.js.
// B. Не совпадает регистр букв в имени файла.
// C. Переменная окружения не добавлена на хостинг.
// D. После добавления переменной не сделали Redeploy.

const answers = [/* "?", "?", "?", "?" */];
console.log(answers.join(", "));
Ожидаемый результат
B, C, A, D
Подсказка 1

Ситуация 3 — про GitHub Pages и «файл не найден в корне»: какая настройка отвечает за подпапку?

Подсказка 2

В ситуациях 2 и 4 речь про переменные, но в 2 значения нет совсем, а в 4 — значение есть, только старое (пересборки не было).

09 ИИ-блок: ревью кода и README

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

Правила курса, которые здесь особенно важны

1. Сначала 15 минут сами: пройдите по чек-листу и запишите свои замечания. 2. Только потом просите ревью — и сравните со своим списком. 3. Не принимайте правку, которую не можете объяснить своими словами. 4. После каждой правки запускайте код: поведение не должно измениться. 5. README и портфолио-текст — переписывайте своими словами: по шаблонным фразам ИИ-текст видно сразу.

Ревью по чек-листу: плохо и хорошо

Формула промпта курса: контекст → задача → ограничения → формат ответа → проверка.

улучши мой код
Я учу JavaScript: знаю функции, массивы, объекты, DOM и fetch. Ниже функция из моего мини-проекта «каталог товаров». Проверь её по чек-листу: 1) имена; 2) размер функции; 3) дублирование; 4) ранний возврат; 5) магические числа; 6) разделение логики и отображения; 7) комментарии. Формат ответа: нумерованный список замечаний «пункт чек-листа → где в коде → в чём проблема → почему это важно». Только замечания, без переписывания кода целиком. Не предлагай библиотеки и новые технологии, не меняй поведение функции. Если по пункту всё в порядке — так и напиши. Код: [вставьте функцию]

Почему второй промпт работает лучше: ИИ знает ваш уровень, получил конкретный список критериев, не может «улучшить» то, о чём не просили, и ответ вы можете сразу сверить со своим списком.

Фильтр советов: принять или отклонить

Каждый совет ИИ проходит через пять вопросов. Достаточно одного «нет» — и совет не принимаем (или откладываем):

ВопросЕсли «нет»
1. Я могу объяснить правку своими словами?Не принимаем — пока не разберёмся
2. Поведение кода осталось прежним?Это уже не рефакторинг: отдельная задача и отдельный коммит
3. Это решает реальную проблему (есть пункт чек-листа)?Отклоняем: «улучшение ради улучшения»
4. Метод или библиотека существует?Проверяем в MDN. ИИ иногда выдумывает несуществующие методы
5. Код стал проще, а не сложнее?Отклоняем: лишние зависимости, паттерны и «умные» однострочники
Обычно стоит принять
  • Вынести магическое число в константу
  • Переименовать x, data во что-то понятное
  • Удалить мёртвый код и отладочный console.log
  • Заменить вложенные if ранним return
  • Вынести повторяющийся кусок в функцию
Обычно стоит отклонить
  • «Подключите библиотеку / TypeScript / Redux» ради одной функции
  • Комментарий к каждой строке
  • Имена на 40 символов
  • «Оберните всё в try/catch»
  • Ключ или пароль «для удобства» прямо в коде
  • Метод, которого нет в MDN
Опасный совет: тихое изменение поведения

Самые коварные правки выглядят как «улучшение читаемости», но чуть-чуть меняют результат: >= превращается в >, пропадает проверка на пустой массив. Ловит их только запуск на граничных значениях — как в задаче 5.4.

Практика: ревью с ИИ

Ниже — настоящие по духу ответы ИИ-ревью. Ваша задача — быть ревьюером ревьюера.

ЗАДАЧА 9.1

Принять или отклонить

средняя

ИИ проверил функцию и выдал восемь советов. Для каждого впишите в массив verdicts: "A" (принять) или "R" (отклонить). Опирайтесь на пять вопросов выше.

Можно редактировать · Ctrl + Enter
/*
function calcTotal(items, promo) {
  let total = 0;
  for (const item of items) {
    total += item.price * item.qty;
  }
  if (promo === "NAVRUZ") {
    total = total * 0.85;
  }
  if (total > 300) {
    total = total + 0;
  } else {
    total = total + 25;
  }
  return total;
}
*/

// Советы ИИ:
// 1. Числа 0.85, 300 и 25 вынесите в именованные константы.
// 2. Замените цикл for...of на reduce — reduce всегда лучше циклов.
// 3. Строка «total = total + 0» ничего не делает — удалите её.
// 4. Добавьте комментарий над каждой строкой функции.
// 5. Переименуйте total в finalTotalAmountWithPromoAndDeliveryCalculated.
// 6. Расчёт по промокоду вынесите в отдельную функцию applyPromo.
// 7. Оберните всё тело функции в try/catch на случай ошибки.
// 8. Подключите lodash, чтобы суммировать цены.

const verdicts = [/* "A" или "R" для советов 1–8 */];
console.log(verdicts.join(","));
Ожидаемый результат
A,R,A,R,R,A,R,R
Подсказка 1

Принимаем то, что делает код понятнее без изменения поведения: константы (1), мёртвая строка (3), отдельная функция (6).

Подсказка 2

Разбор отказов. 2 — «всегда лучше» неверно: цикл здесь читается не хуже. 4 — шум вместо пользы. 5 — имя в 50 символов хуже, чем total. 7 — try/catch без причины прячет ошибки. 8 — зависимость ради одной суммы.

ЗАДАЧА 9.2

Принять, отклонить или проверить

сложная

Второе ревью, потоньше. Теперь три вердикта: "A" — принять сразу (понятнее, поведение то же); "T" — полезно, но меняет поведение: отдельной задачей с тестами, не в рефакторинге; "R" — отклонить (не нужно, вредно или выдумано).

Можно редактировать · Ctrl + Enter
/*
function getCheapProducts(products) {
  const result = [];
  for (let i = 0; i < products.length; i++) {
    if (products[i].price < 20) {
      result.push(products[i]);
    }
  }
  return result;
}
*/

// Советы ИИ:
// 1. Замените цикл на products.filter(...), а число 20 вынесите в константу MAX_PRICE.
// 2. Используйте встроенный метод products.filterByPrice(20) — он делает то же самое быстрее.
// 3. Замените «<» на «<=»: «до 20 сомони» логичнее включать границу.
// 4. Загружайте цены с сервера, вписав ключ прямо в функцию: const KEY = "sk_live_...".
// 5. Переименуйте result в cheapProducts.
// 6. Перед возвратом отсортируйте товары по цене по возрастанию.
// 7. Добавьте TypeScript и Redux — так функция станет надёжнее.

const verdicts = [/* "A", "T" или "R" для советов 1–7 */];
console.log(verdicts.join(","));
Ожидаемый результат
A,R,T,R,A,T,R
Подсказка 1

Проверьте метод из совета 2 в MDN: у массивов нет filterByPrice. Это выдумка (R). Ключ в коде (4) — утечка секрета (R).

Подсказка 2

Советы 3 и 6 — не про читаемость: граница 20 попадёт в результат, а порядок товаров изменится. Может, так и нужно — но это решение владельца продукта, отдельный коммит. Поэтому «T».

ЗАДАЧА 9.3

Проверяем рефакторинг ИИ

средняя

ИИ «упростил» функцию доставки в одну строку. Он клянётся, что поведение то же. Проверьте: пройдите по списку входов, вызовите обе версии и выведите только те входы, где ответы различаются, в формате сумма член: было → стало (например, 100 false: 25 → 0).

Можно редактировать · Ctrl + Enter
function getShipping(sum, isMember) {
  if (isMember) return 0;
  if (sum >= 300) return 0;
  return 25;
}

function getShippingAI(sum, isMember) {
  return isMember || sum > 300 ? 0 : 25;
}

const inputs = [[100, false], [300, false], [300, true], [500, false], [0, true], [299.99, false]];

// ваш код: цикл по inputs и сравнение
Ожидаемый результат
300 false: 0 → 25
Подсказка 1

for (const [sum, isMember] of inputs) — деструктуризация пары. Внутри сравните getShipping(sum, isMember) и getShippingAI(sum, isMember).

Подсказка 2

Если не равны — console.log(`${sum} ${isMember}: ${before} → ${after}`). Ошибка ИИ: исходный код считал бесплатной доставку от 300 включительно, а «упрощённый» — только больше 300.

README и текст для портфолио с ИИ

README — первое, что видит рекрутёр на странице репозитория. Хорошо, когда он отвечает на четыре вопроса: что это, на чём сделано, как запустить, где посмотреть. ИИ быстро соберёт каркас, но есть правило: ИИ получает только факты, которые вы ему дали. Иначе он дорисует функции, которых нет.

напиши readme для моего сайта
Помоги написать README для учебного проекта. Факты (используй только их, ничего не выдумывай): название — «Каталог товаров»; что делает — показывает товары с dummyjson.com, есть поиск по названию и пагинация; стек — HTML, CSS, JavaScript, Vite; запуск — npm install, затем npm run dev; демо — [ссылка]. Структура: что это (2 предложения) → стек → как запустить (команды в блоке кода) → скриншот (оставь место для картинки ./screenshot.png) → что дальше (3 идеи). Тон простой, без громких слов вроде «революционный». Если фактов не хватает — задай вопросы, а не придумывай. Проверка: все команды в тексте должны быть реальными для проекта на Vite.
Напиши 4 предложения о моём проекте для портфолио начинающего frontend-разработчика: какую задачу решает; что сделал я лично (поиск, пагинация, состояния загрузки и ошибки); стек; ссылка на демо [ссылка]. Первое лицо, без воды и клише вроде «страстный разработчик». Покажи два варианта разной длины.

Что проверять в тексте ИИ

  • Выдуманные функции и технологии. «Авторизация», «база данных», «покрытие тестами 95%» — если этого нет в проекте, вычёркиваем.
  • Команды. Выполните каждую из README на чистой папке: npm install, npm run dev. Работает — оставляем.
  • Скриншот и ссылка на демо — ставите вы сами, ИИ их не знает.
  • Шаблонные обороты («данный проект представляет собой…», «современное решение») — перепишите своими словами. Рекрутёр слышит голос человека, а не нейросети.
  • Честность. Что сделали сами, а что с помощью ИИ — можно сказать прямо. Никто не ждёт, что вы писали без помощников; ждут, что вы понимаете код.

Практика: README без выдумок

ЗАДАЧА 9.4

Найди выдумки в README

лёгкая

Факты о проекте: HTML, CSS, JavaScript, Vite; данные берутся с dummyjson.com; есть поиск и пагинация; вёрстка адаптивная; нет входа в аккаунт, базы данных и тестов. ИИ написал семь утверждений для README. Впишите в массив invented номера выдуманных (по возрастанию).

Можно редактировать · Ctrl + Enter
// Черновик README от ИИ:
// 1. Поиск по названию товара и постраничный вывод.
// 2. Вход через аккаунт Google.
// 3. Запуск: npm install, затем npm run dev.
// 4. Данные хранятся в базе PostgreSQL.
// 5. Адаптивная вёрстка: сайт удобно открывать на телефоне.
// 6. Покрытие автотестами — 95%.
// 7. Данные о товарах берутся с dummyjson.com.

const invented = [/* номера */];
console.log(invented.join(", "));
Ожидаемый результат
2, 4, 6
Подсказка 1

Сверяйте каждое утверждение со списком фактов: входа, базы данных и тестов в проекте нет.

Подсказка 2

Выдумки — вход через Google, PostgreSQL и 95% покрытия. Остальные четыре пункта правдивы.

10 Практика на уроке: ревью соседа и деплой группы

Вторая половина урока — командная: вы проверяете и улучшаете проект соседа, а потом вся группа выкладывает свои проекты в интернет. Это ровно то, что происходит в реальной команде: код-ревью → правки → релиз.

ВремяЧто делаем
10 минРазбор типичных проблем на проекторе, повтор чек-листа
10 минЧасть A: ревью проекта соседа по чек-листу (сначала сами, без ИИ)
25 минЧасть B: рефакторинг по замечаниям — на отдельной ветке
30 минЧасть C: деплой всех проектов группы
10 минЧасть D: README с ИИ и общий список ссылок
5 минИтоги: сколько баллов набрал проект до и после

Часть A. Ревью проекта соседа (в парах)

  1. Обменяйтесь ссылками на репозитории. Клонируйте проект соседа: git clone ссылка, затем npm install и npm run dev. Убедитесь, что он запускается.
  2. Откройте печатный чек-лист (раздел 11). Пройдите по 12 пунктам и поставьте отметки в колонке «До».
  3. Запишите 5–8 замечаний в одном формате: файл:строка — пункт № — что не так и почему — как исправить. Пример: src/cart.js:14 — пункт 5 — число 300 без имени, непонятно, что это порог доставки — вынести в FREE_DELIVERY_FROM.
  4. Замечания должны быть вежливыми и конкретными: «функция calc называется непонятно» — плохо; «в calc переименовать t в total, чтобы было видно, что суммируем» — хорошо. Критикуем код, а не человека.
  5. Обсудите замечания голосом: автор вправе не согласиться, если объяснит, почему.

Часть B. Рефакторинг по замечаниям

  1. Автор создаёт ветку: git switch -c refactor/clean-code.
  2. Идёт по замечаниям по одному. После каждого: запуск → всё работает так же → git commit -m "extract delivery constants" (сообщение — по правилам урока 19).
  3. Проверка поведения: открыть приложение и пройти те же сценарии, что до правок (поиск, пагинация, ошибка сети). Что-то поменялось — откат: git restore ..
  4. В конце: npm run build без ошибок. Затем git push ветки.
  5. Повторно пройдите чек-лист, заполните колонку «После». Цель — не меньше 10 из 12.

Часть C. Деплой всех проектов группы

  1. Проект 1 → Vercel по шагам из раздела 08. Время цели — 10 минут. Не получилось? Читайте лог сборки: ошибка почти всегда указана в последних строках.
  2. Проект 2 → GitHub Pages: base, workflow, Settings → Pages. Те, кто закончил раньше, помогают соседям по очереди — объяснять, а не нажимать за другого.
  3. Проверка ссылки: откройте в режиме инкогнито и на телефоне, в DevTools → Console не должно быть красных ошибок, в Network — ни одного 404.
  4. Все ссылки вписываются в общую таблицу группы (формат: имя, проект, ссылка на сайт, ссылка на репозиторий).

Часть D. README с ИИ

  1. Подготовьте факты о проекте: что делает, стек, команды запуска, ссылка на демо.
  2. Попросите ИИ собрать README по промпту из раздела 09.
  3. Проверьте по списку «Что проверять»: вычеркните выдумки, запустите команды, добавьте скриншот и ссылку, перепишите шаблонные фразы.
  4. Закоммитьте README.md в корень и отправьте.
Готово, когда

У проекта есть: чек-лист ≥ 10/12, живая ссылка, README со скриншотом, .env.example и нет секретов в истории. Покажите результат соседу и преподавателю.

11 Чек-лист качества кода (печатная версия)

Один чек-лист на всю группу: по нему вы проверяете свои проекты, проекты соседей и (позже) финальный проект. Распечатайте страницу (Ctrl + P на Windows, Cmd + P на macOS) — панель и подсказки при печати скроются, а таблица останется. Либо сохраните её в PDF.

Проект: ____________________

Автор: ____________________

Ревьюер: ____________________

Дата: ______________

№ПунктКак проверитьДоПосле
Код
1Имена говорят, что этоНет a, x1, data, temp; булевы — is/has/can; функции — глаголы☐☐
2Маленькие функцииКаждая делает одну вещь, в имени нет «и», помещается на экран☐☐
3DRYНет скопированных блоков; повторяющееся вынесено в функцию или данные☐☐
4Ранний возвратВложенность не глубже 2–3 уровней, нет лишних else после return☐☐
5КонстантыНет магических чисел и строк — всё в UPPER_SNAKE_CASE вверху файла☐☐
6Логика отдельно от отображенияФункции-расчёты не трогают DOM и не печатают в консоль☐☐
7КомментарииОбъясняют «почему»; нет закомментированного кода и комментариев-повторов☐☐
8ЧистотаНет отладочных console.log, неиспользуемых переменных и функций, var☐☐
Проект
9СекретыКлючей нет в коде и истории; .env.local в .gitignore; есть .env.example☐☐
10Сборкаnpm run build проходит без ошибок и предупреждений☐☐
11READMEЧто это, стек, как запустить, скриншот, ссылка на демо☐☐
12Живая ссылкаОткрывается в инкогнито и на телефоне; в Console нет ошибок, в Network нет 404☐☐

Итого: до рефакторинга ____ / 12, после ____ / 12.

Три главных замечания ревьюера:

1. ______________________________

2. ______________________________

3. ______________________________

Как оценивать

Пункт засчитывается, только если выполнен во всём проекте, а не в одном файле. 10–12 баллов — проект готов показывать; 7–9 — нужна доработка; ниже 7 — начните с пунктов 1, 5 и 8: они проще всего и сильнее всего улучшают впечатление.

12 Домашнее задание

Задание: отрефакторить мини-проект 2, задеплоить два проекта, оформить README и прислать ссылки. Результат урока — заполненный чек-лист качества кода и живые ссылки на проекты.

  1. Оцените до. Пройдите мини-проект 2 по чек-листу из раздела 11 до любых правок. Запишите результат (X из 12) и список нарушений.
  2. Отрефакторьте. Создайте ветку refactor/clean-code. Не меньше пяти осмысленных коммитов — каждый про одну правку («rename cart variables», «extract delivery constants», «split render from calc»). Поведение приложения не меняется.
  3. Оцените после. Повторите чек-лист. Цель — не меньше 10 из 12. Слейте ветку в основную обычным способом (через PR, как в уроке 19).
  4. Задеплойте два проекта. Мини-проект 2 — на Vercel; ещё один проект (мини-проект 1 или любой другой) — на GitHub Pages. Обе ссылки должны открываться без ошибок в консоли.
  5. Секреты и окружение. В обоих репозиториях есть .env.example, нет настоящих ключей, переменные (если есть) заданы на хостинге.
  6. README. В каждом репозитории: что это, стек, как запустить, скриншот, ссылка на демо, «что дальше». Черновик можно сделать с ИИ, но переписать своими словами и проверить все команды.
  7. Текст для портфолио. 3–4 предложения о мини-проекте 2: задача, что сделали вы лично, стек, ссылка.

Что сдавать

  • Ссылки на два репозитория и две живые ссылки на сайты.
  • Заполненный чек-лист (колонки «До» и «После») со счётом.
  • Журнал рефакторинга: таблица минимум из пяти строк — «пункт чек-листа → что изменили → название коммита».
  • Текст для портфолио (в README или в отдельном файле).

Как оцениваем (0–10)

ЧтоБаллы
Рефакторинг: чек-лист после ≥ 10 из 12, поведение не изменилось3
Деплой: обе ссылки открываются, нет ошибок в консоли и 4043
README: все разделы, скриншот, рабочие команды, нет выдуманных функций2
Секреты и .env.example; сборка проходит1
Журнал рефакторинга и честная оценка «до» (≥ 5 осмысленных коммитов)1

✓ Шпаргалка урока

  • Имена: переменная — существительное, функция — глагол, булево — is/has/can, константа — UPPER_SNAKE. Без data, temp, x1.
  • Функция делает одну вещь. «И» в описании — повод разделить. Ориентир — один экран.
  • Ранний возврат: сначала отсекаем плохие случаи if (...) return ...;, основная логика остаётся плоской, else после return не нужен.
  • DRY: повторилось в третий раз — вынесите в функцию или данные. Не объединяйте то, что похоже случайно.
  • Магические числа → именованные константы вверху файла, единицы измерения в имени.
  • Логика ≠ отображение: расчёт возвращает данные, render показывает, обработчик склеивает.
  • Комментарий отвечает «почему», а не «что». Закомментированный код — удалять (Git помнит).
  • Рефакторинг меняет форму, не поведение: тест → маленький шаг → запуск → коммит. Либо рефакторим, либо чиним баг — не одновременно.
  • Сборка: npm run build → dist/, проверка — npm run preview. dist в Git не кладём. Регистр имён файлов должен совпадать.
  • Переменные окружения: import.meta.env.VITE_*, значения — строки, после правки .env перезапуск. .env.local — в .gitignore, .env.example — в репозиторий. В браузере нет секретов.
  • Vercel: Import репозитория → preset Vite → переменные → Deploy; после смены переменных — Redeploy. GitHub Pages: base: "/репозиторий/" + Settings → Pages → GitHub Actions + workflow.
  • ИИ-ревью: просим замечания по чек-листу, а не «улучши». Совет принимаем, если понимаем его, поведение то же, проблема реальна, метод существует и код проще. README — только по фактам, переписать своими словами.