Недели 11-14 · Async Race · ФИНАЛ
🧭 ← HTML Builder · ← Roadmap · 🏁 ФИНАЛ bootcamp — следующего таска нет
⚠️ Это финал bootcamp. Здесь сходится всё что ты делал 10 недель: вёрстка из Slider, Figma из Shelter, JS-логика из Shelter P3, типизация и архитектура из последующих тасок. Без этого compound — задача неподъёмная. С compound — закроешь за 4 недели на 600/600.
🎯 Что строим
Single Page Application для дрэг-рейсинга радиоуправляемых тачек. Два View — Garage (CRUD тачек + анимация гонки) и Winners (таблица победителей с сортировкой). Backend — готовый mock-сервер (клонируется отдельно), фронт — твой, без единого фреймворка.
Идея: клиент хочет устроить соревнование между своими радиоуправляемыми машинами. Каждый «контроллер» — это HTTP API: start engine, stop engine, drive. Твоя задача — обернуть это в UI: создавать машины, выбирать им цвет, генерить пачками по 100 штук, запускать гонку, показывать победителя, вести таблицу winners с пагинацией и сортировкой.
Стек жёстко зафиксирован:
- TypeScript (без
any, безas, без!— -100 при нарушении) - Vanilla SPA — только нативный DOM API (Bootstrap CSS опционально, любые JS-фреймворки =
-600) - Vite / Webpack — обязателен бандлер
- ESLint unicorn config — обязателен
- Функции ≤ 40 строк — обязательно
📄 Полное задание на GitHub → · 🎥 Demo video → · 🔧 Server mock →
🏷 Required Skills (как заявлено в задании)
TypeScript · Single Page Application · vanilla DOM rendering · Fetch API · async/await · REST · CSS animations · requestAnimationFrame · pagination · sorting · state management · Vite/Webpack
🚫 Запреты (нарушишь — баллы улетают)
| ❌ | Что нельзя | Что вместо этого | Штраф |
|---|---|---|---|
| 🔴 | React / Vue / Angular / jQuery / Lodash / Material UI / любая UI-либа | vanilla DOM API + свои модули | -600 (всё обнуляется) |
| 🔴 | any (явный или неявный) |
точные типы, unknown + narrowing |
-100 |
| 🔴 | Type assertions (foo as Bar) |
type guards, valid types | -100 |
| 🔴 | Non-null assertions (y!) |
проверка на null/undefined |
-100 |
| 🔴 | Функции > 40 строк | декомпозиция | -40 |
| 🔴 | Не проходит eslint-unicorn | пройти линтер | -40 |
| 🟡 | Magic numbers / strings | вынеси в const |
по линтеру |
| 🟡 | Смешать API, UI и state в одном файле | модульная архитектура | по ревью |
| 🟢 | Bootstrap CSS — разрешён для стилизации | — | — |
| 🟢 | CSS Modules / Sass / Less / PostCSS / Tailwind / clsx — разрешены | — | — |
💡 «Flaky requests» — когда юзер спамит Start/Stop кнопки, mock-сервер иногда отвечает
404/429. По заданию это не баг. Не тратьте время на обработку.
💡 Bug penalties: major bug (фича работает но ломается после манипуляций + ошибки в консоли) = -30. Minor bug (ломается без ошибок в консоли) = -10. Кросс-чек смотрит ВСЁ.
⛰ Compound из всего bootcamp
Это последний таск — он не учит новому в одиночку, он синтезирует. Каждая прошлая неделя дала кусок, который тут собирается:
| Откуда | Что переиспользуешь |
|---|---|
| Slider | transform: translateX, transition, относительные единицы, mobile-адаптив 500px |
| Shelter P1 | Семантика, BEM, Figma-разбор макета (если будешь делать кастомный UI) |
| Shelter P2 | Mobile-first, breakpoints, responsive layout для гонки на 500px |
| Shelter P3 | DOM API, события, рендеринг карточек, pagination логика |
| Codejam / последующие | Командная работа, code review (понадобится для cross-check) |
| HTML Builder (прошлая неделя) | Декларативный API для построения DOM-деревьев, factory-функции, type-safe тегирование |
Без compound тут невозможно. Если на любой из прошлых задач ты остановился — вернись и закрой. Async Race раскроется только если в голове сложилась полная картина «как vanilla-фронт превращается в работающее SPA».
📚 Что изучить (по порядку)
⚠️ Финал — иди жёстко по порядку. Это не Slider где можно угадать. Здесь 12 блоков, каждый закрывает один из 600 баллов задачи. Пропустишь типизацию — словишь -100. Пропустишь модульную архитектуру — словишь штрафы за длинные функции и magic numbers.
📥 Что должен знать ДО старта (compound)
Если хоть один блок ниже не пройден — назад к Slider/Shelter:
- Что такое HTML · Семантическая разметка · Блочная модель
- Что такое CSS · _MOC Flexbox · _MOC Grid
- Введение в JavaScript · Переменные · Function Declaration
- _MOC DOM · Поиск элементов · createElement и createTextNode
- События · Делегирование событий
- Объекты · Методы массивов -- map, filter, reduce · Деструктуризация
Не открыл хоть одно — закрой. Иначе застрянешь.
1 · Compound: HTML Builder → Async Race
Зачем: в HTML Builder ты сделал декларативный API для построения DOM. В Async Race он становится твоим «маленьким React» — ты будешь рендерить карточку машины через builder, а не через копипасту document.createElement.
Перечитай свой HTML Builder. Спроси себя:
- Как мой builder типизирует тег и атрибуты?
- Что он возвращает —
HTMLElementили generic? - Могу ли я переиспользовать его 1:1 в Async Race, или нужно расширить?
Если HTML Builder был на JS — тут переписываешь на TS. Generics и type guards пригодятся.
Self-check: объясни на пальцах разницу между «императивный код создаёт DOM» и «декларативный API описывает DOM». Почему второе ремонтопригоднее?
2 · TypeScript обязателен ⭐⭐⭐
Зачем: ВСЁ задание держится на типах. any / as / ! = -100. Это не страховка, это требование. Учись писать так, чтобы TS-компилятор не давал ошибок без читерства.
База:
- Что такое TypeScript — зачем вообще
- TS базовые типы —
string,number,boolean,null,undefined, литеральные - interface vs type -- когда что — для
Car,Winner,RaceResult - TS Narrowing и Type Guards — вместо
asпишешьif (typeof x === 'string') - Generics — для API-слоя и builder'а
Практика:
- Типизация API-ответов —
fetch.then(r => r.json() as Car)← так НЕЛЬЗЯ. Учись правильно.
Как обойти запрет на as:
response.jsonвозвращаетPromise<unknown>→ пиши type guard:function isCar(x: unknown): x is Car { ... }- Атрибуты DOM возвращают
string | null→ проверяйif (value === null)явно document.querySelectorвозвращаетElement | null→ не пиши!, пишиif (el === null) return
Self-check: напиши type guard для Car { name: string; color: string; id: number }. Объясни в чём разница interface и type — когда какой выбираешь? Что вернёт JSON.parse('{}') в TS strict mode и как с этим жить без as?
3 · SPA-архитектура без фреймворков ⭐
Зачем: ты собираешь приложение которое выглядит и ведёт себя как React-app, но без React. Это значит — сам решаешь как хранить state, как ре-рендерить, как роутить.
- SPA -- что такое и чем отличается от MPA — концепция, история, чем отличается от MPA
- Декларативный vs Императивный UI — главное архитектурное решение
- _MOC SPA — карта темы, посмотри что есть
Решения которые надо принять до того как кодить:
- State — один глобальный объект?
class State? Pub/sub? — выбери одно и держись. - Render — полная перерисовка View или точечные мутации DOM? (Точечные эффективнее но сложнее).
- Routing —
History APIили hash-роутинг? (/garagevs#garage). - Communication — как кнопка «start race» сообщает таблице winners о новом победителе? (Event bus, callback, прямая ссылка).
Self-check: опиши за 3 минуты архитектуру своего приложения: где state, где API, где UI, как они общаются. Если не можешь — ты ещё не готов писать код.
4 · REST + CRUD + Fetch с типами ⭐
Зачем: mock-сервер — REST. GET /garage, POST /garage, PATCH /garage/:id, DELETE /garage/:id, и три endpoint'а для engine: PATCH /engine?id=X&status=started/stopped/drive. Без чёткого API-слоя у тебя будет хаос из fetch по всему коду.
- REST API — модель, ресурсы, методы
- CRUD и HTTP-методы — какой метод что делает
- Fetch API — основы,
Request,Response, headers, body - Promise — основа
fetch, чтобы понимать что под капотом - async-await — синтаксис, который требует задание
- Типизация API-ответов — generics в API-слое
Архитектурное правило: в коде должен быть один модуль api/ (или services/api.ts) который единственный делает fetch. Все View вызывают функции из него: getCars, createCar(car), startEngine(id). Не разбрасывай fetch по компонентам.
Self-check: напиши типизированную функцию getCars(page: number, limit: number): Promise<{ cars: Car; total: number }>. Где она достаёт total? (Подсказка: header X-Total-Count). Что вернёт твоя функция если сеть упадёт?
5 · Анимация: requestAnimationFrame + CSS transforms ⭐⭐
Зачем: машина должна доехать от старта до финиша. Анимация ДОЛЖНА быть плавной на 500px и держать кадр при 7 одновременных машинах. CSS-only (transition) не подойдёт — тебе нужно прерывать анимацию в произвольной точке (на 500 от drive), а это умеет только rAF.
- requestAnimationFrame — основа, цикл, отмена через
cancelAnimationFrame - transform -- rotate, scale, translate — двигаем через
translateX(px), НЕ черезleft: - transition -- плавные переходы — для кнопок и не-критичных анимаций
Архитектура engine flow:
- Юзер нажимает Start → UI отправляет
PATCH /engine?status=started→ получает{ velocity, distance }. - Считаешь время:
time = distance / velocity(миллисекунды). - Запускаешь rAF-цикл: каждый кадр сдвигаешь
transform: translateX(progress * trackWidth). - Параллельно отправляешь
PATCH /engine?status=drive. Если он вернёт 500 — твой rAF-цикл должен остановиться в текущей позиции. - Если выиграл — отправляешь сообщение в state.
Подводный камень: trackWidth — это ширина дорожки, которая зависит от viewport. На resize пересчитывай.
Self-check: напиши псевдокод rAF-анимации. Где сохранишь requestId чтобы можно было отменить? Что произойдёт если юзер кликнет Stop в середине анимации?
6 · Routing — History API ⭐
Зачем: Garage и Winners должны быть на разных URL (/garage, /winners или #garage/#winners). При обновлении страницы юзер должен попасть на тот же View. State (страница, фильтры) сохраняется при навигации.
- History API —
pushState,popstate, как роутинг строится поверх него - SPA -- что такое и чем отличается от MPA — почему мы не делаем full-page reload
Что должно быть у тебя:
- Функция
navigate(path)— единственная, кто вызываетhistory.pushState. - Слушатель
popstate— реагирует на back/forward кнопки браузера. - Маппинг
path → View— простая table-driven логика.
Self-check: что произойдёт если юзер на /winners?page=3 нажмёт F5? Куда он попадёт? Чем history.pushState отличается от location.href = ...?
7 · State management в vanilla TS
Зачем: «при переключении между Garage и Winners состояние сохраняется — номер страницы, инпуты, выбранный цвет». Это твой главный архитектурный челлендж. Без state-слоя у тебя будет 100500 локальных переменных и баги.
Концептуально:
- Один источник истины —
class AppStateилиconst state: State. - View не хранят свой state — они читают из глобального state и подписываются на изменения.
- Изменения state идут через методы/функции (
state.setGaragePage(2)), не через прямую мутацию полей.
Полезные паттерны (без библиотек):
- Pub/sub через
EventTarget(нативный, типизируется). - Observable через
Proxy(продвинутый вариант). - Просто
class Stateс методами + событиеstate-changedна котором View подписаны.
Связи с твоей базой:
- Декларативный vs Императивный UI — state-driven рендер это декларативный подход
- События — pub/sub строится поверх
EventTarget - Объекты · Классы
Self-check: опиши на одной странице свой state: какие поля? кто меняет? кто читает? Если переключусь на Winners и вернусь на Garage — какие поля сохранятся, какие сбросятся?
8 · Pagination + Sorting
Зачем: Garage = 7 машин на страницу. Winners = 10 на страницу + сортировка по wins / best time, asc / desc. Это не «фича сверху», это 30 + 30 + 25 баллов.
Сервер уже умеет:
- Pagination:
GET /garage?_page=1&_limit=7(json-server convention) - Sorting:
GET /winners?_sort=wins&_order=DESC - Total: header
X-Total-Count
Что делаешь ты:
- UI-контролы (Prev/Next/номер страницы) → меняют state → перезапрос данных.
- Сортировочные клики по колонкам → меняют state → перезапрос с другими query-параметрами.
- Disabled-состояния кнопок (Prev на первой странице, Next на последней).
Связи:
- REST API · Fetch API — параметры query
- Методы массивов -- map, filter, reduce · Методы массивов -- map, filter, reduce — если что-то досортируешь на клиенте (но лучше на сервере)
Self-check: общее число машин = 23, страница 4, лимит 7. Сколько машин на странице? Что вернёт сервер? Какие кнопки активны?
9 · Сборка: Vite
Зачем: обязателен бандлер. Vite быстрее и проще Webpack — бери его, если не любишь страдать.
Минимум:
npm create vite@latest async-race -- --template vanilla-ts- Настроить
baseпод gh-pages (base: '/async-race/'вvite.config.ts) npm run build+gh-pages -d distдля деплоя
Связи:
- gh-pages деплой — публикация
- Repo Workflow для bootcamp — общий workflow для всех задач
Self-check: что произойдёт с относительными путями в index.html если ты не настроишь base? Что Vite делает с TS-файлами на dev и на build?
10 · ESLint unicorn + код-стандарты
Зачем: требование задания. Если линтер не проходит — -40. Unicorn — очень строгий, многое будет ругаться на привычные паттерны.
Что особенно бесит unicorn:
for (let i = 0; i < arr.length; i++)→ используйfor...ofили методы массиваarr[0]→arr.at(0)(или checkif (arr.length > 0))- Сокращения в именах (
btn,el,idx) → пиши полное (button,element,index) nullvsundefined— будет требовать единообразия- Magic numbers — выноси в
const FINISH_TIMEOUT_MS = 500
Связи:
- ESLint (если файл есть в твоей базе)
Self-check: что напишет unicorn если ты вызовешь array.forEach? Что предложит вместо arr[arr.length - 1]?
11 · Архитектура: модули API / UI / State ⭐⭐
Зачем: требование «модульная архитектура — чёткое разделение API, UI, state». Это самое размытое требование, но именно по нему ревьюер ставит или не ставит крупные баллы.
Минимальная структура проекта:
src/
├── api/
│ ├── garage.ts // getCars, createCar, updateCar, deleteCar
│ ├── engine.ts // startEngine, stopEngine, drive
│ └── winners.ts // getWinners, createWinner, updateWinner
├── state/
│ ├── app-state.ts // глобальный state + pub/sub
│ └── types.ts // Car, Winner, RaceResult interfaces
├── views/
│ ├── garage/
│ │ ├── garage-view.ts
│ │ ├── car-card.ts
│ │ └── pagination.ts
│ └── winners/
│ └── winners-view.ts
├── ui/
│ ├── html-builder.ts // твой builder из прошлой задачи
│ ├── button.ts
│ └── input.ts
├── animation/
│ └── race-animation.ts // rAF-логика
├── router/
│ └── router.ts // History API обёртка
├── constants.ts // магические числа в одном месте
└── main.ts // entry point
Главное правило: API не знает про UI, State не знает про DOM, UI читает из State и вызывает API. Зависимости только сверху вниз.
Self-check: если завтра решишь заменить REST на WebSocket — сколько файлов изменишь? Если только api/ — ты молодец. Если 20 файлов — у тебя нет архитектуры.
12 · Workflow (последний раз)
- Repo Workflow для bootcamp — отдельный публичный репо
async-race - Git Commit Convention — conventional commits + timestamps
- gh-pages деплой — деплой обязателен (или Netlify, см. Netlify Drop)
- PR Description Schema — PR-описание по схеме
- Cross-Check процесс — три дня после дедлайна оцениваешь чужих
Подводный камень: mock-сервер юзер кросс-чека запустит у себя локально. Если твой фронт жёстко вшит на localhost:3000 — он не сможет его прогнать на проде. Сделай переменную окружения / config-файл с URL сервера.
✅ Чек-лист критериев (600 баллов)
Basic structure · 240
View configuration · 80
- Два View: Garage и Winners (
+25) - Garage показывает имя, номер страницы, общее число тачек (
+15) - Winners показывает имя, номер страницы, общее число записей (
+15) - State сохраняется при переключении View — page, input, color (
+25)
Garage view · 160
- CRUD тачек: create / update / delete / list. Delete удаляет из обеих таблиц (
+60) - Цвет выбирается из RGB-палитры, отражается на изображении (
+25) - Кнопки update/delete возле каждой тачки (
+15) - Pagination 7 тачек на страницу (
+30) - Кнопка генерит 100 случайных тачек: имена из 2 частей × ≥10 опций, цвет рандом (
+30)
Car animation · 140
- Кнопки start/stop возле каждой тачки (
+25) - Start engine: ждём velocity → анимация → drive request. 500 на drive → стоп в текущей позиции (
+60) - Stop engine: ждём stop response → тачка возвращается на старт (
+25) - Кнопки disabled-логика: start disabled во время drive, stop disabled на старте (
+15) - Анимация плавная на 500px viewport (
+15)
Race animation · 100
- Start race запускает гонку всех тачек на текущей странице (
+40) - Reset race возвращает всех на старт (
+30) - После финиша первой — сообщение с именем победителя (
+30)
Winners view · 120
- После гонки победитель попадает в Winners (
+40) - Pagination 10 на страницу (
+30) - Колонки: №, image, name, wins, best time. Wins инкрементятся, best time обновляется только если лучше (
+25) - Сортировка по wins и best time, asc и desc (
+25)
Без штрафов
- Нет UI-фреймворков (иначе
-600) - TypeScript, нет
any/as/!(иначе-100) - Функции ≤ 40 строк, eslint-unicorn проходит (иначе
-40) - Нет major-багов (-30 каждый) и minor-багов (-10 каждый)
🧠 Self-check перед финальным коммитом
Не нажимай git push, пока не ответишь:
- Архитектура. Если попрошу нарисовать схему: модули, зависимости, поток данных — нарисую за 5 минут?
- Типизация. Прогнал
tsc --noEmit --strict— 0 ошибок?grep -rn ' as \| any \| !' src/— пусто (кроме комментариев)? - State. Если переключаюсь Garage → Winners → Garage — номер страницы и инпуты сохранены? Какой объект это хранит?
- Engine flow. Что произойдёт если: (а) start → быстро stop, (б) start → drive вернёт 500, (в) start → юзер уходит на Winners?
- Race. Если 7 тачек стартуют одновременно — какая из них окажется в winners? Что если две финишируют в одном кадре?
- Winners table. Если та же тачка выиграла второй раз с худшим временем — что изменится в её строке? А с лучшим?
- Linter.
npm run lint— 0 ошибок? Прогнал на чистом репо без.eslintcache? - Bundle.
npm run buildсобирается без warning? Размер dist разумный (не 5 МБ)? - Deploy. Открыл gh-pages в инкогнито — работает? Все пути относительные?
- Cross-check. Reviewer склонирует mock-сервер на свой
localhost:3000— мой URL сервера этоlocalhost:3000или вынесен в config?
➡️ Compound forward — после bootcamp начинается карьера
Это финал bootcamp. Дальше — реальная работа. Что переходит:
В реальные проекты
- TypeScript — будет в каждой вакансии middle/senior. Async Race — твой первый serious TS-проект в портфолио.
- Архитектура vanilla SPA — даёт понимание что React/Vue делают под капотом. Junior без этого = «знает React, не знает frontend».
- REST + типизированный API-слой — паттерн, который ты переиспользуешь во ВСЕХ проектах: меняется фреймворк, не меняется идея «один файл = один источник правды для бэкенда».
- rAF + transform — анимации в любом проекте. Knowledge transfer 1:1.
- State management из первых принципов — когда дойдёшь до Redux / Zustand / Pinia, поймёшь зачем они существуют, а не «потому что в туториале так сказали».
Карьерный roadmap
- _MOC Процессы — там разделы про собеседования, поиск работы, soft skills. Открой и спланируй ближайшие 3 месяца.
- CV. Async Race + Shelter + Codejam в портфолио — это уже сильное резюме. Async Race — главный pet-project, описывай его подробнее остальных.
- Собесы. Async Race — отличный материал. На вопросы «расскажи как ты делал state» / «расскажи как делал routing без React» / «как типизировал API» — у тебя готовые ответы из твоего же кода.
- Технические собесы. Из Async Race вытаскиваются темы: event loop (rAF, async/await), TypeScript narrowing, REST, кэширование, race conditions (Start spam → 404/429), архитектурные паттерны.
Углубление (по интересу, не обязательно для junior)
- Frameworks. Возьми Async Race и перепиши на React/Vue/Solid — почувствуешь чем они помогают и где мешают.
- Тесты. Vitest + Playwright на Async Race — закроет вопрос «есть ли у тебя опыт с тестами».
- Backend. Перепиши mock-сервер на свой NestJS — закроешь полный fullstack-цикл.
- Performance. Профилируй Race на 100 машинах в Chrome DevTools — узнаешь про reflow, paint, composite layers вживую.
📚 Внешние ресурсы
- 📄 Полное задание
- 🔧 Async Race API (mock-сервер) — GitHub
- 🎥 Demo video
- Single Page Application — Wikipedia
- TypeScript Handbook
- TS Deep Dive — book
- Fetch API — MDN
- Using Promises — MDN
requestAnimationFrame— MDN- CSS Transforms — MDN
- CSS Transitions — MDN
- History API — MDN
- eslint-plugin-unicorn
- Vite — Getting Started
- RGB Color Wheel — Colorspire
- json-server — pagination & sort docs — то, что у тебя под капотом mock-сервера