Реальное собеседование SENIOR FRONTEND на 350к в банк
Сегодня мы разберем собеседование на позицию Senior Frontend-разработчика в крупный банк, где кандидат с 7-летним опытом работы демонстрирует уверенное владение вёрсткой, сборкой проектов и библиотекой React. Несмотря на некоторые затруднения в теоретических нюансах JavaScript и экосистеме React, кандидат показывает практический подход к решению задач, подкрепленный реальным опытом создания UI-библиотек и оптимизации производительности приложений.
Вопрос 1. Расскажите о себе и своём опыте работы в разработке.
Таймкод: 00:00:17
Ответ собеседника: Правильный. Фронтенд-разработкой занимается около 7 лет, из них около 6 лет — с JavaScript. Начинал с вёрстки HTML/CSS, затем jQuery, потом перешёл на React и его экосистему (Redux, React Router и т.д.) — с React работает около 5 лет. Имеет небольшой опыт бэкенда (JS, PHP), что помогает при взаимодействии с бэкенд-разработчиками. Работал в нескольких компаниях над различными проектами, в том числе разрабатывал библиотеку компонентов для дочерней компании Сбербанка (селекты, автокомплиты и т.д.), взаимодействовал с бизнесом, выявлял требования. Библиотека позволила сократить сроки MVP-проектов с 4 до 2 месяцев. Изначально библиотека писалась на основе Kendo UI с React, затем полностью переписана с нуля. Сначала использовался Flow, потом перешли на TypeScript. Тестирование включало юнит-тесты на Jest, тест-либ и E2E-тесты на Cypress. Тестовые кейсы описывали тестировщики, а код тестов писали разработчики, позже выделили отдельных тестировщиков.
Правильный ответ:
Ответ собеседника — это пример того, как следует рассказывать о себе на интервью. Он структурирован, конкретен и демонстрирует глубину опыта. Давайте разберём, почему он хорош и что можно было бы добавить для ещё более сильного впечатления.
Ключевые элементы сильного ответа «Расскажите о себе»:
- Хронология и эволюция: Кандидат не просто перечисляет технологии, а показывает свой профессиональный рост: от вёрстки до сложных фреймворков. Это демонстрирует способность учиться и адаптироваться.
- Конкретные достижения с измеримым результатом: Фраза «библиотека позволила сократить сроки MVP-проектов с 4 до 2 месяцев» — это золотой стандарт. Она показывает, что работа кандидата имела прямое влияние на бизнес-метрики. Всегда старайтесь привязывать свои достижения к цифрам (ускорение, снижение затрат, повышение стабильности).
- Глубина в одной области: Подробный рассказ о разработке библиотеки компонентов показывает экспертизу в создании переиспользуемого кода, работе с дизайн-системами и взаимодействии с бизнесом. Это уровень Senior/Lead.
- Технический стек и его обоснование: Упоминание перехода от Flow к TypeScript показывает понимание важности типизации для поддержки крупных проектов. Это не просто знание инструмента, а понимание его ценности.
- Понимание процесса разработки: Рассказ о тестировании (юнит, E2E) и взаимодействии с тестировщиками показывает, что кандидат видит разработку как целостный процесс, а не только написание кода.
Что можно было бы добавить для усиления ответа (если бы спросили подробнее):
- Причины перехода технологий: Почему перешли от jQuery к React? Было ли это связано с ростом сложности проекта, требованиями к производительности или командой?
- Роль в команде: Был ли он единственным разработчиком библиотеки, или руководил небольшой командой? Это покажет лидерские качества.
- Сложности и их преодоление: Были ли сложности при переписывании библиотеки с нуля? Как решались проблемы совместимости или обучения команды новым подходам?
- Взаимодействие с бэкендом: Упомянут небольшой опыт бэкенда. Можно было бы кратко привести пример, как это помогло в конкретной ситуации (например, в проектировании API или отладке интеграционных проблем).
Пример идеального расширенного ответа (если бы кандидат захотел добавить деталей):
«...Кроме того, мой опыт бэкенда на Node.js помог мне в одном проекте предложить более эффективную структуру ответа API, что сократило количество запросов с фронтенда на 30% и улучшило время загрузки страницы. При разработке библиотеки компонентов мы столкнулись с проблемой производительности при рендеринге больших списков в автокомплите. Я предложил и реализовал виртуализацию списка, что решило проблему и стало стандартом для всех подобных компонентов в компании...»
Таким образом, ответ кандидата уже очень сильный. Он демонстрирует не просто набор навыков, а инженерное мышление, ориентацию на результат и понимание бизнес-контекста. Это именно то, что ищут в опытных разработчиках.
Вопрос 2. Чем занимались на последнем месте работы?
Таймкод: 00:04:55
Ответ собеседника: Правильный. Работал над зарплатным проектом. Изначально выполнял роль фронтенд-архитектора: создал новый сервис, перепроектировал взаимодействие с бэкендом, обеспечил интеграцию микросервиса в общую архитектуру и связь с другими приложениями. Затем занимался разработкой фич и консультировал мидл-разработчиков.
Правильный ответ:
Ответ собеседца демонстрирует высокий уровень ответственности и архитектурное мышление. Давайте разберём, почему этот ответ сильный, и как можно было бы его дополнить для ещё более полной картины.
Сильные стороны ответа:
- Чёткое разделение ролей по времени: Кандидат показывает свой рост — от архитектора до разработчика и ментора. Это демонстрирует гибкость и способность решать задачи разного уровня.
- Архитектурные задачи: Создание нового сервиса, перепроектирование взаимодействия с бэкендом, интеграция в микросервисную архитектуру — это задачи уровня Senior+/Lead. Показывает понимание системного дизайна.
- Менторство: Консультирование мидл-разработчиков — важный навык, который указывает на лидерские качества и способность делиться знаниями.
- Предметная область: Зарплатный проект — это критически важная бизнес-система, где важны надёжность, безопасность и точность расчётов.
Что можно было бы уточнить или дополнить (если бы интервьюер спросил подробнее):
-
Конкретика архитектурных решений:
- Какие паттерны проектирования использовали для взаимодействия с бэкендом? (REST, GraphQL, gRPC?)
- Как решали проблему согласованности данных между микросервисами?
- Использовали ли вы Event-Driven Architecture для связи между приложениями?
-
Технические детали:
- Какой стек технологий выбрали для нового сервиса и почему?
- Как обеспечивали типобезопасность при взаимодействии с бэкендом? (TypeScript интерфейсы, OpenAPI генерация типов?)
- Как организовано состояние приложения? (Redux, Zustand, React Query?)
-
Масштаб и результаты:
- Сколько пользователей/транзакций обслуживает сервис?
- Какие метрики улучшились после перепроектирования? (время отклика, количество ошибок, скорость разработки новых фич?)
- Сколько мидлов консультировал и по каким темам?
Пример расширенного ответа:
«...При создании нового сервиса мы выбрали React + TypeScript на фронтенде и перешли от монолитного REST API к микросервисам с gRPC для внутреннего взаимодействия. Это позволило снизить время отложенной загрузки ключевых виджетов на 40%. Для синхронизации данных между сервисами использовали паттерн Event Sourcing с Kafka. Также я внедрил кодогенерацию TypeScript-типов из OpenAPI-спецификации бэкенда, что сократило количество ошибок интеграции на 60% и ускорило онбординг новых разработчиков...»
Важный момент для интервью:
Если вы упоминаете архитектурные задачи, будьте готовы ответить на уточняющие вопросы о принятых решениях и их альтернативах. Интервьюер может спросить: «Почему выбрали именно этот подход?», «Какие компромиссы были?», «Что бы изменили сейчас?». Это проверяет глубину понимания и способность к рефлексии.
Вопрос 3. Почему ищете другую работу и что хотите найти в новой компании?
Таймкод: 00:06:10
Ответ собеседника: Правильный. Рассматривает различные варианты, общается с интересными компаниями. Знает компанию как один из крупнейших банков. Работал с людьми, которые ранее работали в этой компании, и получил положительные отзывы о технической силе команды. Интересен проект, связанный с единой фронтенд-платформой. Готов заниматься разработкой частей приложения с нуля, а также интересуется архитектурными задачами — настройкой окружения, пайплайнов, архитектуры и последующей разработкой фич.
Правильный ответ:
Ответ собеседника выстроен грамотно: он не критикует текущего работодателя, демонстрирует осведомлённость о компании-работодателе и чётко формулирует свои профессиональные интересы. Разберём структуру и дадим рекомендации по усилению.
Сильные стороны ответа:
- Позиционирование как кандидата на рынке: «Рассматриваю варианты» — это нормальная позиция, которая не создаёт ощущение отчаяния. Вы выбираете работодателя, а не бежите от проблем.
- Осведомлённость о компании: Упоминание о знакомых, работавших в компании, и их положительных отзывах — это мощный сигнал. Показывает, что вы не бросаете резюме наугад, а целенаправленно выбираете место.
- Конкретный интерес к проекту: «Единая фронтенд-платформа» — это не абстрактное «хочу интересные задачи», а понимание конкретного направления. Демонстрирует, что вы изучили вакансию и подготовились.
- Гибкость в задачах: Готовность и к разработке с нуля, и к архитектурным задачам — показывает зрелость и понимание, что в реальных проектах нужна разнопловая работа.
Что можно было бы добавить для усиления:
- Мотивация роста: Можно честно сказать, чего не хватает на текущем месте. Например: «На текущем проекте я уже реализовал архитектуру и выстроил процессы. Сейчас хочу применить этот опыт на более масштабном проекте с большим количеством команд и пользователей».
- Ценности компании: Если знаете что-то конкретное о культуре компании (технические блоги, open-source процессы, конференции), упомяните это: «Мне импонирует, что компания делится экспертизой через технический блог и участвует в разработке open-source инструментов».
- Долгосрочная перспектива: Покажите, что вы думаете о развитии: «Вижу себя как технического лида фронтенд-направления через 2-3 года и хочу расти в компании, где есть такая возможность».
Чего следует избегать:
- Критики текущего работодателя, коллег или менеджмента.
- Фокуса только на зарплате: «Хочу больше денег» — допустимо, но не должно быть единственной мотивацией.
- Размытых формулировок: «Хочу что-то новое» без объяснения, что именно.
Пример усиленного ответа:
«...Кроме того, на текущем месте я прошёл путь от проектирования архитектуры до её стабилизации. Сейчас проект перешёл в фазу поддержки, и я ищу возможность применить накопленный опыт на проекте с большим масштабом и техническими вызовами. Единая фронтенд-платформа — это именно такой проект, где архитектурные решения влияют на множество команд и миллионы пользователей. Также для меня важно работать в сильной технической культуре, где можно учиться у коллег и делиться своим опытом...»
Важный совет:
Ответ на этот вопрос — это не только о том, что вы хотите получить, но и о том, что вы можете дать. Всегда связывайте свои ожидания с ценностью, которую вы принесёте компании.
Вопрос 4. Как была организована разработка на последнем проекте — от получения задачи до деплоя?
Таймкод: 00:09:50
Ответ собеседника: Правильный. Использовались двухнедельные спринты с артефактами из Jira. Процесс начинался с груминга, где команда знакомилась с задачами и обсуждала их. Через день проводилось планирование с выставлением стори-поинтов и дообсуждением вопросов. После разработки фичи создавался pull request, который ревьюили разработчики из смежных команд. PR проходил пайплайн: проверка TypeScript, тесты на Jest. После принятия PR всё собиралось в Team City, развёртывалось на стенд в OpenShift. Затем задача переводилась в Jira в статус для тестировщиков.
Правильный ответ:
Ответ собеседника даёт хорошую общую картину процесса. Давайте разберём его детальнее и дополним тем, что интервьюер мог бы ожидать услышать от кандидата уровня Senior/Lead.
Сильные стороны ответа:
- Структурированность: Кандидат последовательно описывает весь цикл — от планирования до деплоя. Это показывает понимание полного цикла разработки.
- Конкретные инструменты: Упоминание Jira, Team City, OpenShift демонстрирует опыт работы с enterprise-инструментами CI/CD.
- Кросс-командное ревью: Ревью от смежных команд — это зрелая практика, которая улучшает качество кода и способствует обмену знаниями.
- Автоматизация проверок: TypeScript и Jest в пайплайне — стандарт для современного фронтенда.
Что можно было бы дополнить для более полной картины:
- Размер и состав команды: Сколько человек в команде? Есть ли выделенный QA, дизайнер, аналитик?
- Code Review практики:
- Сколько апрувов требовалось для мержа?
- Были ли чек-листы для ревью?
- Как обрабатывались конфликты мнений?
- Стратегия ветвления: Git Flow, Trunk-Based Development, Feature Flags?
- Тестирование:
- Были ли E2E тесты? Какой фреймворк (Cypress, Playwright)?
- Какой был code coverage?
- Были ли визуальные регрессионные тесты?
- Деплой:
- Какой стратегии деплоя придерживались (blue-green, canary)?
- Были ли автоматические откаты при проблемах?
- Как мониторили здоровье приложения после деплоя?
- Метрики и улучшения:
- Как измеряли эффективность процесса (lead time, cycle time)?
- Были ли ретроспективы и как внедряли улучшения?
Пример расширенного ответа:
«...Наша команда состояла из 6 фронтенд-разработчиков, 2 бэкендеров, QA-инженера и продуктового аналитика. Мы использовали Trunk-Based Development с feature flags для долгоживущих фич. Для каждого PR требовалось минимум 2 апрува, один из которых — от разработчика смежной команды, чтобы обеспечить кросс-функциональное понимание кодовой базы.
Пайплайн включал несколько этапов: линтинг (ESLint, Prettier), проверка типов TypeScript, юнит-тесты с Jest (мы поддерживали покрытие не ниже 80%), сборку и запуск интеграционных тестов. После мержа в main автоматически запускался деплой на staging-стенд в OpenShift, где наши QA-инженеры проводили ручное тестирование. Для продакшена использовали canary-деплой — сначала 5% трафика, мониторинг ошибок через Sentry, и если метрики в норме, постепенное увеличение до 100%.
Мы отслеживали ключевые метрики: lead time (от создания задачи до деплоя) в среднем составлял 3 дня, change failure rate был менее 2%. На ретроспективах обсуждали узкие места и внедряли улучшения — например, автоматизировали генерацию changelog из commit messages...»
Важный момент:
Если вы описываете процессы, будьте готовы ответить на вопрос «Что бы вы улучшили?». Это показывает критическое мышление и стремление к совершенствованию. Например: «Я бы добавил автоматические визуальные регрессионные тесты для компонентов дизайн-системы, чтобы ловить непреднамеренные изменения внешнего вида».
Вопрос 5. Был ли настроен CI/CD на проекте?
Таймкод: 00:11:34
Ответ собеседника: Правильный. Да, CI/CD был реализован через Team City. Код хранился в Bitbucket, где настраивались хуки для сборки. Пайплайн включал проверка TypeScript, запуск тестов на Jest. После успешного мёржа запускалась сборка в Team City, и фича выкатывалась на стенд через OpenShift.
Правильный ответ:
Ответ корректный, но довлакий. Для кандидата уровня Senior/Lead интервьюер ожидает более глубокого понимания CI/CD процессов. Давайте разберём, что стоит добавить.
Что уже хорошо покрыто:
- Конкретные инструменты: Team City, Bitbucket, OpenShift
- Базовые этапы пайплайна: проверка типов, тесты, деплой
- Понимание триггеров: хуки в Bitbucket при мёрже
Что стоит раскрыть подробнее:
Этапы CI пайплайна (Continuous Integration):
- Install — установка зависимостей с кэшированием
- Lint — статический анализ кода (ESLint, Stylelint)
- Type Check — проверка типов TypeScript
- Unit Tests — юнит-тесты с Jest, отчёт о покрытии
- Build — сборка приложения, проверка размера бандла
- Integration Tests — интеграционные тесты
Этапы CD пайплайна (Continuous Delivery/Deployment):
- Deploy to Staging — автоматический деплой на staging-окружение
- Smoke Tests — базовые проверки работоспособности
- E2E Tests — сквозные тесты (Cypress, Playwright)
- Deploy to Production — деплой в продакшен (ручной или автоматический)
- Post-deploy Verification — проверка метрик после деплоя
Пример расширенного ответа:
«...CI/CD пайплайн был реализован через Team City с хранением кода в Bitbucket. Процесс был разделён на несколько этапов:
CI-часть запускалась автоматически при создании или обновлении PR:
- Установка зависимостей с кэшированием node_modules для ускорения
- Линтинг через ESLint и Prettier с проверкой форматирования
- Проверка типов TypeScript — это был обязательный гейт, без которого PR нельзя было мержить
- Юнит-тесты на Jest с порогом покрытия в 80% — при падении ниже пайплайн падал
- Сборка приложения с анализом размера бандла через bundle-analyzer — мы следили, чтобы новый код не увеличивал бандл более чем на 5%
CD-часть запускалась после мёржа в main:
- Автоматический деплой на staging-стенд в OpenShift
- Запуск smoke-тестов для проверки базовой работоспособности
- Уведомление тестировщиков в Slack
- Деплой на продакшен был полуавтоматическим: требовался ручной апрув от ответственного, после чего происходил canary-деплой — 5% трафика, мониторинг 15 минут, затем полный ролл-аут
Для отката использовали механизм roll-back в OpenShift — при обнаружении проблем через мониторинг Sentry можно было откатиться на предыдущую версию за пару минут...»
Дополнительные темы для обсуждения:
- Environment Management: Как управлялись окружениями (dev, staging, prod)? Использовали ли вы feature flags?
- Secrets Management: Как хранились секреты (ключи API, пароли)?
- Infrastructure as Code: Использовали ли Terraform, Helm charts для описания инфраструктуры?
- Monitoring: Какие метрики отслеживали после деплоя (error rate, latency, CPU)?
- Notifications: Как команда узнавала о проблемах (Slack, PagerDuty)?
Важный момент:
Если вы настраивали CI/CD самостоятельно, подчеркните это. Если только пользовались готовым — расскажите, как предлагали улучшения. Интервьюер оценивает не только знание инструментов, но и инициативу в улучшении процессов.
Вопрос 6. Были ли настроены редакторы кода (ESLint, Prettier, Husky) на проекте?
Таймкод: 00:12:36
Ответ собеседника: Правильный. ESLint и Prettier были настроены. Husky (хуки) на этом проекте не использовались, но есть опыт работы с ними на других проектах. Также был опыт настройки конфигураций ESLint и Prettier.
Правильный ответ:
Ответ хороший — кандидат честно указал, что Husky не использовался на текущем проекте, но опыт с ним есть. Для Senior уровня стоит раскрыть тему глубже.
Что стоит добавить о ESLint:
Популярные конфигурации и плагины:
eslint-config-airbnb— строгий набор правилeslint-config-prettier— отключение правил, конфликтующих с Prettiereslint-plugin-import— проверка импортовeslint-plugin-reactиeslint-plugin-react-hooks— правила для Reacteslint-plugin-jsx-a11y— доступность@typescript-eslint— правила для TypeScript
Пример конфигурации ESLint:
// .eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'prettier', // Должен быть последним
],
plugins: ['@typescript-eslint', 'react', 'jsx-a11y', 'import'],
rules: {
'react/react-in-jsx-scope': 'off', // Не нужно в React 17+
'@typescript-eslint/explicit-module-boundary-types': 'off',
'import/order': ['error', {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
}],
},
settings: {
react: {
version: 'detect',
},
},
};
Что стоит добавить о Prettier:
Пример конфигурации:
// .prettierrc
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
Что стоит добавить о Husky и lint-staged:
Husky позволяет запускать проверки перед коммитом и пушем, что предотвращает попадание «грязного» кода в репозиторий.
Настройка Husky + lint-staged:
# Установка
npm install --save-dev husky lint-staged
# Инициализация Husky
npx husky install
# Добавление хука pre-commit
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml}": [
"prettier --write"
]
}
}
Дополнительные инструменты качества кода:
- commitlint — проверка формата коммитов (Conventional Commits)
- editorconfig — единые настройки редактора для всех разработчиков
- stylelint — линтинг CSS/SCSS
Пример расширенного ответа:
«...ESLint и Prettier были настроены с конфигурацией Airbnb. Мы использовали @typescript-eslint для строгой проверки типов и плагин import/order для автоматической сортировки импортов. Конфигурация была вынесена в отдельный npm-пакет, который подключался во всех проектах команды — это обеспечивало единообразие.
Husky на текущем проекте не использовался, но на предыдущем я настроил Husky с lint-stinted для автоматического форматирования и линтинга перед коммитом. Это значительно сократило количество «косметических» коммитов типа «fix formatting». Также настроил commitlint для проверки формата коммитов по Conventional Commits — это упростило автоматическую генерацию changelog...»
Важный момент:
Если вы настраивали эти инструменты самостоятельно, упомяните конкретные проблемы, которые решали. Например: «Настроил автофикс импортов при сохранении в VS Code, что сэкономило команде время на ручную сортировку».
Вопрос 7. Был ли опыт настройки сборки приложения? Какие сборщики использовались?
Таймкод: 00:13:14
Ответ собеседника: Правильный. Работал с Webpack версий 3 и 4, с пятой версией пока не работал. Знаком с Vite, слышал о нём как о новом сборщике. Отметил, что принципы работы сборщиков плюс-минус похожи.
Правильный ответ:
Ответ показывает опыт работы с Webpack, но для Senior уровня стоит раскрыть тему значительно глубже. Давайте разберём ключевые аспекты настройки сборки.
Webpack — ключевые концепции:
1. Entry и Output:
// webpack.config.js
module.exports = {
entry: {
main: './src/index.tsx',
admin: './src/admin/index.tsx',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true,
},
};
2. Loaders — трансформация файлов:
module.exports = {
module: {
rules: [
// TypeScript
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
// CSS с модулями
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
},
},
},
],
},
// Изображения
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8kb — inline как base64
},
},
},
],
},
};
3. Plugins — расширение функциональности:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
// Анализ размера бандла (только для анализа)
process.env.ANALYZE && new BundleAnalyzerPlugin(),
].filter(Boolean),
};
4. Code Splitting и оптимизация:
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
runtimeChunk: 'single',
},
};
Webpack 5 vs Webpack 4 — ключевые отличия:
- Module Federation — микрофронтенды без фреймворков
- Asset Modules — встроенная поддержка ассетов (замена file-loader, url-loader)
- Persistent Caching — кэширование между сборками
- Tree Shaking — улучшена поддержка ESM
- Top Level Await — поддержка await на верхнем уровне
Vite — современная альтернатива:
Vite использует ESM нативно и esbuild для транспиляции, что даёт огромный прирост скорости в dev-режиме.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns'],
},
},
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
Сравнение сборщиков:
| Характеристика | Webpack 5 | Vite | esbuild | Parcel |
|---|---|---|---|---|
| Dev-сервер | Медленный | Очень быстрый | — | Быстрый |
| Production build | Быстрый | Быстрый (Rollup) | Очень быстрый | Средний |
| Конфигурация | Гибкая | Простая | Минимальная | Zero-config |
| Плагины | Огромная экосистема | Растущая | Минимальные | Средняя |
| Tree Shaking | Да | Да | Да | Да |
| Module Federation | Да | Нет | Нет | Нет |
Пример расширенного ответа:
«...Работал с Webpack 3 и 4, настраивал конфигурации с нуля: loaders для TypeScript, CSS-модулей, изображений; plugins для извлечения CSS, анализа бандла, генерации HTML. Реализовал code splitting для ленивой загрузки роутов через React.lazy и dynamic imports, что сократило размер начального бандла на 40%.
С Webpack 5 пока не работал в production, но изучал документацию и экспериментировал с Module Federation для микрофронтенд-архитектуры. Vite использовал в pet-проектах — впечатлила скорость dev-сервера благодаря нативному ESM и esbuild для горячей перезагрузки. Для production-сборки Vite использует Rollup, что даёт отличную оптимизацию...»
Важный момент:
Если вы настраивали сборку, будьте готовы ответить на вопросы об оптимизации: «Как уменьшали размер бандла?», «Как ускоряли сборку?», «Как настраивали кэширование?». Это покажет практический опыт, а не только теоретические знания.
Вопрос 8. Как устроен конфигурационный файл Webpack? Что он включает в себя?
Таймкод: 00:13:41
Ответ собеседника: Правильный. Конфигурационный файл Webpack включает: лоадеры (loaders) для обработки различных типов файлов (шрифты, изображения, CSS, стили и т.д.), указание пути для билда (папка назначения), дополнительные настройки (прокси, обработка падений на страницы). Также указываются расширения файлов для сборки (TSX, JSX, JS) и подключаются плагины.
Правильный ответ:
Ответ покрывает основные элементы, но для Senior уровня стоит дать более структурированное и детальное описание.
Структура конфигурации Webpack:
1. Entry Points (Точки входа):
module.exports = {
entry: {
main: './src/index.tsx',
vendor: './src/vendor.ts',
},
// Или динамический entry
entry: () => {
return {
main: './src/index.tsx',
admin: './src/admin.tsx',
};
},
};
2. Output (Выход):
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
publicPath: '/',
clean: true, // Webpack 5: очистка папки перед сборкой
},
};
3. Mode (Режим):
module.exports = {
mode: 'production', // 'development' | 'production' | 'none'
// development: быстрая сборка, source maps, отладка
// production: минификация, tree shaking, оптимизация
};
4. Module Rules (Лоадеры):
module.exports = {
module: {
rules: [
// TypeScript
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true, // Для скорости — без проверки типов
},
},
],
exclude: /node_modules/,
},
// CSS Modules
{
test: /\.module\.scss$/,
use: [
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
'sass-loader',
],
},
// Обычный CSS
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: ['style-loader', 'css-loader'],
},
// Изображения
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 10kb
},
},
generator: {
filename: 'images/[name].[hash:8][ext]',
},
},
// Шрифты
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]',
},
},
],
},
};
5. Resolve (Разрешение модулей):
module.exports = {
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
},
};
6. Plugins (Плагины):
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const DefinePlugin = webpack.DefinePlugin;
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico',
minify: process.env.NODE_ENV === 'production',
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
new DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
}),
new CopyPlugin({
patterns: [
{ from: 'public/locales', to: 'locales' },
],
}),
],
};
7. DevServer (Сервер разработки):
module.exports = {
devServer: {
port: 3000,
hot: true,
open: true,
historyApiFallback: true, // Для SPA
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
client: {
overlay: {
errors: true,
warnings: false,
},
},
},
};
8. Optimization (Оптимизация):
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
return `vendor.${packageName.replace('@', '')}`;
},
},
},
},
runtimeChunk: 'single',
},
};
9. Source Maps:
module.exports = {
devtool: process.env.NODE_ENV === 'production'
? 'source-map'
: 'eval-cheap-module-source-map',
};
10. Devtool и Performance:
module.exports = {
performance: {
hints: 'warning',
maxEntrypointSize: 512000, // 500kb
maxAssetSize: 512000,
},
stats: {
modules: false,
children: false,
},
};
Полный пример конфигурации:
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
mode: isProduction ? 'production' : 'development',
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
publicPath: '/',
clean: true,
},
devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: { dataUrlCondition: { maxSize: 10 * 1024 } },
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
isProduction && new MiniCssExtractPlugin(),
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
}),
process.env.ANALYZE && new BundleAnalyzerPlugin(),
].filter(Boolean),
optimization: {
splitChunks: { chunks: 'all' },
},
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
},
};
Пример расширенного ответа:
«...Конфигурация Webpack включает несколько ключевых секций: entry для точек входа, output для указания папки сборки и именования файлов, module.rules для лоадеров (TypeScript, CSS, ассеты), resolve для алиасов и расширений, plugins для расширения функциональности (HtmlWebpackPlugin, MiniCssExtractPlugin, DefinePlugin), optimization для code splitting и минификации, devServer для локальной разработки с HMR и проксированием API. Также настраиваю source maps в зависимости от окружения и performance hints для контроля размера бандла...»
Вопрос 9. Чем плагины Webpack отличаются от лоадеров?
Таймкод: 00:14:59
Ответ собеседника: Правильный. Лоадеры пропускают через себя определённые типы файлов (CSS, изображения и т.д.) и обрабатывают/преобразуют их. Плагины же расширяют функциональность самого Webpack и позволяют выполнять пост-обработку бандла после загрузки всех модулей.
Правильный ответ:
Ответ корректный и хорошо передаёт суть различий. Давайте дополним его деталями для более полного понимания.
Лоадеры (Loaders):
Лоадеры — это трансформаторы, которые применяются к исходному коду файлов перед тем, как они попадут в бандл. Они работают на уровне отдельных файлов.
Ключевые характеристики лоадеров:
- Применяются к файлам до добавления в граф зависимостей
- Выполняются последовательно, справа налево (снизу вверх)
- Каждый лоадер отвечает за один тип трансформации
- Конфигурируются в
module.rules
Пример цепочки лоадеров:
{
test: /\.scss$/,
use: [
'style-loader', // 3. Внедряет CSS в DOM
'css-loader', // 2. Разрешает @import и url()
'sass-loader', // 1. Компилирует SCSS в CSS
],
}
Популярные лоадеры:
ts-loader/babel-loader— транспиляция TypeScript/JavaScriptcss-loader— обработка CSSstyle-loader— внедрение стилей в DOMsass-loader/less-loader— компиляция препроцессоровfile-loader/asset modules— обработка файлов
Плагины (Plugins):
Плагины — это расширения, которые подключаются к жизненному циклу Webpack и могут выполнять действия на любом этапе сборки. Они работают на уровне всего бандла.
Ключевые характеристики плагинов:
- Имеют доступ ко всему процессу сборки через систему хуков
- Могут выполнять действия на любом этапе (compile, emit, done и т.д.)
- Конфигурируются в массиве
plugins - Могет влиять на весь бандл, а не на отдельные файлы
Пример плагина:
const webpack = require('webpack');
class MyCustomPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyCustomPlugin', (compilation, callback) => {
// Доступ ко всем файлам бандла
for (const filename in compilation.assets) {
console.log(`Processing: ${filename}`);
}
callback();
});
}
}
Популярные плагины:
HtmlWebpackPlugin— генерация HTML с подключением бандловMiniCssExtractPlugin— извлечение CSS в отдельные файлыDefinePlugin— определение глобальных константCleanWebpackPlugin— очистка папки сборкиBundleAnalyzerPlugin— визуализация размера бандлаCopyWebpackPlugin— копирование файловCompressionPlugin— сжатие gzip/brotli
Сравнительная таблица:
| Характеристика | Лоадеры | Плагины |
|---|---|---|
| Уровень работы | Отдельные файлы | Весь бандл |
| Когда выполняются | До добавления в граф | На любом этапе сборки |
| Назначение | Трансформация файлов | Расширение функциональности |
| Конфигурация | module.rules | plugins |
| Порядок выполнения | Справа налево | По порядку в массиве |
| Доступ к графу зависимостей | Нет | Да (через хуки) |
Аналогия для понимания:
Представьте конвейер на фабрике:
- Лоадеры — это станки, которые обрабатывают отдельные детали (шлифуют, красят, сверлят)
- Плагины — это управляющие системы, которые контролируют весь конвейер (запуск, остановка, упаковка готовой продукции, отчётность)
Пример расширенного ответа:
«...Лоадеры работают на уровне отдельных файлов — они трансформируют исходный код перед добавлением в граф зависимостей. Например, ts-loader компилирует TypeScript в JavaScript, а css-loader обрабатывает @import и url() в CSS-файлах. Лоадеры выполняются цепочкой, справа налево.
Плагины работают на уровне всего процесса сборки. Они подключаются к системе хуков Webpack и могут выполнять действия на любом этапе — от начала компиляции до записи файлов на диск. Например, HtmlWebpackPlugin генерирует HTML-файл с автоматической подссылкой на собранные бандлы, а DefinePlugin заменяет переменные окружения на этапе компиляции.
Ключевое отличие: лоадер преобразует файл, плагин расширяет возможности самого Webpack...»
Вопрос 10. Как CSS поддерживает адатптивность и кроссбраузерность вёрстки?
Таймкод: 00:16:13
Ответ собеседника: Правильный. Адаптивность поддерживается через принцип Mobile First — сначала вёрстка для телефонов, затем для планшетов и компьютеров. Используются медиазапросы для указания ширины экрана и соответствующих стилей. Также помогают CSS-фреймворки (Bootstrap с грид-системой). Кроссбраузерность обеспечивается через Autoprefixer, который добавляет вендорные префиксы (-webkit-, -moz- и т.д.) для различных CSS-свойств. Подключение происходит на этапе сборки через Webpack.
Правильный ответ:
Ответ хороший, покрывает основные аспекты. Давайте расширим его для более полного понимания темы.
Адаптивность (Responsive Design):
1. Принцип Mobile First:
Mobile First — подход, при котором стили сначала пишутся для мобильных устройств, а затем расширяются для больших экранов через min-width медиазапросы.
/* Базовые стили — для мобильных */
.container {
padding: 16px;
font-size: 14px;
}
/* Планшеты */
@media (min-width: 768px) {
.container {
padding: 24px;
font-size: 16px;
}
}
/* Десктоп */
@media (min-width: 1024px) {
.container {
padding: 32px;
max-width: 1200px;
margin: 0 auto;
}
}
2. Стандартные брейкпоинты:
/* Маленькие телефоны */
@media (min-width: 320px) { }
/* Большие телефоны */
@media (min-width: 480px) { }
/* Планшеты */
@media (min-width: 768px) { }
/* Малые десктопы */
@media (min-width: 1024px) { }
/* Большие десктопы */
@media (min-width: 1200px) { }
/* Экстра большие экраны */
@media (min-width: 1440px) { }
3. Современные CSS-технологии для адаптивности:
CSS Grid:
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
Flexbox:
.flex-container {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.flex-item {
flex: 1 1 300px; /* grow shrink basis */
}
Container Queries (современный подход):
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}
@container card (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
}
Логические свойства:
/* Вместо margin-left / margin-right */
.element {
margin-inline: 16px;
margin-block: 8px;
padding-inline-start: 20px;
}
4. Адаптивные изображения:
img {
max-width: 100%;
height: auto;
}
/* Или через picture */
<picture>
<source media="(min-width: 1024px)" srcset="large.webp" type="image/webp">
<source media="(min-width: 768px)" srcset="medium.webp" type="image/webp">
<img src="small.jpg" alt="Description" loading="lazy">
</picture>
Кроссбраузерность (Cross-browser Compatibility):
1. Autoprefixer:
Автоматически добавляет вендорные префиксы на основе данных Can I Use.
/* Исходный код */
.element {
display: flex;
user-select: none;
backdrop-filter: blur(10px);
}
/* После Autoprefixer */
.element {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
Настройка через browserslist:
// package.json
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
2. Normalize.css / Reset CSS:
Единообразное отображение элементов во всех браузерах.
/* Пример Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
3. Feature Queries (@supports):
Проверка поддержки свойств браузером.
.element {
/* Fallback */
display: block;
}
@supports (display: grid) {
.element {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
}
@supports not (display: grid) {
.element {
display: flex;
flex-wrap: wrap;
}
}
4. Polyfills:
Для поддержки старых браузеров.
// Пример: Intersection Observer polyfill
if (!('IntersectionObserver' in window)) {
await import('intersection-observer');
}
5. CSS-переменные с fallback:
.element {
color: #333; /* Fallback */
color: var(--text-color, #333);
}
Настройка Autoprefixer в Webpack:
// webpack.config.js
const autoprefixer = require('autoprefixer');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
autoprefixer,
],
},
},
},
],
},
],
},
};
Пример расширенного ответа:
«...Адаптивность реализую через Mobile First подход с использованием медиазапросов. Для сложных макетов применяю CSS Grid с auto-fit и minmax(), что позволяет создавать адаптивные сетки без медиазапросов. Для компонентов, которые должны адаптироваться к размеру контейнера, а не viewport, использую Container Queries.
Кроссбраузерность обеспечиваю через Autoprefixer с настройкой browserslist в package.json. Для критичных CSS-свойств использую @supports с fallback-стилями. Также подключаю Normalize.css для единообразного отображения элементов. При необходимости добавляю polyfills для старых браузеров через условный импорт...»
Вопрос 11. Как сделать кастомный чекбокс с использованием CSS?
Таймкод: 00:18:36
Ответ собеседника: Правильный. Стандартный чекбокс скрывается (делается невидимым), а поверх него с помощью псевдоэлементов ::before или ::after добавляется кастомный элемент, который стилизуется. Состояние включён/выключен отслеживается через псевдокласс :checked.
Правильный ответ:
Ответ верный. Давайте дополним его практическими примерами и рассмотрим разные подходы.
Подход 1: Скрытие нативного чекбокса + label
<div class="checkbox-wrapper">
<input type="checkbox" id="custom-checkbox" class="checkbox-input">
<label for="custom-checkbox" class="checkbox-label">
Согласен с условиями
</label>
</div>
/* Скрываем нативный чекбокс */
.checkbox-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
/* Контейнер для кастомного чекбокса */
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
/* Кастомный квадрат */
.checkbox-label::before {
content: '';
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
background: white;
transition: all 0.2s ease;
flex-shrink: 0;
}
/* Состояние checked */
.checkbox-input:checked + .checkbox-label::before {
background: #007bff;
border-color: #007bff;
}
/* Галочка через псевдоэлемент */
.checkbox-input:checked + .checkbox-label::after {
content: '';
position: absolute;
left: 7px;
top: 3px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* Состояние focus для доступности */
.checkbox-input:focus-visible + .checkbox-label::before {
outline: 2px solid #007bff;
outline-offset: 2px;
}
/* Состояние disabled */
.checkbox-input:disabled + .checkbox-label {
opacity: 0.5;
cursor: not-allowed;
}
/* Состояние hover */
.checkbox-input:not(:disabled) + .checkbox-label:hover::before {
border-color: #007bff;
}
Подход 2: Галочка через SVG background
.checkbox-label::before {
content: '';
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
background-color: white;
background-repeat: no-repeat;
background-position: center;
background-size: 12px;
transition: all 0.2s ease;
}
.checkbox-input:checked + .checkbox-label::before {
background-color: #007bff;
border-color: #007bff;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
}
Подход 3: Анимированный чекбокс
.checkbox-animated .checkbox-label::before {
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.checkbox-animated .checkbox-input:checked + .checkbox-label::before {
transform: scale(1.1);
}
.checkbox-animated .checkbox-input:checked + .checkbox-label::after {
animation: checkmark 0.3s ease forwards;
}
@keyframes checkmark {
0% {
width: 0;
height: 0;
opacity: 0;
}
50% {
width: 6px;
height: 0;
opacity: 1;
}
100% {
width: 6px;
height: 12px;
opacity: 1;
}
}
Подход 4: Toggle Switch (переключатель)
<div class="toggle-wrapper">
<input type="checkbox" id="toggle" class="toggle-input">
<label for="toggle" class="toggle-label">
<span class="toggle-text">Выкл</span>
</label>
</div>
.toggle-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle-label {
display: flex;
align-items: center;
width: 60px;
height: 30px;
background: #ccc;
border-radius: 15px;
cursor: pointer;
position: relative;
transition: background 0.3s ease;
}
.toggle-label::before {
content: '';
position: absolute;
width: 26px;
height: 26px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.toggle-input:checked + .toggle-label {
background: #4caf50;
}
.toggle-input:checked + .toggle-label::before {
transform: translateX(30px);
}
.toggle-input:focus-visible + .toggle-label {
outline: 2px solid #4caf50;
outline-offset: 2px;
}
Важные моменты для доступности (a11y):
-
Не используйте
display: noneдля скрытия чекбокса — это делает его недоступным для screen readers и клавиатурной навигации. -
Используйте
opacity: 0илиclip-pathдля визуального скрытия с сохранением доступности. -
Добавьте
:focus-visibleдля видимого фокуса при клавиатурной навигации. -
Свяжите label с input через
for/idили вложенность. -
Добавьте ARIA-атрибуты при необходимости:
<input
type="checkbox"
id="agree"
aria-describedby="agree-description"
>
<label for="agree">Согласен</label>
<p id="agree-description" class="sr-only">
Отметьте, если согласны с условиями использования
</p>
Утилитарный класс для скрытия (визуально скрытый, но доступный):
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Пример расширенного ответа:
«...Для кастомного чекбокса скрываю нативный input через opacity: 0 с абсолютным позиционированием, а стилизованный элемент создаю через псевдоэлемент ::before на label. Состояние checked отслеживаю через селектор :checked + .checkbox-label. Галочку реализую через трансформацию border или SVG background.
Важно сохранить доступность: не использую display: none, добавляю :focus-visible для клавиатурной навигации и правильно связываю label с input через for/id. Для сложных случаев добавляю ARIA-атрибуты...»
Вопрос 12. Как можно поддерживать новые конструкции JavaScript (ES6+) в старых браузерах?
Таймкод: 00:20:12
Ответ собеседника: Правильный. С помощью полифилов. В опыте был случай, когда при разработке библиотеки компонентов нужно было поддерживать Android Explorer (9 или 10 версия). Полифил подключался и добавлялся в проект для обеспечения совместимости.
Правильный ответ:
Ответ верный, но для Senior уровня стоит раскрыть тему глубже, показав понимание различных подходов и инструментов.
Транспиляция vs Полифилы:
Важно различать два подхода:
- Транспиляция — преобразование синтаксиса (стрелочные функции, классы, деструктуризация)
- Полифилы — реализация новых API (Promise, Array.from, Object.assign)
1. Babel — транспилятор JavaScript:
Babel преобразует современный синтаксис в совместимый со старыми браузерами.
// Исходный код (ES6+)
const greet = (name) => `Hello, ${name}!`;
const users = ['Alice', 'Bob'];
const [first, ...rest] = users;
// После Babel (ES5)
var greet = function greet(name) {
return 'Hello, ' + name + '!';
};
var users = ['Alice', 'Bob'];
var first = users[0];
var rest = users.slice(1);
Настройка Babel:
// babel.config.json
{
"presets": [
["@babel/preset-env", {
"targets": "> 0.5%, last 2 versions, not dead",
"useBuiltIns": "usage",
"corejs": 3
}]
],
"plugins": [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator"
]
}
2. Core-js — полифилы для новых API:
// Импорт конкретных полифилов
import 'core-js/features/promise';
import 'core-js/features/array/from';
import 'core-js/features/object/assign';
import 'core-js/features/symbol';
// Или весь core-js (не рекомендуется для production)
import 'core-js';
3. @babel/polyfill (устаревший, заменён core-js):
// Было (Babel 7.4-)
import '@babel/polyfill';
// Стало (Babel 7.4+)
import 'core-js/stable';
import 'regenerator-runtime/runtime';
4. useBuiltIns: "usage" — автоматическое подключение полифилов:
// babel.config.json
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
При таком подходе Babel автоматически анализирует код и подключает только необходимые полифилы на основе browserslist.
5. Полифилы для Web API:
// Intersection Observer
import 'intersection-observer';
// Resize Observer
import 'resize-observer-polyfill';
// Fetch API
import 'whatwg-fetch';
// Web Components
import '@webcomponents/webcomponentsjs';
// Custom Elements
import '@webcomponents/custom-elements';
6. Условная загрузка полифилов:
// Проверяем поддержку и загружаем полифил при необходимости
if (!('IntersectionObserver' in window)) {
await import('intersection-observer');
}
// Или через dynamic import с feature detection
const loadPolyfills = async () => {
const polyfills = [];
if (!('Promise' in window)) {
polyfills.push(import('core-js/features/promise'));
}
if (!('fetch' in window)) {
polyfills.push(import('whatwg-fetch'));
}
if (!('IntersectionObserver' in window)) {
polyfills.push(import('intersection-observer'));
}
await Promise.all(polyfills);
};
await loadPolyfills();
7. Polyfill.io — сервис для автоматической загрузки полифилов:
<script src="https://polyfill.io/v3/polyfill.min.js?features=Promise,fetch,IntersectionObserver"></script>
8. Настройка browserslist:
// package.json
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"Firefox ESR",
"not dead",
"not ie 11"
]
}
9. Интеграция с Webpack:
// webpack.config.js
module.exports = {
entry: ['core-js/stable', 'regenerator-runtime/runtime', './src/index.js'],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
}]
]
}
}
}
]
}
};
10. Распространённые полифилы:
| API | Полифил |
|---|---|
| Promise | core-js/features/promise |
| Array.from | core-js/features/array/from |
| Object.assign | core-js/features/object/assign |
| Symbol | core-js/features/symbol |
| Map/Set | core-js/features/map, core-js/features/set |
| Fetch | whatwg-fetch |
| IntersectionObserver | intersection-observer |
| ResizeObserver | resize-observer-polyfill |
| Custom Elements | @webcomponents/custom-elements |
Пример расширенного ответа:
«...Для поддержки ES6+ в старых браузерах использую комбинацию Babel и полифилов. Babel транспилирует синтаксис (стрелочные функции, классы, деструктуризация) в ES5. Для новых API (Promise, Array.from и т.д.) подключаю полифилы из core-js.
Настраиваю Babel с пресетом @babel/preset-env и useBuiltIns: "usage" — это позволяет автоматически подключать только те полифилы, которые реально используются в коде, на основе browserslist. Для Web API (IntersectionObserver, Fetch) использую отдельные полифилы с условной загрузкой через динамический import, чтобы не нагружать современные браузеры.
В одном проекте нужно было поддерживать старый Android Browser — подключил core-js с полным набором полифилов и добавил полифилы для IntersectionObserver и Custom Elements...»
Вопрос 13. Какие типы данных есть в JavaScript? Какие коллекции данных вы знаете?
Таймкод: 00:21:13
Ответ собеседника: Правильный. Типы данных делятся на примитивы и непримитивы. Примитивы: число, строка, null, boolean, undefined, символ, bigint. Непримитивы: объект. Из коллекций данных знакомы Map и Set. Map — это коллекция ключ-значение, отличающаяся от объекта тем, что ключи могут быть любого типа. Основан на хэш-таблице, что обеспечивает быстрый доступ к данным (O(1)). Set — структура данных для хранения только уникальных значений.
Правильный ответ:
Ответ хороший, но есть небольшая неточность: объект — это не примитив, а ссылочный тип. Давайте дополним и расширим ответ.
Типы данных JavaScript:
Примитивы (8 типов):
-
number — числа (целые и дробные)
let int = 42;let float = 3.14;let nan = NaN; // тоже numberlet infinity = Infinity; -
string — строки
let str = 'Hello';let template = `Value: ${value}`; -
boolean — логические значения
let isTrue = true;let isFalse = false; -
undefined — неопределённое значение
let x;console.log(x); // undefined -
null — отсутствие значения
let empty = null; -
symbol — уникальные идентификаторы (ES6)
const id = Symbol('id');const id2 = Symbol('id');console.log(id === id2); // false -
bigint — большие целые числа (ES2020)
const big = 9007199254740991n;const big2 = BigInt('12345678901234567890'); -
object — объекты (включая массивы, функции, даты и т.д.)
const obj = { key: 'value' };const arr = [1, 2, 3];const fn = () => {};
Коллекции данных:
1. Object — обычный объект:
const obj = {
name: 'John',
age: 30,
};
// Ограничения:
// - Ключи только строки или Symbols
// - Нет гарантии порядка (до ES2015)
// - Нет свойства length
2. Map — коллекция ключ-значение:
const map = new Map();
// Ключи любого типа
map.set('string', 'value');
map.set(42, 'number key');
map.set({ id: 1 }, 'object key');
map.set(() => {}, 'function key');
// Методы
map.get('string'); // 'value'
map.has('string'); // true
map.delete('string');
map.size; // 3
// Итерация
for (const [key, value] of map) {
console.log(key, value);
}
map.forEach((value, key) => {
console.log(key, value);
});
// Преимущества перед Object:
// - Ключи любого типа
// - Порядок вставки сохраняется
// - Быстрый доступ O(1)
// - Есть свойство size
// - Лучше производительность при частом добавлении/удалении
3. Set — коллекция уникальных значений:
const set = new Set();
set.add(1);
set.add(2);
set.add(2); // Дубликат игнорируется
set.add(3);
set.size; // 3
set.has(2); // true
set.delete(2);
// Уникальность по ссылке для объектов
const obj1 = { id: 1 };
const obj2 = { id: 1 };
set.add(obj1);
set.add(obj2); // Добавится, т.к. разные ссылки
// Удаление дубликатов из массива
const arr = [1, 2, 2, 3, 3, 4];
const unique = [...new Set(arr)]; // [1, 2, 3, 4]
// Операции над множествами
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
// Пересечение
const intersection = new Set([...setA].filter(x => setB.has(x)));
// Объединение
const union = new Set([...setA, ...setB]);
// Разность
const difference = new Set([...setA].filter(x => !setB.has(x)));
4. WeakMap — Map со слабыми ссылками:
const weakMap = new WeakMap();
// Только объекты как ключи
const key = {};
weakMap.set(key, 'value');
// Нет итерации, нет size
// Автоматическая сборка мусора при удалении ключа
// Использование: приватные данные, кэширование
class PrivateData {
#data = new WeakMap();
set(obj, value) {
this.#data.set(obj, value);
}
get(obj) {
return this.#data.get(obj);
}
}
5. WeakSet — Set со слабыми ссылками:
const weakSet = new WeakSet();
// Только объекты
const obj = {};
weakSet.add(obj);
// Нет итерации, нет size
// Автоматическая сборка мусора
// Использование: маркировка объектов
const processed = new WeakSet();
function process(obj) {
if (processed.has(obj)) {
return; // Уже обработан
}
processed.add(obj);
// Обработка...
}
6. Array — массивы:
const arr = [1, 2, 3];
// Основные методы
arr.push(4); // Добавить в конец
arr.pop(); // Удалить с конца
arr.unshift(0); // Добавить в начало
arr.shift(); // Удалить с начала
// Функциональные методы
arr.map(x => x * 2);
arr.filter(x => x > 1);
arr.reduce((acc, x) => acc + x, 0);
arr.find(x => x === 2);
arr.findIndex(x => x === 2);
arr.some(x => x > 2);
arr.every(x => x > 0);
arr.forEach(x => console.log(x));
// Сортировка
arr.sort((a, b) => a - b);
// Spread и деструктуризация
const newArr = [...arr, 4, 5];
const [first, second, ...rest] = arr;
Сравнение коллекций:
| Характеристика | Object | Array | Map | Set | WeakMap | WeakSet |
|---|---|---|---|---|---|---|
| Тип ключей | String, Symbol | Числовой индекс | Любой | — | Только объекты | Только объекты |
| Порядок | С 2015 | Да | Да | Да | Нет | Нет |
| Итерация | for...in | for...of | for...of | for...of | Нет | Нет |
| Размер | Object.keys().length | length | size | size | Нет | Нет |
| Слабые ссылки | Нет | Нет | Нет | Нет | Да | Да |
Пример расширенного ответа:
«...В JavaScript 8 типов данных: 7 примитивов (number, string, boolean, undefined, null, symbol, bigint) и object (включая массивы, функции, даты).
Из коллекций использую Map и Set. Map — коллекция ключ-значение, где ключи могут быть любого типа, в отличие от обычного объекта. Сохраняет порядок вставки и имеет свойство size. Set — коллекция уникальных значений, удобна для удаления дубликатов.
Также знаком с WeakMap и WeakSet — они используют слабые ссылки, что позволяет сборщику мусора автоматически удалять элементы. WeakMap удобен для хранения приватных данных объектов, WeakSet — для маркировки обработанных объектов...»
Вопрос 14. Какие методы работы с массивами вы наиболее часто используете? Для чего используется цикл for...of и в чём его отличие от for...in?
Таймкод: 00:25:27
Ответ собеседника: Правильный. Чаще всего используются: reduce, forEach, filter. Цикл for...of проходит по итерируемому объекту (массивы, строки, Set, Map). for...in используется для перебора свойств объекта. Для определения итерируемости коллекции можно использовать Symbol.iterator — функцию, которая отвечает за то, как итерируются элементы объекта.
Правильный ответ:
Ответ хороший, но есть неточность: for...of не работает с обычными объектами (они не итерируемы). Давайте расширим и дополним.
Наиболее используемые методы массивов:
1. Трансформация:
// map — преобразование каждого элемента
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(x => x * 2); // [2, 4, 6, 8]
// flatMap — map + flat(1)
const sentences = ['Hello world', 'Good morning'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'Good', 'morning']
2. Фильтрация:
// filter — отбор элементов по условию
const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.filter(x => x % 2 === 0); // [2, 4, 6]
// find — первый элемент по условию
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
];
const user = users.find(u => u.id === 2); // { id: 2, name: 'Jane' }
// findIndex — индекс первого элемента по условию
const index = users.findIndex(u => u.name === 'Jane'); // 1
3. Агрегация:
// reduce — свёртка в одно значение
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, x) => acc + x, 0); // 15
// Группировка через reduce
const users = [
{ name: 'John', role: 'admin' },
{ name: 'Jane', role: 'user' },
{ name: 'Bob', role: 'admin' },
];
const grouped = users.reduce((acc, user) => {
(acc[user.role] = acc[user.role] || []).push(user);
return acc;
}, {});
// { admin: [{ name: 'John' }, { name: 'Bob' }], user: [{ name: 'Jane' }] }
4. Проверка:
// some — хотя бы один элемент удовлетворяет условию
const numbers = [1, 2, 3];
const hasEven = numbers.some(x => x % 2 === 0); // true
// every — все элементы удовлетворяют условию
const allPositive = numbers.every(x => x > 0); // true
// includes — массив содержит элемент
const fruits = ['apple', 'banana', 'orange'];
fruits.includes('banana'); // true
5. Итерация:
// forEach — перебор элементов
users.forEach((user, index) => {
console.log(index, user.name);
});
for...of vs for...in:
for...of — перебор значений итерируемых объектов:
// Массив
const arr = [10, 20, 30];
for (const value of arr) {
console.log(value); // 10, 20, 30
}
// Строка
const str = 'Hello';
for (const char of str) {
console.log(char); // H, e, l, l, o
}
// Set
const set = new Set([1, 2, 3]);
for (const value of set) {
console.log(value); // 1, 2, 3
}
// Map
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value); // a 1, b 2
}
// NodeList (DOM)
// for (const node of document.querySelectorAll('div')) { }
// Обычный объект — ОШИБКА!
// const obj = { a: 1, b: 2 };
// for (const x of obj) { } // TypeError: obj is not iterable
for...in — перечисление свойств объекта:
// Объект
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
console.log(key, obj[key]); // a 1, b 2, c 3
}
// Массив (не рекомендуется!)
const arr = ['a', 'b', 'c'];
arr.custom = 'property';
for (const index in arr) {
console.log(index, arr[index]);
// 0 a, 1 b, 2 c, custom property
}
// for...in перебирает ВСЕ перечислимые свойства,
// включая унаследованные от прототипа
Ключевые отличия:
| Характеристика | for...of | for...in |
|---|---|---|
| Что перебирает | Значения | Ключи/индексы |
| Для чего | Итерируемые объекты | Перечислимые свойства |
| Прототип | Нет | Да (все перечислимые) |
| break/continue | Да | Да |
| await | Да | Нет |
Symbol.iterator — протокол итератора:
// Создание итерируемого объекта
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
}
const range = new Range(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Spread тоже работает
const arr = [...new Range(1, 5)]; // [1, 2, 3, 4, 5]
Пример расширенного ответа:
«...Из методов массивов чаще всего использую map для трансформации, filter для фильтрации, reduce для агрегации. Также find для поиска элемента, some/every для проверки условий.
for...of перебирает значения итерируемых объектов: массивы, строки, Set, Map. for...in перечисляет перечислимые свойства объекта, включая унаследованные от прототипа — поэтому для массивов его лучше не использовать.
Итерируемость определяется наличием Symbol.iterator — это функция, возвращающая объект с методом next(). Можно создать свой итерируемый объект, реализовав этот протокол...»
Вопрос 15. Как отправить сетевой запрос на сервер, получить данные и вывести их в консоль с использованием промисов?
Таймкод: 00:28:02
Ответ собеседника: Правильный. Самый простой способ — использовать fetch. Указываем URL, fetch возвращает response. Затем можно вызвать .json() для получения данных и вывести их через console.log. Если конкретно с промисом — создаём промис, где два метода: resolve (при успешном ответе) и reject (при ошибке).
Правильный ответ:
Ответ корректный. Давайте дополним его практическими примерами и рассмотрим разные подходы.
1. Fetch API с промисами:
// Базовый пример
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Users:', data);
})
.catch(error => {
console.error('Error:', error);
});
2. Fetch с async/await:
async function fetchUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Users:', data);
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
fetchUsers();
3. Fetch с параметрами запроса:
async function fetchWithParams() {
const params = new URLSearchParams({
page: 1,
limit: 10,
sort: 'name',
});
const response = await fetch(
`https://api.example.com/users?${params}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
},
}
);
const data = await response.json();
console.log(data);
}
4. POST-запрос:
async function createUser(userData) {
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Created user:', data);
return data;
} catch (error) {
console.error('Error:', error);
}
}
createUser({ name: 'John', email: 'john@example.com' });
5. XMLHttpRequest с промисами:
function fetchWithXHR(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.ontimeout = () => reject(new Error('Timeout'));
xhr.send();
});
}
fetchWithXHR('https://api.example.com/users')
.then(data => console.log(data))
.catch(error => console.error(error));
6. Обёртка для fetch с таймаутом:
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
// Использование
fetchWithTimeout('https://api.example.com/users', {}, 3000)
.then(data => console.log(data))
.catch(error => console.error(error));
7. Обёртка с retry:
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.log(`Attempt ${i + 1} failed: ${error.message}`);
if (i === retries - 1) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
}
fetchWithRetry('https://api.example.com/users', {}, 3, 1000)
.then(data => console.log(data))
.catch(error => console.error('All retries failed:', error));
8. Axios (популярная альтернатива):
import axios from 'axios';
async function fetchWithAxios() {
try {
const response = await axios.get('https://api.example.com/users', {
params: { page: 1, limit: 10 },
headers: { Authorization: 'Bearer token123' },
timeout: 5000,
});
console.log(response.data);
return response.data;
} catch (error) {
if (error.response) {
// Сервер ответил с ошибкой
console.error('Server error:', error.response.status);
} else if (error.request) {
// Запрос отправлен, но ответа нет
console.error('No response:', error.request);
} else {
console.error('Error:', error.message);
}
}
}
Пример расширенного ответа:
«...Для сетевых запросов использую Fetch API, который возвращает промис. Базовый вызов: fetch(url).then(response => response.json()).then(data => console.log(data)).catch(error => console.error(error)).
Важно проверять response.ok — fetch не отклоняет промис при HTTP-ошибках (4xx, 5xx), только при сетевых проблемах. Для более сложных сценариев создаю обёртки: с таймаутом через AbortController, с retry-логикой при временных сбоях.
async/await делает код чище: const response = await fetch(url); const data = await response.json(). Но не забываю оборачивать в try/catch...»
Вопрос 16. В чём отличие куки от Local Storage? Какой способ хранения токена авторизации наиболее безопасен?
Таймкод: 00:30:43
Ответ собеседника: Правильный. Local Storage доступен только на клиенте, и любой человек может зайти в инструменты разработчика и прочитать/изменить данные. Куки можно читать и на сервере, и на клиенте. Для безопасности куки можно защитить флагом HttpOnly, чтобы к ней нельзя было обратиться из JavaScript. Самый безопасный вариант хранения токена — кука с флагом HttpOnly и передача по протоколу HTTPS.
Правильный ответ:
Ответ отличный, покрывает все ключевые аспекты. Давайте дополним его деталями.
Сравнение хранилищ:
| Характеристика | Cookie | Local Storage | Session Storage |
|---|---|---|---|
| Размер | ~4KB | ~5-10MB | ~5-10MB |
| Доступ с сервера | Да | Нет | Нет |
| Срок жизни | Настраиваемый | Бессроково | До закрытия вкладки |
| Отправка с запросами | Автоматически | Нет | Нет |
| Доступ из JS | Да (без HttpOnly) | Да | Да |
| API | document.cookie | localStorage | sessionStorage |
Флаги куки:
// Установка куки с флагами
document.cookie = 'name=value; expires=Thu, 01 Jan 2025 00:00:00 UTC; path=/; Secure; HttpOnly; SameSite=Strict';
Ключевые флаги безопасности:
-
HttpOnly — запрещает доступ из JavaScript (защита от XSS)
// Сервер устанавливает заголовокSet-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict// document.cookie не увидит эту куку -
Secure — кука отправляется только по HTTPS
Set-Cookie: token=abc123; Secure -
SameSite — защита от CSRF
// Strict — кука не отправляется при кросс-доменных запросахSet-Cookie: token=abc123; SameSite=Strict// Lax — кука отправляется только при GET-запросах с другого доменаSet-Cookie: token=abc123; SameSite=Lax// None — кука отправляется всегда (требует Secure)Set-Cookie: token=abc123; SameSite=None; Secure
Способы хранения токена (от менее к более безопасному):
1. Local Storage (небезопасно):
// Сохранение
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIs...');
// Получение
const token = localStorage.getItem('token');
// Отправка с запросом
fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
Проблемы: Доступен из любого JavaScript-кода, уязвим к XSS-атакам.
2. Cookie без HttpOnly (средняя безопасность):
// Установка из JS
document.cookie = 'token=abc123; path=/; Secure; SameSite=Lax';
// Чтение
const token = document.cookie
.split('; ')
.find(row => row.startsWith('token='))
?.split('=')[1];
Проблемы: Доступен из JavaScript, уязвим к XSS.
3. Cookie с HttpOnly (рекомендуется):
// Сервер устанавливает куку
// Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict; Path=/
// Кука автоматически отправляется с каждым запросом
fetch('/api/users', {
credentials: 'include', // Важно для кросс-доменных запросов
});
Преимущедства: Недоступна из JavaScript, защищена от XSS.
4. Разделение токена (максимальная безопасность):
// Access Token — короткоживущий, в памяти JS
let accessToken = null;
// Refresh Token — долгоживущий, в HttpOnly cookie
// Set-Cookie: refresh=xyz789; HttpOnly; Secure; SameSite=Strict; Path=/auth
// Функция для получения access token
async function getAccessToken() {
if (accessToken) return accessToken;
// Refresh token автоматически отправляется с запросом
const response = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include',
});
const data = await response.json();
accessToken = data.accessToken;
// Автоматическое обновление перед истечением
setTimeout(() => {
accessToken = null;
}, data.expiresIn * 1000 - 60000);
return accessToken;
}
// Использование
async function fetchUsers() {
const token = await getAccessToken();
const response = await fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
return response.json();
}
Пример расширенного ответа:
«...Cookie отличается от Local Storage тем, что автоматически отправляется с каждым HTTP-запросом и доступна серверу. Local Storage хранит больше данных, но доступен только клиенту.
Для хранения токена авторизации самый безопасный вариант — HttpOnly cookie. Она недоступна из JavaScript, что защищает от XSS-атак. Добавляю флаги Secure (только HTTPS) и SameSite=Strict (защита от CSRF).
Ещё более безопасный подход — разделение токенов: access token хранится в памяти JS (живёт мало), refresh token — в HttpOnly cookie. Это ограничивает ущерб при компромитации любого из токенов...»
Вопрос 17. Что такое React Fragment, зачем он нужен и где используется?
Таймкод: 00:33:18
Ответ собеседника: Правильный. React Fragment — это пустой блок в JSX, который не добавляет лишний элемент в DOM. Используется, когда нужно вернуть несколько элементов без обёртки в div, чтобы не нарушать структуру вёрстки (например, в таблицах с жёсткой структурой table/thead/tbody). Fragment не видно в разметке. Ему можно задать атрибут key при использовании в списках.
Правильный ответ:
Ответ отличный, покрывает все ключевые аспекты. Давайте дополним его примерами.
Что такое Fragment:
Fragment — это специальный компонент, который позволяет группировать несколько элементов без добавления дополнительного узла в DOM.
Синтаксис:
// Полный синтаксис
import { Fragment } from 'react';
function Component() {
return (
<Fragment>
<h1>Заголовок</h1>
<p>Параграф</p>
</Fragment>
);
}
// Сокращённый синтаксис (рекомендуется)
function Component() {
return (
<>
<h1>Заголовок</h1>
<p>Параграф</p>
</>
);
}
Пробрема без Fragment:
// Ошибка: JSX должен иметь один корневой элемент
function Component() {
return (
<h1>Заголовок</h1>
<p>Параграф</p>
);
}
// Решение без Fragment — лишний div
function Component() {
return (
<div>
<h1>Заголовок</h1>
<p>Параграф</p>
</div>
);
}
Случаи использования:
1. Таблицы:
function Table() {
return (
<table>
<thead>
<tr>
<TableHeader />
</tr>
</thead>
<tbody>
<TableBody />
</tbody>
</table>
);
}
function TableHeader() {
// Без Fragment пришлось бы оборачивать в div, что сломает таблицу
return (
<>
<th>Имя</th>
<th>Возраст</th>
<th>Город</th>
</>
);
}
function TableBody() {
const users = [
{ id: 1, name: 'John', age: 30, city: 'NYC' },
{ id: 2, name: 'Jane', age: 25, city: 'LA' },
];
return (
<>
{users.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>{user.city}</td>
</tr>
))}
</>
);
}
2. Списки с условиями:
function UserList({ users, showAdmins }) {
return (
<ul>
{users
.filter(user => !showAdmins || user.isAdmin)
.map(user => (
<Fragment key={user.id}>
<li>{user.name}</li>
{user.isAdmin && <li className="badge">Admin</li>}
</Fragment>
))}
</ul>
);
}
3. Семантическая вёрстка:
function Article() {
return (
<article>
<h1>Заголовок статьи</h1>
{/* Не хотим оборачивать в div для стилизации */}
<>
<p>Первый параграф</p>
<p>Второй параграф</p>
<blockquote>Цитата</blockquote>
</>
<footer>Автор: Иван</footer>
</article>
);
}
4. Flexbox/Grid контейнеры:
function CardList() {
return (
<div style={{ display: 'flex', gap: '16px' }}>
<Card />
<Card />
<Card />
</div>
);
}
function Card() {
return (
<>
<h3>Заголовок</h3>
<p>Описание</p>
<button>Действие</button>
</>
);
}
Fragment с key (для списков):
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<Fragment key={todo.id}>
<li>{todo.title}</li>
{todo.description && <li className="description">{todo.description}</li>}
</Fragment>
))}
</ul>
);
}
Важное ограничение:
// Работает только с полным синтаксисом
<Fragment key={item.id}>...</Fragment>
// Сокращённый синтаксис НЕ поддерживает key
<>...</> // Ошибка: key не передать
Когда использовать Fragment vs div:
| Используйте Fragment | Используйте div |
|---|---|
| Не нужна обёртка в DOM | Нужна обёртка для стилей |
| Семантика важна | Нужен контейнер для layout |
| Таблицы, списки | Flexbox/Grid контейнер |
| Не нужны обработчики событий | Нужен onClick и т.д. |
Пример расширенного ответа:
«...React Fragment — это компонент, который не создаёт элемент в DOM. Использую его, когда нужно вернуть несколько элементов из компонента без лишней обёртки в div.
Основные случаи: таблицы (где div сломает структуру), списки, семантическая вёрстка. Сокращённый синтаксис <>...</> удобнее, но не поддерживает key — для списков нужен полный <Fragment key={id}>...</Fragment>.
Fragment не добавляет узел в DOM, что улучшает производительность и сохраняет чистоту разметки...»
Вопрос 18. В чём отличие управляемого и неуправляемого инпута в React? Когда лучше использовать каждый из них?
Таймкод: 00:34:43
Ответ собеседника: Правильный. Управляемый инпут контролируется через value и onChange — состояние хранится в React и обновляется через setState. Неуправляемый инпут сам управляет своим значением внутри, и получить его можно при сабмите. Управляемые лучше использовать, когда нужно точно знать значение (селекты, автокомплиты, сложные объектные структуры). Неуправляемые — когда значение нужно только при сабмите.
Правильный ответ:
Ответ отличный, покрывает все ключевые аспекты и даже упоминает проблему производительности. Давайте дополним его примерами.
Управляемый (Controlled) инпут:
import { useState } from 'react';
function ControlledForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Имя"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit">Отправить</button>
</form>
);
}
Преимущества управляемых инпутов:
- Мгновенный доступ к значению
- Валидация на каждом изменении
- Условное отображение на основе значения
- Программное изменение значения
- Форматирование ввода
Неуправляемый (Uncontrolled) инпут:
import { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log({
name: nameRef.current.value,
email: emailRef.current.value,
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={nameRef}
defaultValue=""
placeholder="Имя"
/>
<input
type="email"
ref={emailRef}
defaultValue=""
placeholder="Email"
/>
<button type="submit">Отправить</button>
</form>
);
}
Сравнение:
| Характеристика | Управляемый | Неуправляемый |
|---|---|---|
| Хранение состояния | React state | DOM |
| Доступ к значению | Мгновенный | Через ref |
- Ре-рендер при каждом вводе | Да | Нет | | Валидация в реальном времени | Легко | Сложнее | | Производительность | Хуже при многих инпутах | Лучше | | Тестирование | Легче | Сложнее |
Когда использовать управляемые:
// 1. Валидация в реальном времени
function ValidatedInput() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setEmail(value);
if (value && !value.includes('@')) {
setError('Введите корректный email');
} else {
setError('');
}
};
return (
<div>
<input
type="email"
value={email}
onChange={handleChange}
className={error ? 'error' : ''}
/>
{error && <span className="error-text">{error}</span>}
</div>
);
}
// 2. Форматирование ввода
function PhoneInput() {
const [phone, setPhone] = useState('');
const handleChange = (e) => {
const value = e.target.value.replace(/\D/g, '');
const formatted = value.replace(
/(\d{3})(\d{3})(\d{4})/,
'($1) $2-$3'
);
setPhone(formatted);
};
return (
<input
type="tel"
value={phone}
onChange={handleChange}
placeholder="(123) 456-7890"
/>
);
}
// 3. Зависимые поля
function DependentFields() {
const [country, setCountry] = useState('');
const [city, setCity] = useState('');
// Сброс города при смене страны
const handleCountryChange = (e) => {
setCountry(e.target.value);
setCity(''); // Программный сброс
};
return (
<>
<select value={country} onChange={handleCountryChange}>
<option value="">Выберите страну</option>
<option value="ru">Россия</option>
<option value="us">США</option>
</select>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
disabled={!country}
/>
</>
);
}
Когда использовать неуправляемые:
// 1. Простые формы с отправкой
function SimpleSearch() {
const inputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
search(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button type="submit">Найти</button>
</form>
);
}
// 2. Загрузка файлов
function FileUpload() {
const fileRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const file = fileRef.current.files[0];
uploadFile(file);
};
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={fileRef} />
<button type="submit">Загрузить</button>
</form>
);
}
// 3. Интеграция с не-React кодом
function ThirdPartyIntegration() {
const inputRef = useRef();
useEffect(() => {
// Инициализация jQuery плагина
$(inputRef.current).datepicker();
}, []);
return <input type="text" ref={inputRef} />;
}
Оптимизация управляемых инпутов:
// 1. Вынос инпута в отдельный компонент
function OptimizedForm() {
return (
<form>
<InputField name="firstName" />
<InputField name="lastName" />
<InputField name="email" />
{/* Каждый инпут ре-рендерит только себя */}
</form>
);
}
const InputField = memo(function InputField({ name }) {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={name}
/>
);
});
// 2. Debounce для частых обновлений
function DebouncedInput() {
const [inputValue, setInputValue] = useState('');
const [debouncedValue, setDebouncedValue] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(inputValue);
}, 300);
return () => clearTimeout(timer);
}, [inputValue]);
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<p>Debounced: {debouncedValue}</p>
</div>
);
}
Пример расширенного ответа:
«...Управляемый инпут контролируется React через value и onChange — состояние хранится в компоненте. Неуправляемый использует ref для доступа к DOM-элементу напрямую.
Управляющие инпуты использую для: валидации в реальном времени, форматирования ввода, зависимых полей, сложных форм. Неуправляемые — для простых форм, загрузки файлов, интеграции с не-React библиотеками.
Проблема управляемых инпутов — ре-рендер при каждом вводе. Оптимизирую через вынос в отдельные компоненты с memo или debounce для частых обновлений...»
Вопрос 19. Как организовать валидацию формы с большим количеством инпутов без ущерба для производительности?
Таймкод: 00:37:11
Ответ собеседника: Правильный. Можно использовать библиотеку Formik, которая хорошо валидирует формы. Если писать кастомное решение — создать хук useValidation, который принимает поля и схему валидации, и возвращает валидированные данные. Также можно вынести управление состоянием каждого инпута в отдельный компонент (ValidationInput), где инпут сам контролирует своё значение и валидирует себя.
Правильный ответ:
Ответ хороший, предлагает несколько подходов. Давайте дополним его примерами и рассмотрим другие решения.
1. React Hook Form (рекомендуется):
import { useForm } from 'react-hook-form';
function LargeForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('firstName', { required: 'Имя обязательно' })}
placeholder="Имя"
/>
{errors.firstName && <span>{errors.firstName.message}</span>}
<input
{...register('email', {
required: 'Email обязателен',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Некорректный email',
},
})}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
{/* Ещё 38 инпутов */}
<button type="submit">Отправить</button>
</form>
);
}
2. Кастомный хук с изоляцией состояния:
// useField.js
function useField(validate) {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const [touched, setTouched] = useState(false);
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
if (touched) {
setError(validate(newValue));
}
};
const handleBlur = () => {
setTouched(true);
setError(validate(value));
};
return {
value,
error,
touched,
handleChange,
handleBlur,
};
}
// FormInput.js
const FormInput = memo(function FormInput({ label, validate, ...props }) {
const field = useField(validate);
return (
<div>
<label>{label}</label>
<input
value={field.value}
onChange={field.handleChange}
onBlur={field.handleBlur}
{...props}
/>
{field.touched && field.error && (
<span className="error">{field.error}</span>
)}
</div>
);
});
// LargeForm.js
function LargeForm() {
const formRef = useRef({});
const register = (name, ref) => {
formRef.current[name] = ref;
};
const handleSubmit = (e) => {
e.preventDefault();
const data = {};
const errors = {};
Object.entries(formRef.current).forEach(([name, ref]) => {
data[name] = ref.getValue();
const error = ref.validate();
if (error) errors[name] = error;
});
if (Object.keys(errors).length === 0) {
console.log(data);
}
};
return (
<form onSubmit={handleSubmit}>
<FormInput
label="Имя"
validate={(v) => !v && 'Имя обязательно'}
ref={(ref) => register('name', ref)}
/>
<FormInput
label="Email"
validate={(v) => !v.includes('@') && 'Некорректный email'}
ref={(ref) => register('email', ref)}
/>
{/* Ещё 38 инпутов */}
<button type="submit">Отправить</button>
</form>
);
}
3. Zod + React Hook Form:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
firstName: z.string().min(1, 'Имя обязательно'),
lastName: z.string().min(1, 'Фамилия обязательна'),
email: z.string().email('Некорректный email'),
age: z.number().min(18, 'Минимум 18 лет'),
password: z.string().min(8, 'Минимум 8 символов'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Пароли не совпадают',
path: ['confirmPassword'],
});
function ValidatedForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(schema),
});
const onSubmit = async (data) => {
await submitForm(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('firstName')} placeholder="Имя" />
{errors.firstName && <span>{errors.firstName.message}</span>}
<input {...register('email')} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('age')} type="number" placeholder="Возраст" />
{errors.age && <span>{errors.age.message}</span>}
<input {...register('password')} type="password" placeholder="Пароль" />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Отправка...' : 'Отправить'}
</button>
</form>
);
}
4. Debounced валидация:
function useDebouncedValidation(value, validate, delay = 300) {
const [error, setError] = useState('');
const [isValidating, setIsValidating] = useState(false);
useEffect(() => {
if (!value) {
setError('');
return;
}
setIsValidating(true);
const timer = setTimeout(() => {
const validationError = validate(value);
setError(validationError);
setIsValidating(false);
}, delay);
return () => clearTimeout(timer);
}, [value, validate, delay]);
return { error, isValidating };
}
function DebouncedInput({ validate, ...props }) {
const [value, setValue] = useState('');
const { error, isValidating } = useDebouncedValidation(value, validate);
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
{...props}
/>
{isValidating && <span>Проверка...</span>}
{error && <span className="error">{error}</span>}
</div>
);
}
5. Ленивая валидация (только при blur и submit):
function LazyValidationForm() {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = (name, value) => {
switch (name) {
case 'email':
return !value.includes('@') ? 'Некорректный email' : '';
case 'password':
return value.length < 8 ? 'Минимум 8 символов' : '';
default:
return !value ? 'Поле обязательно' : '';
}
};
const handleChange = (name, value) => {
setValues((prev) => ({ ...prev, [name]: value }));
// Валидируем только если поле уже было тронуто
if (touched[name]) {
setErrors((prev) => ({ ...prev, [name]: validate(name, value) }));
}
};
const handleBlur = (name) => {
setTouched((prev) => ({ ...prev, [name]: true }));
setErrors((prev) => ({ ...prev, [name]: validate(name, values[name]) }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Валидируем все поля
const newErrors = {};
Object.keys(values).forEach((name) => {
const error = validate(name, values[name]);
if (error) newErrors[name] = error;
});
setErrors(newErrors);
setTouched(Object.keys(values).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {}));
if (Object.keys(newErrors).length === 0) {
console.log(values);
}
};
return (
<form onSubmit={handleSubmit}>
{['firstName', 'lastName', 'email', 'password'].map((name) => (
<div key={name}>
<input
value={values[name] || ''}
onChange={(e) => handleChange(name, e.target.value)}
onBlur={() => handleBlur(name)}
placeholder={name}
/>
{touched[name] && errors[name] && (
<span className="error">{errors[name]}</span>
)}
</div>
))}
<button type="submit">Отправить</button>
</form>
);
}
Сравнение подходов:
| Подход | Плюсы | Минусы |
|---|---|---|
| React Hook Form | Производительность, мало кода | Зависимость |
| Formik | Популярный, гибкий | Медленнее при многих полях |
| Кастомный хук | Полный контроль | Больше кода |
| Zod + RHF | Типобезопасность | Кривая обучения |
Пример расширенного ответа:
«...Для форм с большим количеством инпутов рекомендую React Hook Form — он использует неуправляемые инпуты через refs, что минимизирует ре-рендеры. Для валидации подключаю Zod через zodResolver.
Если пишу кастомное решение — выношу каждый инпут в отдельный компонент с локальным состоянием. Форма получает значения только при сабмите. Также использую debounced валидацию для полей, требующих проверки на сервере (например, уникальность email).
Ключевые принципы: изолировать состояние инпутов, валидировать лениво (при blur/submit), использовать memo для предотвращения лишних ре-рендеров...»
Вопрос 20. Что такое рефы (refs) в React, зачем их используют и в чём их особенность?
Таймкод: 00:40:24
Ответ собеседника: Правильный. Рефы используются для доступа к реальной DOM-узлу (ноде), а не к JSX-элементу. Например, для подключения к инпуту или управления его состоянием напрямую. Также рефы можно использовать для хранения значений между перерендерами компонента (в отличие от state, значение в рефе не вызывает ре-рендер при изменении).
Правильный ответ:
Ответ отличный, покрывает ключевые аспекты. Давайте дополним его примерами.
Создание и использование рефов:
import { useRef, useEffect } from 'react';
function RefExample() {
const inputRef = useRef(null);
useEffect(() => {
// Фокус при монтировании
inputRef.current.focus();
}, []);
const handleClick = () => {
// Доступ к DOM-элементу
inputRef.current.select();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Выделить</button>
</div>
);
}
Основные случаи использования:
1. Доступ к DOM-элементам:
function TextInputWithFocus() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
const selectText = () => {
inputRef.current.select();
};
const getInputValue = () => {
console.log(inputRef.current.value);
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Фокус</button>
<button onClick={selectText}>Выделить</button>
<button onClick={getInputValue}>Получить значение</button>
</div>
);
}
2. Управление медиа:
function VideoPlayer() {
const videoRef = useRef(null);
const play = () => videoRef.current.play();
const pause = () => videoRef.current.pause();
return (
<div>
<video ref={videoRef} src="/video.mp4" />
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
</div>
);
}
3. Измерение элементов:
function MeasureElement() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
setDimensions({ width, height });
});
observer.observe(divRef.current);
return () => observer.disconnect();
}, []);
return (
<div ref={divRef}>
<p>Ширина: {dimensions.width}px</p>
<p>Высота: {dimensions.height}px</p>
</div>
);
}
4. Хранение значений без ре-рендера:
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
};
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>{count}</p>
<button onClick={startTimer}>Старт</button>
<button onClick={stopTimer}>Стоп</button>
</div>
);
}
5. Хранение предыдущего значения:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Текущее: {count}</p>
<p>Предыдущее: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
6. Интеграция с сторонними библиотеками:
function DatePickerIntegration() {
const inputRef = useRef(null);
const pickerRef = useRef(null);
useEffect(() => {
// Инициализация jQuery DatePicker
pickerRef.current = $(inputRef.current).datepicker({
dateFormat: 'yy-mm-dd',
onSelect: (date) => {
console.log('Выбрана дата:', date);
},
});
return () => {
$(inputRef.current).datepicker('destroy');
};
}, []);
return <input ref={inputRef} type="text" />;
}
7. ForwardRef — передача рефа дочернему компоненту:
import { forwardRef } from 'react';
const FancyInput = forwardRef(function FancyInput(props, ref) {
return (
<div className="fancy-input">
<input ref={ref} {...props} />
</div>
);
});
function Parent() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<FancyInput ref={inputRef} placeholder="Введите текст" />
<button onClick={focusInput}>Фокус</button>
</div>
);
}
Ref vs State:
| Характеристика | useRef | useState |
|---|---|---|
| Ре-рендер при изменении | Нет | Да |
| Когда обновляется | Синхронно | Асинхронно |
| Для чего | Хранение DOM-ссылок, мутабельных значений | UI-состояние |
| Начальное значение | useRef(initialValue) | useState(initialValue) |
Паттерны с рефами:
// 1. Счётчик рендеров
function RenderCounter() {
const renderCount = useRef(0);
renderCount.current += 1;
return <p>Рендеров: {renderCount.current}</p>;
}
// 2. Флаг первого рендера
function useIsFirstRender() {
const isFirst = useRef(true);
useEffect(() => {
isFirst.current = false;
}, []);
return isFirst.current;
}
// 3. Хранение callback без пересоздания
function useEventCallback(callback) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
return useCallback((...args) => {
return callbackRef.current(...args);
}, []);
}
Пример расширенного ответа:
«...Refs предоставляют доступ к DOM-элементам напрямую. Использую их для: управления фокусом, измерения элементов, интеграции с не-React библиотеками, хранения мутабельных значений без ре-рендера.
Ключевое отличие от state — изменение ref не вызывает ре-рендер. Это полезно для хранения таймеров, предыдущих значений, флагов.
Для передачи рефа дочернему компоненту использую forwardRef. Для кастомных хуков с рефами — useCallback с ref вместо зависимости...»
Вопрос 21. Какие основные хуки React вы знаете и используете в работе? Для чего нужен forwardRef?
Таймкод: 00:42:21
Ответ собеседника: Правильный. Основные хуки: useState, useEffect, useRef, useCallback, useMemo. Также упомянут useLayoutEffect. forwardRef нужен для прокидывания рефа от родительского компонента к дочернему функциональному компоненту.
Правильный ответ:
Ответ хороший. Давайте расширим его, добавив примеры и дополнительные хуки.
Основные хуки React:
1. useState — управление состоянием:
function Counter() {
const [count, setCount] = useState(0);
// Ленивая инициализация
const [expensiveValue] = useState(() => {
return computeExpensiveValue();
});
// Функциональное обновление
const increment = () => setCount((prev) => prev + 1);
return <button onClick={increment}>{count}</button>;
}
2. useEffect — побочные эффекты:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Загрузка данных
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUser(userId)
.then((data) => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
});
return () => {
cancelled = true; // Очистка при размонтировании
};
}, [userId]);
// Подписка на события
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
3. useLayoutEffect — синхронный эффект перед отрисовкой:
function Tooltip({ children, content }) {
const [tooltipHeight, setTooltipHeight] = useState(0);
const ref = useRef(null);
// Выполняется ДО отрисовки — нет мерцания
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
return (
<div ref={ref}>
{children}
<div style={{ top: tooltipHeight }}>{content}</div>
</div>
);
}
4. useCallback — мемоизация функций:
function Parent() {
const [count, setCount] = useState(0);
// Функция не пересоздаётся при каждом рендере
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
<Child onClick={handleClick} />
</div>
);
}
const Child = memo(function Child({ onClick }) {
console.log('Child render');
return <button onClick={onClick}>Click me</button>;
});
5. useMemo — мемоизация значений:
function ExpensiveComponent({ items, filter }) {
// Вычисление кэшируется
const filteredItems = useMemo(() => {
console.log('Filtering...');
return items.filter(filter);
}, [items, filter]);
return (
<ul>
{filteredItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
6. useRef — доступ к DOM и хранение значений:
function FocusInput() {
const inputRef = useRef(null);
const renderCount = useRef(0);
renderCount.current += 1;
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<input ref={inputRef} />
<p>Рендеров: {renderCount.current}</p>
</div>
);
}
forwardRef — передача рефа:
// Без forwardRef — ошибка
function FancyButton(props) {
return <button className="fancy" {...props} />;
}
// С forwardRef
const FancyButton = forwardRef(function FancyButton(props, ref) {
return (
<button ref={ref} className="fancy" {...props}>
{props.children}
</button>
);
});
// Использование
function Parent() {
const buttonRef = useRef(null);
const focusButton = () => {
buttonRef.current.focus();
};
return (
<div>
<FancyButton ref={buttonRef}>Click me</FancyButton>
<button onClick={focusButton}>Focus</button>
</div>
);
}
Кастомные хуки:
// useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
Дополнительные хуки:
| Хук | Назначение |
|---|---|
| useReducer | Сложное состояние |
| useContext | Доступ к контексту |
| useImperativeHandle | Кастомный API для ref |
| useId | Уникальный ID (React 18) |
| useTransition | Несрочные обновления (React 18) |
| useDeferredValue | Отложенное значение (React 18) |
Пример расширенного ответа:
«...Основные хуки: useState для состояния, useEffect для побочных эффектов, useRef для доступа к DOM и хранения значений, useCallback и useMemo для оптимизации.
useLayoutEffect отличается от useEffect тем, что выполняется синхронно до отрисовки — полезно для измерений, где важно избежать мерцания.
forwardRef позволяет передать ref дочернему функциональному компоненту. Без него ref не будет работать, так как функциональные компоненты не имеют экземпляров. Использую для UI-библиотек компонентов, где родителю нужен доступ к DOM-узлу дочернего элемента...»
Вопрос 22. Расскажите об особенностях использования useEffect. Что он заменяет из жизненного цикла классовых компонентов?
Таймкод: 00:43:26
Ответ собеседника: Правильный. useEffect заменяет три жизненных цикла классовых компонентов: componentDidMount, componentDidUpdate и componentWillUnmount. Принимает два аргумента: тело функции и массив зависимостей. Если массив пустой — эффект срабатывает один раз при монтировании. Если указаны зависимости — эффект срабатывает при изменении этих зависимостей. Для очистки используется функция, возвращаемая из эффекта (return function), которая вызывается при размонтировании или перед повторным выполнением эффекта.
Правильный ответ:
Ответ отличный, покрывает все ключевые аспекты. Давайте дополним его примерами и деталями.
Соответствие жизненным циклам классов:
// Классовый компонент
class UserProfile extends React.Component {
componentDidMount() {
// Загрузка данных
this.fetchUser(this.props.userId);
// Подписка на события
window.addEventListener('resize', this.handleResize);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser(this.props.userId);
}
}
componentWillUnmount() {
// Отписка от событий
window.removeEventListener('resize', this.handleResize);
// Отмена запроса
this.controller.abort();
}
}
// Функциональный компонент с useEffect
function UserProfile({ userId }) {
// componentDidMount
useEffect(() => {
console.log('Компонент смонтирован');
}, []);
// componentDidMount + componentDidUpdate (при изменении userId)
useEffect(() => {
console.log('userId изменился:', userId);
}, [userId]);
// componentDidMount + componentWillUnmount
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// Все три жизненных цикла вместе
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, controller.signal);
return () => {
controller.abort(); // componentWillUnmount
};
}, [userId]); // componentDidUpdate
}
Массив зависимостей — детали:
function Example({ userId, filter }) {
// 1. Без массива зависимостей — выполняется после КАЖДОГО рендера
useEffect(() => {
console.log('После каждого рендера');
});
// 2. Пустой массив — выполняется один раз при монтировании
useEffect(() => {
console.log('Только при монтировании');
}, []);
// 3. С зависимостями — при монтировании и при изменении зависимостей
useEffect(() => {
console.log('userId или filter изменился');
}, [userId, filter]);
// 4. Несколько useEffect для разных целей
useEffect(() => {
// Логика, связанная с userId
}, [userId]);
useEffect(() => {
// Логика, связанная с filter
}, [filter]);
}
Функция очистки (cleanup):
function ChatRoom({ roomId }) {
useEffect(() => {
// Настройка (setup)
const connection = createConnection(roomId);
connection.connect();
// Очистка (cleanup)
return () => {
connection.disconnect();
};
}, [roomId]);
// Порядок выполнения при изменении roomId:
// 1. Рендер с новым roomId
// 2. React выполняет cleanup предыдущего эффекта (disconnect)
// 3. React выполняет новый эффект (connect)
}
Практические примеры:
// 1. Подписка на WebSocket
function useWebSocket(url) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
setMessages((prev) => [...prev, JSON.parse(event.data)]);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
return () => {
ws.close();
};
}, [url]);
return messages;
}
// 2. Таймер с очисткой
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{seconds}</div>;
}
// 3. Загрузка данных с AbortController
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((data) => {
setData(data);
setLoading(false);
})
.catch((err) => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// 4. Слушатель событий
function useEventListener(eventName, handler, element = window) {
useEffect(() => {
element.addEventListener(eventName, handler);
return () => {
element.removeEventListener(eventName, handler);
};
}, [eventName, handler, element]);
}
// 5. Документ title
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
return () => {
document.title = 'Default Title';
};
}, [title]);
}
Частые ошибки:
// Ошибка: пропущена зависимость
function BadExample({ userId }) {
useEffect(() => {
fetchUser(userId); // userId не в зависимостях!
}, []); // Эффект не обновится при изменении userId
}
// Правильно
function GoodExample({ userId }) {
useEffect(() => {
fetchUser(userId);
}, [userId]);
}
// Ошибка: объект как зависимость (новый объект при каждом рендере)
function BadExample({ config }) {
useEffect(() => {
doSomething(config);
}, [config]); // Бесконечный цикл!
}
// Правильно: деструктуризация или useMemo
function GoodExample({ config }) {
const { url, method } = config;
useEffect(() => {
doSomething({ url, method });
}, [url, method]);
}
useLayoutEffect vs useEffect:
function Tooltip({ children }) {
const [height, setHeight] = useState(0);
const ref = useRef(null);
// useLayoutEffect — синхронно ДО отрисовки
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setHeight(height);
}, []);
return (
<div ref={ref}>
{children}
<div style={{ top: height }}>Tooltip</div>
</div>
);
}
Пример расширенного ответа:
«...useEffect объединяет componentDidMount, componentDidUpdate и componentWillUnmount. Массив зависимостей контролирует, когда эффект выполняется: пустой — только при монтировании, с зависимостями — при их изменении.
Функция очистки (cleanup) возвращается из эффекта и вызывается перед повторным выполнением эффекта или при размонтировании. Это важно для отписки от событий, отмены запросов, очистки таймеров.
Важно включать все используемые значения в массив зависимостей. Для объектов и функций использую useCallback и useMemo, чтобы избежать бесконечных циклов...»
Вопрос 23. Какие методы оптимизации веб-приложений вы используете на практике? Расскажите подробнее о code splitting.
Таймкод: 00:47:19
Ответ собеседника: Правильный. Для оптимизации React-приложений используются хуки мемоизации (useCallback, useMemo). Из инструментов — React DevTools Profiler для отслеживания рендеров и узких мест. Также можно откладывать загрузку скриптов, размещая их внизу страницы. Из Webpack — code splitting (разделение бандла на чанки), что ускоряет загрузку приложения.
Правильный ответ:
Ответ хороший, покрывает основные аспекты. Давайте расширим его детальными примерами.
Code Splitting в React:
1. React.lazy + Suspense:
import { lazy, Suspense } from 'react';
// Ленивый импорт компонента
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<Router>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</Router>
);
}
2. Разбиение по роутам:
// routes.js
import { lazy } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
export const routes = [
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
{ path: '/contact', element: <Contact /> },
];
// App.js
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
{routes.map((route) => (
<Route key={route.path} path={route.path} element={route.element} />
))}
</Routes>
</Suspense>
);
}
3. Разбиение по компонентам:
// Тяжёлый компонент загружается только при необходимости
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowChart(true)}>Показать график</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
Prefetch и Preload:
// Предзагрузка при наведении мыши
function NavLink({ to, children }) {
const prefetch = () => {
// Динамический импорт для предзагрузки
import(`./pages/${to}`);
};
return (
<Link to={to} onMouseEnter={prefetch}>
{children}
</Link>
);
}
// Или через webpack magic comments
const Dashboard = lazy(() => import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */
'./pages/Dashboard'
));
Webpack — настройка code splitting:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Отдельный чанк для vendor-библиотек
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
// Общий чанк для часто используемого кода
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
runtimeChunk: 'single',
},
};
Другие методы оптимизации:
1. Мемоизация:
// React.memo для компонентов
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <div>{/* Тяжёлые вычисления */}</div>;
});
// useMemo для значений
function FilteredList({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(filter);
}, [items, filter]);
return <List items={filteredItems} />;
}
// useCallback для функций
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return <Child onClick={handleClick} />;
}
2. Виртуализация списков:
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
3. Оптимизация изображений:
// Ленивая загрузка изображений
function LazyImage({ src, alt }) {
return (
<img
src={src}
alt={alt}
loading="lazy"
decoding="async"
/>
);
}
// Использование WebP с fallback
<picture>
<source srcSet="image.webp" type="image/webp" />
<source srcSet="image.jpg" type="image/jpeg" />
<img src="image.jpg" alt="Description" loading="lazy" />
</picture>
4. Web Workers для тяжёлых вычислений:
// worker.js
self.onmessage = function(e) {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// useWorker.js
function useWorker(workerScript) {
const [result, setResult] = useState(null);
const workerRef = useRef(null);
useEffect(() => {
workerRef.current = new Worker(workerScript);
workerRef.current.onmessage = (e) => {
setResult(e.data);
};
return () => workerRef.current.terminate();
}, [workerScript]);
const postMessage = useCallback((data) => {
workerRef.current.postMessage(data);
}, []);
return { result, postMessage };
}
5. Service Worker для кэширования:
// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = ['/', '/styles.css', '/app.js'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Пример расширенного ответа:
«...Для оптимизации использую code splitting через React.lazy и Suspense — это позволяет загружать компоненты только когда они нужны. Разбиваю по роутам, чтобы каждая страница загружалась отдельно.
Для мемоизации — React.memo, useMemo, useCallback. Для длинных списков — виртуализация через react-window. Изображения — lazy loading и WebP.
В Webpack настраиваю splitChunks для выделения vendor-кода в отдельный чанк и runtimeChunk для кэширования. Использую webpackPrefetch для предзагрузки критических маршрутов...»
Вопрос 24. Расскажите подробнее об опыте оптимизации загрузки приложения с использованием lazy loading и prefetching.
Таймкод: 00:49:46
Ответ собеседника: Правильный. Был опыт рефакторинга раздела приложения, где загрузка занимала 25-30 секунд. Проблема была решена путём разбиения запроса: первый запрос загружался за 3 секунды и показывал пользователю начальные данные, а на втором экране подгружались остальные данные. Использовались prefetch и lazy loading с динамическими импортами (React.lazy).
Правильный ответ:
Ответ демонстрирует реальный опыт оптимизации. Давайте дополним его техническими деталями и инструментами.
Инструменты для анализа и оптимизации:
1. React DevTools Profiler:
// Обёртка для замера времени рендера
import { Profiler } from 'react';
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log({
id, // Идентификатор компонента
phase, // 'mount' | 'update' | 'nested-update'
actualDuration, // Время рендера
baseDuration, // Время рендера без оптимизаций
});
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MainContent />
</Profiler>
);
}
2. Webpack Bundle Analyzer:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
}),
],
};
3. Lighthouse (Chrome DevTools):
# Запуск через CLI
npm install -g lighthouse
lighthouse https://example.com --view
4. Performance API:
// Замер времени загрузки компонента
function usePerformanceMeasure(componentName) {
useEffect(() => {
const start = performance.now();
return () => {
const end = performance.now();
console.log(`${componentName}: ${end - start}ms`);
};
}, [componentName]);
}
// Или через PerformanceObserver
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.duration}ms`);
}
});
observer.observe({ entryTypes: ['measure', 'navigation'] });
Практические техники оптимизации:
1. Прогрессивная загрузка данных:
function Dashboard() {
// Загружаем критические данные первыми
const { data: criticalData } = useQuery({
queryKey: ['critical'],
queryFn: fetchCriticalData,
});
// Некритические данные загружаем после
const { data: secondaryData } = useQuery({
queryKey: ['secondary'],
queryFn: fetchSecondaryData,
enabled: !!criticalData, // Только после загрузки критических
});
if (!criticalData) return <CriticalSkeleton />;
return (
<div>
<CriticalSection data={criticalData} />
{secondaryData ? (
<SecondarySection data={secondaryData} />
) : (
<SecondarySkeleton />
)}
</div>
);
}
2. Prefetch при наведении мыши:
function PrefetchLink({ to, children }) {
const prefetchRoute = useCallback(() => {
// Предзагрузка компонента
const component = import(`./pages/${to}`);
// Предзагрузка данных
queryClient.prefetchQuery({
queryKey: [to],
queryFn: () => fetchPageData(to),
});
}, [to]);
return (
<Link to={to} onMouseEnter={prefetchRoute}>
{children}
</Link>
);
}
3. Приоритизация ресурсов:
<!-- Preload критических ресурсов -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<!-- Prefetch для следующих страниц -->
<link rel="prefetch" href="/dashboard.js">
<link rel="prefetch" href="/settings.js">
<!-- Preconnect к внешним доменам -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
4. Оптимизация изображений:
function OptimizedImage({ src, alt, width, height }) {
const [loaded, setLoaded] = useState(false);
return (
<div style={{ position: 'relative', width, height }}>
{/* Placeholder во время загрузки */}
{!loaded && (
<div
style={{
position: 'absolute',
background: '#f0f0f0',
width: '100%',
height: '100%',
}}
/>
)}
<picture>
<source
srcSet={`${src}.webp`}
type="image/webp"
/>
<source
srcSet={`${src}.jpg`}
type="image/jpeg"
/>
<img
src={`${src}.jpg`}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
onLoad={() => setLoaded(true)}
style={{
opacity: loaded ? 1 : 0,
transition: 'opacity 0.3s',
}}
/>
</picture>
</div>
);
}
5. Оптимизация шрифтов:
/* Font Display Swap — текст виден сразу */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
unicode-range: U+0000-00FF; /* Только латиница */
}
/* Preload критических шрифтов */
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
>
6. Виртуализация для больших списков:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div
style={{
height: virtualizer.getTotalSize(),
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
7. React Query для кэширования:
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 минут
cacheTime: 10 * 60 * 1000, // 10 минут
refetchOnWindowFocus: false,
},
},
});
function DataComponent({ id }) {
const { data, isLoading } = useQuery({
queryKey: ['item', id],
queryFn: () => fetchItem(id),
placeholderData: () => {
// Используем данные из списка как placeholder
return queryClient
.getQueryData(['items'])
?.find((item) => item.id === id);
},
});
if (isLoading) return <Skeleton />;
return <div>{data.name}</div>;
}
Метрики производительности (Core Web Vitals):
| Метрика | Описание | Цель |
|---|---|---|
| LCP | Largest Contentful Paint | < 2.5s |
| FID | First Input Delay | < 100ms |
| CLS | Cumulative Layout Shift | < 0.1 |
| TTFB | Time to First Byte | < 800ms |
| FCP | First Contentful Paint | < 1.8s |
Пример расширенного ответа:
«...Для анализа использую React DevTools Profiler, Webpack Bundle Analyzer и Lighthouse. В одном проекте загрузка страницы занимала 30 секунд — решил проблему прогрессивной загрузкой: критические данные загружаются за 3 секунды, остальные — после.
Использую React.lazy с Suspense для code splitting по роутам. Prefetch при наведении мыши на ссылки. Для изображений — lazy loading, WebP с fallback, preload критических ресурсов.
Для больших списков — виртуализация через react-virtual. Для кэширования данных — React Query с staleTime и placeholderData. Шрифты — font-display: swap и preload...»
Вопрос 25. Какие Utility Types в TypeScript вы знаете и используете?
Таймкод: 00:51:43
Ответ собеседника: Правильный. Названы следующие Utility Types: Record, Partial, Required, Pick, Omit, Extract. Также упомянут редкий тип.
Правильный ответ:
Ответ хороший, но можно значительно расширить. Давайте рассмотрим все основные Utility Types с примерами.
Основные Utility Types:
1. Partial<T> — все свойства необязательные:
interface User {
id: number;
name: string;
email: string;
}
// Все свойства становятся необязательными
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }
function updateUser(id: number, updates: Partial<User>) {
// Можно передать только часть свойств
}
updateUser(1, { name: 'John' }); // OK
updateUser(1, { email: 'john@example.com' }); // OK
2. Required<T> — все свойства обязательные:
interface Config {
apiUrl?: string;
timeout?: number;
}
// Все свойства становятся обязательными
type RequiredConfig = Required<Config>;
// { apiUrl: string; timeout: number; }
const config: RequiredConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};
3. Readonly<T> — все свойства только для чтения:
interface User {
id: number;
name: string;
}
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = { id: 1, name: 'John' };
user.name = 'Jane'; // Error: Cannot assign to 'name' because it is a read-only property
4. Record<K, T> — объект с ключами K и значениями T:
// Объект с ключами-строками и значениями типа string
type StringMap = Record<string, string>;
const colors: StringMap = {
red: '#ff0000',
green: '#00ff00',
};
// Объект с конкретными ключами
type Role = 'admin' | 'user' | 'guest';
type RolePermissions = Record<Role, string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read'],
};
5. Pick<T, K> — выбрать только указанные свойства:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Только id и name
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }
const preview: UserPreview = { id: 1, name: 'John' };
6. Omit<T, K> — исключить указанные свойства:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Все свойства кроме password
type PublicUser = Omit<User, 'password'>;
// { id: number; name: string; email: string; }
const publicUser: PublicUser = {
id: 1,
name: 'John',
email: 'john@example.com',
};
7. Extract<T, U> — извлечь типы, которые есть в обоих:
type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';
type Common = Extract<A, B>; // 'b' | 'c'
// Практический пример
type Status = 'pending' | 'success' | 'error' | 'loading';
type FinalStatus = Extract<Status, 'success' | 'error'>; // 'success' | 'error'
8. Exclude<T, U> — исключить типы, которые есть в U:
type A = 'a' | 'b' | 'c';
type B = 'b' | 'c';
type OnlyA = Exclude<A, B>; // 'a'
// Практический пример
type Status = 'pending' | 'success' | 'error' | 'loading';
type NonFinalStatus = Exclude<Status, 'success' | 'error'>; // 'pending' | 'loading'
9. NonNullable<T> — исключить null и undefined:
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
function processString(value: NonNullable<MaybeString>) {
// value гарантированно не null и не undefined
return value.toUpperCase();
}
10. ReturnType<T> — тип возвращаемого значения функции:
function getUser() {
return { id: 1, name: 'John', email: 'john@example.com' };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string; }
// Практический пример
async function fetchUsers() {
const response = await fetch('/api/users');
return response.json();
}
type Users = Awaited<ReturnType<typeof fetchUsers>>;
11. Parameters<T> — типы параметров функции:
function createUser(name: string, age: number, email: string) {
// ...
}
type CreateUserParams = Parameters<typeof createUser>;
// [string, number, string]
// Практический пример
type UserParams = Parameters<typeof createUser>;
const params: UserParams = ['John', 30, 'john@example.com'];
createUser(...params);
12. Awaited<T> — извлечь тип из Promise:
type PromiseType = Promise<{ id: number; name: string }>;
type Resolved = Awaited<PromiseType>; // { id: number; name: string }
// Работает с вложенными Promise
type DeepPromise = Promise<Promise<string>>;
type DeepResolved = Awaited<DeepPromise>; // string
Дополнительные Utility Types:
// InstanceType — тип экземпляра класса
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
type UserInstance = InstanceType<typeof User>;
// User
// ThisType — тип this в объекте
interface Methods {
getValue(): number;
setValue(value: number): void;
}
interface State {
value: number;
}
const obj: State & ThisType<Methods> = {
value: 0,
getValue() {
return this.value; // this имеет тип Methods
},
setValue(value) {
this.value = value;
},
};
// OmitThisParameter — убрать параметр this
function toHex(this: Number) {
return this.toString(16);
}
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
console.log(fiveToHex()); // '5'
// ThisParameterType — извлечь тип this
function toHex2(this: Number) {
return this.toString(16);
}
type ThisType = ThisParameterType<typeof toHex2>; // Number
Практические примеры комбинирования:
// API Response типы
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
// Тип для создания пользователя (без id и createdAt)
type CreateUserDto = Omit<User, 'id' | 'createdAt'>;
// Тип для обновления (все поля необязательные, кроме id)
type UpdateUserDto = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;
// Публичный профиль (без email)
type PublicProfile = Omit<User, 'email'>;
// Словарь пользователей по ID
type UserMap = Record<number, User>;
// Только для чтения
type ReadonlyUserMap = Readonly<UserMap>;
Пример расширенного ответа:
«...Использую следующие Utility Types: Partial для обновления объектов, Required для конфигураций, Readonly для иммутабельности, Record для маппинга, Pick и Omit для выбора/исключения свойств, Extract и Exclude для работы с union типами, NonNullable для исключения null, ReturnType и Parameters для извлечения типов функций, Awaited для работы с Promise.
Часто комбинирую их: Omit<User, 'id'> & Pick<User, 'id'> для DTO, Record<Role, Permissions> для маппинга прав. Это делает типы более точными и переиспользуемыми...»
