Text
                    
Антон Кузьмин Разработка фронтендприложений 2025
ББК 32.988.02-018 УДК 004.738.5 К89 Кузьмин Антон К89 Разработка фронтенд-приложений. — СПб.: Питер, 2025. — 496 с.: ил. — (Серия «Библиотека программиста»). ISBN 978-5-4461-4272-9 Эта книга — ваш прямой путь к проектированию и реализации современных фронтенд-приложений. Автор, действующий руководитель команды разработчиков, делится знаниями, проверенными в реальных проектах. Вы не только изучите теорию, но и начнете применять ее через примеры кода, упражнения и задания, развивая навыки, востребованные у лидеров IT-рынка. Книга для тех, кто уже освоил JavaScript и хочет стать профи в фронтенд-разработке. 16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.) ББК 32.988.02-018 УДК 004.738.5 Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. В книге возможны упоминания организаций, деятельность которых запрещена на территории Российской Федерации, таких как Meta Platforms Inc., Facebook, Instagram и др. Издательство не несет ответственности за доступность материалов, ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию все ссылки на интернет-ресурсы были действующими. ISBN 978-5-4461-4272-9 © ООО Издательство «Питер», 2025 © Серия «Библиотека программиста», 2025 © Антон Кузьмин, 2025
КРАТКОЕ СОДЕРЖАНИЕ Введение..................................................................................................................................................... 17 От издательства.......................................................................................................................................... 23 Глава 1. Фронтенд-разработка.................................................................................................................. 24 Глава 2. С чем имеет дело фронтенд-разработчик.................................................................................. 35 Глава 3. Основы проектирования и разработки....................................................................................... 96 Глава 4. Состояние. ..................................................................................................................................135 Глава 5. UI.................................................................................................................................................206 Глава 6. Управление рендерингом компонентов с помощью изменяющегося URL.............................325 Глава 7. Файлы приложения...................................................................................................................364 Глава 8. Разработка фронтенд-приложения..........................................................................................370 Глава 9. Совместная работа над приложением......................................................................................467 Глоссарий..................................................................................................................................................484
ОГЛАВЛЕНИЕ Введение..................................................................................................................................................... 17 Что вам даст эта книга.................................................................................................................. 17 Почему стоит прочесть эту книгу, если вы планируете заняться . фронтенд-разработкой.................................................................................................................. 17 Для кого написана эта книга...................................................................................................... 18 Кратко о главах книги................................................................................................................... 18 Последовательность чтения глав.............................................................................................. 19 TypeScript.......................................................................................................................................... 20 Как читать эту книгу..................................................................................................................... 20 Почему упражнения и практические задания отделены от изучаемого . материала.......................................................................................................................................... 22 От издательства.......................................................................................................................................... 23 Глава 1. Фронтенд-разработка.................................................................................................................. 24 1.1. С чего начиналась фронтенд-разработка....................................................................... 24 1.1.1. Осознание способов решения проблем............................................................... 24 1.1.2. Приложения на основе страниц............................................................................. 24 1.1.3. Приложения на основе AJAX и библиотек........................................................ 25 1.1.4. Фронтенд-приложения............................................................................................. 26 1.1.5. Эпоха искусственного интеллекта и виртуальной реальности.................. 28 1.2. Разработка фронтенд-приложения................................................................................. 30 1.2.1. Окружение разработки............................................................................................. 30 1.2.2. Управление зависимостями.................................................................................... 31 1.2.3. Язык и кодирование. Трансляция......................................................................... 32 1.2.4. Сборка приложения................................................................................................... 32 1.2.5. Поиск проблем в коде................................................................................................ 33 1.2.6. Оформление кода........................................................................................................ 33 1.2.7. Тестирование................................................................................................................ 33 1.2.8. Контроль версий и обмен кодом............................................................................ 33 1.2.9. Код-ревью...................................................................................................................... 33 1.2.10. Заключение................................................................................................................. 34
Оглавление  5 Глава 2. С чем имеет дело фронтенд-разработчик.................................................................................. 35 2.1. Возможности программ на JavaScript............................................................................ 35 2.1.1. С чем и как взаимодействует программа на JS................................................. 35 2.1.2. Работа с данными........................................................................................................ 36 2.1.3. Платформенная функциональность.................................................................... 38 2.2. Платформы.............................................................................................................................. 39 2.2.1. Что представляют собой платформы и зачем они нужны............................ 39 2.2.2. Уникальность функциональности........................................................................ 39 2.2.3. Важные выводы........................................................................................................... 40 2.3. Браузер и его объекты функциональности................................................................... 40 2.3.1. Где платформа, а где JavaScript.............................................................................. 40 2.3.2. Браузер — платформа с набором программ, . предоставляющих API......................................................................................................... 42 2.3.3. Библиотеки................................................................................................................... 43 2.3.4. Библиотеки React и ReactDOM............................................................................ 44 2.3.5. Преимущество библиотек........................................................................................ 44 Упражнения и практические задания............................................................................ 45 2.4. Загрузка приложения........................................................................................................... 45 2.4.1. Как осуществляется загрузка................................................................................. 45 2.4.2. Парсинг содержимого строки, помеченной как HTML................................ 45 2.4.3. Прослушивание событий......................................................................................... 46 2.4.4. Жизнь приложения.................................................................................................... 47 2.4.5. Другой взгляд на HTML.......................................................................................... 47 2.5. DOM........................................................................................................................................... 47 2.5.1. Как все могло бы быть сложным........................................................................... 47 2.5.2. Что такое модель......................................................................................................... 48 2.5.3. Что такое документ.................................................................................................... 49 2.5.4. Что такое Document Object Model (DOM)........................................................ 50 2.5.5. Свойство document и операции в DOM.............................................................. 53 2.5.6. Операции поиска узлов в дереве........................................................................... 53 2.5.7. Обход дерева DOM.................................................................................................... 54 2.5.8. Создание и добавление узлов в дерево................................................................ 62 2.5.9. Удаление узлов из DOM-дерева........................................................................... 66 2.5.10. Изменение атрибутов и свойств узлов DOM-дерева................................... 68 2.5.11. Узлы дерева DOM и их свойства........................................................................ 72 Упражнения и практические задания............................................................................ 74 2.6. События..................................................................................................................................... 75 2.6.1. Что такое событие....................................................................................................... 75 2.6.2. Некоторые виды событий и источники.............................................................. 75 2.6.3. Физические действия................................................................................................ 76 2.6.4. Действия, генерируемые программой................................................................. 76 2.6.5. Событие — это процесс............................................................................................. 76 2.6.6. Работа с событиями в коде...................................................................................... 77 2.6.7. Группировка событий по типу............................................................................... 79
6  Оглавление 2.6.8. Объект события........................................................................................................... 84 2.6.9. Три стадии события................................................................................................... 85 2.6.10. Иная точка зрения на события............................................................................. 88 Упражнения и практические задания............................................................................ 88 2.7. Асинхронное программирование..................................................................................... 89 2.7.1. Инструкции языка программирования............................................................... 89 2.7.2. Кратко и упрощенно о работе браузера в операционной системе............. 89 2.7.3. Что такое асинхронное программирование....................................................... 90 2.7.4. Последовательное выполнение программы...................................................... 90 2.7.5. Event Loop..................................................................................................................... 91 2.7.6. Выполнение асинхронного кода в браузере. Задачи . и микрозадачи......................................................................................................................... 93 2.7.7. Главная разница между очередями задач и микрозадач............................... 94 2.7.8. Дополнительно............................................................................................................ 95 Глава 3. Основы проектирования и разработки....................................................................................... 96 3.1. О разработке приложения.................................................................................................. 96 3.1.1. Введение......................................................................................................................... 96 3.1.2. Смена точки зрения на разработку....................................................................... 96 3.1.3. Разработка информационных систем.................................................................. 97 3.2. Как разрабатываются информационные системы в организациях...................... 97 3.2.1. Работающее веб-приложение — это готовый продукт................................... 97 3.2.2. С чего начинается то, что мы видим как веб-приложение........................... 99 3.2.3. Требования.................................................................................................................... 99 3.2.4. Краткое описание всего процесса разработки................................................100 3.2.5. Команда........................................................................................................................101 3.2.6. За что несет ответственность фронтенд-разработчик..................................102 3.3. Граф. Дерево..........................................................................................................................103 3.3.1. Введение.......................................................................................................................103 3.3.2. Граф. Вершина. Ребро. Дуга..................................................................................103 3.3.3. Неориентированный и ориентированный графы..........................................105 3.3.4. Взвешенный граф......................................................................................................108 3.3.5. Компоненты связности...........................................................................................109 3.3.6. Маршрут. Цепь. Путь. Цикл.................................................................................110 3.3.7. Дерево...........................................................................................................................113 3.3.8. Свойства графов, дающие отличительные особенности.............................116 3.3.9. Моделирование с помощью графов....................................................................117 3.3.10. Продолжайте изучать графы..............................................................................118 Упражнения и практические задания..........................................................................118 3.4. Система....................................................................................................................................118 3.4.1. Введение.......................................................................................................................118 3.4.2. Что такое система.....................................................................................................118 3.4.3. Строение и действие системы..............................................................................119 3.4.4. Понятия, описывающие составляющие............................................................120
Оглавление  7 3.4.5. Понятия, описывающие действия системы.....................................................121 3.4.6. Признак системы .....................................................................................................122 3.4.7. Примеры связей.........................................................................................................122 3.4.8. Виды структур............................................................................................................122 3.4.9. Важное свойство структуры фронтенд-приложения...................................126 3.4.10. Заключение...............................................................................................................126 3.5. Проектирование...................................................................................................................126 3.5.1. Что такое проектирование.....................................................................................126 3.5.2. Двойная работа..........................................................................................................129 3.5.3. Проектирование приложения — проектирование системы.......................129 3.5.4. Определение компонентов — страниц приложения.....................................130 3.5.5. Создание структуры UI-компонентов приложения.....................................130 3.5.6. Определение источников данных.......................................................................131 3.5.7. Состояние....................................................................................................................132 3.5.8. Не только UI-компоненты.....................................................................................132 3.5.9. Откладывание решения..........................................................................................132 3.6. Реализация приложения...................................................................................................132 3.6.1. Реализация..................................................................................................................132 3.6.2. Разница между проектированием и реализацией.........................................133 3.6.3. Цель реализации.......................................................................................................133 3.6.4. Порядок реализации................................................................................................133 3.6.5. На какие области обратить внимание при реализации...............................134 3.6.6. Взаимная зависимость проектирования и реализации...............................134 Глава 4. Состояние. ..................................................................................................................................135 4.1. Библиотека Redux...............................................................................................................135 4.1.1. Состояние приложения..........................................................................................135 4.1.2. Для чего нужна библиотека redux.......................................................................135 4.1.3. Развитие и текущее состояние.............................................................................135 4.1.4. Redux.............................................................................................................................136 4.1.5. Порядок действий.....................................................................................................137 4.2. Стор...........................................................................................................................................139 4.2.1. Главный компонент библиотеки.........................................................................139 4.2.2. Ключевые принципы стора...................................................................................139 4.2.3. Работа с библиотекой в приложении.................................................................139 4.2.4. Методы стора..............................................................................................................140 4.2.5. Некоторые доводы в пользу библиотеки..........................................................140 4.3. Редьюсер.................................................................................................................................141 4.3.1. Что такое редьюсер...................................................................................................141 4.3.2. Чистая функция........................................................................................................142 4.3.3. Неизменяемость........................................................................................................143 4.3.4. Какую структуру стора выбрать..........................................................................144 4.3.5. Начальное состояние...............................................................................................147 4.3.6. Наименование редьюсера.......................................................................................147
8  Оглавление 4.4. Экшен. Селектор..................................................................................................................147 4.4.1. Что такое экшен.........................................................................................................147 4.4.2. Наименование экшена.............................................................................................148 4.4.3. Action creators.............................................................................................................149 4.4.4. Хранение наименований типов экшенов в виде констант..........................150 4.4.5. Что такое селектор....................................................................................................150 4.4.6. Зачем нужен селектор.............................................................................................150 Упражнения и практические задания..........................................................................152 4.5. Middleware. Enhancer..........................................................................................................152 4.5.1. Что такое middleware...............................................................................................152 4.5.2. Разница между next и store.dispatch...................................................................154 4.5.3. Как использовать middleware для асинхронного кода.................................154 4.5.4. Enhancer.......................................................................................................................156 4.5.5. Как создать свой энхансер.....................................................................................158 Упражнения и практические задания..........................................................................158 4.6. Redux Toolkit.........................................................................................................................158 4.6.1. Что представляет собой библиотека..................................................................158 4.6.2. Redux Thunk...............................................................................................................159 4.6.3. Reselect..........................................................................................................................160 4.6.4. Immer.............................................................................................................................161 4.6.5. Autodux.........................................................................................................................162 4.6.6. Redux DevTools..........................................................................................................163 4.6.7. RTK Query...................................................................................................................164 4.6.8. normalizr.......................................................................................................................164 4.7. Установка Redux Toolkit и настройка стора..............................................................165 4.7.1. Установка Redux Toolkit........................................................................................165 4.7.2. Создание и настройка стора..................................................................................166 4.7.3. Объект настроек configureStore...........................................................................167 4.7.4. Middleware в configureStore..................................................................................168 4.7.5. Энхансеры в configureStore...................................................................................170 4.8. createSlice................................................................................................................................171 4.8.1. Что делает createSlice..............................................................................................171 4.8.2. Свойства конфигурационного объекта createSlice.......................................172 4.8.3. initialState....................................................................................................................172 4.8.4. reducers.........................................................................................................................173 4.8.5. extraReducers..............................................................................................................175 4.8.6. reducerPath..................................................................................................................175 4.8.7. selectors.........................................................................................................................175 4.8.8. Возвращаемое значение из createSlice...............................................................176 4.8.9. Функция в свойстве reducers................................................................................176 4.8.10. combineSlices............................................................................................................178 4.8.11. inject и injectInto.....................................................................................................179 4.8.12. extraReducers............................................................................................................181 Упражнения и практические задания..........................................................................182
Оглавление  9 4.9. RTK Query..............................................................................................................................183 4.9.1. Для чего предназначен RTK Query....................................................................183 4.9.2. Использование RTK Query...................................................................................184 4.9.3. Внедрение в код для совместного использования с другим состоянием Redux Toolkit...............................................................................185 4.9.4. createApi.......................................................................................................................185 4.9.5. Объект конфигурации createApi.........................................................................186 4.9.6. baseQuery и queryFn.................................................................................................186 4.9.7. endpoints.......................................................................................................................188 4.9.8. reducerPath..................................................................................................................189 4.9.9. tagTypes........................................................................................................................189 4.9.10. Оставшиеся свойства............................................................................................190 4.9.11. setupListeners............................................................................................................191 4.9.12. Возвращаемое значение функцией createApi...............................................191 4.9.13. middleware.................................................................................................................192 4.9.14. endpoints.....................................................................................................................192 4.9.15. reducer.........................................................................................................................192 4.9.16. Другие свойства и методы...................................................................................192 Упражнения и практические задания..........................................................................193 4.10. Redux DevTools.....................................................................................................................193 4.10.1. Redux DevTools и установка...............................................................................193 4.10.2. Подключение к приложению.............................................................................193 4.10.3. Интерфейс DevTools.............................................................................................194 4.10.4. Режимы экшенов....................................................................................................195 4.10.5. Действия с экшенами............................................................................................195 4.10.6. Выбор приложения................................................................................................195 4.10.7. Выбор инструмента обзора данных ................................................................196 4.10.8. Хронология экшенов.............................................................................................197 4.10.9. Выбор источника....................................................................................................198 4.10.10. Вид представления данных и их детальный просмотр...........................198 4.10.11. Диспатчер, слайдер..............................................................................................199 Упражнения и практические задания..........................................................................201 4.11. Как разрабатывать приложение с использованием Redux Toolkit.....................201 4.11.1. Что позволяет делать Redux Toolkit (RTK)..................................................201 4.11.2. Процедура разработки..........................................................................................201 4.11.3. Как выделить состояние и примеры................................................................202 4.11.4. Шаг 3 процедуры разработки приложения...................................................202 Упражнения и практические задания..........................................................................203 4.12. Что нужно изучить самостоятельно..............................................................................203 4.12.1. Уже изучено..............................................................................................................203 4.12.2. Redux Toolkit. Редьюсеры и экшены...............................................................203 4.12.3. Redux Toolkit. Middleware . ................................................................................204 4.12.4. RTK Query................................................................................................................204
10  Оглавление Глава 5. UI.................................................................................................................................................206 5.1. О библиотеке React.js.........................................................................................................206 5.1.1. Для чего нужна React..............................................................................................206 5.1.2. Необходимость библиотеки..................................................................................206 5.1.3. Ответственность библиотеки...............................................................................207 5.1.4. Как устроено взаимодействие библиотек с браузером................................208 5.1.5. Немного о компоненте............................................................................................209 5.2. Элемент, компонент, приложение.................................................................................209 5.3. Вывод компонента: JSX.....................................................................................................210 5.3.1. Чем является компонент........................................................................................210 5.3.2. История развития компонентов в React...........................................................212 5.3.3. Обработка данных современного компонента . на основе функции..............................................................................................................214 5.3.4. Эффекты компонента..............................................................................................214 5.3.5. Вывод компонента и JSX........................................................................................215 5.3.6. Что возвращает компонент....................................................................................216 5.4. Вывод компонента: композиция.....................................................................................217 5.4.1. Композиция компонентов.....................................................................................217 5.4.2. Более сложная композиция...................................................................................219 Упражнения и практические задания..........................................................................221 5.5. Обработка данных и их источники: свойства............................................................222 5.5.1. Источники данных компонента...........................................................................222 5.5.2. Что такое свойства....................................................................................................222 5.5.3. Константы....................................................................................................................223 Упражнения и практические задания..........................................................................224 5.6. Обработка данных и их источники: состояние..........................................................224 5.6.1. Состояние....................................................................................................................224 5.6.2. Как создается состояние компонента................................................................224 5.6.3. Хук..................................................................................................................................225 5.6.4. Хуки в React................................................................................................................225 5.6.5. Хук useState................................................................................................................226 5.6.6. Изменение состояния и рендер............................................................................228 5.6.7. Изменение состояния в компоненте..................................................................229 5.6.8. Рендеры и их отображение на веб-странице...................................................229 5.6.9. Хуки React и рендеры..............................................................................................231 5.6.10. Если не изменилось состояние..........................................................................231 5.6.11. Общепринятый способ именования................................................................231 5.6.12. Важные выводы.......................................................................................................231 Упражнения и практические задания..........................................................................232 5.7. Эффекты.................................................................................................................................232 5.7.1. Что такое эффект......................................................................................................232 5.7.2. Хук useEffect...............................................................................................................232 5.7.3. Зависимости...............................................................................................................234 5.7.4. Сторонние API и события......................................................................................236
Оглавление  11 5.8. 5.9. 5.10. 5.11. 5.12. 5.7.5. Зависимости, не принимающие участия ни в установке, ни в функции очистки........................................................................................................236 5.7.6. Порядок вызова.........................................................................................................236 Упражнения и практические задания..........................................................................238 Дополнительно о компонентах.......................................................................................238 5.8.1. «Виды» компонентов...............................................................................................238 5.8.2. Свойство children......................................................................................................239 5.8.3. Использование метода массивов map для вывода.........................................240 5.8.4. Слот для JSX...............................................................................................................241 5.8.5. Передача компонентов по ссылке.......................................................................243 Упражнения и практические задания..........................................................................244 Обработка событий.............................................................................................................244 5.9.1. Как назначить обработчик событий...................................................................244 5.9.2. Порядок действий для назначения обработчика...........................................246 5.9.3. Виды событий в React.............................................................................................246 5.9.4. Именование обработчиков событий .................................................................246 5.9.5. Объект события React.............................................................................................247 5.9.6. Назначение событий за пределами React.........................................................248 Упражнения и практические задания..........................................................................249 Группа компонентов, работающих в связке...............................................................250 5.10.1. Обмен данными между компонентами...........................................................250 5.10.2. Обмен данными между компонентами одного уровня.............................251 5.10.3. Подъем состояния..................................................................................................252 5.10.4. Несколько компонентов, которые должны знать состояние . друг друга................................................................................................................................254 5.10.5. Если компоненты импортируются из сторонней . библиотеки.............................................................................................................................254 5.10.6. Если ближайший общий родитель не является . непосредственным родителем обоих компонентов..................................................254 Контекст и хук useContext................................................................................................254 5.11.1. Что такое контекст и зачем нужно с ним работать.....................................254 5.11.2. Как использовать контекст в коде....................................................................257 5.11.3. Изменение контекста............................................................................................259 5.11.4. Обратная связь от дочерних компонентов в корневой . компонент...............................................................................................................................260 5.11.5. Несколько контекстов...........................................................................................261 Упражнения и практические задания..........................................................................262 Формы......................................................................................................................................262 5.12.1. Что такое формы.....................................................................................................262 5.12.2. Входные данные......................................................................................................262 5.12.3. Формы и React.........................................................................................................263 5.12.4. Теги для создания форм.......................................................................................263 5.12.5. Виды тегов, используемых для создания форм...........................................264 5.12.6. Управляемые компоненты..................................................................................264
12  Оглавление 5.13. 5.14. 5.15. 5.16. 5.17. 5.12.7. input.............................................................................................................................265 5.12.8. События.....................................................................................................................267 5.12.9. form..............................................................................................................................268 5.12.10. FormData.................................................................................................................269 5.12.11. Валидация...............................................................................................................271 5.12.12. Дополнительные источники.............................................................................274 НОС..........................................................................................................................................275 5.13.1. Что такое НОС........................................................................................................275 5.13.2. Перерендеры и измерение производительности.........................................276 5.13.3. Оптимизация перерендеров................................................................................279 5.13.4. Второй параметр НОС memo.............................................................................280 useReducer — второй хук для работы с состоянием.................................................281 5.14.1. Хук useReducer........................................................................................................281 5.14.2. Механизм работы составляющих.....................................................................282 5.14.3. Задание начального состояния..........................................................................284 5.14.4. Порядок действий для создания состояния в компоненте с помощью useReducer.......................................................................................................285 5.14.5. Использование useReducer..................................................................................285 Упражнения и практические задания..........................................................................285 useRef........................................................................................................................................286 5.15.1. Хук useRef.................................................................................................................286 5.15.2. Где чаще всего применяется хук ......................................................................287 5.15.3. Совместная работа хука useRef и атрибута элемента ref..........................287 5.15.4. Практика применения useRef в DOM-узлах.................................................288 5.15.5. Хранение ссылки на DOM-узел в родителе..................................................289 Упражнения и практические задания..........................................................................291 Другие хуки React................................................................................................................291 5.16.1. useMemo.....................................................................................................................291 5.16.2. Когда имеет смысл использовать useMemo...................................................293 5.16.3. Хук useCallback.......................................................................................................293 5.16.4. В каких случаях использовать хук useCallback...........................................294 5.16.5. Как использовать хук useCallback....................................................................295 5.16.6. Разница между useMemo и useCallback..........................................................296 5.16.7. Хук useId....................................................................................................................297 Кастомные хуки....................................................................................................................298 5.17.1. Что такое кастомный хук.....................................................................................298 5.17.2. Переиспользование обработки данных .........................................................298 5.17.3. Составляющие кастомного хука и некоторые правила.............................298 5.17.4. Как написать кастомный хук..............................................................................301 5.17.5. Хуки React, задействуемые в кастомных хуках...........................................302 5.17.6. Как приспособиться писать кастомный хук или просто облегчить его создание................................................................................303 Упражнения и практические задания..........................................................................303
Оглавление  13 5.18. Объединение библиотек React и Redux.......................................................................304 5.18.1. Библиотека React-Redux......................................................................................304 5.18.2. Provider.......................................................................................................................304 5.18.3. Хуки.............................................................................................................................305 5.19. Новый взгляд на React.......................................................................................................306 5.19.1. Что здесь освещено................................................................................................306 5.19.2. На что направлять внимание..............................................................................306 5.19.3. Структура приложения и роли компонентов в ней....................................307 5.19.4. Чтение компонента................................................................................................310 5.19.5. Обмен данными между компонентами...........................................................311 5.19.6. Все есть функция, но роли разные....................................................................318 5.19.7. DAG.............................................................................................................................320 5.19.8. DAG React-компонентов......................................................................................320 5.19.9. DAG вызовов → DOM..........................................................................................321 Упражнения и практические задания..........................................................................323 5.20. Что нужно изучить самостоятельно..............................................................................323 5.20.1. Хуки.............................................................................................................................323 5.20.2. lazy................................................................................................................................324 5.20.3. startTransition и useTransition............................................................................324 Глава 6. Управление рендерингом компонентов с помощью изменяющегося URL.............................325 6.1. О библиотеке React-Router-Dom...................................................................................325 6.1.1. Назначение библиотеки.........................................................................................325 6.1.2. React Router позволяет создавать SPA.............................................................326 6.2. Визуализация работы React Router...............................................................................327 6.2.1. Граф компонентов приложения без библиотеки React Router.................327 6.2.2. Граф компонентов приложения, управляемый с помощью библиотеки React Router............................................................................328 6.2.3. Заключение.................................................................................................................331 6.3. Конструирование графа приложения с помощью конфигурации......................331 6.3.1. Конфигурация — главный настраиваемый элемент.....................................331 6.3.2. Способы задания конфигурации приложения...............................................331 Упражнения и практические задания..........................................................................334 6.4. Настройка Route..................................................................................................................334 6.4.1. Указание более одного сегмента в пути............................................................335 6.4.2. Случай первый, когда указано более одного сегмента в пути...................335 6.4.3. Случай второй, когда маршрут содержит вложенные . маршруты................................................................................................................................336 6.4.4. Динамический сегмент и параметры URL.......................................................338 Упражнения и практические задания..........................................................................339 6.5. Загрузка данных и обработка ошибок..........................................................................340 6.5.1. Что такое загрузка данных.....................................................................................340 6.5.2. Динамический сегмент и загрузка данных......................................................340
14  Оглавление 6.6. 6.7. 6.8. 6.9. 6.5.3. Параметр загрузчика request.................................................................................341 6.5.4. Возврат ответов.........................................................................................................341 6.5.5. Утилита json................................................................................................................342 6.5.6. Возврат ошибки.........................................................................................................342 6.5.7. errorElement................................................................................................................342 6.5.8. Всплытие ошибки.....................................................................................................344 6.5.9. Заключение.................................................................................................................344 Компонент Form...................................................................................................................344 6.6.1. Что представляет собой компонент Form........................................................344 6.6.2. action..............................................................................................................................345 6.6.3. method...........................................................................................................................346 6.6.4. state................................................................................................................................349 6.6.5. Заключение.................................................................................................................350 Лэйаут......................................................................................................................................350 6.7.1. Псевдолэйаут..............................................................................................................352 6.7.2. Лэйаут для одного маршрута................................................................................352 6.7.3. Маршрут с лэйаутом................................................................................................352 6.7.4. Link.................................................................................................................................353 Упражнения и практические задания..........................................................................353 Составляющие библиотеки..............................................................................................354 6.8.1. Функции......................................................................................................................354 6.8.2. Хуки...............................................................................................................................355 6.8.3. Компоненты................................................................................................................358 Что изучить самостоятельно............................................................................................360 6.9.4. Определения терминов, используемых в React Router...............................362 Глава 7. Файлы приложения...................................................................................................................364 7.1. Как организовать файлы приложения.........................................................................364 7.1.1. Цель...............................................................................................................................364 7.1.2. Элементы приложения, размещаемые в файлах............................................364 7.1.3. Принцип 1: роли компонентов — главный критерий их распределения по папкам............................................................................................365 7.1.4. Принцип 2: группировка и глубина размещения остальных . элементов в папках зависит от количества ролей компонентов, . в которых они задействуются............................................................................................... 366 7.1.5. Чтение...........................................................................................................................367 7.1.6. Количество элементов в файле и условие, уточняющее . принцип 2................................................................................................................................368 7.1.7. Файл index.js...............................................................................................................368 7.1.8. Компоненты, используемые только в одном месте.......................................368 7.1.9. Конкретное назначение функциональности...................................................369 7.1.10. Рост приложения....................................................................................................369 Упражнения и практические задания..........................................................................369
Оглавление  15 Глава 8. Разработка фронтенд-приложения..........................................................................................370 8.1. Проектирование 3RApp.....................................................................................................370 8.1.1. Основное выполняемое действие. Что принять во внимание...................370 8.1.2. Процедура проектирования..................................................................................371 8.1.3. Структура приложения...........................................................................................389 8.1.4. Качество исполнения схем....................................................................................390 8.1.5. Если изменять последовательность шагов процедуры или игнорировать проектирование вовсе и сразу начинать . с написания кода..................................................................................................................390 8.1.6. Дополнительный стимул, или Как не делать ненужную работу..............392 Упражнения и практические задания..........................................................................392 8.2. Реализация 3RApp. Настройки.......................................................................................392 8.2.1. Настройки....................................................................................................................393 8.2.2. Подготовка файлов приложения перед написанием кода..........................394 8.2.3. Аналитика....................................................................................................................396 8.2.4. Выбор подхода использования CSS...................................................................398 8.3. Реализация 3RApp. Написание кода.............................................................................403 8.3.1. Что подразумевается под реализацией.............................................................403 8.3.2. Очередность реализации компонентов.............................................................403 8.3.3. Пример очередности реализации компонентов.............................................404 8.3.4. Объект конфигурации стора ReduxToolkit.....................................................406 8.3.5. Объект конфигурации маршрутизатора React Router.................................406 8.3.6. Составляющие, необходимые для реализации компонента......................406 8.3.7. Исходные данные в отношении поведения, реализуемого . в компоненте..........................................................................................................................409 8.3.8. Решение, которое нужно придумать для реализации . компонента.............................................................................................................................412 8.3.9. Реализация компонента..........................................................................................412 8.3.10. Хуки.............................................................................................................................425 8.3.11. Время, затрачиваемое на реализацию.............................................................426 8.3.12. Семантический HTML.........................................................................................427 Упражнения и практические задания..........................................................................427 8.4. Пример реализации компонентов с помощью конструктора...............................427 8.4.1. Layout............................................................................................................................428 8.4.2. ShedulePage.................................................................................................................429 8.4.3. SheduleItemDetails....................................................................................................434 8.4.4. Заключение.................................................................................................................438 8.5. Использование процедуры проектирования 3RApp и реализация . 3RApp-приложения.............................................................................................................438 8.5.1. Этап 1. Применение процедуры проектирования . 3RApp-приложения.............................................................................................................439 8.5.2. Этап 2. Реализация 3RApp.....................................................................................451 Упражнения и практические задания..........................................................................457
16  Оглавление 8.6. Использование нейросетей во фронтенд-разработке.............................................458 8.6.1. Причина........................................................................................................................458 8.6.2. Новое поколение инструментов..........................................................................458 8.6.3. GigaChat.......................................................................................................................458 8.6.4. Как задействовать ....................................................................................................458 8.6.5. Немного про «общение» с ИИ.............................................................................460 8.6.6. Используем GigaChat для написания кода......................................................461 8.6.7. Используем GigaCode для написания кода.....................................................463 Упражнения и практические задания..........................................................................466 Глава 9. Совместная работа над приложением......................................................................................467 9.1. Как изучать существующее приложение.....................................................................467 9.1.1. Метод погружения....................................................................................................467 9.1.2. Формирование структуры.....................................................................................469 9.1.3. Вывод............................................................................................................................470 9.2. Код-ревью...............................................................................................................................471 9.2.1. Что такое код-ревью.................................................................................................471 9.2.2. Эмоции и код-ревью................................................................................................471 9.2.3. Качество код-ревью..................................................................................................472 9.2.4. Неочевидное становится явным..........................................................................473 9.2.5. Код-ревью — это групповой процесс ................................................................473 9.2.6. Быть в курсе изменений.........................................................................................473 9.2.7. Обучаться....................................................................................................................474 9.2.8. Обучать.........................................................................................................................474 9.2.9. Код-ревью — это общение......................................................................................474 9.3. Налаживаем процедуру код-ревью в группе..............................................................475 9.3.1. Код-ревью — этап процесса производства программного продукта.......475 9.3.2. Составляющие код-ревью......................................................................................476 9.3.3. Пример взаимодействия при код-ревью...........................................................477 9.3.4. Группа в мессенджере..............................................................................................477 9.3.5. Время прохождения код-ревью. Где могут возникать трудности............478 9.3.6. Инструмент и метод создания качественного ПР.........................................478 9.3.7. Как внедрить или улучшить процедуру код-ревью......................................479 9.4. Как быстро стать своим в новом коллективе.............................................................479 9.4.1. Нужны ли написанные правила..........................................................................480 9.4.2. Правила крупных компаний.................................................................................480 9.4.3. Новые договоренности............................................................................................481 9.4.4. Как составить чек-лист...........................................................................................481 9.4.5. Как составить чек-лист, если у группы нет практики код-ревью............483 Глоссарий..................................................................................................................................................484
ВВЕДЕНИЕ Что вам даст эта книга Вы научитесь проектировать и реализовывать фронтенд-приложения 3RApp, вести совместную работу по код-ревью, изучать существующие приложения. Разберетесь с библиотеками React, React Router и Redux Toolkit. По-новому взглянете на React и научитесь разрабатывать с бо́льшей легкостью. Познакомитесь с основной идеей браузера. Узнаете, как располагать компоненты приложения в файлах и папках и почему именно так. А еще — как использовать нейросети для ускорения разработки. Почему стоит прочесть эту книгу, если вы планируете заняться фронтенд-разработкой Наверняка вы не доверяете всем подряд — и правильно делаете. Тогда почему вы должны доверять автору книги? Конечно, за годы становления IT-сферы ей посвятило время множество людей, имена которых сегодня были бы неизвестны, если бы не их открытия и публикации. Кто-то сформулировал принципы, кто-то ввел повторяющиеся способы решения задач, кто-то создал целые области и направления. Без практического применения знаний подобное немыслимо. Поэтому данная книга полностью построена на практике. Это не скомпилированный1 и не перефразированный материал, а итог реальных достижений. Автор — действующий лидер команды разработчиков в одной из известных организаций. Неважно, кто вы — начинающий или опытный разработчик. Здесь собраны знания, которые помогут вам понять фронтенд-разработку как таковую или ее конкретные стороны. Представьте себе, что вы пришли на новое место работы и получили наставника, погружающего новичка во фронтенд-разработку с нуля до полного понимания. Ведь нет разницы, как он это сделает. Данная книга — одна из возможных форм наставничества. Добро пожаловать на корабль фронтенд-разработки! В путь! 1 Компилировать — делать компиляцию (в информатике это слово имеет другое значение, здесь приведено обычное, литературное). Компиляция (от лат. compilatio, букв. — «ограб­ ление») — составление сочинений на основе чужих исследований или чужих произведений (литературная компиляция) без самостоятельной обработки источников; работа, составленная таким методом.
18  Введение Для кого написана эта книга Книга написана для тех, кто уже знаком с языком программирования JavaScript (JS) и хотел бы войти во фронтенд-разработку, а также подтянуть свои знания в этой области. Цель книги — обучение. Как автор я хочу, чтобы вы научились понимать изучаемое. Понимание отличается от запоминания: запоминая, вы копируете и не сможете воспользоваться тем, что запомнили, если изменятся условия, а понимая, приобретаете способность применять полученные знания независимо от условий, в которых все происходит. Книга содержит основы, без владения которыми разработчик действует не понимая — чаще всего просто копирует примеры из интернета и вставляет их в свой код. Моя задача как автора — изменить ситуацию и добиться того, чтобы сложилась картина, проясняющая, как задействовать изучаемое. Кратко о главах книги Глава 1 «Фронтенд-разработка» представляет собой ознакомительное введение в разработку фронтенд-приложений. Вы узнаете, из каких элементов она состоит на уровне предприятия. В англоязычной среде это называется enterprise. Глава 2 «С чем имеет дело фронтенд-разработчик» вобрала в себя разрозненные темы, являющиеся базовыми составляющими разработки. Начнем с того, что можно создать на JavaScript. Затем рассмотрим, какую функциональность дает конкретная платформа, подробно изучим браузер и асинхронное написание программы. Здесь же будет раскрыт принцип работы UI-библиотек. Глава 3 «Основы проектирования и разработки» раскрывает основы, на которых строится создание приложений: как появляется продукт в организациях, как применять графы для моделирования и свободно понимать составляющие приложений. Кроме того, дан обзор проектирования и разработки, включающей в себя не только написание кода. Глава 4 «Состояние» знакомит с базовыми принципами работы библиотек Redux и Redux Toolkit, а также с их составляющими и правилами применения библиотек в коде приложения. Глава 5 «UI» знакомит с основными элементами библиотеки React и правилами ее использования в коде приложения. Компоненты рассмотрены не только как статические единицы, но и во взаимодействии. Другие составляющие библиотеки раскрыты детально. А в конце главы, в разделе «Новый взгляд на React», собрана по-настоящему важная информация. С этой главы начинает формироваться профессиональный взгляд на приложение. Глава 6 «Управление рендерингом компонентов с помощью изменяющегося URL» описывает ключевые этапы работы библиотеки React-Router-Dom (React Router), ее составляющие и правила ее применения в коде приложения. Эта глава учит создавать приложения любой сложности и задавать в коде компоненты, зависящие от пути в URL любой длины.
Последовательность чтения глав  19 Глава 7 «Файлы приложения» учит организовывать файлы приложения. Здесь раскрыт способ расположения элементов приложения в файлах и папках, который позволяет быстро понять, с чем имеет дело тот или иной элемент приложения. Будет очень кстати знание материалов из предыдущих глав. Глава 8 «Разработка фронтенд-приложения» полностью строится на информации предыдущих глав и раскрывает, как осуществить проектирование для 3RApp, а также какие области к этому относятся. Она знакомит с процедурой проектирования 3RApp (кстати, впервые описанной именно в литературе по фронтенд-разработке) и процедурой реализации. В ней впервые представлена последовательность создания компонентов и напоследок — основы использования нейросетей для ускорения разработки. Ведь эта книга о современной фронтенд-разработке. Глава 9 «Совместная работа над приложением» помогает новичкам изучить приложение, содержит информацию о код-ревью и о том, как наладить эту процедуру в группе. И кроме того, учит быстрой адаптации в новом коллективе. Глоссарий — это список терминов, используемых в книге. Последовательность чтения глав Знакомство с каждой темой, относящейся к фронтенд-разработке, может проходить по-разному. Глубина погружения в ту или иную тему тоже у каждого своя. Например, с DOM связано множество объектов и методов. У всех разный опыт работы с ним, а также чтения информации по нему. Один разработчик мог ничего не читать о DOM, но взаимодействовать с объектами и методами в коде. Другой только читал. Третий и читал, и взаимодействовал. Трудно предсказать, какой у читателя уровень владения темами, изложенными в книге. Поэтому каждая тема описывается как незнакомая или малознакомая. Я разделил читателей на группы в зависимости от имеющегося опыта и уровня подготовки и могу дать следующие рекомендации. Всем читателям. Начав читать главу, просмотрите ее разделы. Если тема конкретного раздела вам знакома, пропускайте его и переходите к следующему. Однако если вы не уверены в своих знаниях, пробегитесь по разделу — возможно, есть что-то, с чем вам еще не приходилось сталкиваться. Начинающим фронтенд-разработчикам. Рекомендую последовательно изучать главы книги. Практикующим фронтенд-разработчикам. Я рекомендую более сложную последовательность действий. y Главы 1 и 2 — просмотреть и, если найдется что-то, с чем не сталкивались, ознакомиться. y Глава 3 — ознакомиться, особенно если разделы 3.2–3.4 вызывают вопросы. y Главы 4, 5 и 6 — просмотреть названия разделов. Вероятно, возникнет желание в чем-то лучше разобраться. Обязательно изучить раздел 5.19, где представлена абсолютно новая информация. И скорее всего — полностью главу 6. y Глава 7 — обязательно ознакомиться.
20  Введение y Глава 8 — обязательно ознакомиться. Подобный материал для фронтенд- разработки публикуется впервые. y Глава 9 — желательно ознакомиться. y К глоссарию можно обращаться периодически за разъяснением того или иного слова. Учитывайте также, что многие разделы завершаются упражнениями для закрепления материала и практическими заданиями для самостоятельной работы. Ответы прилагаются. TypeScript Разработка уровня энтерпрайз1 ведется на языке TypeScript, но в книге для целей обучения используется JavaScript. Это сделано намеренно. TypeScript включает в себя типы и дополнительные элементы языка, без знания которых трудно читать код примеров и тем более самостоятельно писать код. Может получиться такая картина: вы полны энтузиазма написать код задания, но сталкиваетесь с банальной проблемой языка TypeScript и вместо того, чтобы решать задачу, начинаете разбираться с ней. В итоге вы зря тратите время, злитесь и через пару примеров все бросаете. Использование же JavaScript исключает борьбу с неожиданными и неизвестными проблемами. Программированию на TypeScript нужно учиться отдельно. Отличное знание ключевых данных, изложенных в книге, плюс знание библиотек и TypeSrcipt равно крутой энтерпрайз. И не забываем об алгоритмах для построения решений и о работе со структурами данных. Как читать эту книгу Пытайтесь вникать в идеи. Если что-то не поняли сразу, разберитесь. Разобраться — значит понять все составляющие. Точно не нужно запоминать. Если поймете прочитанное, то вам будет легко этим пользоваться. Самый правильный способ изучения таков: прочитать, понять и сразу попробовать использовать. У вас будет достаточно упражнений и практических заданий. Упражнения ориентированы на тренировку нового — вы повторяете только что изученное в коде. Через них я, автор книги, веду вас к усвоению материала. Практические задания подразумевают самостоятельную работу и нахождение решения, когда нужно создать код или что-то смоделировать в виде изображения, а главное — применить пройденный материал. Они составлены так, что их нельзя выполнить бездумно. Это сделано намеренно — чтобы определить, понят материал 1 Энтерпрайз — русское произношение английского слова enterprise. Под этим словом подразумевается написание приложений для организаций, оказывающих услуги широким слоям населения, а также то, что предприятие использует надежные решения. Надежным можно считать решение, которое имело опыт эксплуатации и протестировано в разных ситуациях, а также имеет стабильную версию.
Как читать эту книгу  21 или нет. Если вы не можете выполнить упражнение, значит, есть момент, в котором не разобрались. Либо необходимо повторить попытку. В любом случае если вам не удается сделать практическое задание сейчас, то в будущем наверняка возникнут аналогичные сложности с этой темой — пока вы не разберетесь по-настоящему. В книгах по IT-технологиям используются разные подходы. Я предлагаю такой. Одна область разработки — это глава. Глава делится на разделы. Раздел — отдельная тема. Например, глава «UI» рассказывает о React, а «Формы» и «Эффекты» — разделы, где описываются конкретные вещи. Чтобы узнать новый материал и попрактиковаться в его применении, я предлагаю вам следующий порядок действий. 1. Вы изучаете один раздел. 2. Открываете упражнения, приведенные после раздела, и тренируете конкретные моменты. 3. Выполнив упражнения, открываете практические задания и выполняете их. Поскольку у разных разработчиков не бывает одинаковых решений, к практическим заданиям я предлагаю свои решения. Все честно: вы получаете не только (и не всегда) трудную задачу, но и вариант ее решения. Скачать материалы можно из следующих репозиториев GitVerse: y упражнения: https://github.com/3RApp/drills; y практические задания: https://github.com/3RApp/problems; y ответы к практическим заданиям: https://github.com/3RApp/answers; y код, иллюстрируемый в книге: https://github.com/3RApp/code. Скачайте заранее все упражнения, практические задания, ответы и код к листингам. При чтении книги рекомендую держать приведенный в ней код под рукой. Листинги, как правило, даются в сокращенном виде. Оставшийся код можно увидеть в файлах, скачанных из репозитория на gitverse.ru. При этом код, относящийся к разделу и являющийся главным, приводится полностью. Все листинги в работоспособном состоянии. Имейте в виду, что код лишь поясняет изучаемый материал. После изучения выполняются упражнения, затем — практические задания. После главы 8 я предлагаю поэтапно создать фронтенд для интернет-магазина. К тому моменту вы уже будете способны сделать подобное. По сути, речь идет о большом практическом задании, хотя вы получите фрагмент заготовленного кода, что ускорит его выполнение. Что делать, если не удается приступить к выполнению упражнения или практического задания? Вам нужно еще раз посмотреть материал раздела, а именно — то место, на разработку которого направлено упражнение или практическое задание. Раз возникает трудность в применении, есть что-то, что вы не до конца усвоили. Однако если вы чувствуете, что понимаете и можете делать задание, пусть медленно, то просто продолжайте — медлительность появляется из-за слабого владения темой. Упражнения и практические задания как раз помогут ускориться. Иногда ускорение происходит прямо в момент выполнения задания, поскольку приходит понимание. Еще у меня важная просьба: если вы не можете справиться с практическим заданием, то выполните действие из предыдущего абзаца, снова ознакомьтесь
22  Введение с материалом и попробуйте сделать упражнение еще раз, а затем вновь приступайте к практическому заданию. Возможный ответ смотрите, только выполнив задание, — такой подход позволит развить навыки, ради которых написана данная книга. Цель этой книги — дать людям возможность понять фронтенд-приложения и научить легко ориентироваться в их создании с нуля или доработке уже существующих. Для этого даются базовые знания, в том числе о современных библио­ теках и подходах. Я не стремлюсь сделать так, чтобы вы запомнили, как писать код с использованием библиотек, а хочу, чтобы в России фронтенд-разработчики подходили к созданию приложений с бо́льшим пониманием. Удалось мне это или нет, судить вам — читателям. Почему упражнения и практические задания отделены от изучаемого материала Почему выбрана такая последовательность действий: 1) знакомство (чтение материала); 2) упражнение в применении; 3) практическое задание? В ее основе лежит убеждение, что сначала должна появиться идея, как что-то устроено и работает, а листинги кода иллюстрируют применение. После вы тренируете использование на практике только что изученного, опираясь на самые распространенные способы действий, и в конце должны самостоятельно выполнить практическое задание. Теперь ключевой момент. Если пытаться сразу практиковать, то есть писать код по ходу чтения (авторы других популярных книг создают код решения по мере движения в разделе), то, с моей точки зрения, вы ограничитесь написанием кода и пониманием того, как он должен выглядеть. А если сначала изучить материал, затем потренироваться на заготовленных упражнениях и, наконец, самостоятельно выполнить практическое задание, то тема будет усвоена. Критерий простой: если вы не можете выполнить практическое задание, значит, что-то не поняли и не усвоили и вам нужно вернуться к материалу раздела. Переход к упражнениям важен тем, что после изучения материала у вас появляется возможность соотнести собственные мысли с тем, как реализовать (выразить) понимание в виде текста программы. Иногда, пока он не написан, картина не складывается и понимание не приходит. Без чтения вы тоже не смогли бы найти решение. Если ограничиться упражнениями, то непонятно, усвоена тема или нет. Представляете, как вы будете себя чувствовать, когда в работе нужно использовать знание по вроде бы изученной теме, но вы обнаруживаете, что не можете его применить. Хуже, если это выясняется на собеседовании. Я имею в виду ситуацию, когда вы уверены, что что-то знаете, но применить знания не удается. Ответы можно найти в интернете, но при проверке знаний у вас не будет такой возможности. Поэтому знать и уметь применять — верный способ быть профессионалом, которого желают заполучить компании.
ОТ ИЗДАТЕЛЬСТВА Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (­издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
Глава 1 ФРОНТЕНД-РАЗРАБОТКА 1.1. С чего начиналась фронтенд-разработка 1.1.1. Осознание способов решения проблем Сегодня фронтенд-разработчик располагает целым набором инструментов, а также библиотек, которые используются для разработки приложений, выполняемых в браузере. Плюс в самих браузерах появилось довольно много функциональных возможностей. Но так было не всегда, и данная область — разработка фронтендприложений — формировалась постепенно. На заре становления интернета не существовало понятия «фронтенд-приложение». Был термин, вошедший в лексикон, — «клиент-серверное1 приложение». Чуть позже, когда мы рассмотрим исторические стадии развития функциональности, станет понятно, что такое фронтенд-приложение. Инструменты для его разработки тоже появились со временем. В эволюции приложений можно выделить три крупных этапа: y приложения на основе веб-страниц; y приложения на основе AJAX и библиотек; y приложения, создаваемые с помощью инструментов и декларативных биб­ лиотек. 1.1.2. Приложения на основе страниц Самые первые приложения — это клиент-серверные приложения, которые назывались сайтами2. По сравнению со страницами в браузере они были настолько простыми, что скорее представляли собой электронные книги и каталоги. Да, они были функциональными, но функциональность обеспечивалась не за счет 1 2 Клиент-серверное приложение — распределенное приложение, основанное на модели вычислений, в которой клиент запрашивает услуги у другой сущности — сервера. В типичном для бизнес-систем клиент-серверном приложении клиент выполняется на персональном компьютере, а расположенный на удаленной и более производительной машине сервер предоставляет ему услуги по доступу к хранящимся на сервере данным. Клиентская часть приложения обычно оптимизируется для взаимодействия с пользователем, в то время как серверная часть предоставляет функциональность, совместно используемую многими пользователями. Сайт — любой ресурс, который отображается в браузере как документ, а не как код или изображения и появляется в ответ на адрес, набранный в адресной строке браузера. Сайт может быть простым или дополнительно обмениваться данными с сервером.
1.1. С чего начиналась фронтенд-разработка  25 задействования языка JavaScript и работы с DOM1, а за счет приложения, которое функционировало на сервере. DOM в том виде, в каком мы знаем ее сегодня, появилась в результате постепенного развития. Движение с одной страницы сайта на другую осуществлялось путем перехода по ссылке. Предыдущая страница выгружалась из браузера, а новая загружалась. То есть происходила полная перезагрузка страниц. Да, использовалась оптимизация запрашиваемых ресурсов —некоторые из них загружались из кэша, но то была оптимизация трафика для сервера. На клиенте все время происходила перезагрузка. В то время клиент-серверное приложение в виде сайта старались разукрасить или сделать как можно более функциональным за счет серверного приложения. 1.1.3. Приложения на основе AJAX и библиотек AJAX не конкретная технология, а набор технологий, каждая из которых развивается сама по себе. Чтобы не перечислять все, что предлагалось для использования в AJAX, и не превращать объяснение в доклад о технологиях прошлого, я дам простое определение. Можно сказать, что AJAX подразумевает использование JavaScript в браузере, чтобы обмениваться с сервером данными в формате XML с помощью объекта браузера XMLHttpRequest. Название AJAX — это первые буквы слов в словосочетании Asynchronous JavaScript And XML, что переводится как «асинхронный JavaScript и XML». Авторы некоторых книг по JavaScript негодуют из-за присутствия XML и в названии XMLHttpRequest, и в AJAX. Однако появление именно XML вполне оправданно. Какой еще способ представления данных можно было выбрать в начале 2000-х годов? Напомню, что формат JSON Дуглас Крокфорд, можно сказать, «придумал» в 2002 году. Приложения, которые задействовали подход AJAX, уже не перегружались при каждом взаимодействии пользователя со страницей. Это создавало положительный пользовательский опыт и в целом увеличивало скорость работы клиент-серверного приложения за счет уменьшения трафика2. Однако с точки зрения фронтенд-приложения сами приложения, за редким исключением, были основаны на прямой работе с DOM и CSS. DOM браузеров на заре становления интернета не была реализована по единым стандартам, как мы можем видеть это сегодня. И даже сегодня наблюдаются 1 2 DOM (Document Object Model) — объектная модель документа. Есть целый раздел, посвященный этой модели, где она рассматривается подробно. Сейчас дам общее представление: объектная модель документа — это модель, взаимодействие с которой позволяет менять содержимое документа, отображаемого в браузере. Браузер дает API для изменений, а чтобы разработчики понимали, что менять, документ представляется моделью в виде структуры дерева, чьими элементами являются объекты, созданные на основе тегов из запрошенного с сервера HTML-кода. Таким образом, воздействуя на модель через API, разработчик влияет на то, что отображается в браузере. Трафик — перемещение, поток данных в передающей среде, например поток сообщений (пакетов передаваемых данных) в Глобальной сети. Также это означает загруженность сети (по аналогии с автотранспортом на дорогах). Трафик состоит из передаваемых данных и служебной информации, необходимой для организации их прохождения.
26  Глава 1 Фронтенд-разработка расхождения со стандартом W3C1. А в 2006 году появление библиотеки jQuery осуществило прорыв в скорости создания веб-приложений. Библиотека предлагала множество методов для работы с DOM и AJAX, но главное — разработчикам больше не надо было беспокоиться о разности в реализации DOM в браузерах. Чуть раньше, в 2005 году, появилась библиотека Prototype.js, которая, пусть и в гораздо меньшей степени, предлагала функции для работы с AJAX и DOM. Начало активному развитию веб-приложений положили 2005–2006 годы, развитие библиотек и инструментов началось чуть позднее. Нас в первую очередь интересуют приложения на стороне фронта. Они в тот момент напрямую манипулировали DOM, CSS и данными. То есть фронтенд-приложение представляло собой реакцию на события, а также прямую манипуляцию DOM и данными. Можно сказать, что после накопления опыта создания таких фронтенд-приложений разработка перешла на новый уровень. 1.1.4. Фронтенд-приложения Эпоха2, которую я обозначил как «фронтенд-приложения», началась в 2013 году с появления библиотеки React.js и длится по сей день. Уже есть первые признаки ее завершения. Свое ви́дение данного процесса я изложил в следующем подразделе, а пока вернемся к началу эпохи. Факторов3, повлиявших на переход к новой эпохе, четыре. Первый — технологии и подходы, второй — инструменты, третий — библиотеки, четвертый — пользовательский опыт и обратная связь. Каждый из них внес свой вклад в определенный промежуток. В качестве точки отсчета, начала эпохи я выбрал момент появления библиотеки React.js. Взгляните на рис. 1.1. На инструменты (точнее, на увеличение их количества) сильно повлияло возникновение Node.js. Инструменты позволяют ускорять разработку фронт-приложений плюс дают удобство. Сегодня мы используем их повсеместно. Чуть позже появился репозиторий npm, ускоривший разработку в разы за счет того, что теперь устанавливать зависимости можно одной командой в консоли. Развитие технологий тоже началось с Node.js. На рис. 1.1 видно, что с 2009 по 2015 год произошел их бурный рост. Эти технологии используются до сих пор и продолжают развиваться. Стоит отметить: несмотря на то что JavaScript уже существовал, в 2015 году стандарт ECMAScript был пересмотрен и произошло качественное изменение языка. В него добавили новые элементы, например, для асинхронного написания программ, а именно — Promise. 1 2 3 W3C — World Wide Web Concorcium. Слово «консорциум» означает объединение компаний и частных лиц на основе общего соглашения для осуществления какого-либо мероприятия. Консорциум W3C — это международное сообщество, где члены-организации, постоянные сотрудники и другие люди, участвующие в деятельности, вместе разрабатывают стандарты веба. Директором консорциума является Тим Бернерс Ли. Эпоха — продолжительный период времени, имеющий какие-либо характерные особенности. Фактор — существенное обстоятельство, способствующее какому-либо процессу, явлению.
1.1. С чего начиналась фронтенд-разработка  27 Рис. 1.1. Эпохи разработки, технологий, инструментов и библиотек Тогда же появились новые библиотеки и стали очевидны недостатки в производительности прямой работы с DOM-моделью. Для лендинга нормально манипулировать DOM и данными напрямую, но не для высоконагруженных сервисов. Компания Facebook1 к 2013 году разработала библиотеку React.js, которая отличалась от существовавших на тот момент тем, что отделяла создание интерфейса от его воплощения в DOM браузера. Такой подход значительно повысил производительность приложения. Одновременно разработчики выделили среди других компонентов строительный элемент приложения, ускорив саму разработку. Самое интересное, что теперь приложение, работающее в браузере, действительно можно было назвать приложением. Извиняюсь за тавтологию2, но именно с этой библиотеки, по моему мнению, начался рост веб-приложений как явления. Отныне приложение не манипулировало DOM напрямую. Разработчик описывал, как должен выглядеть интерфейс, с помощью данных и компонентов, а библиотека React в совокупности с библиотекой ReactDOM вычисляла посредством виртуальной DOM3, что нужно изменить в реальной DOM, чтобы получить предписываемое компонентами. То есть разработчику больше не требовалось манипулировать DOM (императивный подход), достаточно описать состояния и внешний вид (декларативный подход). Конечно, на рынок стали выходить конкуренты, например Vue.js, Angular. Сообщество, использовавшее React, накопило соответствующий опыт, и стало 1 2 3 Компания Meta, которой принадлежит Facebook, признана экстремистской, и ее деятельность запрещена на территории Российской Федерации. Тавтология — повторение того же самого другими словами без уточнения смысла. Виртуальная DOM — это структура данных в виде дерева, содержащая точно такие же связи между узлами, как те, что должны быть в реальной DOM в самом браузере. Каждый узел в виртуальной DOM содержит данные, используемые в узлах реальной DOM. Следует отметить, что реальные DOM, выстраиваемые браузером, имеют гораздо больше свойств в каждом узле, чем виртуальные. Главные данные одного узла виртуальной DOM — это связи с другими узлами (отношения «родительский — дочерний»); данные, которые должны быть отображены в виде текста на странице, и данные о стилях.
28  Глава 1 Фронтенд-разработка очевидно, что управлять состоянием всего приложения нужно иным образом, нежели с применением пропсов и стейтов. Так появились библиотеки Redux и Mobx, которые можно использовать совместно с React. Чуть позднее в React (версия 16.3 от 2018 года) появился Context, позволявший не передавать данные вручную в явном виде, через все дерево компонентов от родителя к нужному дочернему компоненту. Для более предсказуемого управления целыми поддеревьями компонентов была разработана библиотека React-Router-Dom. Описываемая эпоха продолжает развиваться по сей день. Многие из упомянутых выше библиотек сильно выросли и определили современный стек технологий, которыми пользуются для создания веб-приложений или SPA1. В книге описан исключительно современный стек и даже некоторые более передовые сервисы. Теперь посмотрим, что нас ждет в ближайшем будущем. 1.1.5. Эпоха искусственного интеллекта2 и виртуальной реальности На рис. 1.1 я поставил знак вопроса, чтобы вы озадачились тем, что нас действительно ждет. Развитие идет по нескольким направлениям: y технологии; y инструменты; y библиотеки. Инструменты и библиотеки упрощают использование технологий и вводят дополнительные абстракции для более легкого моделирования работы с данными. В технологиях это также платформы, для которых разрабатываются приложения. На языке информатики это исполнитель: изменится платформа — изменятся и инструмент, и библиотека, и подход к разработке, а также появится другая технология, которую надо будет изучить и использовать. Сейчас активно распространяется браузер. Данная тенденция сохранится на какое-то время, пока не появится другое приложение, вероятно, в совокупности с новым типом девайса3. Все тот же, правда уже вчерашний, Facebook, а сегодня — Meta продвигает идею метавселенной, и ведутся диалоги о создании единого стандарта для метавселенных, что правильно. Ведь есть опыт по разработке браузеров, каждый из которых идет своим путем. Единый стандарт позволит сэкономить время и быстрее развиваться. А как насчет искусственного интеллекта (ИИ)? Что он нам даст? Все просто — ИИ ускорит разработку. Уже сегодня доступны инструменты, позволяющие на 1 2 3 SPA (Single Page Application) — приложение, исключающее загрузку страниц при переходе по ссылке. SPA загружается в браузер один раз, а в дальнейшем взаимодействие пользователя обрабатывается так, что хоть и создается видимость смены внешнего вида страниц, но все происходит без загрузки новой HTML-страницы. Искусственный интеллект — синтетическая система, демонстрирующая «умное» поведение. Определение взято из книги: Харбанс Р. Грокаем алгоритмы искусственного интеллекта. — СПб.: Питер, 2023. Девайс — физическое устройство, позволяющее выполнять определенную функцию. В данном случае будет воспроизводить изображения и, вероятно, даже имитировать тактильные ощущения.
1.1. С чего начиналась фронтенд-разработка  29 основе промпта1 получить готовый код. Таковыми являются GigaChat2 и GigaCode3 от «Сбера». В ближайшее время можно ожидать появления других инструментов, значительно ускоряющих написание кода и, вероятно, даже создающих полное приложение на основе небольшого количества входных данных. Позволить себе такие крупные разработки может или крупная корпорация, такая как «Сбер», Google, Microsoft, Oracle, SAS и т. п., или группа увлеченных энтузиастов, придумавшая какую-то идею и на основе open source — решений, каковыми являются языковые модели GPT, RuGPT4, выпустившая продукт, покрывающий потребность в автоматизации, которая ранее была доступна только специалистам. Пока браузеры не будут сдавать свои позиции в силу распространенности, но создание приложений значительно ускорится, а вслед за этим изменится инфраструктура, на которой они работают. Браузеров станет недостаточно. Посудите сами. У вас в руках — инструменты, работающие на основе нейросетей и способные написать типовой код, включая внесение необходимых изменений. В качестве «железа» есть компьютер с огромными ресурсами, а вы по-прежнему пользуетесь браузером, способным разве что воспроизводить картинки и видео. Конечно, нет! Возможно, браузер — как идея, а не как программа — перейдет в девайсы для виртуальной реальности и станет 3D с тактильным интерфейсом. От чего зависит появление новых технологий? Только от нас с вами — чем больше вы вкладываетесь в разработку новых технологий, тем быстрее они приходят на рынок. Действует прямая закономерность: если вы направляете внимание лишь на изучение библиотек и технологий, то и получаете знания библиотек и технологий, а еще можете создавать продукты на их основе. Если же вы ориентируетесь на разработку и придумывание чего-то нового, что до вас никто не делал, ну разве что высказывал идеи, то и получаете то, что может стать «новым стандартом», как это было с jQuery, React, Redux, React Router, с языком TypeScript. Поэтому надо действовать не только в использовании имеющегося, но и в придумывании нового. Стоит ли переживать по поводу того, что сегодня из каждого утюга кричат про языковые модели, искусственный интеллект и т. п.? Нет. Приложения не состоят из одного текста и никогда только из него состоять не будут. Они состоят из гипертекста 1 2 3 4 Промпт — запрос к языковой модели, на который предполагается ответ, имеющий смысл для человека. GigaChat — диалоговая ИИ-модель, которая отвечает на вопросы, сочиняет тексты, пишет код и рисует картинки. Говорит на русском и понимает английский. GigaCode — ИИ-ассистент, который позволяет генерировать код на разных языках с применением ИИ, выполнять автозаполнение и создавать запросы. GPT (Generative Pretrained Transformer) — генеративный, предварительно обученный трансформер. Трансформер — это нейронная сеть, обрабатывающая всю последовательность текста сразу, одновременно выявляя взаимосвязи между словами, а не слово за словом. Нейронная сеть представляет собой модель, которая, по мнению исследователей моделей и мира, лежит в основе разума (речь о мыслительной деятельности, которая будто бы происходит в мозгу, состоящем из нейронов) человека и которую можно перенести в сферу обработки данных. Таким образом, создается впечатление, что программа, использующая нейронную сеть, обладает знаниями и способна к разумным ответам. Сеть — это элементы, содержащие связи с другими элементами, по которым могут передаваться данные.
30  Глава 1 Фронтенд-разработка и медиа. Приложения должны иметь структуру, в них воплощаются замыслы людей, а не машин. Без фронтенд-разработчиков это неосуществимо. ИИ ускорит вашу работу. Однако запросов от населения и бизнеса относительно приложений меньше не станет, их количество будет увеличиваться. Растет население, растет осознание и качество жизни, растет потребность. Сами посудите, как бы вы сегодня работали на прошловековых технологиях создания приложений без интернета? Да никак! И работы все прибавляется. Ровно то же самое вас ждет в текущий момент времени. Так что давайте погружаться в область фронтенд-разработки. 1.2. Разработка фронтенд-приложения 1.2.1. Окружение разработки Знание JavaScript открывает возможность для изучения фронтенд-разработки, где он активно задействуется. Однако написание кода, состоящего из функций и UIкомпонентов, не единственное обязательное действие. Разработка приложения включает в себя несколько этапов и шагов в них, состоит из индивидуальной и групповой работы. Конечно, если вы работаете одни, то группового этапа нет. В любом случае вы ведете поиск готовых решений, конструируете компоненты и функции, пишете тесты, проверяете работоспособность и синтаксическую корректность кода. Если приходите в готовый проект, а так происходит чаще всего, вы, кроме того, знакомитесь с кодом. При этом подавляющее большинство проектов осуществляется в группе, поэтому необходимо делать код-ревью изменений, которые вносят в репозиторий другие разработчики. Если вы создаете приложение с нуля, то необходимо его спроектировать и, прежде чем начать писать код, сделать настройки для сборки приложения и проверки корректности синтаксиса. По мере роста кодовой базы нужно создавать документацию, где описываются соглашения и идеи, закладываемые в основу приложения. И конечно, одно из первых действий в начале любой работы — определить библиотеки и фреймворки, которые будут использоваться, то есть техстек. Помимо написания или изменения кода, следует научиться пользоваться готовыми инструментами для разработки. Посмотрим, какие области нужно контролировать фронтенд-разработчику и какие существуют инструменты (рис. 1.2). В прямоугольнике показаны фронтенд-приложение и библиотеки, имеющие к нему прямое отношение. В овалах черным шрифтом прописаны области, с которыми нужно иметь дело при разработке, а бледно-серым — инструменты, часто применяемые в данной области. Трудно отрицать очевидные вещи, например то, что разработка начинается в какой-то момент и продолжается, продолжается, продолжается, но однажды останавливается. Момент начала важен потому, что перед тем, как начать писать код, нужно сделать настройку будущего окружения. На рис. 1.2 это выглядит именно как ближний круг приложения. Сегодня фронтенд-разработчику надо умело применять и библиотеки, и весь набор инструментов для разработки. Умелое применение начинается со знакомства, понимания и обучения, затем следует успешное применение.
1.2. Разработка фронтенд-приложения  31 Рис. 1.2. Области, которые нужно контролировать фронтенд-разработчику Начнем с обзора каждой области. Инструменты, за исключением системы контроля версий и сервисов для код-ревью, работают на node.js. Системы контроля версий работают непосредственно в операционной системе, а сервисы для код-ревью и хранения репозитория1 приложения доступны в интернете. 1.2.2. Управление зависимостями Зависимость — это модуль2, обычно называемый пакетом, содержащий выражения на языках JavaScript, TypeScript и импортируемый для использования в программе. Зависимость может быть любого размера и выполнять одну и более функций или вычислений. Такой модуль, в свою очередь, тоже может иметь зависимости. Поясне­ния даны на рис. 1.3. Чтобы не запутаться в словах, надо понимать разницу между реестром и репозиторием. Реестр — просто список чего-либо. Репозиторий — место, где что-либо хранится. Применительно к npm реестр содержит оформленные модули; нам, как пользователям, доступен список всех хранящихся там модулей. Когда пишем код 1 2 Репозиторий — место хранения кода в файлах, откуда его можно получить или где можно разместить, а также дополнить или удалить. Модуль (в программировании) — оформленный специальным образом функционально законченный и самостоятельный (в том числе по отношению к компиляции или загрузке) блок кода, взаимодействие с которым осуществляется через его внешний интерфейс. Раздельная трансляция (separate compilation) модулей важна при создании больших систем. кроме того, каждый отдельный модуль может повторно использоваться в других проектах. Разбиение программы на модули, модульность (modularity), существенно облегчает ее понимание, разработку, документирование, отладку, модификацию и сопровождение. В зависимости от контекста синонимами этого термина являются unit, programm unit и package. Источник: Пройдаков Э. М., Теплицкий Л. А. Большой англо-­русский толковый словарь по вычислительной технике и информационным технологиям. — М.: РТСофт, 2015.
32  Глава 1 Фронтенд-разработка и с помощью системы контроля версий размещаем его в месте хранения (репозитории), у нас нет специального списка модулей, как в npm, и мы не оформляем каждый JavaScript-модуль как npm-модуль. Рис. 1.3. Зависимости приложения Для управления зависимостями используются программы npm, yarn и др.; это — инструмент. Стоит отметить, что он предназначен для работы с внешними зависимостями — это видно на рис. 1.3. 1.2.3. Язык и кодирование. Трансляция Под трансляцией понимается два вида процессов. Первый — процесс преобразования исходного кода программы непосредственно в машинный код плюс некоторые другие процессы, что позволяет запускаться программе на компьютере. Например, вы написали программу на алгоритмическом языке С и, используя компилятор gcc, создали исполняемый файл. Второй — процесс перевода программы, написанной на одном языке, в код программы на другом языке. Также ко второму процессу относится перевод программы, написанной на одной версии языка, в код программы, написанный на другой версии того же языка. Самое популярное действие — перевод последней версии языка JavaScript, например версию JavaSript 2024 в версию JavaScript 5.1. 1.2.4. Сборка приложения Сборка приложения, или упаковывание, — это подготовка кода приложения для его выполнения в браузере и подготовка ресурсов для использования в приложении. Под ресурсами понимаются изображения (PNG, JPEG, GIF и др.), CSS-файлы, аудио и видео (если они располагаются у вас и не управляются особым образом). Для данной процедуры используется инструмент под названием «сборщик», например webpack.
1.2. Разработка фронтенд-приложения  33 1.2.5. Поиск проблем в коде Поиск проблем в коде означает, что существует некое «идеальное положение дел», то, как должен быть написан код, и происходит сравнение кода вашего приложения с идеальным. Несоответствия называют «проблемой». Для поиска проблем используют инструменты, называемые линтерами (linter). В английском языке linter — это машина по удалению маленьких пуховых волокон, которые прилипают к хлопковому семени после измельчения. То есть название заимствовано из другой области. Линтер ESLint разработал Николас Закас, инженер из Google. Линтер JSLint — Дуглас Крокфорд, писатель, лектор и программист, известный по разработанному им формату JSON. 1.2.6. Оформление кода Оформление кода — это то, как выглядит (форматирование) написанный код JavaScript, CSS, JSON и др. Для такой работы используют специальный инструмент, например prettier. 1.2.7. Тестирование Тестирование — область, по величине сравнимая с разработкой, поэтому рассматривать ее мы не будем. Коснемся данной темы в настройках, в разделе 8.2 «Реализация 3RApp. Настройки». 1.2.8. Контроль версий и обмен кодом Контроль версий — это управление исходными текстами и документами, используемыми при разработке приложений. Обмен кодом — это размещение сделанных изменений согласно определенной процедуре в репозитории, из которого другие разработчики могут эти изменения получить. Так достигается более быстрая синхронизация кода приложения до самой последней версии изменений. Одной из популярнейших сегодня систем является Git. Широко распространенные ресурсы для размещения кода — github.com, bitbucket.org. И недавно появившийся российский — gitverse.ru. 1.2.9. Код-ревью Код-ревью (code review) — это процесс, когда другой разработчик (чаще разработчики) смотрит код, написанный вами в качестве решения задачи, с целью определить его соответствие: y поставленной задаче; y правилам, принятым в группе (code style, по-русски — «код-стайл»).
34  Глава 1 Фронтенд-разработка При выявлении несоответствий другой разработчик оставляет комментарий и, возможно, рекомендацию по исправлению. Мы рассмотрим, как проводить код-ревью и быстро погружаться в код в новой команде. 1.2.10. Заключение Все инструменты, которые несколько лет назад приходилось настраивать вручную, сегодня автоматизированы. Если вы фронтенд-разработчик и разрабатываете приложение, нужно использовать инструмент CRA (create-react-app). Как создавать приложения, в том числе в ходе совместной работы, вы узнаете из этой книги.
Глава 2 С ЧЕМ ИМЕЕТ ДЕЛО ФРОНТЕНД-РАЗРАБОТЧИК 2.1. Возможности программ на JavaScript 2.1.1. С чем и как взаимодействует программа на JS Любая функциональность доступна программе лишь благодаря «железу» компьютера. Под «железом» понимается как базовое оборудование — материнская плата, диск, так и расширяющее оборудование — сетевая, аудио- и видеокарты. Нет определенного «железа» — не будет доступна и функциональность. Простой пример с модулем Wi-Fi. Если в вашем компьютере его нет, к интернету придется подключаться с помощью сетевого кабеля. Или возьмем сетевую карту: нет карты — нет интернета. Эта, казалось бы, очевидная вещь говорит о том, что создаваемые нами программы в конечном итоге исполняются в «железе». Программы взаимодействуют с «железом» через операционную систему. При этом язык программирования можно рассматривать с разных сторон. Одна из них — то, с чем может взаимодействовать программа, созданная на конкретном языке. Например, если вы напишете программу на языках С/С++ и транслируете ее в исполняемый файл, то сможете запускать данную программу в операционной системе. Из нее можно взаимодействовать с памятью компьютера и каналами ввода/вывода разных устройств (монитор, сеть) или файловой системой. Реализуется это не напрямую, а через API операционной системы, для чего созданы встроенные в языки С и С++ стандартные библиотеки. Если в компьютере есть конкретное физическое устройство, то операционная система предоставит к нему доступ через обобщенный интерфейс. Программы на С/С++ транслируются в машинный код. После запуска они исполняются как процесс в операционной системе. С языком JavaScript дело обстоит иначе. Написанная на нем программа способна взаимодействовать только с API, предоставленным платформой1, где она может исполняться. Новые программные объекты, которые могут исполняться в операционной системе и взаимодействовать с «железом», JS не создает. Их предоставляет платформа, 1 Платформа — это программа, такая как браузер или Node.js, исполняемая в операционной системе и предоставляющая API к определенной функциональности, например к запросам по сети или к взаимодействию с файловой системой. Платформа принимает в качестве входных данных текст программы, написанный на языке JavaScript, поэтому API доступен через глобальные идентификаторы прямо в тексте программы. Например, запросы по сети можно выполнять через идентификатор fetch, а получать информацию о текущем местоположении объекта document — через глобальный идентификатор window, свойство location.
36  Глава 2 С чем имеет дело фронтенд-разработчик например браузер. Поэтому программа, написанная на JavaScript, не сможет обес­ печить иной функциональности, кроме обработки данных. Однако если задействуются объекты платформы, то можно создавать сценарии, по функциональности не уступающие программам, созданным на Java, C++ или С. Примеры мы увидим далее. Так вот, программа, написанная на JavaScript, может использовать объекты функциональности платформы, получать от них данные и передавать им данные, управлять ими посредством API. Схематично разница программ на C/C++ и JavaScript показана на рис. 2.1. Рис. 2.1. Разница программ на С/С++ и JavaScript 2.1.2. Работа с данными Под работой с данными понимается несколько действий с ними: y изменение значений, хранящихся в переменных или структурах данных; y изменение структуры данных; y генерация данных; y вычисления на основе данных. Рассмотрим примеры для всех четырех случаев работы с данными на JavaScript. Изменение значений, хранящихся в переменных или структурах данных, хорошо иллюстрирует пример с массивом, где каждое число удваивается, как показано в листинге 2.1. Листинг 2.1. Изменение значений чисел, хранящихся в массиве const plainNumbers = [2, 3, 5, 7, 11, 13, 17, 19, 23]; const doubleNumber = (numbers) => numbers.map(number => number * 2); const evenNumbers = doubleNumber(plainNumbers);
2.1. Возможности программ на JavaScript  37 Переходим к изменению структуры данных. Допустим, у нас есть объект сотрудников, разделенных по половому признаку, но нам нужно объединить два массива в один с указанием полового признака. Это и будет изменением структуры данных. Как такое сделать, показано в листинге 2.2. Листинг 2.2. Преобразование объекта с массивами в массив объектов const workers = { man: [{ name: 'Александр', age: 32 }, { name: 'Федор', age: 36 }, { name: 'Валентин', age: 31 }], woman: [{ name: 'Анжела', age: 28 }, { name: 'Ирина', age: 32 }], }; const const const const enhanceWorkers = (array, prop) => array.map(item => ({...item, ...prop})); man = enhanceWorkers(workers.man, { sex: 'man' }); woman = enhanceWorkers(workers.woman, { sex: 'woman' }); enhancedWorkers = [...man, ...woman]; Помимо преобразования данных или структур, их можно генерировать. Каким образом это происходит, показано в листинге 2.3. Листинг 2.3. Генерация чисел от 0 до 10 000 // limit — это верхняя граница, определяющая максимальное // псевдослучайное число, которое должно быть сгенерировано function *generateNumbers(limit){ while(true){ yield Math.floor(Math.random() * limit); } } const generator = generateNumbers(10000); generator.next(); generator.next(); // ... generator.next(); В этом примере генератор создает итератор, у которого метод next можно вызывать бесконечное число раз, и он каждый раз будет возвращать число в пределах от 0 до 10 000. Последний, четвертый, пример касается вычисления. Вычисление — это обработка числовой информации ручным или машинным способом; действия над числами для нахождения искомого. Это слово часто встречается: его используют для описания действия над выражениями в языках программирования, причем для любых типов данных, например const str = someStr.concat(otherStr), а также для описания последовательности действий в программе, например if (value) {…} else { for (let i = 0; …}. Есть еще значение, связанное с догадками, но это относится к человеку, а не к компьютеру. В английском языке почти идентичные определения. Так почему значения в разговорном языке касаются чисел, а мы используем это слово и для порядка
38  Глава 2 С чем имеет дело фронтенд-разработчик действий, и для нахождения итогового значения, например строки? Возможно, потому, что компьютер хранит данные в виде чисел, а информатика берет свое начало в математике. Ну а математика имеет дело с числами и операциями над ними. Рассмотрим пример подсчета средней длины слов в предложении, показанный в листинге 2.4. С одной стороны, у нас будут строковые данные. С другой — мы произведем вычисление, а именно найдем среднее арифметическое длины слов в предложении. Листинг 2.4. Средняя длина слов в предложении const sentence = "Пушкин желал таковой участи нашей Отчизне"; const averageWordLength = sentence .split(/\s/) .reduce((average, word, index) => Math.floor((average + word.length) / (index === 0 ? 1 : 2)), 0); Поясню, что в выражении wordLength производится не что иное, как вычисление. Ведь возвращается длина строки или количество входящих в нее символов. С вычислениями в JavaScript дела обстоят шикарно, но кое-чего все-таки не хватает. Например, привычно выводить результаты в консоль. А для этого нужно задействовать объекты платформы, в которой запускается программа на JS. Приведенные примеры показывают, что вы можете сделать, не задействуя платформенную функциональность. Теперь вполне закономерно задействовать последнюю. 2.1.3. Платформенная функциональность Воспользуемся REST-сервисом с общедоступного сервера, который предоставляет данные для осуществления запросов, и выведем их в консоль (листинг 2.5). Листинг 2.5. Задействование функциональности, предоставляемой платформой async function getComment(id = 1){ const answer = await fetch( `https://jsonplaceholder.typicode.com/comments/${id}`); const json = await answer.json(); } return json; getComment(3).then(comment => console.log(comment)); В листинге 2.5 задействовано сразу два платформенных программных компонента — объекты fetch и console. Первый осуществляет запросы по протоколам HTTP и HTTPS к URL, а второй содержит методы для вывода данных в консоль браузера. Словосочетание «функциональность, предоставляемая платформой» звучит непривычно, но описывает суть. Под этим понимается вся совокупность объектов,
2.2. Платформы  39 предоставляющих свой API, которые можно использовать в программах на JavaScript. Это доступные глобально объекты (document, Navigator, location, history, Geolocation и др.) и методы (prompt, alert, parseInt, parseFloat и др.). «Глобально» значит «доступные в любом месте программы». Другие платформы, такие как Node.js, имеют собственные объекты, предоставляющие функциональность. Поэтому важным моментом в становлении разработчика является изучение API тех объектов и методов, которые есть в платформе, для которой пишется приложение. Чем больше вы знаете, тем быстрее будете писать приложение. На самом деле некоторые объекты требуют гораздо больше знаний, чем может казаться на первый взгляд. Возьмем WebRTC. В данном случае для создания полноценного сервиса не только потребуется знание фронтенда, но и придется создать дополнительную инфраструктуру. Чуть проще document, который, кстати, почему-то часто называют «функциональностью, доступной в JavaScript», хотя это объект браузера и доступ к нему можно получить не только из JavaScript. Чтобы лучше познакомиться с возможностями этого объекта, нужно время, потому что он задействует дополнительную функциональность — события. Любая программа состоит из выражений, генерирующих и преобразующих данные и структуры, из вычислений и использования платформенной функцио­ нальности. По сути, программа JavaScript — совокупность выражений по обработке данных и задействованию платформенной функциональности. Поскольку современные программы вышли за рамки банальных, их нужно организовывать специальным образом. Это мы и будем изучать в книге. 2.2. Платформы 2.2.1. Что представляют собой платформы и зачем они нужны Платформа — это сервис, который может выполнить программу, написанную на JavaScript. Она обязательно предоставляет какую-либо, свойственную только ей функциональность. Сегодня существует три вида платформ. Первый — браузеры. Второй — серверные Deno и Node.js. Третий — облачные сервисы. Обращаю внимание на то, что признаком, по которому выделяют виды платформ, является уникальная функциональность. При этом для обработки программ всех трех видов может использоваться один и тот же механизм. Рассмотрим функциональность, доступную на разных платформах. 2.2.2. Уникальность функциональности В разделе 2.3 будет раскрыта функциональность браузера. Но, поскольку книга о фронтенд-разработке, главной платформой должен стать браузер и ему необходимо посвятить основное описание. В браузере можно манипулировать историей сессии, которая была осуще­ ствлена во вкладке. Это делается с помощью идентификатора history. Вы, конечно, не сумеете переписать историю, но при посещении URL доступно добавление
40  Глава 2 С чем имеет дело фронтенд-разработчик данных в объект и перемещение по посещенным URL. Изначально такая возможность не предусмотрена ни в Node.js, ни в облачном сервисе, о котором речь пойдет ниже. В Node.js, в отличие от браузера, доступна работа с файловой системой. Поэтому в своей программе вы сможете выполнить с ней любую операцию. Кроме того, в Node.js доступ к идентификаторам осуществляется через предварительный импорт модуля, который присутствует в стандартном наборе модулей, поставляемом при инсталляции, или может быть дополнительно установлен. Модуль для работы с файловой системой называется node:fs. В облачном сервисе, как правило, недоступна функциональность, которая есть в браузере или Node.js. Чаще предлагается собственная функциональность. Примером является AWS. Из российских разработок — облачная платформа (виртуальный ассистент) «Сбера» «Салют», для которой можно создать приложение, скажем, Chat App1 или Canvas App, доступные пользователям приложения «СберБанк Онлайн» и платформы «Салют». 2.2.3. Важные выводы Поскольку сам язык без функциональности платформы может обеспечить только работу с данными, необходимо изучать платформенные единицы функциональности. Они обеспечат действие, представляющее собой готовый алгоритм или функцию, обеспечиваемую «железом» компьютера (сервера). Это говорит о том, на какие области следует направить свое внимание. Используя API из программы, вы сможете получить доступ к физическому исполнению. Например, определить местоположение пользователя и предложить ему услуги организаций, расположенных поблизости. Или создать чат-бот, через который пользователь сможет и взаимодействовать с самим чат-ботом, и затем связаться с оператором через микрофон и видеокамеру. Другим примером здесь может служить функциональность проведения оплаты, доступная на платформе. Программа пишется для платформы и рассчитана на ее функциональность. Поэтому через API, предоставляемый платформой, мы можем взаимодействовать с доступной на ней функциональностью и данными. 2.3. Браузер и его объекты функциональности 2.3.1. Где платформа, а где JavaScript В интернете можно встретить разные вопросы. Например, как в JavaScript на странице браузера найти элемент с определенным идентификатором? Ничего необычного, однако формулировка «как в JavaScript...» некорректная, потому что здесь задействуется API, а не объекты самого языка. Выражение в листинге 2.6 представляет собой обращение к объекту document, который присутствует 1 https://developers.sber.ru/portal/categories/salute-app-development.
2.3. Браузер и его объекты функциональности  41 в браузере и раскрывает свойства и методы для полного контроля содержимого, внешнего вида и поведения HTML-страницы на вкладке браузера. Листинг 2.6. Присвоение переменной результата метода getElementById const audio = document.getElementById('player'); Но как, не касаясь конкретных объектов браузера или языка, определить, что относится к браузеру, а что — к JavaScript? Это достаточно просто, если вы уже имели дело с разработкой и использовали API браузера. И совсем не так очевидно, если такого знакомства не было или, например, вы использовали библиотеки и подход «возьму готовое решение». Попробую объяснить в двух словах. Если рассмотреть только классы языка JavaScript и объекты, то станет ясно, что вы можете создать лишь данные или структуры1 из данных. Если рассмотреть браузер, то он предоставит вам интерфейсы, классы и объекты, с помощью которых можно управлять содержимым веб-страниц и другими действиями браузера. Да, в нем также есть функции, создающие или обрабатывающие данные, но они специфичны и не нуждаются в языке. Рассмотрим пример на рис. 2.2. Рис. 2.2. Классы и объекты JavaScript. Интерфейсы, объекты и классы браузера В JavaScript вы задаете данные и структуры данных. Больше нет ничего, чем вы могли бы оперировать. Однако этого более чем достаточно. Остальные элементы языка направлены на преобразование данных (операторы), управление ходом выполнения программы (условные инструкции и циклы) — синхронным и асинхронным (конструкторы Promise и async/await), а также на работу с модулями. Но еще раз повторюсь: сейчас мы рассматриваем только объекты и классы. 1 Структура — это то, каким образом однородные элементы объединены в более крупное сообщество. Каждый элемент может быть элементарным (например, строка или число) или какой-либо структурой, объединяющей разнородные данные — посредством связей или соединяя их по порядку. Никаким другим образом структуру не создать. С помощью связей формируются объекты и связные списки, а соединение по порядку дает массивы. Конечно, существуют структуры, объединяющие два этих базовых способа.
42  Глава 2 С чем имеет дело фронтенд-разработчик Сам язык, к сожалению, не предоставляет никаких средств ввода/вывода. А для полноценной работы нам требуется взаимодействие, нужно получать и отправлять данные. Я имею в виду разработчиков, реализующих замыслы, как свои, так и заказчиков в виде программ. Поэтому необходим исполнитель, или, если взглянуть по-другому, нужно, чтобы где-то исполнялась наша программа. Браузер — один из таких исполнителей, предоставляющий возможность запускать программу и множество интерфейсов для доступа к функциональности. Нет необходимости думать о ее реализации: все, что может понадобиться, уже реализовано, остается лишь задействовать созданное. Функциональность, предоставляемая браузером, доступна в виде объектов, классов и функций. 2.3.2. Браузер — платформа с набором программ, предоставляющих API В процессе разработки, особенно для заказчика, мы обращаем внимание на сроки и возможность реализации того или иного запроса. Иногда, что называется, глаз замыливается. Это значит, работа по написанию кода для приложения стала ежедневной необходимостью и мы начинаем мыслить только в пределах используемых библиотек. Такое со всяким может случиться. Посмотрим на браузер как на платформу, где есть много программ, с которыми можно взаимодействовать прямо сейчас. Взгляните еще раз на рис. 2.2 и воспринимайте каждое слово как название программы, предоставляющей API и используемой непосредственно из программы на JavaScript. Тот факт, что она написана для вебприложения при задействовании лишь document, fetch, encodeURIComponent и т. п., вовсе не означает, что у нее ограниченные возможности. Единственное ограничение — это множество функциональных возможностей самого браузера, которое полезно знать. По сути, браузер — это платформа с набором программ, к которым вы получаете доступ с помощью API. Воспринимайте браузер как платформу с компонентами, которые вы можете задействовать из своей программы на JavaScript. Все, что для этого требуется, — указать идентификатор нужного компонента. Рассмотрим листинг 2.7. Листинг 2.7. Функциональность, предоставляемая браузером const h1 = document.createElement('h1'); const body = document.body; const text = document.createTextNode('Привет, Мир!'); h1.appendChild(text); body.innerHTML = ''; body.appendChild(h1); Если данный код выполняется в консоли браузера на уже имеющейся странице, то он заменит все содержимое узла body страницы одним элементом h1 с текстом «Привет, Мир!». Это так же просто, как если бы вы в своей программе создали объект, задали методы и вызывали бы их. Ничего сверхъестественного и мистического.
2.3. Браузер и его объекты функциональности  43 С помощью компонентов браузера вы можете создать для пользователей любое визуальное представление, которое будет динамически меняться в зависимости от действий пользователя или временных интервалов (с помощью setTimeout, setInterval). А еще — настроить взаимодействие с удаленным сервером или выполнение работы параллельно, организовать очень быстрый чат, шифровать или кэшировать данные, управлять историей действий пользователя, работать с двоичными файлами. Кроме того, доступно улучшение производительности приложения с помощью WebAssembly. После того как вы изучили JavaScript, можно заняться объектами, классами и функциями платформы, для которой планируете писать программы. Вы должны знать, как устроена функциональность платформ, на которых будет работать ваша программа. Почему? Да просто потому, что, не зная, через какой объект и его методы доступна необходимая функциональность, невозможно написать программу. Плюс не забывайте, что функциональность платформ постоянно дополняется и улучшается. 2.3.3. Библиотеки Библиотека — это совокупность классов, объектов и функций, имеющих направленность. Под направленностью понимается тематика. Библиотека предназначена или только для работы с языком и при этом используется в разных платформах, или для работы с функциональностью конкретной платформы, а также с функциональностью на нескольких платформах одновременно. Как правило, она инкапсулирует чаще всего используемую функциональность для определенного направления. Библиотеки бывают общего назначения и на определенную тему. Например, lodash — JavaScript-библиотека общего назначения, привносящая удобные утилиты для обработки данных. Ramda содержит набор функций, которые предназначены для функционального программирования. Redux ориентирована на состояние приложения, но нет никакой разницы, где она работает, — это чисто JavaScriptбиблиотека. А такие библиотеки, как ReactDOM, Vue, jQuery, React Router, предназначены для платформы и конкретно для браузера. ReactDOM и Vue работают с DOM, это их основное назначение. Они используют API событий, API Document и др. React Router завязана на API History и API Location. Что касается библиотеки jQuery, то она шире общего назначения при работе с API браузера, так как охватывает многие объекты браузера для построения кросс-браузерных приложений1. При работе с платформой библиотека выполняет роль промежуточного слоя между платформой и программой. Она определяет, как будет выглядеть код. Ведь, задействуя библиотеку, вы обращаетесь к ее классам, объектам, функциям и глобальным данным и либо совсем не используете функциональность платформы напрямую, либо используете небольшую часть, а оставшуюся задействуете через библиотеку. 1 Кросс-браузерный означает способность работать в браузерах разных производителей или в разных версиях одного производителя.
44  Глава 2 С чем имеет дело фронтенд-разработчик 2.3.4. Библиотеки React и ReactDOM Если вы используете React, то это будет одна из главных зависимостей в коде вместе с ReactDOM, которые определяют, как вы строите код. Помимо того что задействуются функции и объекты React, сама библиотека, правда с использованием других инструментов, привносит в разработку дополнительную синтаксическую конструкцию — JSX (рис. 2.3). Она позволяет заменить вызовы функций React.createElement кодом, подобным HTML-коду. Таким образом сокращается время на создание компонентов и очень сильно упрощается написание того, что их рендерит1. Библиотека React привносит в разработку удобство, идеи, с помощью чего должно строиться приложение, работающее в браузере, и при этом избавляет от необходимости думать об оптимизации. Рис. 2.3. React — посредник между браузером и приложением, предоставляющим свой API 2.3.5. Преимущество библиотек С одной стороны, библиотеки заставляют играть по их правилам, с другой — экономят время. И вот почему. Библиотеки поддерживает определенная группа людей, которые за вас работают и тестируют новую функциональность, прежде чем та попадет в новый релиз. Взгляните на ситуацию по-другому: если бы не существовало какой-то библиотеки, то ее пришлось бы разрабатывать вам. Однако есть в происходящем и минус — придется изучить функциональность, иначе вы не сможете ее использовать. Замечу, что иногда это не самая простая задача. Для некоторых библиотек не существует хорошего руководства или они бестолково написаны, даже несмотря на громкие имена компаний-создателей. Если бы вы придумывали функциональность сами, то ее было бы просто использовать. Однако не всегда можно додуматься до тех же самых идей, которые воплощены в сегодняшних библиотеках. Так происходит не в силу скудности мировоззрения, а из-за малого числа коммуникаций на данную тему и постановки задач перед собой, точнее, перед группой единомышленников. Если бы вы собрали группу разработчиков и сформулировали задачу, которую хотите решить, ваша группа сама бы придумала решения, которые сегодня воплотили инженеры компаний, разработавших React, Vue, Angular, ramda и т. д. В группе 1 Слово «рендерить» вошло в русский лексикон как есть, по произношению английского термина. Переводится как «изображать, исполнять, превращать, воспроизводить».
2.4. Загрузка приложения  45 вероятность нахождения решения растет пропорционально входящему в нее количеству людей. И последнее. То, что существует множество хорошо написанных библиотек, не значит, что не нужно пытаться создавать новые. Многие боятся осуждения за попытку, некоторые отнекиваются фразами типа: «Ну зачем придумывать велосипед». Однако сегодняшние реалии против этого — скорее наоборот. Единственная неудача, которая может постичь в разработке своей библиотеки, — остаться в состоянии провалившейся попытки вместо реализации конкретного плана для группы разработчиков в достижении конкретной цели по созданию конкретного продукта. Ведь разработка библиотеки сегодня — это план, а также техническая и административная организация работы, которую можно выполнить группой энтузиастов или с помощью нанятых специалистов. При этом необходимо подумать в том числе о финансировании. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 2.3. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 2.3. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 2.3 и ознакомьтесь с моим решением. 2.4. Загрузка приложения 2.4.1. Как осуществляется загрузка Когда адрес вашего приложения набран в браузере, прежде всего производится GET-запрос1 ресурса, расположенного по введенному URL. Сервер, обслуживающий запросы, отправляет в ответ текстовую строку, помечая тип содержимого — HTMLтекст. Браузер, получив ответ и поняв, что содержимое нужно интерпретировать как HTML, приступает к следующему шагу — парсингу, когда рассматривается только содержимое тела ответа, а все прочее опускается. 2.4.2. Парсинг содержимого строки, помеченной как HTML Слово «парсинг» переводится как «синтаксический анализ, разбор». В случае с браузером это разбор строки, которая пришла от сервера в ответ на запрос и содержимое которой помечено как HTML. Данный разбор сопровождается одновременным 1 GET-запрос — вид запроса по протоколу HTTP, подразумевающий передачу параметров запроса в самом URL. Параметры могут отсутствовать.
46  Глава 2 С чем имеет дело фронтенд-разработчик действием — созданием будущей страницы, которую увидит пользователь. Рас­ смотрим пример. Допустим, строка ответа от сервера выглядит так, как в листинге  2.8. Листинг 2.8. Строка ответа сервера <html><head><meta charset="utf-8"><title>Приложение</title><script src="application.js"></script></head><body><h1>Привет, Мир!</h1></body></html> На основе данной строки браузер начинает строить дерево объектов, именуемое DOM — Document Object Model, или по-русски «объектная модель документа». Под словом «документ» здесь подразумевается страница. DOM мы детально изучим в следующем разделе. А пока достаточно знать, что это некая модель страницы, состоящая из объектов, с которой вы можете взаимодействовать в вашем коде. В HTML-документе присутствуют разные виды тегов. Например, такие, как script, iframe и link, обрабатываются сразу. То есть если во время анализа встречается тег script, то выполняется GET-запрос для загрузки файла, расположенного по указанному абсолютному или относительному адресу в атрибуте src, и сразу осуществляется его исполнение. Если это JavaScript-код, то выполняется сценарий JavaScript, а если HTML-код, содержащийся в iframe, то происходит его парсинг. То же касается содержимого файлов стилей. Когда содержимое файла, прикрепленного к странице, проанализировано, продолжается анализ со следующей строчки — и так до конца строки HTML. А как обрабатываются, например, ресурсы с изображениями или видео? Точно так же, через GET-запрос, с той разницей, что загрузка осуществляется асинхронно (без ожидания ее завершения) и лишь затем анализируется следующая строчка. Синхронно загружается лишь JavaScript-код по умолчанию, и у вас есть возможность изменить данную установку. 2.4.3. Прослушивание событий Когда анализатор доходит до конца HTML-строки, происходит событие DOMContentLoaded. С этого момента наступает второй этап работы — прослушивание событий. Браузер ждет ввод от пользователя. Скрипты (JavaScript) уже загружены, и они назначили обработчики событий для некоторых узлов дерева DOM. Загрузка такого содержимого, как изображения, шрифты, видео, продолжает осуществляться, а необходимое для этого время зависит от объема данных, которые в них содержатся. Браузер выводит на страницу загруженные ресурсы, но сама структура страницы уже загружена, поэтому пользователь имеет возможность с ней взаимодействовать — вводить данные в формы с клавиатуры, нажимать на кнопки или другие элементы, с которыми можно взаимодействовать. Когда загрузятся все оставшиеся ресурсы, такие как видео и изображения, происходит событие load. Это отображается в виде переставшего крутиться колеса на вкладке браузера, возле крестика. В коде это событие, на которое вы потенциально можете отреагировать.
2.5. DOM  47 2.4.4. Жизнь приложения Приложение живет таким образом. Вы запрашиваете его по адресу, и загружается строка HTML-кода. Браузер разбирает эту строку и строит на ее основе DOM, а также выполняет код указанных в ней скриптов; загружает стили и применяет их; далее загружает другие ресурсы, тоже указанные в строке HTML-кода. После события DOMContentLoaded он ждет ввода от пользователя. На него приложение реагирует выполнением функций, установленных разработчиком, в качестве обработчиков конкретных событий или выполняет действие, предусмотренное по умолчанию для элемента страницы и не переопределенное разработчиком, например нажатие на ссылку (элемент a). Приложение что-то выполняет, иначе оно не имеет смысла. В  конечном счете пользователь его закрывает. Это основной и общий сценарий, без погружения в детали какого-либо приложения. 2.4.5. Другой взгляд на HTML Можно сказать, что HTML — это указания программе (браузеру), какие заранее известные программы (объекты) в какой последовательности и с какими настройками должны быть помещены в вывод документа (страница, отображаемая пользователю). Заранее известными программами или объектами являются, например, программа, отображающая видео, задаваемая тегом video, или программа воспроизведения аудио, задаваемая тегом audio. Кроме того, есть объекты, которые не являются программами и с которыми взаимодействует пользователь. Они отвечают за местоположение на экране, например выделение места, что определяется такими тегами, как div, section, header, footer и др. С помощью HTML мы через текст (с использованием тегов, понятных браузеру) управляем набором программ, который будет доступен пользователю и с которым он сможет взаимодействовать на странице. 2.5. DOM 2.5.1. Как все могло бы быть сложным Представим мир, в котором Тим Бернерс Ли1 не создал бы гипертекст, и тогда у нас не было бы привычных HTML, CSS, JS. Вместо него другой ученый, например Ли Хуньсэ из Китая, захотел бы, чтобы люди имели возможность создавать рекламные объявления о своих компаниях, продуктах или просто размещать информацию о себе. При этом ученый имел бы исключительно коммерческие интересы и повел бы себя как предприниматель, не желающий бесплатно расставаться со своей идеей на пользу человечества, поэтому решил извлечь выгоду из каждого этапа ее использования. Как бы действовал такой ученый-предприниматель? Скорее всего, он выбрал бы способ получения платы с тех, кто размещает информацию о своих товарах, то есть 1 Тим Бернерс Ли — создатель Веб.
48  Глава 2 С чем имеет дело фронтенд-разработчик с рекламодателей. А, допустим, для читателей это было бы бесплатно, но потребовалось бы установить себе на компьютер программу «Коммуникатор-читалка» для чтения и просмотра. И вот у нас есть программа, позволяющая людям читать тексты, смотреть видео и даже общаться в чате с помощью голоса и видео. Однако, чтобы создавать страницы, нужна специальная программа-редактор, полноценно работающая, пока действует оплаченная лицензия, которую необходимо периодически продлевать. Обновления вносятся исключительно этой программой-редактором. Наверное, такой ученый-предприниматель постарался бы запутать все возможности «халявного» использования технологии, чтобы никто без его ведома не мог пользоваться новой технологией бесплатно. А раз так, пришлось бы изрядно потрудиться, чтобы защитить свое детище. Представим, что сервер отправлял бы на клиент не HTML (JS, CSS), а, скажем, набор двоичного кода, куда были бы включены и тексты, и изображения, и программы. Для чего? Ну, просто так решил наш ученый, чтобы усложнить нелицензионное использование. Теперь представьте себе, каково было бы отредактировать страницу, например, при выявлении орфографических ошибок. Наверное, это было бы настолько сложно, что количество желающих использовать данную идею снизилось бы и наверняка появилась бы альтернативная технология. Слава богу, сегодня мы имеем гораздо лучшее настоящее, чем я описал: вместо «Коммуникатора-читалки» есть браузер, а вместо страниц, содержащих двоичный код и все программы, — текстовые документы. Сам браузер содержит все необходимые программы в установленном состоянии, и все, что от нас требуется, — правильно управлять с помощью сценариев на языках JavaScript, CSS, HTML. 2.5.2. Что такое модель Модель — это объект (не в коде, а в физической Вселенной), который заменяет другой объект, но воспроизводит какие-то его свойства. Модель применяется, когда нет возможности либо необходимости использовать основной объект или это по какой-то причине затратно. Модель чего-либо создается по-разному. Она может быть текстовой — в виде описания, графической — в виде схемы или изображения; сделана в виде копии объекта, в натуральную величину и уменьшенная, а также в виде формулы, если это модель явления. Главное преимущество работы с моделью в том, что она воспроизводит какоелибо свойство или несколько свойств основного объекта. По этой причине к нему не нужно обращаться напрямую, можно работать с моделью и по ней понять, как будет себя вести основной объект. Наглядным примером модели служит макет жилого комплекса. Вероятно, его создают не сами архитекторы, а их помощники, но идея ясна. Когда вы приходите в офис продаж, обычно показывают именно такой макет и рассказывают про будущий ЖК. В итоге у вас складывается представление, как будет выглядеть дом,
2.5. DOM  49 район и т. д. Но видели ли вы настоящий дом, район, улицу? Нет — вы смотрели на модель. Какие основные свойства дома и района воспроизведены с ее помощью? Во-первых, внешний вид ЖК, расположение домов и их место в районе. Во-вторых, относительная высота, цвет стен и т. п. Глядя на модель будущего ЖК, вы сможете принять решение о покупке квартиры. Но фактически вы с ней не взаимодействовали. Примером текстовой модели может являться описание двигателя моторной лодки. Да, вам недоступны для исследования многие его физические свойства, но вы можете узнать характеристики из описания и представить, подойдет ли мотор к вашей лодке. Говоря простыми словами, хватит ли мощности тянуть судно. Пожалуй, самый распространенный вид модели — фотография, которая воспроизводит внешний вид и очертания объекта. По ней можно понять относительные размеры. Например, вы смотрите на снимок здания в Санкт-Петербурге, выполненный в начале XX века, на само здание в начале XXI столетия и видите, что некоторые детали изменились. Расписание, планы на день или год, содержание книги — тоже модели. Если вы в начале дня составили план на ближайшие 24 часа, а затем, подумав, изменили его, то таким образом смоделировали свой день. Не обязательно вносить изменения: вы можете смоделировать день и с первого раза, если утром хорошо себе представляете, что будет к вечеру. Но если вы человек, привыкший мыслить результатами, и если они чем-то не устраивают, то можно перемоделировать день так, чтобы в итоге получить желаемое. Как видите, в жизни мы постоянно используем модели. 2.5.3. Что такое документ Этим словом обозначается два объекта. Первый, всем знакомый, — название свойства объекта window, window.document. Он представляет вкладку браузера, содержащую вывод, который мы задаем с помощью HTML-кода, CSS и JavaScript (имеется в виду, что JS может принять участие в добавлении элементов, их изменении или удалении, а CSS — оказать дополнительное влияние на вывод). Второй — графический вывод. Это то, что вы видите, когда заходите в браузер, открываете вкладку и, вбив адрес, наблюдаете выведенным на экран. Документ, для которого составляется модель. Для его создания в браузерах используется специальный механизм. Например, в браузере Chromium он называется Blink1 (рис. 2.4). 1 Blink — наименование механизма (движок) рендеринга (вывода содержимого на экран), с помощью которого строится DOM и парсится HTML. JavaScript запускается и внед­ ряется в отдельном движке, V8. Иногда рендеринг называют «лэйаут». Какое бы слово ни использовалось, главное, что делает движок Blink, — это подготавливает интерактивное содержимое, которое он выводит в браузере и с которым может взаимодействовать пользователь.
50  Глава 2 С чем имеет дело фронтенд-разработчик Рис. 2.4. Документ 2.5.4. Что такое Document Object Model (DOM) Важное правило моделирования гласит: как только вы создали модель, можно забыть про первоначальный объект, для которого она создавалась, и работать исключительно с моделью. Используя модель, не нужно рассуждать так: «Если я найду элемент, а затем изменю свойство, то браузер преобразует в графику...» Вам достаточно знать, как работать с моделью1. Однако важно правильно понимать, для какого объекта составлена модель. В предыдущем подразделе сказано, что такое документ. В нашем случае именно для него создана DOM. DOM — модель, создаваемая отдельно и независимо от языка программирования, который с ней взаимодействует. Документ на экране изображается 1 Поскольку данная книга не является полноценным справочником, полное описание здесь не приводится. Но есть главное — DOM-основа и основные операции. В текущем разделе приведены только моменты, важные с моей точки зрения. Более подробное описание вы можете найти по адресу https://developer.mozilla.org/en-US/docs/Web/API/Document_ Object_Model.
2.5. DOM  51 механизмом рендеринга, встроенного в браузер. Мы управляем им с помощью объектной модели документа (DOM), а не напрямую. Сама модель создана в виде структуры данных — дерева (рис. 2.5). Соответственно, работая с этим деревом (моделью), вы можете изменять документ. В дереве объектной модели докуменРис. 2.5. Типичное начало дерева DOM та есть родительский узел — document. Существуют и промежуточные узлы, которые чаще всего являются элементами, представляющими теги HTML. Есть концевые узлы, называемые листьями. Как правило, это текст или элемент, представляющий HTML-тег. Чтобы внести изменения в документ, необходимо изменить модель. Так как модель является структурой в виде дерева, то необходимые изменения вносятся в узел (-лы). Узел в модели управляет своими свойствами и связями со своими дочерними узлами, то есть структурой поддерева, родителем которой он является. Например, если у нас есть HTML-страница о турах в горы, то удаление заголовка h1 из модели документа отразится в виде изменения в самом документе: заголовок исчезнет со страницы. Здесь мы вносим изменение в узел body и меняем состав его дочерних элементов (удаляем h1). Рассмотрим пример. Допустим, у нас есть документ и его модель (рис. 2.6). Рис. 2.6. Документ и модель до изменения После удаления из модели узла, который представляет HTML (тег h1), в документе произойдут корректировки — пропадет надпись «Всем привет!». При этом изменится графический вывод, осуществляемый браузером (рис. 2.7).
52  Глава 2 С чем имеет дело фронтенд-разработчик Рис. 2.7. Модель и документ после изменения DOM включает в себя API, через который вы можете взаимодействовать с моделью. Поэтому взаимодействие с DOM, или, другими словами, работа с моделью документа, представляет собой взаимодействие с помощью методов, имеющихся в API DOM. Это также получение значений из свойств, которыми располагают объекты модели, или задание им значений — напрямую либо через методы. Пример взаимодействия с объектной моделью документа с помощью API показан на рис. 2.8. Рис. 2.8. Пример взаимодействия с моделью DOM
2.5. DOM  53 2.5.5. Свойство document и операции в DOM Чтобы стать гуру веб-разработки, нужно изучить API DOM. И не только. Еще, как было сказано в предыдущих разделах, есть сама платформа — браузер, а в нем — объект window, где существует множество разных идентификаторов, представляющих интерфейсы, конструкторы или просто объекты, обеспечивающие доступ к готовой функциональности. Но вернемся к объектной модели документа. Все API модели доступно через свойство document объекта window. Поскольку модель документа представляет собой дерево, его объекты-узлы связаны между собой ссылками. Для взаимодействия с моделью доступен целый ряд операций. Изобразим их и опишем, далее каждой группе операций будет посвящен подраздел. Первая группа — поиск узлов в дереве. Вторая группа — обход узлов дерева. Третья группа — создание и добавление узлов в дерево. Четвертая группа — удаление узлов из дерева. Пятая группа — изменение свойств и атрибутов узла. 2.5.6. Операции поиска узлов в дереве Дерево узлов, которые представляют собой текст или тег, строит браузер на основе HTML-кода страницы, о чем шла речь в предыдущем разделе «Загрузка приложения». Соответственно, у вас есть готовая структура дерева, в которой можно осуществлять поиск узлов, отвечающих определенным критериям (рис. 2.9). Поиск узлов реализуется через ряд методов, Рис. 2.9. Операции в DOM — поиск узлов таких как querySelector, querySelectorAll, getElementById, getElementsByClassName, getElementsByName и getElementsByTagName. Детально методы мы изучим на практике, а здесь ограничимся списком (табл. 2.1). Таблица 2.1. Методы поиска узлов в дереве DOM Метод Что делает Реализуется интерфейсом Возвращает querySelector Осуществляет поиск элемента по CSS-селектору, передаваемому аргументом Element Element | null querySelectorAll Осуществляет поиск элементов по CSS-селектору, передаваемому аргументом Element NodeList (статический) Продолжение 
54  Глава 2 С чем имеет дело фронтенд-разработчик Таблица 2.1 (продолжение) Метод Что делает Реализуется интерфейсом Возвращает getElementById Осуществляет поиск элемента по идентификатору, передаваемому аргументом Document Element | null getElementsByClassName Осуществляет поиск элементов по имени класса, передаваемого аргументом Element HTMLCollection getElementsByName Осуществляет поиск элементов по значению атрибута name Document NodeList (живой) getElementsByTagName Осуществляет поиск элементов по имени тега Element HTMLCollection (живая) (живая) 2.5.7. Обход дерева DOM API-модели документа включают в себя методы обхода узлов в разных направлениях (рис. 2.10). Независимо от того, с какого узла вы начали, это позволяет обойти все узлы дерева. Их удобно обходить как сверху вниз, так и снизу вверх; как слева направо, так и справа налево. За счет чего это достигается? За счет реализации ссылок (в виде свойств) на другие узлы (они же являются объектами), схема которых показана на рис. 2.11. Рис. 2.10. Операции в DOM — обход узлов Рис. 2.11. Ссылки на другие узлы из рассматриваемого узла Node Уже при беглом взгляде на рисунок становится очевидно, что через свойства объекта можно получить ссылку на другие узлы, к которым текущий узел имеет отношение. Рассмотрим конкретный пример. Допустим, у нас есть часть поддерева страницы, показанная на рис. 2.12. Поддерево — часть какого-то дерева. По сути, оно тоже дерево, но чуть меньшего размера. В качестве текущего узла выделим объект с именем main. Теперь посмотрите, как будут выглядеть ссылки каждого объекта в этом дереве (рис. 2.13).
2.5. DOM  55 Рис. 2.12. Дерево с родительским узлом main Рис. 2.13. Ссылки на соседние, дочерние и родительские объекты На рис. 2.13 стрелка, идущая сверху вниз и расположенная слева, означает свойство firstChild, если справа — lastChild. Стрелка снизу вверх означает свойство parentNode, независимо от расположения. Стрелка слева направо — свойство nextSibling, а стрелка справа налево — previousSibling. Итак, обход дерева осуществляется с помощью ссылок. У нас есть свойства, наследуемые двумя интерфейсами: один ряд свойств — от Node, другой — от Element. Свойства в предыдущем абзаце доступны из интерфейса Node (табл. 2.2, 2.3). Таблица 2.2. Свойства узла в дереве DOM, с помощью которых осуществляется обход, доступные в интерфейсе Node Свойство parentNode Значение Возвращает ссылку на родительский узел типа Element, Document, DocumentFragment или null firstChild Возвращает ссылку на первый дочерний узел или null lastChild Возвращает ссылку на последний дочерний узел или null nextSibling Возвращает ссылку на следующий узел, который, как и дочерний, находится в родительском узле, в свойстве childNodes, либо null previousSibling Возвращает ссылку на узел, предшествующий текущему, который, как и дочерний, находится в родительском узле, в свойстве childNodes, либо null
56  Глава 2 С чем имеет дело фронтенд-разработчик Таблица 2.3. Свойства узла в дереве DOM, с помощью которых осуществляется обход, доступные в интерфейсе Element Свойство Значение parentElement Возвращает ссылку на родительский узел, если его тип — Element, в противном случае — null firstElementChild Возвращает ссылку на первый дочерний узел, если его тип — Element, в противном случае — null lastElementChild Возвращает ссылку на последний дочерний узел, если его тип — Element, в противном случае — null nextElementSibling Возвращает ссылку на следующий узел, чей тип — Element, который, как и текущий узел, находится в свойстве children родительского узла (не обязательно тип Element), или null previousElementSibling Возвращает ссылку на предыдущий узел, чей тип — Element, который, как и текущий узел, находится в свойстве children родительского узла (не обязательно тип Element), или null При построении DOM браузер создает все необходимые ссылки между узлами дерева. Когда вы пишете программу, у вас уже есть готовая структура со ссылками. А если вы создаете новый элемент и вставляете его в дерево, то необходимые ссылки на другие элементы устанавливаются автоматически. Этот момент мы исследуем на практическом занятии, а сейчас внимательно рассмотрим ссылки на рис. 2.13. Наверняка первое, что бросается в глаза, — это то, что у элементов li есть только ссылка на родителя, а у элемента ul — ссылки только на первый и последний элементы li. Все действительно так. На рис. 2.11 это показано стрелками и наименованиями firstChild и lastChild. У узла есть ссылка лишь на первый и последний дочерний элементы. Это не значит, что о существовании других дочерних элементов узел ничего не знает. Знает, но не через ссылки, а через свойство, доступное каждому узлу, — childNodes, где содержится объект, подобный массиву1. Далее, в следующем подразделе, мы детально изучим интерфейс Node, который наследуется каждым узлом в дереве, независимо от его типа. Рассмотрим самый первый (крайний слева) элемент li. На рис. 2.13 показано, что у него есть parentNode, nextSibling, firstChild. Однако это небольшое упрощение. Фактически его ссылки и значения такие, как показано на рис. 2.14. На рис. 2.14 видно, что у элемента li присутствует пять исходящих ссылок. Все они всегда есть у узла. А число входящих ссылок меняется в зависимости от ситуации. Какой может быть ситуация с узлом? Рассмотрим наиболее распространенные из них, в том числе частный случай с узлом document. Все узлы DOM-дерева, кроме 1 Объект, подобный массиву, — обычный объект JavaScript, который содержит свойство length и ключи в виде цифр, а для работы в прототипе — некоторые методы, чьи имена и функция аналогичны таковым в прототипе у массивов. Таковым является объект с интерфейсом NodeList. Подробно ознакомиться с ним можно по адресу https://developer.mozilla.org/ en-US/docs/Web/API/NodeList.
2.5. DOM  57 document, имеют минимум одну входящую ссылку от родительского узла или две — firstChild, lastChild. Какая именно будет ссылка, зависит от того, каким по счету дочерним по отношению к родительскому узлу является рассматриваемый узел. Если же он единственный, то будет содержать обе ссылки. Узел document не имеет входящих ссылок от родительского узла, поскольку является самым первым узлом в DOM-дереве. Такой узел называется корневым (рис. 2.15). Рис. 2.14. Фактические ссылки объекта элемента li У узла document входящие ссылки могут быть только от дочерних узлов. По умолчанию их всегда две: одна — от объекта для элемента HTML, другая — от DOCTYPE. Если не указан DOCTYPE, то будет всего одна входящая ссылка от HTML. Но, как сказано выше, любой узел всегда имеет пять исходящих ссылок. Куда ведет ссылка parentNode у объекта document? Никуда, и она равна null. Рассмотрим другую ситуацию, когда узел является концевым. Таковым на рис. 2.13 может быть текстовый узел «Горящие», как и любой другой текстовый узел, являющийся дочерним по отношению к узлу объекта li. У него может быть только две входящие ссылки, как показано на рис. 2.16. Рис. 2.15. Входящие ссылки узла document Рис. 2.16. Входящие ссылки единственного дочернего узла какого-либо родителя На рис. 2.16 показана ситуация, когда дочерний узел является единственным у какого-то родителя и не имеет собственных дочерних узлов, то есть он конечный. Такой узел будет иметь только две входящие ссылки, и обе от родителя, что само собой разумеется. Если узел не конечный, но единственный у родителя, то число входящих ссылок будет таким, как показано на рис. 2.17. Число входящих ссылок на узел будет равно количеству дочерних узлов плюс две от родителя. Осталось рассмотреть две ситуации. У них могут быть вариации, которые не имеет смысла изображать. Первая — когда узел является дочерним, но не первым
58  Глава 2 С чем имеет дело фронтенд-разработчик и не последним для некоторого родителя. От родителя на него не будет входящей ссылки, как показано на рис. 2.18. Рис. 2.17. Входящие ссылки узла, являющегося единственным у какого-то родителя, но не являющегося листовым (конечным) Рис. 2.18. Входящие ссылки узла, являющегося не первым и не последним дочерним для родителя Сразу замечу, что на рис. 2.18 показаны максимально возможные входящие ссылки. Однако узел может не иметь дочерних узлов, поэтому у него максимум будет две входящие ссылки — nextSibling и previousSibling. Оставшиеся ситуации показаны на рис. 2.19. Рис. 2.19. Входящие ссылки узла, в том числе от родителя Если узел не является единственным дочерним, но является последним, то он будет иметь входящую ссылку от родительского узла и от соседнего узла, находящегося с ним на одном уровне. Если же узел не является единственным дочерним, но является первым, то он будет иметь входящую ссылку от родительского узла и от соседнего узла, который является следующим дочерним узлом родителя. Стоит также оговорить, что узел может не иметь своих дочерних узлов; соответственно, входящих ссылок от них у него не будет. Если еще раз просмотреть рис. 2.15–2.19, то станет очевидно, что любой узел имеет минимум две входящие ссылки (кроме document, который может иметь одну от HTML). Но на него может ссылаться большее число объектов. Если же обратиться к рис. 2.14, то станет видно, что узел всегда содержит пять исходящих ссылок. Напомню: фраза «исходящие ссылки» подразумевает фактические ссылки, такие же, как если бы вы ссылались внутри одного объекта на другой объект.
2.5. DOM  59 Рассмотрим листинг 2.9, где присутствуют все пять свойств узла, представляющие собой исходящие ссылки на другие узлы дерева DOM. Он применяется к документу, который приведен на рис. 2.6. Листинг 2.9. Использование пяти свойств узла const main = document.querySelector('main'); const { parentNode, firstChild, lastChild, nextSibling, previousSibling, } = main; console.log('parentNode', parentNode); console.log('firstChild', firstChild); console.log('lastChild', lastChild); console.log('nextSibling', nextSibling); console.log('previousSibling', previousSibling); Если выполнить данный код и посмотреть в консоль, то там будет следующая картина. Скорее всего, вы ожидали, что свойства nextSibling и previousSibling будут равны null, а свойства firstChild и lastChild — ссылаться на объекты, представляющие теги li. Вы не ошиблись, но я сейчас поясню, почему это не так в данный момент, и затем мы добьемся нужных значений. На рис. 2.20 показан вывод скрипта. Рис. 2.20. Вывод скрипта из листинга 2.9 в консоль
60  Глава 2 С чем имеет дело фронтенд-разработчик На снимке экрана видно, что свойства firstChild, lastChild, nextSibling и previousSibling ссылаются на узел, изображенный как #text. Почему? Все просто. Дело в том, что в программе, в которой составляется HTML-код страницы, теги расположены так, как показано на рис. 2.21. Рис. 2.21. HTML-код страницы, для которой вызван код в листинге 2.9 А что здесь удивительного? Код как код. Да, выглядит привычно. Но дело в том, что по умолчанию редактор кода не делает очевидными непечатаемые символы. Вы напечатали тег main, затем нажали клавишу Enter и таким образом вставили символ переноса на новую строку. Это текстовый узел, и он является дочерним узлом объекта, который представляет тег body. Если взглянем на дочерние узлы тега body, то это и обнаружим (рис. 2.22). Если сейчас посмотреть на рис. 2.21, туда, где расположен тег body, то станет заметно, что после него есть перенос строки, затем идет тег header, далее — перенос строки, затем идет тег main, далее — перенос строки, затем идет тег script и после него — тоже перенос строки. Именно это отображает в консоли свойство childNodes объекта для тега body. Но как быть в данной ситуации? Это абсолютно стандартная ситуация, и далее будет показано, как с ней работать. А как себя поведет свойство childNodes для тега body без символов переноса на новую строку, узнаем на практическом занятии. Ранее мы рассмотрели исходящие из узла и входящие в него ссылки. Открою небольшой секрет: когда вы пишете программу для работы с деревом, входящие ссылки не рассматриваете. В разделе они показаны лишь для ориентации, чтобы вы
2.5. DOM  61 понимали, сколько узлов и почему могут ссылаться на конкретный узел. Исследуя входящие ссылки, вы можете получить общую картину — не более. А вот исходящие ссылки дают возможность принимать решения в программе: куда двигаться дальше по дереву объектов. Ссылка всегда строго соединяет два объекта, какого бы типа они ни были. Например, один объект может ссылаться на другой или на null, что будет означать отсутствие ссылки на другой объект. Рис. 2.22. Дочерние элементы тега body И напоследок рассмотрим два листинга, где могут использоваться указанные в табл. 2.2 и 2.3 свойства узлов для организации обхода дерева. Вы освоите прием для обхода узлов на одном уровне и обход узлов сверху вниз по левой стороне. С другими способами обхода вы познакомитесь на практических занятиях. Там же изучите объект TreeWalker, представляющий узлы поддерева документа и позицию в нем. Для начала — использование обхода узлов, расположенных на одном уровне. Лис­ тинг 2.10 показывает, как это реализуется с помощью свойств интерфейса Element. Обход осуществляется среди дочерних элементов узла ul страницы «Туры в горы». Листинг 2.10. Обход узлов, представляющих элементы и расположенных на одном уровне const ul = document.querySelector('ul'); const startLi = ul.firstElementChild; for (let node = startLi; node !== null; node = node.nextElementSibling) { console.log(node); } То, что будет выведено в консоль в результате работы этого кода, показано на рис. 2.23. Теперь используем обход узлов, расположенных на разных уровнях. Листинг 2.11 показывает, как это реализуется с помощью свойств интерфейса Element. Обход осуществляется вглубь по первым дочерним элементам, начиная с узла body страницы «Туры в горы» (рис. 2.24). Листинг 2.11. Обход узлов, представляющих элементы и расположенных на одном уровне for (let node = document.body; node != null; node = node.firstElementChild) { console.log(node); }
62  Глава 2 С чем имеет дело фронтенд-разработчик Рис. 2.23. Вывод в консоль при обходе узлов-элементов, расположенных на одном уровне Рис. 2.24. Вывод в консоль при обходе узлов-элементов, расположенных на разных уровнях и являющихся первым дочерним узлом своего родителя 2.5.8. Создание и добавление узлов в дерево В подразделе 2.5.11 «Узлы дерева DOM и их свойства» мы рассмотрим некоторые типы узлов дерева DOM. Методы для создания разных типов узлов содержатся в интерфейсе Document. В текущем подразделе мы исследуем лишь два метода для создания текстовых узлов и элементов (рис. 2.25). Элемент представляет собой какой-либо HTML-тег, а текстовый узел — текст. Последний может быть только дочерним и не может быть родительским. Элемент может быть и родительским, и дочерним. Оба могут выступать в роли братьев. Рис. 2.25. Операции в DOM — создание Создание элемента или текста либо любого другого узла происходит вне дерева DOM. и добавление узлов в дерево
2.5. DOM  63 Но, как только узел создан, в нужный момент работы фронтенд-приложения вы можете вставить его в дерево. По сути, есть две отдельные операции — создание узла и создание связи между новым и другим узлом. При этом другой узел или уже есть в дереве DOM, или также еще вне его. Наглядно эти операции показаны на рис. 2.26, 2.27. Рис. 2.26. Создание элемента Рис. 2.27. Создание связи Если элемент создан, но пока не связан с деревом, то на странице его содержимое не отобразится. Рассмотрим создание элемента и текстового узла на примере, создадим связь между ними, затем — связь с каким-либо узлом, существующим в DOM-дереве, и посмотрим, как изменится страница. У нас есть страница компании, предоставляющей «Туры в горы». Допустим, мы хотим добавить еще один вид туров — «По 128 000 руб.» (рис. 2.28). Листинг 2.12 содержит код, создающий элемент и текст. Листинг 2.12. Создание элемента и текста const li = document.createElement('li'); const text = document.createTextNode('По 128000 руб.'); На рис. 2.28 видно, что мы сделали. Теперь свяжем текстовый узел и узел li (листинг 2.13). Листинг 2.13. Вставка текстового узла как дочернего в элемент li li.appendChild(text);
64  Глава 2 С чем имеет дело фронтенд-разработчик Рис. 2.28. Элемент li и текстовый узел, созданные, но не связанные ни с одним другим элементом Теперь посмотрим на рис. 2.29, что мы сделали. Рис. 2.29. В элемент li вставлен текстовый узел в качестве дочернего, и они оба по-прежнему вне DOM На рис. 2.29 видна связь между текстом и узлом. Но наше дерево из двух элементов все еще не связано с деревом DOM. Установить связь поможет метод appendChild того узла, с которым мы хотим связать наше дерево из двух узлов. Итак, для этого нужно: 1) найти узел (получить на него ссылку) в дереве DOM, у которого дочерним узлом будет наше дерево из двух узлов; 2) создать связь между узлом дерева DOM и узлом li. Эти методы показаны в листинге 2.14, а результат — на рис. 2.30. Листинг 2.14. Нахождение узла в DOM, с которым будет связано дерево из двух элементов const li = document.createElement('li'); const text = document.createTextNode('По 128000 руб.'); li.appendChild(text); const ul = document.querySelector('ul'); ul.appendChild(li);
2.5. DOM  65 Рис. 2.30. Дерево, вставленное в DOM Эти методы создают элемент li и текстовый узел. Далее текстовый узел связывается с элементом li. Затем в дереве DOM находится узел ul, с которым и связывается узел li. Теперь наше дерево из двух элементов стало частью модели DOM (рис. 2.31, табл. 2.4). На рис. 2.31 видно, что на странице появился новый элемент li с текстом. Рис. 2.31. Дерево, вставленное в DOM
66  Глава 2 С чем имеет дело фронтенд-разработчик Таблица 2.4. Методы создания узлов в дереве DOM Метод Что делает Реализуется интерфейсом Возвращает createElement Создает узел типа Element Element Element createTextNode Создает узел типа TextNode Node TextNode 2.5.9. Удаление узлов из DOM-дерева Для удаления узлов нужно использовать метод removeChild, который есть в интерфейсе Node. Как следует из его названия, он удаляет конкретный дочерний элемент. Однако далее будут даны пояснения, что именно он делает. Здесь под «удаляет конкретный дочерний элемент» подразумевается результат, но не процесс. Чтобы выполнить задачу, необходимо получить ссылку на узел, у которого нужно удалить элемент, и ссылку на сам удаляемый элемент (рис. 2.32). Рис. 2.32. Операции в DOM — удаление узлов из дерева Рассмотрим наш сайт «Туры в горы». Чтобы не повторяться, взглянем на рис. 2.31. Допустим, мы хотим удалить узел li с текстом «по 64 000 руб.». Что ж, нет проблем. Нужно получить ссылку на родителя и сам удаляемый узел, а вслед за этим — применить метод removeChild. Листинг 2.15 показывает, как это реализуется. Листинг 2.15. Удаление узла из дерева DOM const ul = document.querySelector('ul'); const childToDelete = ul.lastChild.previousSibling; ul.removeChild(childToDelete); Если мы сейчас посмотрим, как наше действие повлияло на страницу (рис. 2.33), то увидим, что она изменилась. Теперь разберемся, что именно делает метод removeChild (табл. 2.5). Он удаляет связь, а сам элемент не удалится, пока для этого не наступят необходимые условия. Графически работа метода показана на рис. 2.34. Какие должны быть условия? Если вы сохраните возвращаемое значение в переменной, то узел, связь с которым была разорвана, можете подсоединить в любое другое место или обратно в то же самое место. Подобное упражнение вы выполните на практическом занятии.
2.5. DOM  67 Рис. 2.33. Вид страницы после удаления узла с текстом «по 64 000 руб.» Таблица 2.5. Метод удаления дочернего узла в дереве DOM Метод Что делает Реализуется Возвращает интерфейсом removeChild Удаляет ссылку у родительского узла на некоторый дочерний узел. Для удаления ссылки ее необходимо передать методу в качестве аргумента Node Element | TextNode | null, то есть ссылку на тот объект того типа, для которого удалена связь с родительским узлом Рис. 2.34. Что в действительности делает метод removeChild
68  Глава 2 С чем имеет дело фронтенд-разработчик 2.5.10. Изменение атрибутов и свойств узлов DOM-дерева Слово «атрибут» означает существенный признак, постоянное свойство чего-либо, неотъемлемую принадлежность предмета (рис. 2.35). И действительно, атрибут DOM-узла представляет собой пару «имя — значение», прикрепленную к HTMLэлементу или к другому DOM-узлу. Атрибуты используются для хранения дополнительной информации о DOM-узле, которая может быть использована для его стилизации или функциональности. Например, атрибуты могут содержать информацию о классе, идентификаторе, стиле, URL-ссылке и других пользовательских данных (атрибут data-). Отличие атрибута от свойства заключается в том, что атрибуты являются частью HTMLразметки и определяют исходное состояние элемента при загрузке страницы, в то время как свойства представляют текущее состояние DOM-элемента и могут изменяться в процесРис. 2.35. Операции в DOM — изменение се работы с приложением. Пример атрибута атрибутов и свойств узла показан в листинге 2.16. Листинг 2.16. Атрибуты DOM-узла HTML-тега ```html <a href="https://www.example.com" class="link">Link</a> ``` В данном примере тег <a> является DOM-узлом, атрибуты href и class определяют его URL-ссылку и класс соответственно. Свойство DOM-узла — это JavaScript-свойство, которое предоставляет доступ к разным свойствам и методам объекта-узла в DOM. Свойства DOM-узла являются прямыми ссылками на свойства и методы, предоставляемые браузером для работы с элементами DOM. Они позволяют получать и изменять атрибуты, значения и состояние узлов, такие как текст содержимого, классы, стили, а также запускать определенные методы (например, методы элемента для работы с событиями). Важно отметить, что свойства DOM-узла могут отображаться в различных форматах, в зависимости от конкретной реализации DOM и браузера, в то время как атрибуты обычно представляются в виде строк. В итоге свойства и атрибуты DOM-узла взаимодействуют между собой, изменение одного из них может повлечь изменения в другом. Свойства DOM-узла можно посмотреть в браузере. Самый быстрый способ сделать это — выделить какой-либо элемент в DOM-узле с помощью инструмента выделения и в консоли написать выражение как в листинге 2.17. Листинг 2.17. Вывод свойств узла в консоль console.dir($0)
2.5. DOM  69 В консоли будут видны все свойства DOM-узла, как показано на рис. 2.36. Рис. 2.36. Операции в DOM — изменение свойств узла Самой распространенной операцией по изменению свойств и атрибутов узла может быть изменение CSS-класса узла или CSS-свойства, если речь об элементе, либо изменение текстового содержимого, если о тексте. Рассмотрим эти действия. Прежде всего изменение атрибута class. Атрибуты меняются через свойства объекта или специальные методы. Первым действием является получение доступа к узлу, что уже было изучено в подразделе 2.5.6. Атрибут class изменяется с помощью свойства узла classList (листинг 2.18). Листинг 2.18. Изменение атрибутов узла const ul = document.querySelector('ul'); ul.classList.add('some-class'); Атрибуту class соответствует свойство объекта className. Напомню, что под объектом подразумевается ссылка на тот объект, который, например, найден с помощью метода поиска querySelector или же по ссылке firstElementChild. Если
70  Глава 2 С чем имеет дело фронтенд-разработчик посмотреть на рис. 2.36, то видно, что свойство объекта className содержит пустую строку. Однако после выполнения кода листинга 2.18 оно будет содержать строку some-class, в чем легко убедиться, взглянув на рис. 2.37. Рис. 2.37. Изменение значения свойства className Переходим к свойствам объекта. Некоторые свойства являются таковыми лишь для чтения, но далеко не все. Например, текстовый узел в свойстве nodeValue будет содержать текст, который выводится браузером на экране в каком-либо теге. Сейчас мы в этом убедимся. Нам нужно получить ссылку на этот узел, чтобы увидеть значение свойства nodeValue. В листинге 2.19 показан код, который изменит значение текстового узла. Листинг 2.19. Изменение значения свойства объекта const ul = document.querySelector('ul'); const [textNode] = ul.firstElementChild.childNodes; textNode.nodeValue = "Распродажные"; Как это отразилось на странице, показано на рис. 2.38. Итак, атрибуты меняются, как и свойства. Изменение свойств объекта обычно приводит к изменениям во внешнем виде. Точнее, чаще всего возникает побочный эффект. Таким образом, через свойства можно влиять на многое, например программно управлять прокруткой элемента, CSS-свойствами и классами, изменять содержимое.
2.5. DOM  71 Рис. 2.38. Изменение значения свойства nodeValue объекта, представляющего текстовый узел Напоследок приведу в табл. 2.6 методы добавления CSS-класса. Таблица 2.6. Метод добавления CSS-класса для узла как в дереве DOM, так и вне его Метод Что делает Реализуется интерфейсом Возвращает classList1.add Добавляет CSS-класс в узел, то есть изменяет атрибут class, что выражается изменением свойства className объекта, представляющего этот узел Element undefined Существует еще один способ изменить атрибуты узлов DOM-дерева: атрибут может быть создан отдельно от узла, что само по себе примечательно. Вот как это делается (листинг 2.20). Листинг 2.20. Создание атрибута узла const titleAttr = document.createAttribute('title'); titleAttr.value = 'Это список всех типов туров, имеющихся на сегодня'; const ul = document.querySelector('ul'); ul.setAttributeNode(titleAttr); 1 О других методах, имеющихся в classList, можно прочитать здесь: https://developer.mozilla.org/ en-US/docs/Web/API/Element/classList.
72  Глава 2 С чем имеет дело фронтенд-разработчик С помощью данного способа можно так же, как описано ранее, задать атрибуты accesskey, class, contenteditable, contextmenu; пользовательские атрибуты data-*, dir, draggable, hidden, href, id, lang, spellcheck, style, tabindex, title, translate, name, type, aria-*. Результат действия листинга представлен на рис. 2.39. Рис. 2.39. Создание атрибута и его внедрение в элемент 2.5.11. Узлы дерева DOM и их свойства Если открыть свойства для объекта (являющегося узлом) document, то вы увидите длинный список разных свойств и вдобавок сможете открывать прототипы, где тоже приведены длинные-предлинные списки свойств, но не бесконечные. Если использовать браузер Chrome, то прототипы будут подписаны именами тех интерфейсов, которые реализованы в этом прототипе. Например, если проследить свойства как самого узла document, так и его прототипов, то будет следующая цепочка: document → HTMLDocument → Document → Node → EventTarget → Object.
2.5. DOM  73 Что насчет, допустим, элемента h1? Вот его цепочка: h1 → HTMLHeadingElement → HTMLElement → Element → Node → EventTarget → → Object. А если взять текстовое содержимое узла h1? Его цепочка будет выглядеть так: текстовый узел → Text → CharacterData → Node → EventTarget → Object. Легко заметить, что разные по сути типы узлов в итоге наследуют свойства от нескольких общих интерфейсов, а именно — цепочка Node → EventTarget → Object. Значит ли это, что каждый узел, независимо от типа, одновременно содержит все свойства из Object, EventTarget и Node? Да, все верно. Кроме того, легко заметить, что если подниматься вверх по цепочкам прототипов, начиная с Object, то после интерфейса Node начинаются различия. Так, узел document после Node наследует свойства от Document. Объект, представляющий HTML-теги, после Node наследует от Element, а текстовый узел — от CharacterData. Теперь вспомните свойства из табл. 2.2 и 2.3. Свойства в табл. 2.2 есть у каждого узла, а свойства из табл. 2.3 — только у узлов типа Element. Поскольку каждый тип узла наследует свойства у Node, то независимо от того, на какой из типов узлов вы перешли с предыдущего узла, вам всегда доступны свойства из табл. 2.3. Существуют ли другие типы узлов, кроме приведенных выше? Да, существуют. В том же интерфейсе Node доступно свойство nodeType. Оно только для чтения и возвращает константу, означающую тип узла. В табл. 2.7 приведены значения, доступные для этого свойства. Таблица 2.7. Все возможные значения свойства nodeType узлов DOM Имя Значение ELEMENT_NODE 1 ATTRIBUTE_NODE 2 TEXT_NODE 3 CDATA_SECTION_NODE 4 PROCESSING_INSTRUCTION_NODE 7 COMMENT_NODE 8 DOCUMENT_NODE 9 DOCUMENT_TYPE_NODE 10 DOCUMENT_FRAGMENT_NODE 11 Свойство nodeType возвращает число, которое и говорит о типе узла, если сопоставить с таблицей. Что представляют собой значения? y 1 — элемент наподобие div, p, h1 и т. д. Мы рассмотрели достаточно подобных примеров.
74  Глава 2 С чем имеет дело фронтенд-разработчик y 2 — атрибут элемента. Пример использования узлов данного типа мы расy y y y y y y смотрели в подразделе 2.5.10. 3 — текстовый узел. Примеры текстовых узлов мы рассмотрели ранее. 4 — CDATASection, тип узлов, возможных только в документах типа XML. В двух словах, зачем нужен такой узел. CDATASection — интерфейс, представляющий секцию CDATA, чтобы включить расширенные части текста, которые не предваряют escape-последовательности. Если в HTML включить CDATASection, то он будет проигнорирован, точнее, рассмотрен как комментарий. 7 — Processing Instruction, необходимый для XML-документов. Processing Instruction, или инструкция по обработке, — узел, который внедряет инструкцию, направленную на указанное приложение. Может быть проигнорирован другими приложениями, не распознающими инструкцию. 8 — узел-комментарий. Речь о комментарии, например, в HTML-документе. 9 — корневой узел DOM-дерева. Примеры со свойством document мы уже рассматривали. 10 — узел-тег, представляющий тег DOCTYPE. Его единственная цель заключается в том, чтобы предотвратить переключение браузера в так называемый причудливый режим. Скорее всего, сегодня такая необходимость канула в Лету, но разработчики продолжают использовать данный узел, а IDE предлагают начинать с него писать HTML-документ. Это самый первый тег любого HTML-документа, второй — тег HTML. 11 — узел Fragment. Мы его не рассматривали, но это то, что необходимо знать и использовать. Использование отработаем на практических занятиях. Что такое Fragment, точнее, DocumentFragment? Это некий контейнер для создаваемых вами поддеревьев, который может быть создан вне DOM, а затем вставлен в него одним махом; при этом сам DocumentFragment будет очищен. Известно, что прямая вставка в DOM — медленная операция, поэтому вставка в DOM целого ряда элементов нередко заставляет интерфейс пользователя виснуть. С помощью DocumentFragment процесс можно ускорить: составить часть дерева внутри него и затем один раз вставить в DOM. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 2.5. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 2.5. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 2.5 и ознакомьтесь с моим решением.
2.6. События  75 2.6. События 2.6.1. Что такое событие Событие — это факт произошедшего действия или генерация программой такого факта. Когда событие происходит, движок JavaScript уведомляет JavaScriptпрограмму о том, что в каком-либо из заранее определенных источников произошло физическое действие, либо о том, что было сгенерировано программное событие. События нужны для того, чтобы приложение могло не только получать данные с сервера или через формы от пользователя, но и реагировать на действия, которые могут быть осуществлены пользователем, браузером и самим приложением. От коллег, которые вместе со мной проводят собеседования, я достаточно часто слышу: «Расскажите про события и их обработку в JavaScript». Однако это не совсем корректный вопрос. В JavaScript нет событий. Они есть в платформах, например, в браузере или Node.js, это часть их API. Мы рассмотрим события в браузере. На рис. 2.40 показаны источники событий. Рис. 2.40. Источники событий в браузере и уведомление о произошедшем событии JavaScript-программы 2.6.2. Некоторые виды событий и источники Вот какие события браузер может обработать (сейчас пока просто принимаем это как факт, а далее разберем детально). y Пользователь внес данные в поля и отправил (DOM). y Пользователь нажал клавишу (операционная система).
76  Глава 2 С чем имеет дело фронтенд-разработчик Пользователь нажал кнопку (DOM). Страница загрузилась (Window). Телефон сменил положение с вертикального на горизонтальное (браузер). Пользователь провел указатель мыши над элементом (DOM). В дереве DOM изменилась структура (DOM). Пользователь выделил текст (браузер). Текст скопирован в буфер обмена (браузер). Пользователь уходит со страницы (браузер). Другие события. События бывают двух видов — физические действия и генерируемые программой. y y y y y y y y y 2.6.3. Физические действия Практически все события, с которыми вы будете иметь дело, являются физическими — основаны на физических действиях и происходят сразу после их окончания. Например, событие «Страница загрузилась» происходит после загрузки целого арсенала ресурсов, как то: HTML-документ, все указанные в нем CSS-файлы и JS-файлы, а также изображения. Другой часто приводимый пример — событие «Клик на элементе страницы». Как только пользователь сделал клик (нажал кнопку мыши и отпустил), браузер сразу генерирует событие. В скобках указаны два действия, так как событие «Клик (щелчок)» — нажатая и отпущенная кнопка мыши. Однако можно работать с событиями «Нажата кнопка мыши» и «Отпущена кнопка мыши». 2.6.4. Действия, генерируемые программой Это событие, которое можно создать в программе, а затем все будет точно так же, как в случае с событием после физического действия. Разница лишь в источнике. 2.6.5. Событие — это процесс На самом деле, говоря и описывая события, приходится немного упрощать. Ведь когда говорят о событии, подразумевают цепочку явлений. Что в ней присутствует? Само событие, вызов обработчика события и передача ему в качестве аргумента объекта события. Это наглядно показано на рис. 2.41. Мы изучили ту его часть, с которой вы как разработчик не взаимодействуете, но не рассмотрели, что такое обработчик события и объект события и как с ними взаимодействовать в коде. Этим займемся сейчас. И помните: когда другие разработчики общаются с вами по поводу событий, они всегда имеют в виду то, что на рис. 2.41 показано как часть «3. Уведомление скрипта», а именно работа с событием в коде.
2.6. События  77 Рис. 2.41. Последовательность обработки события 2.6.6. Работа с событиями в коде Для работы с событиями в браузере предусмотрено API. Каждый источник, в котором могут происходить события, обязательно реализует интерфейс EventTarget. Источников, как вы успели убедиться, может быть несколько. Чтобы работать с событиями в программе, нужно зарегистрировать обработчик события для конкретного события в конкретном источнике. Обработчик события — обычная функция. Называется она так потому, что является колбэком для метода, регистрирующего функцию, которая будет вызвана, когда произойдет конкретное событие. Обработчик вызывается движком JavaScript в тот момент, когда событие уже произошло. Вызванному обработчику события в качестве аргумента передается объект произошедшего события, где собраны все необходимые сведения. Чтобы обработчик можно было вызвать, его нужно зарегистрировать в источнике для определенного события. Как это делается? В EventTarget для этой цели есть метод addEventListener, поэтому общая схема регистрации обработчика события для всех источников событий такова (листинг 2.21). Листинг 2.21. Общая схема подключения обработчика события к источнику source.addEventListener( "eventName", eventHandler, isEventBubble ); В листинге 2.22 источником является элемент, представляющий тег li. В переменной li хранится ссылка на данный объект. Результат выполнения этого кода показан на рис. 2.42. Листинг 2.22. Подключение обработчика события «Клик» на элементе (источнике) li «Горящие» const ul = document.querySelector('ul'); const li = ul.firstElementChild;
78  Глава 2 С чем имеет дело фронтенд-разработчик const clickHandler = (eventObject) => { console.log(eventObject); alert(eventObject.target.textContent); }; li.addEventListener('click', clickHandler, true); Рис. 2.42. Результат выполнения листинга 2.22 Кроме подключения обработчика события, существует и обратное действие — удаление обработчика (листинг 2.23). Листинг 2.23. Удаление обработчика событий const ul = document.querySelector('ul'); const li = ul.firstElementChild; const clickHandler = (eventObject) => { console.log(eventObject); alert(eventObject.target.textContent); };
2.6. События  79 li.addEventListener('click', clickHandler, true); setTimeout(() => { li.removeEventListener('click', clickHandler, true); }, 20000); В данном коде по истечении 20 секунд обработчик удаляется. Замечу, что для этого методу removeEventListener нужно передать столько же аргументов, сколько и в addEventListener, а именно: 1) наименование события, совпадающее с тем, что было установлено в addEventListener; 2) ссылку на функцию, которая была установлена в качестве обработчика в addEventListener; 3) то же самое значение, что и в addEventListener (если этот аргумент задавался). Если поступить так, как показано в листинге 2.24, то обработчик продолжит свое действие. Листинг 2.24. Некорректное удаление обработчика событий const ul = document.querySelector('ul'); const li = ul.firstElementChild; li.addEventListener('click', (eventObject) => { console.log(eventObject); alert(eventObject.target.textContent); }, true); setTimeout(() => { li.removeEventListener('click', (eventObject) => { console.log(eventObject); alert(eventObject.target.textContent); }, true); }, 20000); В данном листинге по истечении 20 секунд удаления не произойдет. Почему? Потому что, несмотря на то что функции, переданные в качестве обработчика, выглядят одинаково, это разные объекты и метод removeEventListener получил ссылку не на тот объект, который был установлен методом addEventListener. 2.6.7. Группировка событий по типу Выше было показано, что источник события может быть разный и что до момента уведомления о произошедшем событии разработчику ничего не нужно осуще­ствлять. Правда, следует зарегистрировать событие определенного типа на источнике, от которого нужно получить уведомление. Мы также выяснили, что и физическое, и генерируемое приложением событие для самого приложения выглядит абсолютно одинаково и что вы имеете дело с событием (во время разработки), только когда вызван обработчик события. Поэтому про события обычно не рассуждают с точки зрения источника, уведомляющего движок JavaScript. Как правило, события группируют по более очевидным областям.
80  Глава 2 С чем имеет дело фронтенд-разработчик События мыши. Возникают при взаимодействии пользователя с мышью на странице. Например: y click — при щелчке на элементе кнопкой мыши; y mousedown — при нажатии кнопки мыши на элементе; y mouseup — при отпускании кнопки мыши на элементе; y mousemove — при движении указателя мыши над элементом (листинг 2.25); y mouseover — при наведении указателя мыши на элемент; y mouseout — при выходе указателя мыши за пределы элемента. Листинг 2.25. Назначение события mousemove элементу li const ul = document.querySelector('ul'); const li = ul.firstElementChild; li.addEventListener("mousemove", function(event) { var x = event.clientX; var y = event.clientY; console.log("Координаты курсора мыши: " + x + ", " + y); }); Результат выполнения листинга 2.25 можно посмотреть на рис. 2.43. Рис. 2.43. Результат выполнения листинга 2.25
2.6. События  81 События клавиатуры. Возникают при взаимодействии пользователя с клавиа­ турой на странице. Например: y keydown — при нажатии клавиши на клавиатуре; y keyup — при отпускании клавиши. Пример использования события нажатия клавиши на странице приведен в листинге 2.26. Листинг 2.26. Одновременное назначение событий keydown и keyup корневому узлу DOM-дерева document document.addEventListener("keydown", function(event) { var key = event.key; console.log("Нажата клавиша " + key); }); document.addEventListener("keyup", function(event) { var key = event.key; console.log("Отпущена клавиша " + key); }); Результат выполнения листинга 2.26 можно посмотреть на рис. 2.44. Рис. 2.44. Результат выполнения листинга 2.26
82  Глава 2 С чем имеет дело фронтенд-разработчик Обратите внимание, что элементом, на который назначен обработчик, является document. События форм. Возникают при взаимодействии пользователя с элементами форм на странице, такими как текстовые поля, кнопки отправки и др. Например: y focus — когда элемент формы получает фокус ввода, то есть становится активным и готовым для ввода данных; y blur — когда элемент теряет фокус ввода, то есть перестает быть активным для ввода данных; y submit — при отправке формы; y reset — при сбросе формы. Пример использования события отправки формы представлен в листинге 2.27. Листинг 2.27. Одновременное назначение событий focus и blur элементам input, присутствующим в форме const inputs = document.querySelectorAll('input'); inputs.forEach((input) => { input.addEventListener("focus", function() { input.style.backgroundColor = "salmon"; }); }); input.addEventListener("blur", function() { input.style.backgroundColor = "palegoldenrod"; }); Результат выполнения листинга 2.27 можно посмотреть на рис. 2.45. События документа. Происходят, когда изменяется состояние документа, такое как загрузка, завершение загрузки или изменение размера окна браузера. Например: y load — когда документ загружен; y resize — когда изменяется размер окна браузера (листинг 2.28, рис. 2.46). Листинг 2.28. Назначение события resize объекту браузера window window.addEventListener("resize", function() { console.log('Текущие размеры viewport\n'); console.log('ширина: ', window.innerWidth, 'высота: ', window.innerHeight); }); При изменении ширины или высоты окна браузера меняется и viewport. Данное слово означает прямоугольную область на экране, в которой отображается веб-страница. Viewport определяет размеры и масштаб отображаемой веб-страницы, а также ее расположение на экране. Он может быть изменен с помощью масштабирования или поворота устройства. Существуют и другие группы событий, и другие источники событий, один из которых будет рассмотрен на практических занятиях.
2.6. События  83 Рис. 2.45. Результат выполнения листинга 2.27 Рис. 2.46. Результат выполнения листинга 2.28
84  Глава 2 С чем имеет дело фронтенд-разработчик 2.6.8. Объект события Главный источник информации о событии — объект события, содержащий сведения о произошедшем событии, такие как тип события, координаты указателя мыши в момент события и элемент, на котором оно произошло. У объекта множество свойств, и одно из них — event.target. Это ссылка на элемент, где произошло событие. В листинге 2.29 приводится пример, как использовать данное свойство, а на рис. 2.47 показан результат работы листинга. Листинг 2.29. Задействование свойства event.target const ul = document.querySelector('ul'); const li = ul.firstElementChild; li.addEventListener("click", function(event) { console.log(event.target === li); console.log('event.target: ', event.target); }); Рис. 2.47. Результат выполнения листинга 2.29
2.6. События  85 Кроме того, на основе информации из объекта события можно использовать паттерн программирования, который называется «делегирование». При этом обработчик события назначается на родительский элемент, а не на каждый дочерний элемент в отдельности. Например, если на странице есть список элементов и при щелчке на любом из них нужно выполнить определенное действие, то можно назначить обработчик события на родительский элемент списка и использовать свойство event.target для определения элемента, на котором произошло событие, и выполнения нужного действия. В этом мы попрактикуемся далее. Чтобы просмотреть все имеющиеся свойства в объекте события, достаточно вывести объект в консоль. 2.6.9. Три стадии события Большинство событий, с которыми вы будете иметь дело, происходят в DOM. Событие в DOM проходит три стадии: захват (capture), событие в целевом элементе (target) и всплытие (bubble). Целевым называется тот элемент DOM, на котором событие произошло. На первой стадии событие захватывается на элементе, который находится выше в иерархии DOM, чем целевой элемент, и это элемент document. Затем событие достигает целевого элемента и происходит целевая стадия. В конце событие переходит на стадию всплытия, когда оно возвращается по иерархии DOM вверх, начиная с целевого элемента и заканчивая элементом document. Стадии наглядно показаны на рис. 2.48–2.50. Рассматривается ситуация, когда на веб-странице туров кликнули на самом первом элементе li — «Горящие». Рис. 2.48. Стадия распространения, когда событие происходит в document
86  Глава 2 С чем имеет дело фронтенд-разработчик Рис. 2.49. Стадия, когда событие происходит в целевом элементе Рис. 2.50. Стадия всплытия Методы объекта события. Методы stopPropagation() и stopImmediatePropagation() объекта события используются для остановки дальнейшего распространения события в иерархии DOM. Метод stopPropagation() предотвращает распространение события вверх по иерархии DOM. Это значит, что событие не будет достигать родительских элементов, которые могут также иметь зарегистрированные обработчики событий для данного типа события. Но обработчики событий, зарегистрированные на текущем элементе, будут вызваны. Пример использования метода stopPropagation() показан в листинге 2.30.
2.6. События  87 Листинг 2.30. Применение метода stopPropagation() const ul = document.querySelector('ul'); const li = ul.firstElementChild; ul.addEventListener("click", function(event) { console.log('Произошло событие на элементе UL'); }); li.addEventListener("click", function(event) { event.stopPropagation(); console.log('Произошло событие на элементе LI'); }); При работе этого кода в консоль будет выведена лишь строка Произошло событие на элементе LI (листинг 2.31). Листинг 2.31. Применение метода stopImmediatePropagation() const ul = document.querySelector('ul'); const li = ul.firstElementChild; ul.addEventListener("click", function(event) { console.log('Произошло событие на элементе UL'); }); li.addEventListener("click", function(event) { event.stopImmediatePropagation(); console.log('Обработчик 1 на элементе LI'); }); li.addEventListener("click", function(event) { event.stopImmediatePropagation(); console.log('Обработчик 2 на элементе LI'); }); При работе этого кода в консоль будет выведена лишь строка Обработчик 1 на элементе LI. Обработчик 2 не будет вызван. Разница между методами stopPropagation и stopImmediatePropagation заключается в том, что первый останавливает только дальнейшее распространение события вверх по иерархии DOM, а второй — также выполнение обработчиков событий, зарегистрированных на текущем элементе после текущего обработчика события. Знакомясь с описанием события, обязательно смотрите, какие стадии оно проходит. Событие может не иметь стадии перехвата или всплытия. В таком случае третий аргумент можно опустить. Независимо от того, хотите вы обработать возникновение какого-либо события или нет, оно все равно происходит. Но если вы хотите, чтобы ваше приложение (скрипт) выполняло определенные действия, когда произошло конкретное событие, то должны зарегистрировать обработчик такого события.
88  Глава 2 С чем имеет дело фронтенд-разработчик 2.6.10. Иная точка зрения на события Вы наверняка сталкивались с выражением «побочный эффект». В программировании это означает изменение состояния программы или взаимодействие с внешней средой, происходящее при выполнении функции или операции, которое выходит за пределы основной цели функции или операции. Например, функция, выводящая текст на экран или записывающая данные в файл, имеет побочный эффект, так как ее основная цель — не взаимодействие с внешней средой, а выполнение какой-то вычислительной задачи. Побочный эффект может быть и внутренним, когда функция меняет состояние программы, например изменяет значение глобальной переменной. Побочный эффект мы получаем при выполнении функции: сначала происходит вызов функции, а затем функция вызывает что-то, что создает побочный эффект. В реальной жизни он необходим, допустим, для взаимодействия с пользователем, ввода-вывода данных, обращения к базе данных, отправки запросов к RESTсервисам и получения ответов и т. д. Давайте развернем последовательность и будем отталкиваться от побочного эффекта. Допустим, мы хотим не вызвать побочный эффект, а проследить за тем, когда он произойдет. Как бы это могло выглядеть? На самом деле механизм событий и есть то, что следит за происхождением побочных эффектов и уведомляет программу вызовом функции. Правда, этот механизм не следит за всеми побочными эффектами подряд, лишь за конкретным множеством. То есть здесь обратная цепочка действий. Напоследок отмечу, что тема «События» — одна из важнейших тем и вам нужно получить свое представление о работе каждого элемента, относящегося к ней. Вы должны хорошо потренироваться с объектом события, методами addEventListener и removeEventListener, обработчиками событий, назначением разных событий на разные элементы и объекты в браузере, делегированием, распространением, событием в элементе и всплытием. А еще в обязательном порядке уяснить для себя множество событий и их источники. На практическом занятии мы рассмотрим объект, который может работать с буфером обмена операционной системы. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 2.6. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 2.6. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 2.6 и ознакомьтесь с моим решением.
2.7. Асинхронное программирование  89 2.7. Асинхронное программирование 2.7.1. Инструкции1 языка программирования Обучение разработке на языке программирования высокого уровня всегда начинается с простых инструкций, таких как объявление переменных, разные операторы и, конечно, условная инструкция if, инструкции циклов while, for. Эти инструкции просты и очевидны. Вам один раз сказали, что они из себя представляют, и их действие становится очевидным. Ключевые слова, зарезервированные в языке, очевидны, так как, сделав прямой перевод ключевого слова, вы получаете смысл выполняемого действия в программе. Можно сказать, что здесь присутствует прямое отображение инструкции на действие. Но в случае с асинхронными инструкциями, и в том числе асинхронными циклами, такой очевидности нет. Вы не будете знать, как работает та или иная инструкция, пока не изучите материал, лежащий в ее основе. А если не изучите его, то, глядя на код, вы не будете понимать, что делает тот или иной участок. Само собой, вы не напишете и строчки асинхронного кода самостоятельно. 2.7.2. Кратко и упрощенно о работе браузера в операционной системе Браузер, как любая другая программа, выполняется в операционной системе как процесс. Процесс — это экземпляр программы, запущенный в операционной системе, которая получила для своего выполнения память; ей периодически выделяется время процессора. В памяти, которую выделила программе операционная система, хранятся код программы и данные; там же находятся стек, состояние регистров процессора, указатели на открытые файлы, потоки. Когда программа не запущена и просто лежит на диске, ничего не происходит. Но, как только ее запускают, код программы оказывается в памяти компьютера и под управлением операционной системы начинает шаг за шагом выполняться, периодически прерываясь, чтобы 1 Когда говорят об инструкции языка программирования, речь идет о языках программирования высокого уровня, таких как С/С++, Java, JavaScript и др. Инструкция в подобных языках — ключевое слово, которое используется для управления программой и объявления идентификаторов. Для управления ходом программы используются такие ключевые слова, как if, else, while, for, return и др. Для объявления идентификаторов — ключевые слова var, let, а в языках Java, C++/C используется тип данных как инструкция объявления идентификатора определенного типа, например int, short, string (кроме С). В литературе часто вводят в заблуждение читателей, когда пишут про инструкцию if, называя ее условным оператором. Запомните: оператор — это средство языка программирования для вычисления значения (он всегда получает операнды и возвращает значение), а инструкция только задает ход вычислений, то есть говорит, как поступать далее. Инструкция if, в частности, проверяет истинность какого-либо выражения, затем выполняется либо один блок кода, либо другой. Но сама инструкция ничего не возвращает, а значит, оператором не является. Путаница произошла, скорее всего, из-за того, что в английском языке инструкция — statement, и у этого слова есть другое значение — «оператор». Видимо, переводчики стали ошибочно причислять инструкции к операторам еще, как говорится, при царе Горохе. С тех пор так и повелось.
90  Глава 2 С чем имеет дело фронтенд-разработчик выполнялся другой какой-либо процесс, запущенный параллельно. Поэтому процесс — это копия программы (в действительности набор инструкций), расположенная в оперативной памяти и выполняемая в данный момент. Одни процессы в операционной системе могут порождать другие. То есть, когда вы запустили браузер, он запускает несколько дочерних процессов. Так, по крайней мере, работает браузер Chrome: помимо основного процесса, запускает еще ряд процессов, один — для взаимодействия с сетью, другой — для рендера содержимого страницы. Но процесс запускает и потоки. Поток — это объект, который выполняет определенные действия внутри процесса. Возникает вопрос: как их различать? Процесс может выполняться как один поток или как несколько. Когда несколько потоков выполняются одновременно, они разделяют ресурсы, такие как выделенная процессу память, открытые файлы. На самом деле потоки — абстракции. Используя их, можно реализовать параллельное выполнение программы в многопроцессорных системах. Операционная система Linux, например, управляет потоками, а не процессами. Обращаю внимание на то, что тему «Процессы и потоки, выполняемые в операционной системе» нужно изучать отдельно. Здесь дано лишь поверхностное объяснение, чтобы вы поняли суть — что такое процесс и что такое поток. Изучить процессы и потоки можно, почитав литературу об операционных системах, например книгу «Современные операционные системы»1. 2.7.3. Что такое асинхронное программирование JavaScript-программа в браузере выполняется в одном потоке, который часто называют основным. «Асинхронный» значит «осуществляемый без блокирования основного потока». Посмотрите на это слово не как на антоним2 слова «синхронный», а с точки зрения того, что написано в определении. Получается некая шутка со словами: «синхронный» будет означать «осуществляемый с блокированием основного потока». 2.7.4. Последовательное выполнение программы На минуту забудьте о том, как работает JavaScript-программа в браузере, и представьте, что все действия в ней выполняются строго последовательно, даже те, которые требуют времени для получения результатов. Если бы программе понадобился запрос данных с сервера, то она заставила бы браузер застыть до момента, пока эти данные не придут и не будут обработаны, например выведены на страницу в виде таблицы. В итоге пользователю пришлось бы ждать и браузер выглядел бы как не отвечающий на действия. 1 2 Таненбаум Э. С., Бос Х. Современные операционные системы. 4-е изд. — СПб.: Питер, 2019. Антоним — слово, противоположное по значению другому слову.
2.7. Асинхронное программирование  91 Это упрощенное представление, тем не менее сегодня можно воспроизвести похожую ситуацию, если использовать XMLHttpRequest, как показано в листинге 2.32. Листинг 2.32. Осуществление синхронного запроса const getData = (url, cb) => { const xhr = new XMLHttpRequest(); // Ключевым моментом в том, что запрос производится синхронно, // является третий параметр метода open. // Если его опустить, то запрос по умолчанию будет асинхронным xhr.open('GET', url, false); xhr.addEventListener('load', (data) => { if (xhr.status === 200) { cb && cb(xhr.responseText); } else { console.error('Ошибка:', xhr.status); } }); xhr.send(); }; В листинге код заставляет быть синхронным третий параметр метода open, который по умолчанию имеет значение true, означающее «выполняться асинхронно». А так как у нас false, запрос выполняется синхронно, то есть блокируя взаимодействие документа до момента, пока не поступят и не будут переданы данные в колбэк. Если выполнить этот код, то вы почувствуете задержку. В реальной программе может быть много инструкций для работы с DOM и выполнения вычислений с целью их отображения на canvas. Тогда вы будете чувствовать, как ваш документ блокируется и некоторое время не отвечает. Если написать программу, использующую DOM, синхронный XMLHttpRequest, canvas, video, audio, и выполнять последовательно инструкции, которые заставляют документ застывать, то пользователи перестанут работать с таким приложением. Поскольку JavaScript-программа выполняется в одном потоке, возникла необходимость в способе обойти ограничение последовательного выполнения, и была разработана модель выполнения кода, называемая циклической обработкой событий (Event Loop). Русскоговорящие нередко называют ее «ивент луп». 2.7.5. Event Loop Посмотрим, как устроена среда выполнения (runtime, или рантайм) JavaScript и какие структуры в ней есть в этот момент. Их три: y стек; y очередь; y куча.
92  Глава 2 С чем имеет дело фронтенд-разработчик Стек служит для сохранения объектов вызовов функций. Очередь нужна для постановки в нее сообщений. Сообщение всегда ассоциировано с какой-либо функцией, которая, вызвавшись, его обработает. Куча необходима для хранения объектов. Рантайм имеет смысл рассматривать в привязке к коду. Вы знаете, что есть этап инициализации приложения и затем — этап прослушивания событий. На этапе инициализации интерпретатор JavaScript проходит по каждой строчке кода и если встречает выражение вызова функции, то в стеке создается объект вызова функции. Когда функция вызывается, создается объект вызова функции и он помещается в стек. Как только выполнится ее код, если внутри функции нет вызовов других функций, то из стека данный объект извлекается. Если встречаются структуры данных, то они размещаются в куче. Если встречается вызов следующей функции, то для нее создается объект вызова в стеке. Если внутри данной функции осуществляется вызов другой функции, то и для этой функции создается объект вызова, который помещается в стек. Теперь в стеке два объекта вызова. Затем код последней функции выполнится, и этот объект будет удален из стека, а после и первый. Также в момент инициализации на DOM-элементы регистрируются обработчики событий. Наконец этап инициализации заканчивается и наступает этап прослушивания событий. Допустим, пользователь делает клик на элементе, для которого зарегистрирован обработчик события. Объект события помещается в очередь, и далее все будет работать следующим образом. Поскольку в данный момент в стеке нет объектов функций, из очереди будет извлечено сообщение и очередь окажется пустой. Сообще­ние всегда связано с функцией. Эта функция будет вызвана. Соответственно, в стеке появится объект вызова функции. Затем код функции будет завершен, а объект вызова функции — удален из стека. Далее наступит ожидание нового события. И цикл повторится. Event Loop — это механизм, который постоянно проверяет, есть ли в очереди сообщение. Если есть, то вызывается связанная с ним функция и, как только весь код функции текущего сообщения закончен (стек стал пустым), Event Loop вновь смотрит в очередь и либо извлекает следующее сообщение, либо ожидает появления в очереди новых. Вышеописанное — модель1 неблокирующей обработки кода. Event Loop реализован как в браузере, так и в Node.js, но есть отличия в реализации. Если вы захотите углубиться в детали, то можете изучить более подробную информацию в стандарте HTML2 и документации по Node.js3. 1 2 3 Модель обработки кода — описание принципа работы этого механизма, но не сама реализация. Реализация зависит от браузера. Event Loop в стандарте HTML расположен по адресу https://html.spec.whatwg.org/ multipage/webappapis.html#event-loop-processing-model, а весь раздел — по адресу https:// html.spec.whatwg.org/multipage/webappapis.html#event-loops. Event Loop в Node.js расположен по адресу https://Node.js.org/en/learn/asynchronous-work/ event-loop-timers-and-nexttick.
2.7. Асинхронное программирование  93 2.7.6. Выполнение асинхронного кода в браузере. Задачи и микрозадачи Асинхронное программирование не является областью, созданной специально для JavaScript. Оно появилось гораздо раньше. Код сам по себе не выполняется асинхронно вдруг или просто так. Это задается вами при планировании определенных ситуаций. Если вашему коду JavaScript нужно время для выполнения подсчетов или ожидания ответа, то это кандидат на асинхронный код. Ведь вы не будете пытаться блокировать основной поток. Такой ситуацией может стать, например, необходимость сделать запрос для получения данных с сервера или вызвать функцию через конкретный промежуток времени. Наличие подобных ситуаций означает, что вы должны предусмотреть обработку кода асинхронным способом. Очередь, по сути, основное хранилище, а если быть точным — структура данных, которая есть у Event Loop. Место, куда задача помещается для выполнения. Очередей всегда несколько: одна — для обычных задач, другая — для микрозадач. Иногда обычные задачи называют «макрозадачи», но в HTML-стандарте они называются просто задачами. Задачи, или, как их еще называют, таски (от англ. task), — некие объекты, содержащие набор инструкций для выполнения, а также источник задачи и объект document, с которым она ассоциирована. В задаче есть и другие данные, но это нам не понадобится. По смыслу микрозадачи ничем не отличаются от задач, за исключением того, что у первых более высокий приоритет и другой способ изъятия из очереди. Чтобы не путать очередность выполнения задач и микрозадач, в браузере существует отдельная очередь задач и отдельная очередь микрозадач. Разберем несколько примеров, как Event Loop работает с асинхронным кодом (листинг 2.33). Листинг 2.33. Как Event Loop работает с функцией setTimeout setTimeout(() => alert("Event Loop"), 1000); Функция setTimeout принимает два аргумента (в данном случае): первый — функция, второй — число миллисекунд. Допустим, у нас есть событие «Клик», в обработчике которого единственная инструкция из листинга. Event Loop будет работать следующим образом. Через 1000 миллисекунд функция как задача (также называемая сообщением) поступит в очередь задач. Обращаю внимание: задачи помещаются в очередь через определенный интервал времени, указанный вторым аргументом. То есть до этого момента эта функция не присутствует ни в какой очереди. Затем Event Loop проверяет очередь на наличие сообщений. Допустим, у нас всего одно сообщение. Оно извлекается из очереди, его функция выполняется. В стеке образуется объект вызова функции, которая затем вызывает другую, браузерную функцию alert, и появляется окно с сообщением. Затем пользователь нажимает кнопку — окно закрывается.
94  Глава 2 С чем имеет дело фронтенд-разработчик Если бы до помещения в очередь в ней были другие сообщения, то функция alert выполнилась бы не сразу после помещения в очередь, а только после того, как закончится их выполнение. Возьмем другой пример (листинг 2.34). Листинг 2.34. Приоритет выполнения очередей задач и микрозадач const promise = new Promise((resolve) => { setTimeout(() => { setTimeout(() => console.log(4), 0); resolve(1); }, 1000); }); promise.then(res => { console.log(res); return 2; }).then(res => { console.log(res); return 3; }).then(res => { console.log(res); }); В предыдущем примере были задачи, а в этом листинге задействуются также микрозадачи. Функция в конструкторе промиса начинает выполняться сразу, то есть выполняется setTimeout, а именно ставится на ожидание продолжительностью 1 секунда. По прошествии секунды функция колбэк помещается в очередь задач, откуда извлекается Event Loop и выполняется. В первой строке опять появляется setTimeout, но уже с нулевой задержкой. Функция внутри setTimeout имеет задержку ноль миллисекунд и ставится в очередь задач. В данный момент интерпретатор занят выполнением, поэтому Event Loop пока не забирает ничего из очереди задач. Затем вызывается resolve, которая возвращает объект с методом then, и этот метод сразу вызывается. Известно, что, независимо от возвращаемого значения в функции, его возвращает также объект с методом then. Таким образом вызовы выстраиваются в цепочку. Вызов метода then в цепочке ставит в очередь микрозадач задачи, равные числу вызовов метода в цепочке. Затем Event Loop проверяет очередь микрозадач — она не пустая. Первая микрозадача извлекается и выполняется в стеке, затем вторая, третья. Наконец очередь пуста и Event Loop, если нет вызовов обычных функций, вновь проверяет очередь задач. Там оказывается функция из setTimeout, Event Loop ее извлекает и выполняет. Порядок вывода в консоль будет следующий: 1, 2, 3, 4. 2.7.7. Главная разница между очередями задач и микрозадач Когда из очереди задач извлекается какая-либо, рантайм выполняет ее, а следующую может извлечь только на следующей итерации Event Loop. Если после извлечения задачи очередь не пуста, сначала выполняется код текущей задачи и затем наступает
2.7. Асинхронное программирование  95 время микрозадач. После этого — новое извлечение из очереди задач (если они есть). В момент выполнения задачи в очередь может быть добавлена еще одна задача. Когда извлекается микрозадача и выполняется ее код, Event Loop сначала еще раз проверяет очередь микрозадач. Даже если в момент извлечения в очереди была всего одна микрозадача, которая затем начала выполняться, и во время ее выполнения была добавлена другая, то другая микрозадача тоже будет выполнена. Иногда из-за этого возникает бесконечная очередь из микрозадач и приложение виснет. 2.7.8. Дополнительно Можно задаться вопросом, как разделять задачи и микрозадачи. Для этого нужно знать, какие вызовы в какую очередь встанут. Например, задачами будут: y обработчик события; y функция, вызываемая из платформенных функций setTimeout, setInterval. Примерами микрозадач могут служить: y метод then промиса; y функция, помещенная в очередь микрозадач с помощью платформенной функции queueMicrotask. Из модели видно, что просто вызовы функций не имеют отношения к очередям.
Глава 3 ОСНОВЫ ПРОЕКТИРОВАНИЯ И РАЗРАБОТКИ 3.1. О разработке приложения 3.1.1. Введение Посмотрим на то, что происходит в обучении фронтенд-разработке. Как приходят в эту сферу? Кто-то профильно изучал в институте информатику и, соответственно, устройство информационных систем1. Кто-то окончил курсы по фронтенд-разработке. У первых в принципе не возникает вопросов при написании программ — им достаточно освоить синтаксис языка и некоторые сведения о том, с каким API взаимо­действовать. Совсем иначе обстоят дела у тех, кто всерьез не обучался информатике и имеет за плечами лишь курсы. Этим людям порой приходится туго — и не без причин. С чем их учили работать? С синтаксисом языков программирования JavaScript, TypeScript и, конечно, с библиотеками. А сколько времени уделили тому, как именно разрабатывать приложение? Были ли изучены базовые основы приложения и принципы их моделирования? Возможно, вам говорили, что вы создаете приложение, и вы чувствовали причастность к этому делу. Но максимум, чему вас научили, — это команде create-reactapp и использованию библиотек. Однако не тому, как создать приложение. Кстати, я не говорю, что вышеперечисленное плохо. Напротив, хотя есть парочка «но». Современный стек технологий — так называется набор библиотек, применяемых для реализации приложений, и это не все, что нужно знать для их полноценной разработки. Слово «реализация» выделено специально: я против упущенных знаний и хочу восполнить пробелы. Данная глава в большей степени полезна тем, кто профильно не обучался информатике. Остальным нужно прочитать только разделы «Проектирование» и «Реализация приложения». 3.1.2. Смена точки зрения на разработку Если пытаться разрабатывать приложения, имея пробелы в знаниях, то возникнет ощущение неуверенности. Возможна и другая ситуация: вы просто используете 1 Информационная система — совокупность нескольких компонентов, обеспечивающих взаимодействие пользователя с информацией, извлекаемой и сохраняемой в базе данных. Любая информационная система специализирована и предназначена для использования с конкретной целью. Например, медицинская информационная система включает в себя работу с картой пациента, интернет-магазин — работу с товарами и заказами, онлайнбанк — действия со счетом клиента.
3.2. Как разрабатываются информационные системы в организациях  97 то, чему вас научили, — применяете библиотеки, но периодически ловите себя на мысли, что ваши приложения слегка однобокие. В этой главе мы начнем менять точку зрения на подход к разработке. «Зачем менять точку зрения?» — спросите вы. Действительно, кажется, что все нормально: создатели библиотеки React собрали крутую команду npx create-reactapp appName. Использовал ее — и все, дело в шляпе. Можно продолжать мыслить по-прежнему и ничего не менять. Из-за этого вам никто слова не скажет. А вот что изменится, если взглянуть на то, что вы делали до сих пор, с другой стороны. Вы можете начать контролировать те части вашего приложения, на которые ранее или вообще не обращали внимания или все время откладывали их изучение на потом. Создание приложения не деятельность по «ломанию мозгов» и не сплошное погружение в разум с мыслью «Что делать?». Оно включает в себя гораздо больше, чем владение синтаксисом. Это простая и точная процедура, основы которой описываются в этой главе, а точная пошаговая процедура для разработки 3RApp1 с нуля до реализации дается начиная с раздела 8.1. 3.1.3. Разработка информационных систем Сегодня банальные сайты никому не нужны, зато нужны хорошие информационные системы. Это значит, что трудиться над такой системой должна целая команда. Вы или уже являетесь, или станете частью подобной команды. По этой причине в начале главы говорится о разработке информационных систем — процессе, в котором принимает участие фронтенд-разработчик. Чтобы правильно делать свою работу и разрабатывать приложение, вам потребуются знания о графах и системах. О них идет речь в разделах «Граф. Дерево» и «Система». Это основы для следующих двух разделов — «Проектирование» и «Реализация приложения». Ознакомившись с ними, вы четко разграничите для себя, в какой момент следует проектировать, а в какой — начать реализовывать. Это позволит не впадать в такие крайности, как «мыслить только кодом и библиотеками» или «на все нужно предварительно составлять аналитику». 3.2. Как разрабатываются информационные системы в организациях 3.2.1. Работающее веб-приложение — это готовый продукт Пусть даже у вас совсем простое веб-приложение, но это то, с чем может взаимодействовать пользователь (рис. 3.1). И даже за этим стоит целая команда со своими планами, которые воплощаются в готовом продукте, причем поэтапно. Команда предусматривает множество ролей, и каждая создает определенные 1 3RApp — название стека для разработки SPA, состоящего из библиотек React, React router, Redux toolkit.
98  Глава 3 Основы проектирования и разработки артефакты1, претерпевающие изменения. Она работает над тем, чтобы пользователь в конечном счете мог взаимодействовать с тем, что задумали авторы приложения. Фронтенд-разработчик, как и бэкенд-разработчик, принимает непосредственное участие в разработке. Они не работают в одиночку. Все, что необходимо для успешной реализации, — дело команды, способной осуществить все виды работ. Рис. 3.1. Страница, взаимодействующая с пользователем 1 Артефакт — это результат, полученный определенной ролью в качестве итога работы над задачей. Артефактом работы аналитика будет описание процессов обработки данных, мест хранения, методов запросов с фронт-приложения к серверу, включая все детали. Артефактом работы дизайнера — изображения в графической программе или сервисе внешнего вида страниц приложения. Артефактом работы разработчика — файлы с кодом. Артефактом работы руководителя может быть план или тот же самый артефакт, который получил сотрудник, находящийся в его подчинении.
3.2. Как разрабатываются информационные системы в организациях  99 3.2.2. С чего начинается то, что мы видим как веб-приложение Любое работающее веб-приложение — результат трансформации замыслов в код, его последующая установка на сервер и эксплуатация пользователями. Всегда есть идея веб-приложения или формулирование требований, команда реализации и команда эксплуатации. Каждый из перечисленных элементов делится на составляющие. Можно сказать, что требования — то, что дает старт рабочему процессу. В табл. 3.1 приведены названия ролей, которые принимают участие в воплощении идеи в жизнь, разделенные на три указанные группы. Таблица 3.1. Области реализации и роли, принимающие в этом участие Формулирование требований Качественное изменение Эксплуатация Заказчик Аналитик DevOps1-инженер Владелец продукта Дизайнер Тестировщик Фронтенд-разработчик Заказчик Бэкенд-разработчик 3.2.3. Требования Требования — сформулированные ожидания от информационной системы. Это то, как себя должна вести система в ответ на действия пользователя. Любая информационная система — чей-то замысел. За каждой стоит человек, который выдвинул идею. А воплощал ее в жизнь, скорее всего, целый коллектив. Сегодня информационные системы таковы, что одному человеку, например разработчику, не под силу справиться со всем. Помимо осуществления разработки, нужно тестировать, общаться с пользователями создаваемой системы и описывать, как она должна разрабатываться, а также продумывать ее составляющие с точки зрения пользователей. Такой масштаб сложно охватить в одиночку. Замысел системы существует в уме, и для того, чтобы его смогли понять реализаторы, нужно описать идею в виде ожиданий — что и как должна делать система, из каких частей состоять. Требования всегда создает заказчик — тот, кто взаимодействует с целевой аудиторией, изучает ее потребности, узнает и уточняет пожелания, излагает все это в виде требований к системе. Для команды реализации заказчик — это роль. Он не входит в команду, а взаимодействует с ней обычно через владельца продукта. Данную роль могут выполнять и сам владелец продукта, и даже аналитик, если он взаимодействует с пользователями в плане сбора требований и ожиданий. У целевой аудитории есть определенные задачи или проблемы, решение которых нужно автоматизировать. Заказчик организует сбор пожеланий от пользователей 1 DevOps (от англ. development & operations) — методология автоматизации технологических процессов сборки, настройки и развертывания программного обеспечения. Источник: https://ru.wikipedia.org/wiki/DevOps.
100  Глава 3 Основы проектирования и разработки и, как правило, излагает их в таком виде, чтобы другие участники команды разработки, например аналитик, могли взять задачу в работу. Это так называемые user story — пользовательские истории, содержащие описание функциональности, которая будет представлять некую ценность для пользователей. Пример user story: «Я как пользователь хочу навести мышь на изображение корзины и увидеть список заказанных товаров pop-up1». 3.2.4. Краткое описание всего процесса разработки Представим, что заказчик приходит не с одной простой user story, а с идеей разработки нового веб-приложения, и что решение уже согласовано с архитектором. Обычно в крупных компаниях архитектор принимает участие в нескольких командах и не является постоянным участником ни одной из них. Владелец продукта собирает и уточняет все требования от заказчика. Он оформляет их в виде документа, к которому можно возвращаться по мере необходимости, и формулирует требования в виде задач на исполнение для аналитика и дизайнера. На этом формулирование требований заканчивается и наступает следующий этап — качественное изменение. Он так называется потому, что в этот период времени требования преобразуются в конкретные продукты, имеющие совершенно другие качества. Код, дизайн и аналитика отличаются от просто написанных требований. Аналитик, описав веб-приложение, привлекает к этому процессу заказчика и разработчиков, оформляет все в виде документации и крупных задач. Разработчики встречаются и осуществляют декомпозицию2 крупных задач на более мелкие, которые можно брать в работу. Часть из этого делает сам аналитик. Тем временем дизайнер готовит дизайн и согласовывает его с заказчиком. Далее наступает этап превращения артефактов от аналитика и дизайнера в код. Разработчики тоже заглядывают в требования. Бэкенд-разработчики берут в работу только аналитику и требования, а фронтенд-разработчики — и аналитику, и требования, и дизайн. Когда разработчики — фронтенд или бэкенд — создали код, готовый к работе, они размещают его в репозитории через пул-реквест, откуда он автоматически помещается на так называемый сервер ИФТ3. С этого момента наступает следующий этап — этап эксплуатации. 1 2 3 Pop-up (попап) — название UI-компонента, как правило, входящего в дизайн-системы, который при обычных условиях не виден на странице, но появляется при клике на какомлибо элементе и выглядит как всплывающее окошко с информацией. Такое окошко связано с элементом, на котором производится клик. Pop-up не блокирует остальной интерфейс, как делает модальное окно. Декомпозиция — выявление и выделение работ по их типу для реализации поставленной задачи. Все вместе выделенные работы при их выполнении приведут к полному решению поставленной задачи. Декомпозиция всегда применяется к чему-то сформулированному, как результат, который необходимо получить. ИФТ — интеграционное функциональное тестирование. Это когда ваш код объединяется с кодом, расположенным в репозитории, а затем производится его сборка для выкладки на сервер. Там он может быть запущен на сервере приложений — если это код для бэкенд-­ приложения, или отдан по запросу браузера, если это код для фронтенд-приложения. Таким образом, на подобном сервере может проводиться тестирование веб-приложения в целом.
3.2. Как разрабатываются информационные системы в организациях  101 Часть этапа эксплуатации автоматизирована. При создании пул-реквеста кода, его принятии и последующем слиянии с основной веткой он автоматически размещается на сервере (доставка и интеграция). Настройкой автоматизации занимается DevOps-инженер. Затем, когда новые разработки появились на сервере, производится тестирование. Тестирование веб-приложения осуществляет и автоматизированная система, и тестировщик. Тестировщик — это последнее звено в процессе разработки. Он утверждает, что какая-то функциональность соответствует требованиям. После тестирования веб-приложение попадает на следующий сервер, так называемый сервер приемо-сдаточных испытаний (ПСИ), где его принимают заказчик (-и) и другие представители организации. Если на данном этапе произошло согласование, приложение доставляется в промышленную эксплуатацию, где его могут использовать реальные пользователи. Еще раз отмечу, что этот процесс описан для нового веб-приложения, где участие принимают все роли команды. Однако существуют задачи, требующие работы отдельных ролей. Таковыми являются выявленный в ходе тестирования баг, необходимость внедрить хотфикс1, уменьшить техдолг2, отреагировать на сообщения системы мониторинга, реализовать импрувмент3, изучить код приложения для обнаружения запахов в коде с дальнейшим рефакторингом. Есть и другие задачи, которые не касаются ролей, связанных с качественным изменением. Например, работу организует тимлид, и я не включил его в описание процесса, хотя он принимает непосредственное участие в контроле за выполнением задач командой разработчиков. 3.2.5. Команда Команда не то же самое, что набор людей. Точнее говоря, я вкладываю в это слово определенный смысл. Команда — это когда правильно определены роли; каждая роль старается хорошо выполнять свою часть работы и передавать созданный артефакт другой роли или автоматизированной системе. Это когда есть упорядоченность и нет бардака. Слаженными команды становятся постепенно, в процессе работы. Если сравнить со спортом, то работа команды похожа на эстафету. Эстафета в спорте — это когда один бегун передает другому бегуну какой-либо предмет (тоже называемый эстафетой); второй спортсмен бежит к третьему и также передает ему предмет, а третий бежит к финишу. Это наглядная аналогия. Точно так же в команде каждая 1 2 3 Хотфикс — код, позволяющий быстро исправить обнаруженные ошибки в уже созданном релизе, который установлен на сервере промышленной эксплуатации. Если ошибка возникает на сервере ИФТ или ПСИ и исправления в код вносятся на эти серверы, то это называется «исправление бага». Техдолг — разница между заявленными в реализации требованиями и фактически созданными изменениями, которую нужно дореализовать. При этом отсутствие юнит-тестов для участков, которые должны были быть покрыты ими, но остались не покрыты, тоже образует разницу. Импрувмент (от англ. improvement — «улучшение») — предложения, поступающие от пользователей системы по улучшению какой-либо функциональности или ее добавлению и имеющие низкий приоритет, так как они поступили не от заказчика. От заказчика поступают требования.
102  Глава 3 Основы проектирования и разработки роль передает свой артефакт другой роли. Конечно, в спорте ничего не надо делать с эстафетой, а в команде разработки ситуация сложнее: принял один артефакт, на его основе создал совершенно другой артефакт и передал дальше. Хорошо слаженная работа — это когда владелец продукта четко сформулировал требования к тому, какой должна стать система через две ближайшие недели. Затем аналитик продумал и описал, как это можно реализовать. Далее разработчики, получив требования и аналитику, выполняют свою часть работы — воплощают идеи в коде. После этого тестировщик тестирует созданное разработчиками, а затем происходит развертывание в промышленной среде. В реальности добиваться такой слаженной работы трудно, какая-то часть может отставать. И именно здесь себя проявляет тимлид — разработка идет согласно утвержденному плану. Чтобы картина была правильной, каждый должен прикладывать усилия и при этом не выполнять работу так, чтобы ее нужно было переделывать или доделывать. Плохо слаженная работа — это когда владелец продукта передал аналитику требования, аналитик прочитал их и отложил выполнение, при этом не поставил никого в известность, что сейчас не будет ничем заниматься. Через некоторое время владелец продукта вспоминает о задании и просит аналитика продемонстрировать прогресс. Тот говорит, что раньше не смог взять задание в работу и берется за него только теперь. Он читает и решает ничего не писать — мол, все и так очевидно. Разработчики получают проект в сыром виде и делают свою часть работы. Аналитик потом добирается-таки до задания, видит, что аналитика нужна, пишет ее и передает разработчикам. Те читают и видят, что часть выполненного нужно переделывать. При этом тестировщик уже протестировал то, что они реализовали. Если работа в команде налажена, то все замыслы, озвученные заказчиком, будут развернуты в промышленной эксплуатации согласно запланированному графику, иначе — постоянные переносы сроков. 3.2.6. За что несет ответственность фронтенд-разработчик Задачи для фронтенд-разработчика формулируются на основе созданных другими участниками команды артефактов (аналитика и дизайн) или обнаруженных во время тестирования недочетов, а также требований на улучшения (импрувмент) или необходимости сделать хотфикс. Независимо от типа задачи фронтенд-разработчик выполняет два или одно действие. Первое — проектирование, второе — реализация. В проектировании он придумывает решение, то есть описывает составляющие элементы и связи между ними плюс куда это будет внедрено. В реализации — реализует свою идею, для чего пишет код и юнит-тесты для него. Для некоторых задач проектирование может быть опущено из-за простоты задачи. Основная ответственность фронтенд-разработчика — работающий и покрытый тестами код, написанный в соответствии с принятыми в команде правилами, который в будущем можно поддерживать благодаря понятности реализованных связей между элементами. При этом код в первую очередь должен решать поставленную задачу.
3.3. Граф. Дерево  103 3.3. Граф. Дерево 3.3.1. Введение Данный раздел формирует основы того, что и чем является в графах, а также как их использовать для моделирования тех или иных явлений. Вы будете использовать его материалы при проектировании. В конце раздела приводится процедура моделирования на основе графов. Графы активно применяются в информатике, в двух направлениях: y из кода всей информационной системы составляется граф ее элементов с отображением связей; y пишется код на основе графа. Если вы еще не знакомы с данной областью, сейчас самое время заняться изучением. Здесь я не разбираю алгоритмы для обхода графа, так как здесь сделан упор на моделировании, чтобы вы могли применять это без сомнений, правильно или нет. Алгоритмы обхода описаны во многих других местах. 3.3.2. Граф. Вершина. Ребро. Дуга Если говорить простыми словами, граф — это набор вершин и ребер (дуг). Если углубляться, то нужно давать математическое определение, а поскольку разбор математических свойств нам не понадобится, то и приводить его смысла нет. Мы ограничимся графическим представлением графов. Большинство графов содержат два основных объекта — вершину и ребро (дугу). Вершина представляет объект любой природы. Например, человек, здание, розетка с напряжением 220 В, компьютер, мышь компьютера, компонент в разрабатываемой вами программе. Ребро показывает связь между двумя вершинами. Она может быть любого типа — физическая и нефизическая. Примеры физической связи — дорога, связывающая города, или пролет моста между опорами. Примеры нефизической связи — знакомство с другим человеком, отсылка к автору при цитировании и т. п. Граф применяется исключительно для моделирования отношений между объектами. Пример использования графов для представления человека, двух людей, которые общаются, и коммуникационных линий внутри команды, работающей над программным продуктом, показан на рис. 3.2. Например, на графе «Два человека общаются» показано, что в данный момент они делают именно то, что описано. Точно такой же линией, называемой ребром, показаны коммуникационные связи в группе. В данный момент люди из этой группы, допустим, не общаются, но могут начать общаться, придя на работу. С помощью графов можно моделировать любые ситуации или изобразить имеющиеся отношения, например, между должностями либо ролями, связи городов — с помощью дорог, маршруты между аэропортами, социальные связи и связи между компонентами фронтенд-приложения.
104  Глава 3 Основы проектирования и разработки Рис. 3.2. Несколько разных графов, представляющих человека, общение двух людей и коммуникационные линии в группе по разработке продукта Дополнительно стоит сказать о связях на основе ребер. На графе «Два человека общаются» коммуникация идет в обе стороны: сначала говорит один, затем — второй, потом — новая смена направления. Такую связь изображают с помощью Рис. 3.3. Одностороннее отношение ребра. Но бывают односторонние связи, например дорога с односторонним движением между двумя перекрестками. между вершинами А и Б Ее показывают так, как на рис. 3.3. На рис. 3.3 показано одностороннее отношение между вершинами А и Б. Это значит, что есть отношение А с Б, но нет отношения Б с А. Одностороннюю связь изображают с помощью направленной стрелки, которая называется дугой. Она показывает направление отношения. Структуры данных в JavaScript, создаваемые на основе объектов, чаще всего изображаются именно с помощью дуг. Изучите листинг 3.1 и затем посмотрите на рис. 3.4, где изображен граф структуры, представленной в листинге. Листинг 3.1. Структура данных «бинарное дерево» const binaryTree = { value: 2, left: { value: 1, left: null, right: null, }, right: { value: 4, left: { value: 3, left: null, right: null, }, right: { value: 5, left: null, right: null, }, }, }; Как мы видим на рис. 3.4, у родительского узла, например, с цифрой 2 есть связь с дочерними элементами — цифрами 1 и 4, а у дочерних элементов 1 и 4 связи с родителем нет. Действительно, если посмотреть на листинг 3.1, то ссылки
3.3. Граф. Дерево  105 на родительский элемент у объектов со свойствами value, равными 1 и 4, отсутствуют. Допустимо использовать две дуги, чтобы показать наличие связи между А и Б, а также между Б и А (рис. 3.5). Рис. 3.4. Граф бинарного дерева Рис. 3.5. Одностороннее отношение между вершинами А и Б и одностороннее отношение между вершинами Б и А Может возникнуть вопрос, зачем изображать две дуги, когда можно обойтись одним ребром. Ответ есть в следующем подразделе. 3.3.3. Неориентированный и ориентированный графы Начнем с того, что для описания связей между вершинами могут использоваться ребра и дуги. Исходя из этого граф бывает неориентированным и ориентированным, что показано на рис. 3.6. Рис. 3.6. Неориентированный и ориентированный графы Например, карту города однозначно нужно изображать с помощью ориентированного графа, так как в дорожной сети есть дороги с односторонним движением. На рис. 3.7 показана часть вымышленного города писателей-фантастов и героев мультфильмов, где проезды и улицы с односторонним движением соединяют проспекты с двусторонним движением.
106  Глава 3 Основы проектирования и разработки Рис. 3.7. Участок дорог вымышленного города писателей-фантастов и героев мультфильмов Чтобы изобразить этот участок дорог в виде графа, нужно перекрестки показать вершинами, а дороги — дугами. Для наглядности наложим граф на карту и получим ситуацию как на рис. 3.8. Рис. 3.8. Ориентированный граф, совмещенный с участком карты вымышленного города писателей-фантастов и героев мультфильмов Граф наложен на карту, чтобы пояснить, что с помощью графа моделируются реальные объекты. Хотя географические карты — тоже модели (в данном случае городов), мы к ним привыкли и легко ими пользуемся. Теперь уберем карту, оставим один граф, и у нас получится то, что показано на рис. 3.9. Рис. 3.9. Ориентированный граф перекрестков и дорог вымышленного города писателей-фантастов и героев мультфильмов
3.3. Граф. Дерево  107 Еще один важный момент относительно расположения вершин и дуг графа дорог вымышленного города. На рис. 3.10 показан такой же граф, что и на рис. 3.9, они аналогичные. Два графа, между вершинами которых есть взаимно однозначное соответствие (совпадение меток вершин необязательно), при этом количество ребер, исходящих из обеих вершин и входящих в обе вершины, совпадает, являются изоморфными. Слово «изоморфный» состоит из двух греческих слов — «изо» и «морфе». Первое значит «одинаковый», то есть сходный по свойствам, а второе — «форма». Рис. 3.10. Граф, аналогичный ориентированному При переходе к изоморфному графу графу перекрестков и дорог вымышленного города назревает вопрос: «Как понять расстоя­ писателей-фантастов и героев мультфильмов ние между перекрестками или их взаимное расположение?» Никак, пока не будет внедрено некое значение дуги или ребра. Есть и другая пара моментов, которые нужно понять. Длина дуги или ребра на рисунке графа не играет никакой роли. Ведь дуга или ребро говорят о наличии отношения. Когда вы создаете граф, масштаб не требуется, если, конечно, вашей главной целью не является художественная сторона дела. Дуга или ребро показывают отношение между двумя объектами, которые на графе отмечены вершинами. В реальности два объекта могут примыкать друг к другу или быть плотно прижатыми. Например, как две стены или клетки на шахматной доске. Изображать отношения между близлежащими объектами следует с помощью ребер, как показано на рис. 3.11 и 3.12. Рис. 3.11. Отношение между стенами, показанное с помощью ребра Рис. 3.12. Отношение между клетками на шахматной доске, участки с 8-го до 6-го по вертикали и от a до с по горизонтали Для наглядности приведу пример графа рейсов авиакомпании, осуществляемых с 08:00 до 08:15 по будням (рис. 3.13).
108  Глава 3 Основы проектирования и разработки Рис. 3.13. Граф рейсов авиакомпании, осуществляемых с 08:00 до 08:15 по будням Как видите, плотно примыкающие клетки шахматной доски, равно как и плотно примыкающие стены, а также линия от Москвы до Нью-Йорка изображены так, что может показаться, будто расстояние между клетками на доске практически такое же, как расстояние от Москвы до Нью-Йорка или Владивостока. Поэтому полагаться на длину ребра или дуги на графе ни в коем случае нельзя. Они показывают лишь сам факт отношения. На самом деле этого достаточно. Вероятно, если вы создаете граф для наглядного объяснения материала и хотите показать с помощью дуг или ребер относительность, присутствующую у оригинала, то, конечно, можно задать размеры дуг или ребер относительно оригинала. Но при изображении графов обычно используют цифру, которую пишут над дугой или ребром. 3.3.4. Взвешенный граф Чтобы показать, что отношение имеет числовое выражение, используют число, которое называется «вес». Оно описывает (выражает количественную сторону) какого-либо свойства связи или отношения. Например, если вершины — города, а ребро — автодорога, то весом можно указать расстояние. Граф, у ребер (дуг) которого есть веса, называется взвешенным. Вес пишут над дугой или ребром, как показано на рис. 3.14. Рис. 3.14. Взвешенный граф автодорог
3.3. Граф. Дерево  109 Вес может выражать любое свойство связи (отношения). Это могут быть расстояния, плотность потока1, количество подарков или поздравлений, объем фактически выделенных денежных средств, количество посланных сообщений или TCP-пакетов в сутки, даже объем внимания одного человека к другому. Еще весом можно выразить наличие или отсутствие признака. Например, у нас есть граф коммуникационных линий между участниками группы и с помощью веса, чисел 0 и 1, можно выразить отношения «руководитель — подчиненный» или между мужчиной и женщиной: их наличие — 1, отсутствие — 0. Это показано на рис. 3.15. Рис. 3.15. Взвешенный граф отношений между мужчинами и женщинами в группе, где между всеми участниками есть коммуникационные линии На графе видно, что у Елены и Антона, равно как у Натальи и Ивана, есть отношения. А у Марины ни с кем нет отношений. Однако между всеми участниками группы существует коммуникация. Конечно, показ на графе такого свойства, как наличие-отсутствие признака, избыточен, но в данном случае мне важно показать саму возможность. Граф на рис. 3.14 является взвешенным. Помимо ребер или дуг, вес можно назначать и вершинам. В некоторых источниках вес называют ценой. 3.3.5. Компоненты связности Граф не обязательно должен содержать связи со всеми входящими в него вершинами. Например, он может включать в себя шесть вершин и всего четыре ребра, как показано на рис. 3.16. Выглядит так, будто там не один, а два графа, как в самом начале раздела, на рис. 3.2, где на одном изображении показаны три разных графа. Однако на рис. 3.16 один граф. Каждый подобный компонент, внешне напоминающий отдельный граф, называется компонентом связности. Граф может состоять 1 Плотность потока — показатель, означающий количество каких-либо единиц в единицу времени. Например, если говорят о плотности потока железнодорожных вагонов в сутки, то имеют в виду количество вагонов, проходящих через какой-то участок, например через железнодорожную станцию, допустим, за 24 часа. Период времени может быть разным — и час, и неделя и т. д. Еще у нас есть поток — это непрерывное движение чего-либо.
110  Глава 3 Основы проектирования и разработки из одного и нескольких таких компонентов. На рис. 3.16 в графе присутствует два компонента связности: в одном — четыре вершины и три ребра, в другом — две вершины и одно ребро. Граф, содержащий более одного компонента связности, называется несвязным, а с одним компонентом связности — связным. Самая распространенная ситуация, когда граф можно изобразить в виде нескольких компонентов связности, — например, группа общается в видеочате вроде SberJazz и вдруг у кого-то пропадает интернет-соединение. Аналогичный случай можно смоделировать, как сделано на рис. 3.17. Рис. 3.16. Граф из нескольких компонентов Рис. 3.17. Граф, моделирующий разрыв интернет-соединения во время общения в видеочате SberJazz В одном компоненте связности — четыре вершины и три ребра, в другом — одна вершина и ноль ребер (в данном случае отсутствие связи с кем-либо). 3.3.6. Маршрут. Цепь. Путь. Цикл Когда только начинаешь изучать тему графов, можно запутаться в разнице между маршрутом, цепью, путем и циклом. Если изучать чисто математический источник, не сразу удается понять, чем они отличаются друг от друга. Иногда приводимые определения содержат достаточно информации, с точки зрения того, кто их дает, но читающему это мало помогает. Прежде чем приступить к определениям, обозначим вершины графа. На рис. 3.18 они отмечены буквами латинского алфавита, как принято в математике. Это не обязательно, но при работе с маршрутами, цепями и путями так гораздо удобнее, нежели использовать другие ориентиры. Обычно вершины обозначают маленькими буквами, которые берут с конца алфавита. Однако можно помечать их двумя буквами или буквенно-цифровым сочетанием, а также цифрами. Ребра графа можно пометить или не делать этого. Если помечены только вершины, ничего страшного. В этом случае ребра обозначаются объединениями символов, которыми помечены их смежные вершины. Смежными называются вершины, соединенные ребром. Можно сказать, что они являются концами данного ребра. На рис. 3.18 это вершины w и v, w и t, t и z, z и v, w и u, u и x, x и y, y и v. Соответственно, на данном графе есть ребра wv, vz, zt, tw, wu, ux, xy, yv.
3.3. Граф. Дерево  111 Рис. 3.18. Граф с обозначенными вершинами На этом графе не существует ребер tu, vu, xz и др. Важное замечание! Поскольку слова, которым я сейчас дам определение, присутствуют в обычном языке, используемом каждый день, может сложиться ложное впечатление ясности. Однако в рассматриваемой области они имеют более узкое значение, поэтому текст ниже необходимо прочитать, будто вы видите эти слова впервые. Маршрутом в графе называется чередующаяся последовательность вершин и ребер, соединяющих эти вершины. Таким образом, маршрутом на рис. 3.18 будет, например, последовательность w → u → x → y → v. И последовательность z → v → → w → t → z → v → y → v → z → t → w. А вот последовательность вершин t → v → x → → z → y маршрутом не является. Почему? Потому что маршрут — это чередующаяся последовательность вершин и ребер, а в приведенной нет ребер. Если в маршрут, взятый в качестве первого примера, включить ребра, то он получит следующий вид: w, wu, u, ux, x, xy, y, yv, v. Очевидно, что, когда в последовательности опускается ребро (при перечислении), можно добавить стрелку. Маршрут — самая, так сказать, не ограниченная условиями повторяемости последовательность вершин и ребер. Именно условия повторяемости дают разницу между маршрутом, цепью, путем и циклом. Например, вполне может существовать маршрут w → t → z → v → z → t → w → u → w → u. Как видите, некоторые вершины (а соответственно, и ребра, пусть даже неуказанные) повторяются несколько раз. Маршрут способен показать, например, посещенные социальным работником квартиры и аптеки в течение дня. Или перелет воробьев с дерева на дерево, обход кошкой любимых мест в комнате. Либо движение дизайна веб-приложения от одной роли в команде к другой. Допустим, что при этом каждая роль, отличная от роли дизайнера, высказывает свои замечания и передает работу далее, а дизайнер вносит правки и тоже передает ее другой роли. И однозначно с помощью маршрута задается работа какого-нибудь робота, орудующего на складе. Он посещает исключительно одни и те же места по несколько раз.
112  Глава 3 Основы проектирования и разработки У маршрута, как и у всех иных видов последовательностей вершин и ребер, есть свойство «длина». Это количество ребер. В примере, приведенном выше, у маршрута w → t → z → v → z → t → w → u → w → u длина равна 9, а у маршрута w → u → x → y → v — 4. Введем ограничение. Допустим, мы решили составлять маршрут таким образом, чтобы посещать каждое ребро не более одного раза (рис. 3.19). Рис. 3.19. Связный граф В графе на рис. 3.19 укажем маршрут r → w → t → v → u → y. Мысленно пройдите по нему. В этом маршруте каждое ребро присутствует по одному разу, а вершина w — дважды. Маршрут, в котором ребра присутствуют ровно один раз, называется цепью. В этом графе множество цепей, вы уже можете выявить их самостоятельно. Примером цепи может быть передвижение автомобиля, как на рис. 3.20. Рис. 3.20. Цепь передвижения автомобиля
3.3. Граф. Дерево  113 На рис. 3.20 представлена цепь для ориентированного графа. Каждая дуга появляется ровно один раз, а вершина «Перекресток без разворота» — дважды. Если бы у нас был неориентированный граф и вместо дуг — ребра, то это была бы не цепь, а обыкновенный маршрут. Автомобиль движется от вершины «Предыдущий перекресток» и достигает вершины «Перекресток без разворота», затем — «Место разворота», второй раз «Перекресток без разворота» и вновь «Предыдущий перекресток». Другой пример цепи — передвижение по торговому центру. Допустим, вы стоите перед крылом, ведущим направо и налево. Вы идете направо и движетесь вдоль правой стороны ТЦ, доходите до конца и переходите на левую сторону, двигаясь в обратном направлении — к тому месту, где вошли в правое крыло, но снова выходите в него из левого. Если место, где вы сделали выбор, куда пойти, обозначить как вершину, то ее вы посетили дважды, а каждое ребро (не будем обозначать дугами) — по одному разу. Ограничим маршрут — уже можно сказать, что ограничим цепь — тем, что будем строить последовательность, у которой вершины и ребра присутствуют не более одного раза. На рис. 3.19 таковой является, например, r → w → t → v → y → u → x. Цепь, в которой вершины не встречаются более одного раза, называется путем. Примером пути может быть поездка в метро до места работы — если считать станцию метро, на которую вы вошли, начальной вершиной, а станцию, куда прибыли, конечной. Или места хранения и склады, где находятся посылки. А также путь контейнеровоза (судна, перевозящего контейнеры), который загружают в одном порту, затем он прибывает в другой порт, где происходит частичная разгрузка и дозагрузка, далее идет в другой для аналогичной процедуры и, наконец, в порт прибытия (конечный). Вершиной здесь является порт, ребром — передвижение контейнеровоза между портами. Если начальная вершина цепи совпадает с конечной, такая цепь называется циклом. На рис. 3.19 исходя из данного определения циклами будут последовательности r → w → u → x → r и r → w → t → v → w →u → x → r. Ведь в цепи могут повторяться вершины, но не ребра. В первом случае цикл простой, во втором — составной. Обратите внимание, что, говоря о цикле, мы ведем речь о цепи, а не о пути. Учитывая имеющиеся ограничения, у пути не могут повторяться ни ребра, ни вершины, а в цикле первая и конечная вершины совпадают. Примером цикла может быть следование автобуса или маршрутного такси из пункта отправления со всеми остановками и возвращение в пункт отправления как в конечную точку. Затем данная схема повторяется. В этом случае остановки будут вершинами, а преодолеваемые между ними расстояния — ребрами. 3.3.7. Дерево Дерево — это граф без циклов, где количество ребер на единицу меньше количества вершин и где одна вершина выделена и называется корнем. На рис. 3.21 показано дерево. Допустим, корнем является вершина v. Это дерево создано из графа, взятого с рис. 3.19, путем удаления ребер, которые образовывали циклы. Как правило, деревья изображают более наглядно (рис. 3.22).
114  Глава 3 Основы проектирования и разработки Рис. 3.21. Дерево Рис. 3.22. Дерево, как и на рис. 3.21, но с иным расположением вершин Видно, что это такое же дерево, изменено лишь графическое расположение вершин и ребер, а сам граф прежний. Дерево всегда изображают таким образом, чтобы выделенная вершина, то есть корень, располагалась на самом верху. Если в качестве корня взять другую вершину, например y, то граф не изменится, но согласно принятому правилу придется перерисовать граф. Проделайте это мысленно и увидите, что граф остался прежним, изменилось только его изображение.
3.3. Граф. Дерево  115 Примером дерева может быть схема организации, которая используется для отображения подчиненности должностей. Или каталог книг на определенную тему. Корнем будет тема, далее — основные разделы, потом — категории и так до определенной глубины (рис. 3.23). Рис. 3.23. Дерево, представляющее каталог кулинарных рецептов Показанный на рис. 3.23 каталог книг по кулинарии может быть представлен в коде JavaScript (листинг 3.2). Листинг 3.2. Каталог книг по кулинарии const cooking = { name: "cooking", parent: null, title: "Кулинария", children: [{ name: "meat-poultry", parent: "cooking", title: "Блюда из мяса и птицы", children: [{ name: "lamb", parent: "meat-poultry", title: "Блюдо из баранины", children: [/**/], }], }, { name: "vegetables", parent: "cooking", title: "Блюда из овощей", children: [{ name: "potato", parent: "vegetables",
116  Глава 3 Основы проектирования и разработки title: "Блюдо из картофеля", children: [/**/], }, { }], name: "carrot", parent: "vegetables", title: "Блюдо из моркови", children: [/**/], }, { }; }] name: "fish-seafood", parent: "cooking", title: "Блюда из рыбы и морепродуктов", children: [{ name: "trout", parent: "fish-seafood", title: "Блюдо из форели", children: [/**/], }], Есть ли здесь подвох? Небольшой. Если посмотреть на граф кулинарного каталога, то связи между родительским и дочерним узлами показаны ребрами, то есть связь двусторонняя. Глядя на код структуры каталога, легко заметить, что у родительского объекта есть «честная» ссылка на дочерние элементы, а у дочернего к родителю указано только имя. Такую ситуацию вполне нормально изобразить с помощью графа, как на рис. 3.23. Представьте, что с сервера вам приходит такая структура в формате JSON. У вас есть имя родителя — значит, можно написать функцию, чтобы создать физическую ссылку на родительский узел. Как разработчику вам нужно создать графическое представление структуры данных, которая у вас есть в коде, и наоборот — создать структуру данных в коде на основе их графического представления. Если вы еще не очень хорошо ориентируетесь в этой области, то для развития навыка работы с графами нужно тренироваться — и на бумаге, и в коде плюс учиться представлять в коде из графической схемы и наоборот. 3.3.8. Свойства графов, дающие отличительные особенности Когда у нас есть граф, нужно смотреть на его свойства. Обычно это соотношение количества ребер и количества вершин, количество элементов связности, наличие или отсутствие циклов; количество ребер, соединяющих вершины, или количество входящих и исходящих дуг в вершины/из них, наличие маршрута из каждой вершины в каждую оставшуюся вершину; возможность пройти из одной вершины, обойти все остальные и вернуться в исходную, уточняя, был ли шанс пройти по каждому ребру не более одного раза. Наглядными примерами здесь служат такие графы, как дерево, и такие части графов, как цикл или путь.
3.3. Граф. Дерево  117 Все названные отличительные особенности дают возможность пользоваться вытекающими из них свойствами в тех моделях, которые вы будете составлять при разработке программ или моделировании. Самое интересное, что, независимо от того, для какой предметной области вы составляете модель — будь то расположение магазинов торговой сети в городе или же моделирование взаимодействий в команде, отношений между людьми или маршруты движения курьеров, разносящих товары покупателям, — свойства графов проявляют себя абсолютно одинаково. Поэтому необходимо знать, как их использовать для моделирования. 3.3.9. Моделирование с помощью графов Одно важное свойство есть у всех моделей, независимо от того, с помощью чего они получены: как только модель выполнена, про изначальные условия и реальные объекты можно забыть и работать исключительно с ней. Это надо запомнить и пользоваться. Процедура моделирования очень проста. 0. Определите, есть ли в рассматриваемой системе, которую вы хотите смоделировать, объекты и отношения между ними. Если есть, продолжайте выполнять следующие шаги. Если нет, значит, это не система. Но постарайтесь найти систему в том, что вы пытаетесь моделировать. 1. Выясните, что принять за вершину. 2. В физической или виртуальной системе перечислите и выпишите все вершины. Их можно наносить, помечая или располагая так, как они расположены в реальной системе. 2а. Если нужно, чтобы вершины имели значения, напишите их рядом с вершиной или внутри нее. В этом случае ваш граф будет взвешенным. 3. Определите, какие есть отношения между объектами, принятыми за вершины. Затем проведите ребра или дуги между теми из них, между которыми в реальной системе существуют связи, отношения. 3а. Если вам нужно ввести значения для ребер или дуг, то наносите их над ребрами или дугами. В этом случае ваш граф будет взвешенным. 4. Мысленно отбросьте изначальную систему и взгляните на созданную вами модель как на граф. 5. Изучите свойства получившегося графа и посмотрите, что может происходить в результате перемещений между вершинами, при построении маршрутов, цепей или путей, удалении или добавлении ребер или дуг. С моделью вы можете делать все что хотите. Однако не забывайте, что у вас есть цель и граф и ваши действия с ним должны привести к реализации цели. В нашем мире много разных систем, которые можно моделировать с помощью графов. Это очень важная область знаний. Навык моделирования станет вашим постоянным инструментом. Почему? Да потому, что «читай первое предложение этого абзаца». К сожалению, ни один программист не обходится без рекурсии. Леонард Эйлер, основоположник данной области, начал с того, что смоделировал часть города. Он принял острова за вершины, а мосты — за отношения между ними. Это пример физической связи между физически существующими объектами.
118  Глава 3 Основы проектирования и разработки 3.3.10. Продолжайте изучать графы Вам необходимо глубже погрузиться в графы и, самое главное, тренироваться в решении задач. Прежде всего нужно изучать алгоритмы для работы с разными видами графов. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 3.3. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 3.3. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 3.3 и ознакомьтесь с моим решением. 3.4. Система 3.4.1. Введение Эта глава даст вам ключ к пониманию многих областей, а не только фронтенд-разработки. Вы начнете видеть проявления свойств или признаков систем во многих сферах жизни или в объектах, сотворенных и человеком, и природой. Однако надо понимать, что здесь дан минимум, который всегда изучается в самом начале. Чтобы полноценно освоить системы, вам необходимо изучить общую теорию систем1. 3.4.2. Что такое система Начнем с определения. Система — набор элементов, связанных и взаимодействующих между собой, что выражается в определенном поведении относительно внешней среды. Это общее определение, которое нужно сделать более наглядным. Что же, рассмотрим такую систему, как веб-приложение. Вы только-только узнали, как моделировать элементы и связи между ними с помощью графов. Теперь можете воспользоваться этими знаниями. Самое простое устройство веб-приложения может быть таким, как показано на рис. 3.24. 1 Общая теория систем — область знаний о том, как устроены и ведут себя составные объекты, одушевленные и неодушевленные. В ней рассматриваются свойства, общие для всех подобных объектов. Освоение данной области позволяет научиться создавать сложные и не очень объекты, изучить существующие сложные и не очень объекты. Примерами объектов-систем могут быть: веб-приложение, человек, пруд и его обитатели, двигатель, язык общения людей, китайские иероглифы, погода; общая цель — например, выйти в лидеры рынка по производству парусных яхт. Начало этой области знаний положил биолог Людвиг фон Берталанфи, когда изучал биологические системы.
3.4. Система  119 Рис. 3.24. Простейшее устройство веб-приложения Очевидно, что это простое веб-приложение — здесь опущены детали. И всетаки это система. Почему можно так сказать? Потому что, исходя из определения, на этом рисунке есть набор связанных и взаимодействующих между собой элементов. Фронт-приложение отправляет запросы к серверу приложений, а тот, в свою очередь, запрашивает базу данных, затем формирует ответ и направляет его в качестве ответа обратно во фронт-приложение. Таково поведение данного набора элементов, взаимодействующих с пользователем. Поэтому это система. Теперь представим, что у нас есть набор элементов, таких же, как на рис. 3.24, но без связей. На рис. 3.25 показаны те же самые наборы элементов, но без связей. Это уже не система под названием «веб-приложение», а просто набор элементов. Соответственно, у него не будет и поведения, которого можно ожидать от веб-приложения, потому что отсутствуют связи между элементами, а значит, они не взаимодействуют между собой. Приведя данный пример, я немного забежал вперед, но зато наглядно показал, что отличает систему от набора. Далее вы узнаете, как правильно называются изображенные на рис. 3.25 элементы. Продолжим знакомиться с основами систем. Рис. 3.25. Набор элементов будущего веб-приложения без связей 3.4.3. Строение и действие системы Когда вы смотрите на файлы, из которых состоит веб-приложение, вы видите его составляющие. Вы можете заглянуть в код и увидеть элементы приложения, а еще то, как в нем обрабатываются данные. Однако взаимодействовать с пользователем код не может, пока не запущен в среде исполнения. Только запустив свое приложение, вы ощутите все проявления его поведения и начнете с ним взаимодействовать. Сам по себе код не является одной из составляющих системы — это модель, которая их описывает. Разница между кодом и реальным запущенным приложением примерно такая же, как между чертежом здания и реальным домом. Мы часто пренебрегаем этим фактом и, глядя на код, говорим о нем как о приложении. И это нормально, потому что, когда код оказывается в среде исполнения, ваши функции, которые являются составляющими системы, начинают действовать. Они представляются нам ее элементами.
120  Глава 3 Основы проектирования и разработки Любую систему, в том числе веб-приложение, можно описать с помощью нескольких понятий1, которые делят на две группы: 1) статические, описывающие составляющие системы, когда она не действует; 2) динамические, описывающие систему в действии. Как я уже говорил, здесь мы рассмотрим не все, а только то, что нужно для понимания работы информационных систем. Статическими понятиями являются элемент, компонент, подсистема, связь, структура, цель, а динамическими — состояние и поведение. Тут многие воскликнут: «Ничего себе! Оказывается, вот почему была создана библиотека Redux». И будут правы — эта библиотека входит в состав фронтенд-приложения. Более того, изначально библиотека React была построена с учетом необходимости работать с состоянием. Далее вы узнаете, чем именно является фронт-приложение в составе веб-приложения. 3.4.4. Понятия, описывающие составляющие Элемент — это неделимая часть системы и то, из чего складывается ее структура. В приложениях за элемент принимают функцию, при этом одна функция отличается по смыслу от другой. Элемент всегда обладает какой-то ролью. В качестве примера здесь можно назвать клетку организма. Связи — это отношения между элементами системы. С помощью связей создается структура, без которой системы не могут существовать. Связь — то, что на графах обозначается с помощью ребра или дуги. Примером здесь может быть соединение атомов в молекуле или соединение коленчатого вала, подшипника, блока цилиндров. Пример связи в информационной системе — ссылка из одного объекта на другой (когда вы пишете код в программе JavaScript, задаете именно связь) или обращение клиентского приложения к серверу за данными. Далее будут рассмотрены другие примеры связей. Структура — это то, как взаимосвязаны элементы системы и как они расположены относительно друг друга. Виды структур будут рассмотрены далее в этом разделе. Их примером может быть структура данных «дерево» или структура компонентов фронтенд-приложения, созданного с помощью библиотеки React. Цель — основная причина существования системы. Примером цели информационной системы может быть автоматизация операций какой-либо предметной области. Она встречается чаще всего. Например, возможность осуществлять заказы товаров. 1 Понятие — это слово или фраза, которые называют предмет, действие или состояние из конкретной области, и в ней такое слово или фраза имеют специфическое для данной области значение. Например, в информационных технологиях есть понятие «память», которое означает физическое устройство, способное долговременно или кратковременно сохранять некоторое состояние, интерпретируемое как биты информации. Однако мы знаем, что память есть и у человека, и это совсем другое явление, с физическим миром связанное иными принципами. Так и в области систем существуют понятия, которые описывают некоторые явления, действия и объекты.
3.4. Система  121 Подсистема — относительно независимая часть системы, обладающая свойствами системы и имеющая некую меньшую цель, для достижения которой она и создана. Фронтенд-приложение является подсистемой такой системы, как веб-прило­ жение. В самом фронтенд-приложении тоже могут быть подсистемы, например подсистема аутентификации. Компонент — это часть системы, которую можно разделить на составляющие — элементы, которая не обладает всеми свойствами системы, как это проявляется у подсистем. Компонент, по сути, набор составляющих. Деление на элементы и компоненты иногда условно. Однако в библиотеке React оно очень четкое: элементом является JSX-строка, представляющая тег, а компонентом — самостоятельная, переиспользуемая составляющая, которая рендерит пользовательский интерфейс, состоящий из JSX. Компоненты отличаются друг от друга по смыслу. Например, один компонент может представлять страницу приложения, другой — единственный конкретный элемент, допустим, таблицу. Функция может подсчитывать значения либо делать запросы. 3.4.5. Понятия, описывающие действия системы Состояние — это мгновенный срез системы. Данным понятием описывают своеобразную фотографию системы, остановку в ее развитии. Определяют состояние либо через входные воздействия и выходные результаты, либо через параметры и свойства системы. В физических системах, например в автомобиле, состоянием может быть диапазон работы двигателя (оборотов в минуту), скорость движения, количество потребляемого топлива и др. То есть совокупность показателей. В информационных системах состоянием может быть количество созданных сессий или запросов (в данный момент), тип транзакции в базе данных; оценка балансировщиком нагрузки, на какую реплику отправить запрос; решение отклонить логин и пароль, принятое системой аутентификации; ответ сервера в виде кода HTTP 4221 на запрос клиента. Во фронтенд-приложении интернет-магазина состоянием будет товар, помещенный в корзину и описываемый объектом, наличие или отсутствие факта аутентификации пользователя, история посещений разделов (если она ведется приложением). Поведение — это переход из одного состояния в другое. Это и запросы, и взаимодействие с пользователем, и запуск через определенное время функций, которые изменяют состояние. Например, пользователь нажал кнопку и подразумевается, что при этом должно открыться модальное окно. Нажа­ тие кнопки и вызов функции, изменяющей состояние отображения окна, — это поведение, так как оно привело к изменению состояния отображения окна. Другой пример — пользователь добавил в корзину товар, например нажал на плюс рядом с ним. Где-то в приложении появилась информация о добавленном в корзину 1 Код ответа 422 — ответ на некорректно составленный запрос.
122  Глава 3 Основы проектирования и разработки товаре. Нажатие и дальнейшее движение данных, вызов конкретных функций — тоже поведение. Иногда для описания некоторых моментов системы нужно опускать детали, чтобы не перегружать ими поведение или состояние. Детали можно опустить, потому что они несущественны. Это называется абстрагированием. 3.4.6. Признак системы Благодаря существованию в системе всего вышеперечисленного появляется свойство, которым не обладает каждый отдельный элемент системы или совокупности таких элементов. Данное свойство называют системным. Наличие связей и их структуры дает системе поведение, качественно отличающееся от поведения, которое бы являлось суммой поведений образующих ее элементов. Если взять в качестве примера самолет как систему, то очевидно, что, не будь у него одного крыла, он бы не полетел. Само крыло тоже не сможет полететь. А совокупность всех элементов самолета имеет и состояние, и поведение, которые вместе обеспечивают полет. 3.4.7. Примеры связей Разрабатывая приложение, нужно понимать, как изобразить связи с помощью графа, когда вы смотрите код и хотите его изобразить, и как написать код, когда вы видите граф. В разделе про графы мы рассматривали структуру «дерево», где в узлах присутствовали связи с дочерними узлами, и то, как данная структура изображается с помощью графа. Это типичный пример связей в приложении. Другим примером может служить вызов функции внутри другой функции (листинг 3.3). Листинг 3.3. Вызов одной функции в теле другой для вычисления площади круга const getSquare = (x) => x ** 2; const getCircleSquare = (r) => Math.PI * getSquare(r); Такой вид связи обозначается следующим образом (рис. 3.26). Функция getCircleSquare вызывает в своем теле функцию getSquare . На рис. 3.26 показан направленный граф, и getSquare возвращает результат для getCircleSquare. Рис. 3.26. Связь 3.4.8. Виды структур Структурные связи относительно независимы от элементов. Структурные связи элементов порождают закономерности и могут быть перенесены в другую систему, сохраняя свойства, вытекающие из структуры. По этой причине работают паттерны
3.4. Система  123 проектирования. При этом перенос структурных связей не обязательно должен быть между аналогичными системами или даже системами из одной сферы. Например, есть биологическая система. Обнаружив в ней структуру и перенеся ее в область строительства домов, можно получить те же свойства. Именно так в человеческую деятельность переносят технологии, созданные природой. Существует даже наука бионика1. Теперь мы ознакомимся с используемыми видами структур и примерами их применения. Иерархия2. Подразумевается, что существует некий высший элемент (находящийся на самом верху) и с ним связаны другие элементы, представляющие следующий уровень. С ними, в свою очередь, связаны другие элементы и так далее — до какого-то конечного уровня. Элементы, находящиеся на разных уровнях, отличаются друг от друга тем или иным образом. Верхний всегда управляет нижними, а нижние всегда связаны с верхним, кроме корневого элемента. Этот вид структуры применяется в компаниях для создания организующей схемы. Фронтенд-приложение строится с помощью данной структуры. Самым наглядным доказательством этого является фрагмент фронтенд-приложения, составленный с помощью WebAPI (листинг 3.4). Листинг 3.4. Подсистема «Таймер» const createElementTimer = (createTimer, title) => { const timer = createTimer(); const buttonStartCancel = createTwoStateButton("Начать", "Остановить", timer.start, timer.cancel, true); const buttonReset = createButton("Обнулить", timer.reset); const [minutes, colon, seconds, timerDiv] = ["span", "span", "span", "div"].map(element => createElement(element)); colon.textContent = ":"; const timerTitle = createTitle(title); const update = (min, sec) => { minutes.textContent = min.toString().padStart(2, "0"); seconds.textContent = sec.toString().padStart(2, "0"); }; timer.subscribe(update); [timerTitle, minutes, colon, seconds, buttonStartCancel, buttonReset]. forEach(DOMElement => timerDiv.appendChild(DOMElement)); }; 1 2 return timerDiv; Бионика — наука на стыке биологии и техники, решающая инженерные задачи путем анализа структуры и жизнедеятельности организмов. Иерархия — расположение частей или элементов целого от высшего к низшему или от низшего к высшему.
124  Глава 3 Основы проектирования и разработки Листинг приведен в сокращенном виде, его полная версия есть в листингах кодов, в папке 3. Рекомендую его посмотреть — это полноценная подсистема, которая может работать как самостоятельная система. В теле функции createElementTimer производится вызов нескольких функций, при этом вызываемые функции, в свою очередь, тоже вызывают другие функции. Например, createTimer вызывает calculateTime, а createButton — createElement. Эти вызовы образуют иерархию, в том числе потому, что функции, вмещающие в себя вызовы других функций, качественно отличаются от вызываемых. При этом функция calculateTime вычисляет текущее состояние составляющих времени, а createTimer использует calculateTime, создавая функциональность таймера. Функция createElementTimer создает представление таймера в DOM. Все эти вызовы вместе — подсистема с иерархической структурой, что показано на рис. 3.27. Рис. 3.27. Граф иерархии подсистемы «Таймер» Структуру не обязательно рассматривать строго для всей системы, она есть у любого участка. Обратите внимание на createElement. Такие функции, связанные со многими элементами, называются элементами со слабыми связями. Почему слабыми? Потому что такие элементы не привязаны жестко к одному конкретному элементу. Именно это активно пропагандирует библиотека React. А сама идея позаимствована ими из области систем, что, на мой взгляд, правильно. Сеть1. Прокладывая маршрут в навигаторе, мы имеем дело с сетью. Другой пример — нейросеть, то есть модель, которая используется для имитации мыслительного процесса, перенесенного в компьютер. Она позволяет создать видимость того, что на переданный на вход текстовый вопрос вы получаете как бы осознанный ответ. Обычно нейросеть имеет структуру, показанную на рис. 3.28. 1 Под сетью понимают направленный граф, который используется как модель системы, процесса и т. п. В сети обычно выделяют вершины, играющие роль входов и выходов. Часто под сетью понимается транспортная сеть.
3.4. Система  125 Рис. 3.28. Структура нейросети Любопытства ради посмотрим, какую структуру имеет типичное фронтендприложение (рис. 3.29). Рис. 3.29. Структура типичного фронтенд-приложения Любое приложение React, как правило, начинают с компонента App, но не обязательно. Он обычно состоит из страниц. Страницы включают в себя компоненты, как правило работающие с данными, ради которых пользователи используют сервис. Обычно это непростые компоненты, разбиваемые на меньшие части, чтобы код компонента был не очень длинным и с меньшим количеством переменных состояния. Подобные компоненты могут быть разделены на большее количество подкомпонентов, чем на изображении. В конце концов используются элементарные компоненты, представляющие собой один часто используемый элемент интерфейса, например кнопку. В дальнейшем мы, естественно, затронем эту тему. В главе 5, в разделе 5.19, будет возможность взглянуть на компоненты иначе.
126  Глава 3 Основы проектирования и разработки 3.4.9. Важное свойство структуры фронтенд-приложения Компоненты фронтенд-приложения имеют иерархическую структуру (имеются в виду React-компоненты или веб-компоненты). Из этого следует, что компонент, находящийся уровнем выше, управляет компонентами, расположенными уровнем ниже. Вроде ничего особенного, но это важнейшее свойство структуры. И вот почему. Когда надо управлять целой группой компонентов, вы должны просто подняться на уровень выше и сделать все необходимое для этого. Кроме того, вы можете встроить дополнительный уровень над тем уровнем, поведением которого предстоит управлять. Это свойство структуры выделено еще и для того, чтобы у вас не сложилось впечатление, будто структура чисто коммуникативная. То есть что от родителя поступают только данные — и не более. Однако следует понимать, что родитель ответственен не только за данные, но и за поведение своих дочерних компонентов. Теперь подумайте, на каком уровне компонентов вы стали бы организовывать управление поведением компонентов с помощью аутентификации1. Второе важное свойство заключается в том, что дочерний элемент не может управлять родительским и к родителю могут поступать только данные. Зная два этих свойства, будет намного легче разрабатывать. 3.4.10. Заключение Любая информационная система обладает составляющими, состоянием и поведением. С системой можно взаимодействовать. Ее структура включает в себя элементы, которые, в свою очередь, состоят из компонентов. Из компонентов формируются подсистемы и системы. Важное различие между компонентом и подсистемой — то, что компонент не имеет системных свойств, то есть у него не появляются новые качества, как у подсистемы или системы. Важно понимать, как из элементов и компонентов создаются подсистемы и системы. Это делается с помощью связывания элементов в компоненты, а компонентов — в подсистемы. Как только система создана и начинает действовать, у нее проявляются такие свойства, как состояние и поведение. 3.5. Проектирование 3.5.1. Что такое проектирование Слово «проектирование» может быть непонятным или пониматься неправильно. Оно применяется в разных областях. Например, в судостроении проектированием является и разработка математической модели, чтобы будущее изделие соответствовало требуемым характеристикам, и создание чертежей, и составление 1 Аутентификация — процесс, позволяющий установить, что пользователь, который пытается получить доступ к определенной информации, действительно тот, за кого себя выдает.
3.5. Проектирование  127 графика реализации. Чтобы не запутаться, следует понимать, какое именно действие нужно осуществлять для фронтенд-приложения и нужно ли вообще это делать. Информационные системы воплощаются в виде дистрибутивов1 с кодом, который подлежит развертыванию2 на платформе, где они функционируют. Прежде чем такая система будет воплощена в коде и собрана в дистрибутив, над ней должны поработать архитектор и аналитик. Архитектор, составляя диаграмму последовательности3, указывает исключительно верхнеуровные компоненты системы. Например, в веб-приложении (рис. 3.30), состоящем из микросервисов и фронтендприложения, само фронтенд-приложение выглядит как одна составляющая, наряду с другими, такими как сервис ведения пользователей или аутентификации. На такой диаграмме вы видите фронтенд-приложение как объект, который взаимодействует с разными сервисами с помощью запросов. Деталей о самом фронтенд-приложении там, естественно, нет. Разве что архитектор укажет, какой фреймворк или библиотеку использовать. Системный аналитик создает более по­ дробное описание компонентов системы, но все же не настолько детальное, чтобы фронтенд-разработчик сразу приступил к выполнению. Таким образом, есть целая информационная система, состоящая из разных микросервисов (в показанном выше случае), куда входит и фронтенд-приложение как подсистема данной системы. Получается, нужно проектировать в целом информационную систему и детально каждую ее составляющую. Определим, что такое проектирование фронтенд-приложения. Проектирование фронтенд-приложения — это описание таких составляющих, как компоненты, подсистемы и, главное, связи между компонентами, подсистемами и внешними системами (по отношению к фронтенд-приложению), с помощью которых решаются задачи, стоящие перед приложением. Это не обязательно детальное описание, но оно может быть таковым. В описание должно входить состояние и поведение. Для наглядности имеет смысл пояснить данное выше определение. Под компонентами понимаются веб-компоненты или React-компоненты либо обычные JavaScript-функции, выполняющие такую роль. Под подсистемами — несколько компонентов, связанных вместе и реализующих конкретную функциональность для приложения. Это может быть несколько связанных между собой функций, выполняющих определенную роль. Каждая такая функция является не компонентом, 1 2 3 Дистрибутив — программный продукт, поставляемый в том виде, в каком он требует инсталляции. Развертывание — это выполнение определенных действий, подготавливающих дистрибутив к функционированию. После развертывания дистрибутив, который был, например, просто кодом (допустим, файлом типа war), исполняется. По-английски это deployment. Обычно разработчики используют английский вариант слова — «задеплоить» («развернуть»), «деплой» («развертывание»). Диаграмма последовательности — это диаграмма, обеспечивающая динамическое представление системы и показывающая взаимодействие, где заняты ее компоненты и связи между ними, включая сообщения, которыми они могут обмениваться. По ней также можно понять временной порядок.
128  Глава 3 Основы проектирования и разработки а обычной функцией. Но данная подсистема реализует определенную функциональность и поэтому является подсистемой. Из готовых решений подобной функциональностью обладает библиотека Redux. Под внешними системами понимаются микросервисы с API, к которому можно обратиться через конкретный эндпоинт (endpoint) и получить определенные данные. На рис. 3.30 такими внешними системами являются все микросервисы, кроме самого фронтенд-приложения и актора1. Под состоянием подразумевается состояние приложения или отдельной подсистемы, но не конкретного компонента. Под поведением — то, как приложение преобразует данные или что происходит при наступлении определенного события и после чего изменяется состояние. Рис. 3.30. Диаграмма последовательности 1 Актор — пользователь или система (сервис), которая инициирует взаимодействие. Изображается в виде человечка, как на рис. 3.30.
3.5. Проектирование  129 3.5.2. Двойная работа Проектирование требуется не для всех задач, поступающих на выполнение. Если приложение новое, нужно разработать подсистему или сложный компонент, то оно необходимо. А если задача простая, то проектировать ни к чему. Все определяют объем задачи и количество компонентов, входящих в решение. Как может появиться двойная работа? Имеется в виду ситуация, когда вы написали код, а потом поняли, что его надо полностью переписывать. Когда работа с кодом начинается сразу, минуя этап проектирования (npx create-react-app appname), происходит следующее. После просмотра дизайна вы в уме придумываете какое-то решение, затем пишете пару-тройку компонентов. Однако, пока вы погружались в реализацию, прошло время и принятое решение могло слегка затуманиться. Значит, нужно снова посмотреть дизайн. Чтобы убедиться в соответствии написанного кода задумке, вы смотрите на код, потом обращаетесь к собственному разуму. И так все время. Проектируя, вы записываете то, что приходит на ум, на бумаге или в специальном редакторе — то есть мысль помещается в другой объект. Затем вы пишете код. Подходит момент сравнения задумки и кода. Теперь вы смотрите не в разум и затем на код, а на два объекта одновременно — так вам легче искать несоответствия и вести работу, ведь все в прямом смысле слова перед глазами. А когда вы смотрите в разум, сначала нужно вспомнить замысел, потом — взглянуть на код, и так несколько раз. Это может утомлять. Поэтому проектирование необходимо, в том числе для экономии вашего собственного времени. К тому же оно избавляет от необходимости постоянно что-то вспоминать. Выпишите свое решение на какой-то носитель и пользуйтесь им. 3.5.3. Проектирование приложения — проектирование системы Проектируя приложение, вы в первую очередь решаете, с помощью каких составляющих будет реализовано нужное поведение, соответствующее заявленным требованиям. Отталкиваться необходимо от цели. Поведение определяет, какие вам нужны элементы, состояние и поведение. Но самое важное, чему следует уделить время, — это структура и связи. Поначалу такой подход кажется непривычным, поэтому, чтобы не ломать себе голову, делайте следующее: 1) определите компоненты-страницы приложения, то есть главные разделы; 2) составьте детальную структуру UI-компонентов каждой страницы и всего приложения; 3) создайте связи с источниками данных; 4) определите состояние. На самом деле если вы посмотрите на диаграмму последовательности, то увидите, что связи, которые есть у фронтенд-приложения, — это пользователь и другие подсистемы (см. рис. 3.30). Связь с пользователем осуществляется с помощью  UI, с другими подсистемами — путем запросов согласно протоколу HTTP. Пользователь будет ходить по разделам приложения и, конечно, изменять состояние. Собственно, на этом все.
130  Глава 3 Основы проектирования и разработки Прежде чем продолжить, следует понять, насколько детально нужно проектировать. Допустим, вы составили структуру UI-компонентов приложения. Далее проектировать каждый компонент в отдельности? Нет. Это уже детали реализации, их вы будете воплощать в коде. Сейчас надо составить и зарисовать структуру, чтобы не запутаться в процессе кодирования. Наличие нарисованных связей, как и сама структура, сильно облегчает и ускоряет написание кода. В разделе 8.1 вы познакомитесь с пошаговой процедурой проектирования приложений, а в разделах 8.2 и 8.3 — с тем, как применять такую процедуру к приложениям на основе библиотек React, React Router, Redux. Здесь, в данном разделе, описываются основы, необходимые для общего понимания. 3.5.4. Определение компонентов — страниц приложения Дизайнер создает дизайн, где приводит все страницы приложения, на которых может оказаться пользователь. На их основе вы определите главные UI-компоненты фронтенд-приложения — компоненты страниц. Это очевидный шаг. Достаточно посмотреть дизайн, чтобы понять, какие страницы у вас будут. В редких случаях бывают неочевидные страницы. Дадим определение понятия «страница» — это некий раздел приложения, в котором пользователь может выполнить какие-либо действия с данными, предназначенный для конкретных, специфичных данных. Вполне разумно предположить, что на странице «Корзина» пользователя ждет информация о товарах, которые он туда добавил, и сумма заказа, а на странице «Каталог» — информация о товарах, разделенных на категории с возможностью фильтрации, сортировки. Для разработчика, когда он пишет код, страница — это компонент, а когда он смотрит на приложение как пользователь — интерфейс, раздел сайта (приложения). 3.5.5. Создание структуры UI-компонентов приложения Страницы входят и в этот пункт. Однако их выделяют, так как все дерево компонентов каждой страницы (раздела приложения) — уже детали компонента под названием «страницы». Пользователю важна сама страница, и если он переходит на другую, то так и мыслит — как о переходе. Детали его не интересуют. Предыдущий пункт задал направления деятельности. Теперь нужно определить, какие родительские узлы будут у всех компонентов страниц, затем — продумать составляющие каждой из них. Страница состоит из меньших компонентов. Среди компонентов, выделенных для всех страниц, найдутся общие, которые вы можете переиспользовать. Разделяя крупный компонент на более мелкие и подчиняя их родителю, вы создаете связи. На данном этапе не обязательно продумывать, какие данные будут передаваться, но можно и сделать это. Обычно это решается в момент реализации, при написании кода. Когда составлено дерево UI-компонентов и среди них выделены общие, пора переходить к следующему шагу.
3.5. Проектирование  131 3.5.6. Определение источников данных Этот шаг часто выполняется вместе с предыдущим. Одна часть компонентов будет нуждаться в данных с сервера, другая — в том, чтобы отправить данные, полученные от пользователя, третьей данные вообще не потребуются. На текущий момент составлено дерево компонентов и нужно обозначить, какие компоненты с какими эндпоинтами или локальными хранилищами будут взаимодействовать. Веб-приложение должно делать запросы для осуществления CRUD1-операций. Кроме того, оно может обращаться с точно такими же целями к локальным хранилищам, как ко временным, на время жизни страницы (например, sessionStorage), так и к постоянным (например, localStorage, indexedDB, Cache). На рис. 3.31 показаны источники входных и выходных данных веб-приложения. Рис. 3.31. Источники входных и выходных данных веб-приложения На рис. 3.31 видно, что R-запросы (read) показаны входящими в приложение стрелками, а запросы C (create), U (update) и D (delete) — исходящими. Ввод от пользователя через формы показан входящей стрелкой, а вывод в DOM — исходящей. И R, и C (или U либо D) работают одинаково: запрос — ответ. Однако при этом используются разные методы. Например, для R — GET, для C — POST, для U — PUT и PATCH и для D — DELETE. Но самое важное здесь то, что R не изменяет данные, тогда как остальные предназначены для изменения. По этой причине стрелки у них должны быть разными. Ведь смысл не в том, чтобы на данном шаге проектирования показать правильность работы протокола и изобразить запрос как исходящую коммуникацию, а отобразить действие над данными с помощью запроса; детали будут важны на этапе реализации. То есть при R-запросах мы получаем данные, при остальных — изменяем, удаляем или создаем. На этом шаге основной упор делается на взаимодействии с API, то есть с эндпоинтами. Взаимодействие с API происходит по протоколу HTTP. Ваш API может быть реализован таким образом, что каждый эндпоинт будет поддерживать широкий спектр методов, а также широкий спектр статус-кодов ответов. Исходя из требований, необходимо предусмотреть поддержку в коде фронтенда разных статус-кодов. 1 CRUD — сокращение из первых букв слов create, read, update, delete. То есть «создание, чтение, обновление, удаление» — четыре базовые функции по управлению данными.
132  Глава 3 Основы проектирования и разработки 3.5.7. Состояние Состояние бывает глобальным — для всего фронтенд-приложения и локальным. Вам нужно определить глобальное состояние либо состояние для какого-либо поддерева, то есть для группы компонентов. Локальные состояния компонентов необходимо продумывать в момент написания кода. Иначе вы просто утонете в деталях. При проектировании состояния мы имеем дело с местами расположения данных и их динамикой. Примером здесь может служить аутентификация пользователя, добавленные в корзину товары, фильтрация товаров или результаты поисковых запросов. Вам нужно определить переменные, которые хранят значения и структуры данных, а также компоненты, в которых это состояние будет располагаться и из которых оно будет доступно всему приложению или группе компонентов. Примерами локального состояния являются данные в поле input-компонента. 3.5.8. Не только UI-компоненты Когда в приложении не используются библиотеки, отвечающие за конкретную функциональность, ее нужно реализовывать самостоятельно. Представьте, что вы еще не изучали библиотек React, React Router, Redux, но хорошо знакомы с WebAPI. Вам надо реализовать приложение, вы готовы приступить к работе. Что ж, всю функциональность, которую покрыли бы библиотеки, придется каким-то образом реализовывать. 3.5.9. Откладывание решения Откладывание проектирования на потом, если оно требуется, происходит из-за отсутствия решения. Звучит просто, но, когда в уме нет такого решения, попытка справиться с его отсутствием обычно выглядит так — приступить к чему-то другому, с чем наверняка легко справиться. То есть сделать что-то, что у вас хорошо получается. Но решение от этого не появится. Налицо ловушка: «Не могу сделать то, что требуется — сделаю то, что умею». Выход здесь следующий. Нужно применить знания о системе. Система — это составляющие, которые связаны между собой и создают структуру. Также у системы есть состояние и поведение, и она обладает системным свойством, которое не является суммой свойств всех ее составляющих. Проанализируйте задачу применительно к ним. 3.6. Реализация приложения 3.6.1. Реализация Реализация — это непосредственное написание кода элементов системы на языке программирования и создание связей между ними. В реализацию также входит создание файлов и размещение в них компонентов, организация связей между компонентами, размещенными в файлах. Написанию кода предшествует создание решения.
3.6. Реализация приложения  133 Под компонентом здесь понимается компонент системы как совокупность элементов, не обладающих свойствами подсистемы. Реализация — это часть создания приложения, с которой начинают обучение все практикующие программисты, поэтому она им знакома. Все может показаться очевидным, за исключением одного момента — создания связей. Создание связей выделяется особо, наряду с написанием кода. В большинстве случаев связь выглядит или как ссылка на что-то, или как вызов функции. Создавать связи — привычное действие для разработчиков. Однако именно задаваемое одним разработчиком количество связей определяет сложность чтения кода другим разработчиком, да и самим его автором в будущем. 3.6.2. Разница между проектированием и реализацией После того как определены проектирование и реализация, стоит подчеркнуть некоторые моменты. При реализации необходимо думать и над воплощением элемента. Получается, что это предыдущий этап, тогда зачем было выделять проектирование? Дело в том, что при проектировании приложения вы продумываете его структуру, составляющие (роли элементов) и взаимодействие с другими системами, а также глобальное состояние. Но код не пишете. А на этапе реализации вы продумываете, в чем именно заключается связь между компонентами с помощью кода (вызов функций, передача параметров), и продумываете решение для каждого компонента (элемента), чтобы он выполнял свою роль, и, главное, реализуете это в коде. При реализации вы продумываете решения для элементов и одномоментно работаете индивидуально с каждым. Реализация — последний этап воплощения конкретного элемента системы: берется нереализованный элемент и реализуется. Затем следующий, и так до конца. Это последовательный конвейер создания системы. Созданная на этапе проектирования структура становится как бы проверочным списком и дает ориентиры для реализации. По-другому такой артефакт можно назвать дорожной картой. 3.6.3. Цель реализации Цель реализации — написать код, который выполняет поставленную задачу (требование от заказчиков), делает это без ошибок и в дальнейшем позволяет легко вносить изменения (подразумеваемое требование). 3.6.4. Порядок реализации Компоненты фронтенд-приложения имеют иерархическую структуру. Поэтому реализация должна идти сверху вниз. При этом неважно, создаете вы новое приложение или осуществляете доработку. Ведя разработку сверху вниз, можно сразу проверять работоспособность: при дальнейшей доработке ваше приложение будет просто уточняться. В проектировании мы начали со страниц, но реализация — совсем другое действие. Здесь вы последовательно воплощаете задумки в коде: начинаем с родительского узла, потом создаем дочерние и связываем их.
134  Глава 3 Основы проектирования и разработки 3.6.5. На какие области обратить внимание при реализации При реализации конкретного компонента приложения нужно обращать внимание на такие вещи: y продумывание алгоритма обработки данных; y определение локального состояния, его инициализация, обновление и, возможно, долговременное хранение; y определение того, как должны быть связаны некоторые компоненты, что и как они будут передавать друг другу (передавать данные или вызывать колбэки); y как будут получены и обработаны данные со сторонних сервисов; y какой API платформы будет задействован; y что компонент будет выводить в качестве UI; y что и в каком порядке будет рендериться в DOM. 3.6.6. Взаимная зависимость проектирования и реализации Чтобы правильно проектировать, нужно знать возможности библиотек, задействуе­ мых в реализации, или используемого API. В любом случае реализация скорректирует проектирование. Впоследствии, с увеличением практики, коррекции станет меньше. Нужно отказаться от привычки программировать так, чтобы вас корректировала система (имеется в виду рантайм при запуске программы на исполнение), и прийти к тому, чтобы самостоятельно и заранее определять порядок обработки данных, а также взаимодействия компонентов. Что значит «скорректировала система»? Это происходит, когда вы пришли к выводу, что так, как планировалось, реализовать нельзя. Это не катастрофа, так происходило и происходит с каждым. Однако важно, как часто вас корректирует система, или, правильнее сказать, если вы рассчитываете на ее корректировку, то нужно однозначно изменить подход к разработке. Второй важный момент — принимаете ли вы во внимание недочеты. Подход можно изменить, если чаще проектировать. Если вы столкнулись с тем, что реализовать задуманным способом не получается, значит, нужно выяснить, что именно вы не учли, не предусмотрели, и в будущем учесть и предусмотреть это. Со временем вы научитесь проектировать и реализовывать так, как планировали. Кроме того, подобным путем выявляются паттерны.
Глава 4 СОСТОЯНИЕ 4.1. Библиотека Redux 4.1.1. Состояние приложения Состояние — одно из свойств системы во время ее действия. Нет действия — нет и состояния, которое изменяется. Благо сегодня состояние можно сохранять, а затем продолжать действовать не с нуля, а с конкретного набора данных. Чтобы в приложении работать с состоянием и сделать его более предсказуемо контролируемым, нужен специальный код, работающий с этим состоянием. Библио­ теки — именно то, что нужно, они готовы к использованию. Redux — одна из них. Авторы пишут о данной библиотеке как о позволяющей предсказуемым образом управлять состоянием. Это наводит на мысль, что существует непредсказуемый способ управления состоянием. По собственному опыту работы могу сказать, что такой способ называют, когда состоянием управляют не из единого места, а управление сосредоточено в компонентах приложения. Предсказывать изменение такого состояния сложнее, так как его разные части рассредоточены по приложению и, работая с одними из них, частенько забываешь об остальных. 4.1.2. Для чего нужна библиотека Redux Библиотека Redux нужна для управления и обновления состояния приложения, осуществляемых с использованием событий, называемых экшенами. Это может понадобиться в очень многих приложениях, например в интернет-магазинах, социальных сетях, играх и любых системах, где пользователь переключается между разделами и выполняет действия, требующие запоминания. 4.1.3. Развитие и текущее состояние Создатели библиотеки Redux постоянно ее улучшали. На то было множество причин. В первых версиях библиотеки требовалось писать много шаблонного кода, который буквально рябил перед глазами. Приходилось самостоятельно делать оптимизации, сегодня включенные в Redux разработчиками. Сообщество, которое использовало данную библиотеку, тоже внесло множество дополнений. Redux сегодня — это целый набор библиотек. Однако идеи, лежащие в ее основе, сохранены, изменяются лишь подходы к созданию составляющих.
136  Глава 4 Состояние Постоянные улучшения привели к созданию библиотеки Redux Toolkit, которую и необходимо применять сегодня, — @reduxjs/toolkit. Но прежде, чем начать это делать, следует выяснить, как работает основной компонент — библиотека Redux. Мы рассмотрим многие составляющие @reduxjs/toolkit и начнем с главного. 4.1.4. Redux Библиотека Redux предназначена для управления состоянием любого JavaScriptприложения. То есть вы можете использовать Redux как для приложения, которое работает напрямую с DOM, так и с React (для этого понадобится еще одна библиотека, React-Redux, которой мы коснемся в главе 5). Другой вариант — использовать Redux в приложении на Node.js. Но для работы с ним существуют иные библиотеки, поэтому Redux используют на клиенте. Начнем с изучения листинга 4.1, затем детально рассмотрим составляющие библиотеки, управляющей состоянием приложения. Листинг 4.1. Управление состоянием формы ввода import { createStore } from 'redux'; // 1 const initialState = ['Современная фронтенд-разработка']; // 2 const reducer = (state, action) => { switch (action.type) { case 'ADD_PHRASE': return [...state, action.payload]; default: return state; } }; // 4 // 3 const store = createStore(reducer, initialState); const [output, input, button] = ['output', 'input', 'button',].map( id => document.querySelector(`#${id}`)); // 5 const subscribeStore = () => { // 7 // 6 const state = store.getState(); const fragment = document.createDocumentFragment(); input.value = ''; output.innerHTML = ''; state.forEach((phrase) => { fragment.appendChild(document.createTextNode(phrase)) fragment.appendChild(document.createElement('br')); }); output.appendChild(fragment); }; // 9 // 8 const unsubscribe = store.subscribe(subscribeStore);
4.1. Библиотека Redux  137 button.addEventListener('click', (e) => { e.preventDefault(); const { value } = input; if (value) { // 11 // 10 store.dispatch({ type: 'ADD_PHRASE', payload: value }); } }); Цифрами в листинге помечены участки, которые являются важными составляющими библиотеки Redux. 1. Начальное состояние. 2. Функция-редьюсер. 3. Функция createStore, создающая стор. 4. Стор (хранилище). 5. Колбэк, который будет передан методу стора subscribe. 6. Метод getState для получения состояния из стора. 7. Состояние. 8. Метод subscribe для подписки на изменения состояния в сторе. 9. Функция, возвращенная методом subscribe, в случае вызова прекращающая вызывать колбэки, которые подписались на обновления состояния. 10. Экшен { type: 'CHANGE_PHRASE', payload: value }. 11. Метод dispatch для отправки экшена в стор. Это практически все, чем вы располагаете при работе с Redux. Данный код работает синхронно. 4.1.5. Порядок действий Любое веб-приложение до окончания работы проходит две стадии — инициализацию и ожидание. При инициализации браузер загружает HTML-страницу, читает ее код и, встретив тег <script>, загружает JavaScript-код, а затем по мере движения по коду файла выполняет его. Далее он переходит в режим1 ожидания ввода от пользователя. Если еще раз просмотреть код листинга 4.1, то вы увидите код, инициализирующий наше мини-приложение, и код, который будет выполняться во время ожидания ввода от пользователя. Лучше понять работу листинга и наглядно представить себе порядок вызовов нам поможет схематичное изображение. На рис. 4.1 цифры в кружочках соответствуют цифрам в листинге 4.1. Это составляющие, необходимые для работы с состоянием. На данном рисунке, кроме того, есть цифры, не взятые в кружочки и имеющие наклон. Это шаги или действия, которые совершаются для достижения необходимого 1 Обычно под словом «режим» подразумевают условия работы чего-то. В нашем случае — приложения, которое сначала инициализируется, а затем начинает работать. Вы часто слышите, например, что кофемашина находится в режиме ожидания.
138  Глава 4 Состояние результата. Рисунок поделен на две части. Первая показывает, какие действия и какими составляющими осуществляет код листинга во время загрузки приложения. Вторая — какие действия и какими составляющими код листинга осуществляет при изменении состояния. Рис. 4.1. Схематичное изображение отношений между составляющими Из библиотеки Redux мы задействовали всего одну функцию — createStore, которая создает стор с методами, остальное должны подготовить самостоятельно. Начнем с инициализации. Задается начальное состояние (1), далее — функция, называемая «редьюсер» (2), которая подразумевает два параметра: текущее состояние и экшен. Потом вызывается функция createStore (3), которая возвращает стор (4). Стор содержит несколько методов: getState() (6), dispatch(action) (11) и subscribe() (8). Далее мы их задействуем в коде. Затем объявляется колбэк (5), который должен вызываться при изменении состояния (7) стора (4). Для получения состояния внутри колбэка нужно явно вызывать метод получения состояния (7) getState() (6). Потом вызывается метод стора subscribe() (8), которому передается колбэк (5), и метод возвращает ссылку на метод unsubscribe() (9). На кнопку button назначается обработчик события «Клик», ее нажатие вызывает метод стора dipatch(action) (11), и ему передается объект, в библиотеке Redux называемый экшеном (10). Действия для момента работы приложения показаны на второй части изображения. Пользователь вбивает в поле ввода текст, который желает сохранить в состоянии. Затем он нажимает кнопку button. Если поле не является пустым, то методу dispatch(action) (11) передается экшен (10). На рисунке показано, что метод dispatch(action) (11) передает данные редьюсеру (2), но это небольшое лукавство, созданное для упрощения понимания. На самом деле шаги 2 и 3 осуществляет библиотека. То есть после вызова метода dispatch(action) (11) вызывается редьюсер, которому передается текущее состояние (7) и экшен (10) — первым и вторым аргументами соответственно. Редьюсер вычисляет новое состояние (7) и возвращает его стору (4). Далее, поскольку был подписан колбэк (5) на изменения, он вызывается. Таков основной порядок взаимодействия составляющих для работы библиотеки Redux.
4.2. Стор  139 4.2. Стор 4.2.1. Главный компонент библиотеки Слово «стор» происходит от английского store и переводится как «хранилище». Среди разработчиков прижился английский термин. Стор хранит состояние. Кроме того, у него есть методы, которых мы коснулись в разделе 17, а здесь рассмотрим подробнее. 4.2.2. Ключевые принципы стора Стор Redux основан на трех ключевых принципах. y Единый источник истинности. В Redux все данные приложения хранятся в одном глобальном объекте, известном как стор. Это значит, что все состоя­ ние приложения доступно в одном месте, что упрощает его отслеживание и управление им. y Неизменяемость. Состояние в сторе Redux является неизменяемым, то есть оно не может быть изменено напрямую. Вместо этого любое изменение состояния приводит к созданию нового состояния. Это облегчает отладку и предотвращает неявные изменения состояния, что приводит к предсказуе­ мому поведению приложения. y Изменение состояния происходит только с помощью чистых функций. Для изменения состояния в сторе Redux используется «чистая функция» (редьюсер), что обеспечивает предсказуемость и безопасность. 4.2.3. Работа с библиотекой в приложении Использование библиотеки Redux начинается с создания стора. Как это происходит, показано в листинге 4.2. Листинг 4.2. Пример инициализации стора const initialState = {list: ['Современная фронтенд-разработка']}; const reducer = (state, action) => { const { type, payload } = action; if (type === 'CHANGE_PHRASE') { return { list: [...state.list, payload] } } } return state; const { getState, subscribe, dispatch, } = createStore(reducer, initialState);
140  Глава 4 Состояние const logState = (str) => console.log('state', getState(), str); logState(`До вызова subscribe ${Date.now()}`); const unsubscribe = subscribe(() => logState(`Вызов колбэка ${Date.now()}`)); setTimeout(() => { dispatch({ type: 'CHANGE_PHRASE', payload: 'Первое обновление стора' }); unsubscribe(); }, 3000); setTimeout(() => { dispatch({ type: 'CHANGE_PHRASE', payload: 'Второе обновление стора' }); logState(`После вызова unsubscribe ${Date.now()}`); }, 4000); 4.2.4. Методы стора Функция createStore возвращает стор, в котором есть несколько основных методов — getState, subscribe, dispatch. Вы уже видели, как они работают, в двух листингах. Это происходит синхронно. Метод getState, вызванный в любом месте (разумеется, если вызов нормальный), всегда возвращает текущее состояние. Он вызывается без параметров. Как только произошло обновление стора вызовом метода dispatch, можно сразу получать новое состояние. Метод dispatch активирует обновление стора. Он принимает один аргумент — экшен. Метод subscribe позволяет зарегистрировать колбэки, которые будут вызваны после того, как в сторе обновится состояние, все тем же способом — вызовом метода dispatch с экшеном в аргументе. Метод принимает или объект функции, или ссылку на нее. В листинге 4.1 аргументом передана ссылка, а в листинге 4.2 — объект анонимной функции. Данный метод возвращает функцию, которую обычно именуют unsubscribe. Поскольку несколько функций можно зарегистрировать вызовом для каждой из них метода subscribe, возвращено тоже будет несколько функций unsubscribe, чьи вызовы прекращают дальнейший вызов ранее зарегистрированных функций. Если посмотреть на листинги 4.1 и 4.2, то вы увидите, что четыре названных метода используются повсеместно. Хотя unsubscribe — меньше всего. 4.2.5. Некоторые доводы в пользу библиотеки Когда впервые сталкиваешься с библиотекой Redux, может возникнуть пара закономерных вопросов. y Зачем использовать библиотеку, если можно сохранять любые данные в объекте? В библиотеке Redux реализован подход «подписчик — издатель»: если у объекта, играющего роль издателя, изменяются данные, то он вызывает функции, которые подписались на такое изменение, и передает им обновленные данные. В простом объекте сохранять и изменять значение свойств можно как угодно,
4.3. Редьюсер  141 но рано или поздно придется прийти к другому способу, чтобы предсказуемо контролировать изменение значений этих свойств. y Если в приложении уже применяется библиотека, зачем такие сложности с отправкой экшенов, когда можно взять состояние и поменять на любое, какое захочешь? Представьте себе, что у вас есть две функции, одна из которых создает через API браузера кнопки button, а другая — элементы div. Например, состояние хранится в функции, создающей div. Как вы организуете передачу состояния для кнопки? А если нужно, чтобы еще одна функция получала эти же данные? Используя подход с экшенами, вы сможете обмениваться данными со стором, а другие компоненты будут получать новые данные при его обновлении. 4.3. Редьюсер 4.3.1. Что такое редьюсер Работа с редьюсером без точного понимания, что вы должны создать, приведет к тому, что вы будете частенько ошибаться, разрабатывать пошагово с самопроверкой и кучей выводов в консоль или предпочтете, что называется, запомненный образец кодирования1. Самое простое определение редьюсера — функция, которая принимает какойлибо уже вычисленный результат и какие-либо данные (это может быть и структура в виде объекта), на их основе вычисляет новый результат и возвращает его. Такую функцию можно передать в метод массива reduce() как колбэк, в качестве элементов массива будут выступать данные, а если в качестве начального значения, кроме того, задать вычисленный результат, то на выходе вы получите новый вычисленный результат. В листинге 4.3 наглядно показан итог такого действия. Листинг 4.3. Пример редьюсера const initialState = {list: ['Современная фронтенд-разработка']}; const action_1 = {type: 'ADD_PHRASE', payload: 'Вышла на новый уровень'}; const action_2 = {type: 'ADD_PHRASE', payload: 'И мы многое изучим'}; const action_3 = {type: 'ADD_PHRASE', payload: 'И познакомимся с нейросетями'}; // Обратите внимание: это редьюсер из листинга 4.2 const reducer = (state, action) => { const { type, payload } = action; 1 Запомненный образец кодирования — придуманное описание модели разработки, когда разработчик запомнил, как должен выглядеть работающий код, но не понимает некоторых моментов в нем, не может его изменять по собственному желанию и поэтому предпочитает оставлять его всегда таким, каким он был при знакомстве. Такой подход ведет к тому, что частенько применяется «копипаст» готовых и чужих решений. На самом деле это простое непонимание, которое легко исправить путем взаимодействия с областью, в которой плохо разбираешься. Слово «взаимодействие» важно, потому что, когда у разработчика есть непонятная область, он старается ее избегать. Ключ к пониманию — взаимодействие. Ключ к непониманию — избегание взаимодействия.
142  Глава 4 Состояние if (type === 'ADD_PHRASE') { return { list: [...state.list, payload] } } } return state; const newState = [action_1, action_2, action_3].reduce(reducer, initialState); console.log(newState); Работу этого листинга можно опробовать прямо в консоли. Например, редьюсер может использоваться для суммирования элементов списка, поиска максимального значения или преобразования массива в новую форму. Одна из ключевых особенностей редьюсеров — их независимость от изменяемого состояния. Они служат для создания нового состояния на основе предыдущего, без прямого изменения последнего. Это делает редьюсеры особенно полезными в функциональном программировании, где неизменяемость состояния является важным принципом. 4.3.2. Чистая функция Редьюсер должен быть чистой функцией, чтобы результат получился предсказуемым. Чистой функцией в функциональном программировании называется функция, которая вычисляет возвращаемое значение только на основе входных данных. При этом она не изменяет сами входные данные. Кроме того, такая функция не изменяет никакие переменные в области видимости и не дает побочных эффектов. Ее важной особенностью является свойство идемпотентности. Идемпотентность — свойство функции, которая при любом количестве вызовов, при передаче одних и тех же аргументов всегда возвращает один и тот же результат. Побочным эффектом называется изменение значений глобальных переменных или состояний вычислительной среды. В листинге 4.4 даны три примера побочных эффектов. Листинг 4.4. Примеры побочных эффектов let globalVar = 'Современная фронтенд-разработка'; const fnSideEffect1 = (sentence) => { return globalVar += sentence; }; const fnSideEffect2 = () => Date.now(); const fnSideEffect3 = (sentence) => { const stringified = JSON.stringify(sentence);
4.3. Редьюсер  143 }; localStorage.setItem('example', stringified); console.log(globalVar); fnSideEffect1(' с библиотеками'); console.log(globalVar); console.log(fnSideEffect2()); fnSideEffect3('Сохраним в локал сторэдж'); console.log(fnSideEffect2()); console.log(localStorage.getItem('example')); Функция fnSideEffect1 изменяет глобальную переменную globalVar. Функция fnSideEffect2 все время создает новое значение, поэтому она не является чистой. А функция fnSideEffect3 изменяет состояние среды вычисления. localStorage — хранилище в браузере, в нем появляется запись. Функция-редьюсер в листинге 4.3 является чистой. В разделе 4.2 приведены три принципа, на основе которых работают с библиотекой. Выше был продемонстрирован один из них: «изменение состояния происходит только с помощью чистых функций». 4.3.3. Неизменяемость Второй принцип гласит, что нельзя изменять состояние напрямую. Почему так, демонстрируют листинг 4.5 и рис. 4.2. Листинг 4.5. Изменение состояния напрямую const initialState = {list: ['Современная фронтенд-разработка']}; const reducer = (state, action) => { const { type, payload } = action; if (type === 'ADD_PHRASE') { return { list: [...state.list, payload] } } } return state; const { getState, subscribe, dispatch, } = createStore(reducer, initialState); const state = getState(); const logState = (str) => console.log('state', getState(), str); const unsubscribe = subscribe(() => logState(`через dispatch`));
144  Глава 4 Состояние dispatch({type: 'ADD_PHRASE', payload: 'Библиотеки все еще актуальны'}); state.list.push('Изменим напрямую'); dispatch({type: 'ADD_PHRASE', payload: 'Добавляем еще фразу'}); console.log('state', state); Рис. 4.2. Вывод состояний в консоль кодом из листинга 4.5 Как видно из листинга, изменение напрямую производится между первым и вторым вызовами метода dispatch. Однако при выводе значения состояния, которое изменено напрямую, его нет в состоянии, которое обновляется с помощью метода dispatch. Более того, при изменении напрямую функция logState не вызывается. Это означает, что если изменять состояние напрямую, то код вашего приложения об этом не узнает, так как не будет вызван колбэк, регистрируемый с помощью метода subscribe. 4.3.4. Какую структуру стора выбрать Состояние всегда содержит большое количество разных свойств. Стор в листинге 4.5 содержит объект с одним свойством, и кажется, что если нам требуется хранить больше свойств, то нужно просто добавить еще несколько свойств объекта и управлять этим состоянием. Так действительно можно поступать для управления состоянием. В этом случае у вас должен быть один редьюсер, обрабатывающий все экшены, которые изменяют свойства стора. В репозитории кода к книге, в примере 4.6 «Упрощенный вариант добавления книг в корзину», дан пример такого редьюсера. В листинге 4.6 приводится его часть. Видно, что изначальное состояние содержит большое количество свойств. Листинг 4.6. Состояние и один редьюсер, управляющий всем состоянием const initialState = { language: 'ru', congratulation: { ru: 'Приветствуем тебя',}, shoppingCart: [], notifications: [{ id: 1, message: 'Скидка 30 % на электронику', isRead: false },],
4.3. Редьюсер  145 isLoggedIn: true, user: {/* данные пользователя */}, books: [/* данные о книгах */] }; const rootReducer = (state, action) => { console.log('state', state); // реализация редьюсера } const store = createStore(rootReducer, initialState); Есть всего один редьюсер, который и работает с этим состоянием. Код примера написан таким образом, что, когда экшен отправляется и обрабатывается редьюсером, в консоль выводится состояние до изменения (вывод console.log в листинге) (рис. 4.3). Рис. 4.3. Вывод всего состояния в консоль, передаваемый в один редьюсер Есть у этого подхода и один минус — изначальное состояние нужно передавать единым объектом. Конечно, можно передать пустой объект, и тогда по мере отправки экшенов в сторе будут появляться новые свойства. Однако стор можно организовать иначе. Стор разбивают на множество отдельных областей, по-другому называемых срезами. В коде это обычный объект, а область — все то, что является значением какого-то свойства. Для каждой такой области создают свой редьюсер, чтобы он управлял исключительно ею. Стор можно представить в виде дерева. Более того, сами создатели так и говорят: дерево состояний. На рис. 4.4 вы видите граф, в котором опущена корневая вершина и указаны только области стора в виде узлов, и если есть, то и дочерних узлов. Для каждой области предусмотрен свой редьюсер. В примере 4.7, в репозитории кода к книге, показано, как каждой областью стора управляет свой редьюсер. Часть этого примера приведена в листинге 4.7. Часть — потому что код примера большой, а приводить его весь не имеет смысла, так как вы можете посмотреть его самостоятельно.
146  Глава 4 Состояние Рис. 4.4. Области стора и обрабатывающие их редьюсеры Листинг 4.7. Состояние, которым управляет несколько редьюсеров import { createStore, combineReducers } from 'redux'; const languageReducer = (state = 'ru', action) => { if (action.type === CHANGE_LANGUAGE) { return action.payload; } return state; }; /** другие редьюсеры */ const rootReducer = combineReducers({ language: languageReducer, congratulation: congratulationReducer, shoppingCart: shoppingCartReducer, notifications: notificationsReducer, isLoggedIn: isLoggedInReducer, books: booksReducer, user: userReducer, }); const store = createStore(rootReducer); /** другой код */ Первое, на что нужно обратить внимание, — в редьюсер передается состояние касаемо исключительно языка страницы, а не все целиком. И посмотрите код редьюсера: он ориентирован на работу только с данной областью — ключом language. Остальные редьюсеры, имена которых указаны на рис. 4.4, действуют точно так же:
4.4. Экшен. Селектор  147 они получают состояние для своей области, а реализация направлена исключительно на обработку экшенов с целью ее изменения. Второе, на что следует обратить внимание, — способ объединения редьюсеров и создания стора. В листинге 4.7 использована функция combineReducers, импортированная из библиотеки Redux. Она вспомогательная — превращает объект, чьи свойства являются редьюсерами для конкретных областей состояния, в единый редьюсер, который можно передать в createStore. Впоследствии он будет работать так. Когда ваш код передаст в стор экшен, этот единый редьюсер вызовет каждый редьюсер из объекта, переданного аргументом (см. аргумент функции combineReducer в листинге 4.7) для этого экшена. Каждый редьюсер вернет новое состояние, и в конце общий редьюсер вернет новое дерево состояний (объект со свойствами, переданными аргументом), где некоторые переменные, в зависимости от экшена, имеют новое значение. Редьюсер, созданный функцией combineReducer, называется корневым (root reducer). Корневой редьюсер — это функция, которой передается все дерево состояний. 4.3.5. Начальное состояние Где присваивать начальное состояние? Его можно задавать двумя способами. Первый — то, как ранее было показано в функции createStore. Второй — аргументом. Но когда корневой редьюсер разделен на несколько, состояние передается в каждый редьюсер и исключительно в ту область, за которую он ответственен. Взгляните на примеры 4.6 и 4.7. Как вы будете передавать, разницы нет. Все зависит от вас. 4.3.6. Наименование редьюсера Вы, наверное, заметили, что редьюсеры, несмотря на то что являются функциями, именуются, начиная с существительного. Легко понять, что booksReducer относится к разделу стора books. На одну область стора приходится один редьюсер, при этом он обрабатывает для нее все типы экшенов. С другой стороны, нет ничего ненормального в том, что если в сторе для областей нужны, допустим, только операции CRUD, то редьюсер будет создаваться по типу операции, например, createEntityReducer, который по типу экшена формирует новую сущность для конкретной области. Правда, в этом случае стор должен быть единым объектом, иначе придется написать много ненужного кода. 4.4. Экшен. Селектор 4.4.1. Что такое экшен Русский термин происходит от английского action, то есть «действие». В России прижилось слово «экшен», и, общаясь, разработчики используют именно его. Смысл экшена в том, чтобы указать, какое состояние изменилось, и при необходимости — что
148  Глава 4 Состояние именно подверглось изменениям. Экшен — простой объект. По общепринятому соглашению он состоит из двух свойств, как показано в листинге 4.8. Листинг 4.8. Экшены const action_increment = {type: 'INCREMENT'}; const action_add = {type: 'ADD_PHRASE', payload: 'Новое понимание'}; const action_add_user = {type: 'user/add', payload: { name: 'Антон', email: 'info@designfrontend.com'}}; const action_counter_increment = {type: 'counter/increment'}; Несмотря на общепринятое соглашение, вы можете снабжать экшены другими свойствами, помня, что type — обязательное, payload — нет. Можно вовсе не придерживаться данного соглашения, хотя надо заботиться о других разработчиках, которые уже знакомы с библиотекой. 4.4.2. Наименование экшена Свойство type можно рассматривать как имя произошедшего события. И действительно, в коде приложения метод dispatch с переданным ему экшеном в качестве аргумента всегда вызывается в ответ на что-то. А как вы знаете, вторая стадия работы приложения — ожидание ввода от пользователя — осуществляется исключительно на событиях, производимых пользователем либо платформой, то есть браузером. Например, событие вызова колбэка функцией setTimeout или ответ на запрос, пришедший с сервера. А пользователь в приложении взаимодействует с кнопками или элементами, на которых зарегистрированы обработчики событий, или с формами. В большинстве случаев подобное взаимодействие ведет к тому, что изменяется состояние приложения. Неважно, используете вы библиотеку для работы с состоянием или нет. Ваше приложение всегда должно реагировать на действия пользователя, иначе зачем оно. Создатели библиотеки Redux предлагают мыслить об экшенах как об объектах, описывающих такие события. Разумеется, если их результаты вы должны сохранять в состоянии. В листинге 4.8 приведены имена экшенов. Первые два написаны прописными, два других — строчными буквами, через слеш. Как именно их называть, зависит от ваших предпочтений и от количества экшенов в приложении. Что именно влияет на то, сколько их будет? Количество элементов, с которыми производятся действия. Например, ваш интернет-магазин однозначно работает с товарами и корзиной. Тогда в вашей корзине будут экшены cart/add, cart/delete. Или, например, вы обрабатываете аутентификацию пользователя — user/login, user/logout. Другой вид приложения, где может потребоваться имя экшена из двух слов, — чат. Например, message/send, message/answer, user/setUserId, user/statusOnline. Короткие имена требуются там, где хранится небольшое количество состояний. Допустим, вы управляете только состоянием формы заказа — тогда у вас могут быть такие имена, как changed, submit. Не обязательно писать их прописными буквами. Здесь так сделано для наглядности.
4.4. Экшен. Селектор  149 4.4.3. Action creators Для создания объекта экшена, как правило, прибегают к помощи функций, называемых action creators. В листинге 4.9 приведены их примеры. Листинг 4.9. Примеры action creators export const ADD_PHRASE = 'ADD_PHRASE'; export const CREATE_USER = 'CREATE_USER'; export const createAddPhrase = (phrase) => ({ type: ADD_PHRASE, payload: phrase, }); export const createUser = (user) => ({ type: CREATE_USER, payload: user, }); При взгляде на creators становится понятно, что они однотипные. Иногда все действительно так, но могут быть и различия. Представленные в листинге 4.9 наиболее вероятны, поэтому их создание можно автоматизировать, как показано в листинге 4.10. Листинг 4.10. Автоматизация создания action creators export const ADD_PHRASE = 'ADD_PHRASE'; export const CREATE_USER = 'CREATE_USER'; export const [createAddPhrase, createUser] = [ ADD_PHRASE, CREATE_USER ].map((type) => (payload) => ({ type, payload })); Чтобы отличить action creators от других функций, их надо именовать с ключевого слова create. Кодом листинга 4.10 можно создать все однотипные crea­tors. Впоследствии вам нужно лишь вызвать функцию и передать полезную нагрузку — функция вернет экшен. Это можно увидеть в листинге 4.11. Листинг 4.11. Использование action creators export const ADD_PHRASE = 'ADD_PHRASE'; export const [createAddPhrase] = [ADD_PHRASE] .map((type) => (payload) => ({ type, payload })); button.addEventListener('click', (e) => { e.preventDefault(); const { value } = input; if (value) { store.dispatch(createAddPhrase(value)); } });
150  Глава 4 Состояние 4.4.4. Хранение наименований типов экшенов в виде констант При использовании библиотеки Redux наименование типа экшена используется в двух местах: первое — в action creator либо в самом экшене, второе — в редьюсере. По этой причине, чтобы не дублировать наименования типов экшенов, а задаются они строкой, их помещают в отдельный модуль (то есть в файл), откуда импортируют в редьюсеры и action creators. 4.4.5. Что такое селектор Термин происходит от английского selector — то, что осуществляет действие, указанное глаголом select, который переводится как «выбирать», «выделять», «отмечать». Суффикс or создает отглагольное существительное. Данное значение сродни значению слова «селектор» в CSS. Селектор в Redux — функция. 4.4.6. Зачем нужен селектор Стор может включать в себя много свойств. В сообществе, использующем библио­ теку Redux, про стор обычно говорят как о дереве состояния. Один из примеров приведен в листинге 4.12. Листинг 4.12. Пример стора const state = { todos: [{ id: 'e4d41573', title: 'Сделать упражнение из книги "Современная фронтенд-разработка"', done: true, }, { id: '9fbb87aa', title: 'Изучить раздел о графах', done: true, }], tasks: { immediate: [{ id: 'a849466e', title: 'Сделать вид, что работаю', done: false, }], priority: [{ id: 'd78042ec', title: 'Продумать состав приложения', done: false, }], }, selectDiapason: { from: { day: 17, month: 6, year: 2024, }, to: { day: 25, month: 6, year: 2024, } } };
4.4. Экшен. Селектор  151 Селектор применяется для извлечения состояния из стора, из конкретной ветки дерева состояния. Для такого дерева, как в листинге 4.12, может быть создано несколько селекторов (листинг 4.13). Листинг 4.13. Селекторы const selectAllTodos = (state) => state.todos; const selectAllTasks = (state) => state.tasks; const selectTodoById = (state, id) => state.todos.find((todo) => todo.id === id); const selectTasksById = (state, id) => state.tasks.find((task) => task.id === id); const selectSelectDiapason = (state) => state.selectDiapason; Чтобы селектор можно было отличить от других типов функций, его имя начинается со слова select. Как и в случае с action creators, здесь необходима оптимизация. Обычно так и делается, чтобы не плодить одинаковые по строению функции (листинги 4.14, 4.15). Листинг 4.14. Генерация селекторов // селекторы, выбирающие все элементы const [selectAllTodos, selectAllTasks, selectSelectDiapason] = [ 'todos', 'tasks', 'selectDiapason' ].map((branch) => (state) => state[branch]); // селекторы, выбирающие элементы по id const [selectTodoById, selectTasksById] = [ 'todos', 'tasks' ].map((branch) => (state, id) => state[branch].find((item) => item.id === id)); Листинг 4.15. Использование селектора в мини-приложении const const const const initialState = {list: ['Современная фронтенд-разработка']}; ADD_PHRASE = 'ADD_PHRASE'; createAddPhrase = (phrase) => ({ type: ADD_PHRASE, payload: phrase }); selectAllPhrases = (state) => state.list; const reducer = (state, action) => { const { type, payload } = action; if (type === ADD_PHRASE) { return { list: [...state.list, payload] } } } return state; const { getState, subscribe, dispatch, } = createStore(reducer, initialState);
152  Глава 4 Состояние const logState = (str) => console.log('state', getState(), str); subscribe(() => logState(`через dispatch`)); dispatch(createAddPhrase('Библиотеки все еще актуальны')); dispatch(createAddPhrase('Добавляем еще фразу')); console.log(selectAllPhrases(getState())); Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 4.4. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 4.4. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 4.4 и ознакомьтесь с моим решением. 4.5. Middleware. Enhancer 4.5.1. Что такое middleware Английское слово middleware переводится на русский как «промежуточное программное обеспечение» или «связующее программное обеспечение», а также «проме­ жуточный слой». Произносится «мидлвэе» ([ˈmɪddlweə]). Иногда употребляют слово «мидлвара», но это скорее разговорный вариант. Применительно к библиотеке Redux middleware означает код, используемый для обработки промежуточных действий во время обновления состояния приложения. Использование middleware позволяет задать нужную функциональность между моментом, когда вызывается метод dispatch с экшеном, и моментом, когда экшен достигнет редьюсера. Технически middleware — функция высшего порядка1, которая составляет новую функцию dispatch с дополнительной функциональностью и возвращает такую функцию. Полезность в том, что вы можете добавлять или убирать middleware без необходимости менять код каких-либо функций — просто добавляете вызовы нужных middleware. Получается, что данная функция расширяет возможности без написания дополнительного кода реализации самой добавляемой функциональности. Вам нужно лишь добавить готовые middleware. И разумеется, если функция не существует, ее надо написать. Примером функциональности middleware может быть логирование, как локальное в консоль при разработке, так и на сервер; добавление заголовков запросов к серверу; установка таймера при отправке конкретного экшена в стор; добавление 1 Функция высшего порядка — функция, которая может иметь аргументы, являющиеся функциями, и обязательно возвращает функцию. Таким образом, уже имеющиеся функции обогащаются функциональностью.
4.5. Middleware. Enhancer  153 асинхронной обработки. Таким образом, middleware позволяет обогатить функцио­ нальность работы с состоянием некоторым предусмотренным способом. Middleware не обязательно должны быть созданы вами. Их, как и любой другой npm-пакет, может создавать сторонний разработчик, а вы устанавливаете в приложение. В листинге 4.16 приведен пример «пустой» middleware, однако показывающей, что доступно в теле ее функции. Полный пример можно посмотреть в репозитории кода к книге. Листинг 4.16. Пустая middleware с выводом значений получаемых аргументов в консоль const consollerMiddleware = store => next => action => { console.log('store', store); console.log('next', next); console.log('action', action); return next(action); }; const store = createStore(rootReducer, 0, applyMiddleware(consollerMiddleware)); Все middleware, которые вы хотите добавить, должны быть добавлены аргументами в функцию applyMiddleware, как ссылки на функции. Функция applyMiddleware импортируется из библиотеки Redux (см. полный листинг 4.16). Для большей наглядности код middleware из листинга 4.16 можно представить с помощью инструкций function, как показано в листинге 4.17. Листинг 4.17. Middleware, переписанная с помощью инструкции function function consollerMiddleware (store) { console.log('store', store); return function (next) { console.log('next',next); return function (action) { console.log('action', action); return next(action); }; }; }; Middleware consoller является функцией, чей параметр store при вызове получает аргументом объект, в котором доступно два метода из стора — dispatch и getState. То есть не полный набор методов стора, а только часть. Middleware возвращает функцию с параметром next (в листинге). При вызове она в качестве аргумента получает функцию. Далее эта функция должна быть вызвана с экшеном, чтобы продолжить цепочку выполнения других middleware. Когда их нет
154  Глава 4 Состояние в цепочке, надо, чтобы обновилось состояние в сторе. Если данный метод не будет вызван, то обновления состояния не произойдет. Функция next действует подобно методу стора dispatch, но у них есть отличие, описанное далее в этом разделе. В свою очередь, данная функция тоже возвращает функцию с параметром action (в листинге). При вызове этот параметр, как следует из его имени, будет заменен экшеном, отправленным в стор в данный момент. 4.5.2. Разница между next и store.dispatch Метод next вызывает следующий middleware в цепочке и в конечном счете функцию dispatch, которая отправляет экшен в редьюсер. А метод стора store.dispatch, вызванный внутри middleware, запускает цепочку вызова всех middleware заново, как если бы ваш код вне них вызвал метод dispatch и передал ему экшен с помощью аргумента. Поэтому вся цепочка middleware, включая ту, из которой вызван store.dispatch, будет вызвана повторно. Поэтому нужно ограничивать условием вызов метода store.dispatch внутри middleware, а вызов функции next — нет. Если не ограничить вызов sotre.dispatch условной инструкцией, то вызов цепочек middleware зациклится и появится ошибка RangeError: Maximum call stack size exceeded. Стоит заметить, что ограничивать условием нужно в третьей функции, которая аргументом получает экшен (см. листинг 4.17). 4.5.3. Как использовать middleware для асинхронного кода Наиболее частым применением асинхронного кода является запрос к серверу (функция fetch браузера) или использование таймеров (функция setTimeout браузера) либо интервалов (функция setInterval браузера). Работа с промисами возникает редко. Примеры с запросами к серверу будут рассмотрены в разделе 4.9. В текущем разделе рассмотрим таймер. Вся работа библиотеки Redux является синхронной. Редьюсеры не должны содержать побочных эффектов. Однако без них мы можем создавать только вычисления. Чтобы получить данные с сервера, необходимо вызвать функцию, создающую запрос, или, если нужно, организовать задержку и вызвать такую функцию с помощью таймера. В результате появится побочный эффект. Middleware — то самое место, где можно объединять и Redux, и код с побочными эффектами или асинхронной обработкой данных. К текущему моменту у нас есть все необходимые знания, чтобы использовать middleware для работы с асинхронным кодом. Главный прием заключается в следующем. 1. Создать middleware для работы с асинхронным кодом, задействуя шаблон из трех функций. 2. Условный шаг. Если вы намереваетесь использовать метод store.dispatch, то нужно задать условие, которое выполняется в одном случае и не выполняется в другом. Это условие должно находиться в функции, которая получает экшен в качестве аргумента (см. листинг 4.17).
4.5. Middleware. Enhancer  155 3. Написать код вызова асинхронной функции, а в колбэке, вызывающемся по окончании выполнения асинхронного кода, сделать вызов метода: 1) если нужно вызвать все middleware повторно — вызвать store.dispatch и передать ему экшен, который должен быть отправлен в стор и не должен соответствовать условию в пункте 2; 2) если нужно продолжить с места, где была вызвана асинхронная функция — вызвать next(action). 4. Задать вызов функции next и передать ей аргументом текущий экшен (если есть необходимость в выполнении дальнейших middleware) либо ограничить вызов этой функции условиями (если обновлять стор в текущий момент не требуется). Рассмотрим пример с применением таймера, а затем перепишем его с использованием промисов (листинг 4.18). Листинг 4.18. Асинхронная задержка экшена с типом INCREMENT const [increment, decrement] = actionTypeList.map(type => () => ({ type, meta: {delay: 1000} })); const delayedActionMiddleware = store => next => action => { if (action.type === INCREMENT && action.meta && action.meta.delay) { setTimeout(() => { next(action) }, action.meta.delay); return } } return next(action) const store = createStore( rootReducer, 0, applyMiddleware(delayedActionMiddleware)); Action creators increment, decrement возвращают объект с двумя свойствами — type и meta. В meta есть свойство delay со значением 1000. Это значение задержки для таймера в middleware. Рассмотрим созданный middleware. 1. Задан middleware из трех функций. 2. Задано условие, которое будет истинным при каждой отправке экшена в стор. По этой причине используется метод next(action), а не store.dispatch. 3. Написан код вызова асинхронной функции setTimeout . Когда условие верно (оно верно всегда) и когда экшен имеет тип INCREMENT, запускается setTimeout, производится возврат из middleware значения undefined и не вызывается функция next. Это приводит к тому, что состояние не обновляется. Но через установленный интервал времени происходит вызов функции next(action) и производится обновление состояния. 4. Задан вызов функции return next(action) — на случай, если тип экшена, например, DECREMENT.
156  Глава 4 Состояние Важно также помнить: чтобы производилось обновление интерфейса, необходимо осуществлять подписку на обновления стора, как показано в листинге 4.19. Листинг 4.19. Подписка на обновление состояния стора const changeCounter = state => counter.value = state; store.subscribe(() => { const state = store.getState(); changeCounter(state) }); Любая асинхронная middleware или middleware с побочным эффектом подчиняется схеме, показанной на рис. 4.5. Рис. 4.5. Схема асинхронной или middleware с побочным эффектом 4.5.4. Enhancer Слово еnhancer можно перевести с английского как «усилитель», «расширитель» или «увеличитель». Все эти понятия подходят по смыслу функциональности, которой они обладают. Произносится «инхенсо» ([ɪnˈhɑːnsə]), но в нашем лексиконе предлагаю использовать термин «энхансер» (английские слова русскоязычные люди обычно произносят как бы по-немецки). Энхансер легко спутать с middleware. Мы сначала разберемся с энхансером, а затем поймем разницу между двумя этими понятиями. Технически энхансер — функция высшего порядка, которая объединяет создатель стора и возвращает новый создатель стора. Поскольку стор является не экземпляром, а обычным объектом, чьи свойства — функции, копии могут быть созданы и изменены без изменения изначального стора. Основная разница между энхансером и middleware заключается в их функциональности и месте применения в Redux. Middleware — слой промежуточного программного обеспечения, который обрабатывает экшены перед их достижением редьюсера. Он может быть использован, например, для аутентификации, записи логов или вызова асинхронных операций.
4.5. Middleware. Enhancer  157 Однако middleware не может изменять стор Redux, только экшены перед их обработкой. Энхансеры изменяют структуру стора Redux. Они могут добавлять новые функции или декорировать существующие, чтобы улучшить функциональность. Энхансеры используются, например, для расширения DevTools, создания persist-стора или изменения процесса создания стора. Функция applyMiddleware — энхансер, так как она добавляет дополнительную функциональность в стор, несмотря на то что само действие выполняет middleware. По сути, этот энхансер добавляет новую функциональность с помощью middleware, являясь точкой подключения. Как правило, энхансеры не приходится писать самому. Один из них мы рассмотрим в разделе 4.6 далее, когда будем подключать инструменты в проекте. Энхансер changesTrackerEnhancer показывает, как переопределить методы (листинг 4.20), расположенные в сторе. Саму функциональность, отслеживание предыдущего состояния можно реализовать с помощью middleware, однако энхансер не middleware, и листинг это показывает. Листинг 4.20. Энхансер changesTrackerEnhancer const changesTrackerEnhancer = (createStore) => (reducer, initialState) => { const store = createStore(reducer, initialState); let trackedState = initialState; const listeners = []; const dispatch = (action) => { store.dispatch(action); listeners.forEach((listener) => listener(store.getState(), trackedState)); trackedState = store.getState(); }; const subscribe = (listener) => { listeners.push(listener); return () => { const index = listeners.indexOf(listener); listeners.splice(index, 1); }; }; return { ...store, dispatch, subscribe, }; }; const customEnhancers = compose(applyMiddleware(delayedActionMiddleware), changesTrackerEnhancer); const store = createStore(counterReducer, 0, customEnhancers);
158  Глава 4 Состояние Полный код находится в репозитории. Для объединения двух энхансеров применяется функция compose из библиотеки Redux. Как прием она перенята из области функционального программирования, и на выходе мы имеем одну функцию, включающую в себя все функции, переданные ей аргументами. В итоге получается единый энхансер стора. 4.5.5. Как создать свой энхансер Энхансер создается в несколько шагов. Как и у middleware, у него есть шаблон. Он возвращает стор, а, как известно, стор — объект с методами dispatch, getState, replaceReducer, subscribe и др. В сторе также есть функция с именем @@observable, но как ею пользоваться и зачем она нужна, разработчики Redux ровным счетом ничего не сообщают. Судя по имени, она для внутреннего использования. 1. Объявить функцию, аргументом которой является функция createStore из библиотеки Redux. 2. Эта функция должна вернуть другую функцию, аргументами которой являются редьюсер и начально задаваемое состояние. 3. В теле возвращаемой функции реализовать необходимые методы: dispatch, getState, replaceReducer или subscribe. 4. Вернуть новый стор из возвращаемой функции. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 4.5. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 4.5. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 4.5 и ознакомьтесь с моим решением. 4.6. Redux Toolkit 4.6.1. Что представляет собой библиотека Как было сказано ранее, сегодня для работы с состоянием в приложении необходимо использовать библиотеку Redux Toolkit. Она представляет собой набор библиотек, созданных разными разработчиками — не только теми, кто создал Redux. Redux Toolkit включает в себя библиотеки, показанные на рис. 4.6. Как видно, Redux Toolkit находится в центре, а библиотека, работающая с состоянием, как бы является ее частью. На рисунке представлено условное изображение — конкретные связи той или иной библиотеки в коде будут другими. С момента выхода библиотеки Redux и начала ее использования сообщество столкнулось с рядом неудобств, часть которых мы уже видели в предыдущих
4.6. Redux Toolkit  159 разделах. Это, например, коды редьюсеров, action creators, селекторы. Другими словами, при использовании Redux приходилось писать много шаблонного кода. Кроме того, в некоторых случаях хранение и обновление состояния требовали опти­ мизации — получить их «из коробки»1 было нельзя. Сообщество и разработчики Redux доработали функциональность, чтобы использовать основную библиотеку наиболее эффективно. Так появилась библиотека Redux Toolkit. Сегодня в нее входит несколько библиотек, идеи для которых заимствованы из библиотек autodux и normalizr, на рис. 4.6 обведенных овалом в форме облака с рваными краями. Основ­ ной библиотекой для работы с состоянием по-прежнему остается Redux. Рис. 4.6. Набор библиотек, входящих в Redux Toolkit Обращаю ваше внимание на то, что здесь имеет место некоторое преувеличение. Чтобы разобраться, рассмотрим библиотеку Redux Thunk и плагин Redux DevTools. 4.6.2. Redux Thunk2 Как уже было сказано, такие методы стора, как getState, dispatch и subscribe, работают синхронно. Однако в приложениях, особенно в современных, нужна работа с асинхронностью. Redux Thunk — middleware для библиотеки Redux. Он был разработан, чтобы упростить и улучшить управление асинхронными операциями в приложении на Redux. При работе с асинхронными операциями, например с сетевыми запросами, нам может потребоваться выполнить несколько действий в разное время и с разной временной задержкой. В этом случае нужно использовать Redux Thunk. 1 2 «Из коробки» имеется в виду функциональность, поставляемая библиотекой, которая может быть использована сразу или с дополнительными настройками и не требует самостоятельной разработки. Thunk — слово, заимствованное из области программирования систем. Оно означает подпрограмму, используемую для внедрения вычисления в другую подпрограмму. Thunk в основном применяется для задержки вычисления до тех пор, пока не потребуется его результат, или для вставки операций в начале либо в конце другой подпрограммы. Есть много других вариантов применения в генерации кода компилятором и в модульном программировании. Источник: https://en.wikipedia.org/wiki/Thunk.
160  Глава 4 Состояние Redux Thunk позволяет создавать action creators для работы с асинхронными вызовами — таково его единственное предназначение. Эти creators вызываются, результат передается аргументом в метод dispatch как обычный action creator, и пользователь получает доступ к методу dispatch, который позволяет отправлять новые экшены в Redux store, а также обновлять состояние приложения в асинхронном режиме. Таким образом можно задерживать отправку экшена и делать множество последовательных или параллельных запросов. Пример использования Redux Thunk в приложении может быть следующим (листинг 4.21). Представим, что у нас есть action creator, который должен выполнить сетевой запрос и обновить состояние приложения с полученными данными. Листинг 4.21. Action creator для Redux Thunk export const fetchData = (postId) => { return async (dispatch) => { try { const response = await fetch(`https://jsonplaceholder.typicode.com/ posts/${postId}`); const json = await response.json(); }); dispatch({ type: FETCH_SUCCESS, payload: { data: json, count: postId } } catch (error) { dispatch({ type: FETCH_ERROR, payload: error.message }); } }; }; В данном примере мы создаем action creator fetchData, который принимает ID поста в параметре. Затем возвращаем функцию, которая получает доступ к методу dispatch и выполняет сетевой запрос. После получения данных мы отправляем соответствующий экшен с данными в стор. При возникновении ошибки отправляем экшен с информацией о ней. В репозитории кода данной книги, в листинге, дан полностью рабочий пример. 4.6.3. Reselect Это не очень большая библиотека с востребованной функциональностью, прежде всего в React-приложениях, работающих с Redux Toolkit. В разделе 4.4 мы уже рассмотрели совсем простые функции — селекторы. Даже возникает вопрос об их необходимости. Библиотека Reselect дает возможность оптимизировать селекторы в Redux-приложениях. Селекторы — это функции, которые принимают состояние приложения и возвращают из него некоторые вычисленные значения. Они позволяют избегать повторных вычислений и так повышают производительность приложения. Необходимость использовать Reselect возникает, когда у приложения есть сложные селекторы, способные вызываться множество раз во время обновления
4.6. Redux Toolkit  161 состояния Redux. Без оптимизации каждый раз при вызове селектора происходит повторное вычисление, что может быть ресурсоемким. Reselect предлагает эффективное кэширование с использованием мемоизации1, чтобы избежать повторных вычислений селекторов. 4.6.4. Immer Эта библиотека призвана упростить работу разработчика и позволяет изменять состояние так, будто его изменяют напрямую, а не создавая новые копии состояния. Например, если у вас есть большая структура данных и вы привыкли изменять данные напрямую, не создавая новую копию всей структуры, то эта библиотека позволит работать в таком стиле. Однако состояние в самой библиотеке Redux будет обновляться, как и положено. Immer предназначена для работы с редьюсером (листинг 4.22). Листинг 4.22. Создание редьюсера с применением библиотеки Immer const reducer = (state = initialState, action) => { switch (action.type) { case INCREMENT: return produce(state, (draftState) => { draftState.counter++; }); case DECREMENT: return produce(state, (draftState) => { draftState.counter--; }); default: return state; } }; В этом примере мы используем функцию produce из Immer для создания новой версии состояния. Как видите, состояние здесь изменяется напрямую, с помощью оператора инкремента и декремента. Если бы в состоянии был массив и требовалось добавить в этот элемент новый элемент, то можно было бы спокойно использовать метод массивов push, что недопустимо при написании чистой функции. Полный рабочий листинг можно посмотреть в репозитории кода. Библиотека Immer применяется в Redux Toolkit. Ее использование мы рассмотрим далее. 1 Мемоизация — способ оптимизации, применяемый преимущественно для ускорения выполнения программ (но за счет дополнительных затрат памяти). Предусматривает запоминание результатов вычисления функций, с тем чтобы при последующем вызове функции при тех же входных данных можно было не повторять вычисления, а сразу выбирать результаты. Вместо слова «мемоизация» также можно применять «запоминание (сохранение) результатов». Пройдаков Э. М., Теплицкий Л. А. Большой англо-русский словарь по вычислительной технике и информационным технологиям (ВТ/ИТ). — М.: РТСофт, 2015. — 1600 с.
162  Глава 4 Состояние 4.6.5. Autodux Разработчики Redux вдохновились идеей, которая заложена в библиотеке autodux. Она позволяет автоматизировать создание составляющих работы с состоянием. Нужно задать лишь начальное состояние, называемое слайсом1, и получить готовые константы типов экшенов, action creators, селекторы, редьюсеры. Весь этот набор будет работать с заданным вами слайсом. Код листинга 4.22 изменен с использованием библиотеки autodux (листинг 4.23). Листинг 4.23. Создание редьюсера, существующего состояния, action creators и селектора с использованием autodux const { reducer, initial, slice, actions: { increment, decrement, }, selectors: { getCounter, } } = autodux({ slice: 'counter', initial: { counter: 0 }, actions: { increment: state => ({ counter: state.counter + 1 }), decrement: state => ({ counter: state.counter - 1 }), }, selectors: { getCounter: (__, state) => state.counter, } }); const store = createStore(reducer, initial); /** другой код*/ store.dispatch(increment()); Обратите внимание, что редьюсеры созданы автоматически на основании action creators, которые пришлось задать. Еще один важный момент — нигде нет констант типов экшенов. Этот подход переняла библиотека Redux Toolkit. Если заглянуть в репозиторий npmjs.com и поискать библиотеку autodux, то видно, что она не обновляется уже четыре года. Зато идея продолжает жить. Ее и рекомендуется использовать сегодня. 1 Слайс — от англ. slice, что можно перевести как «часть», «срез».
4.6. Redux Toolkit  163 Если вы знакомы с библиотекой Redux и, вероятно, имели опыт взаимодействия с Redux Toolkit, то наверняка отметили для себя, как изменился подход к разработке. Большой вклад в это внесла идея библиотеки autodux. 4.6.6. Redux DevTools Это не библиотека, а плагин1 для браузера. Существуют версии для всех видов браузеров. Плагин Redux DevTools — инструмент разработки для Redux, облегчающий отладку и мониторинг состояния приложения, основанного на Redux, в браузере (рис. 4.7). Он предоставляет разные функции и возможности. y История состояния — позволяет видеть историю изменений состояния Redux и перемещаться по предыдущим состояниям. Это необходимо при отладке ошибок или восстановлении предыдущего состояния. y Просмотр и изменение состояния — позволяет просмотреть текущее состояние Redux-стора и внести изменения на лету. Это полезно для тестирования разных сценариев состояния. y Просмотр истории действий — показывает все события экшенов Redux и их параметры. Помогает понять, какие экшены были запущены и какие данные они передавали. y Переключение между разными режимами времени — позволяет перемотать время и перейти к определенному моменту в истории для тестирования и анализа поведения приложения в разных контекстах. Рис. 4.7. Инструмент разработки Redux DevTool 1 Плагин — небольшое программное обеспечение, которое подключается к более крупной программе. Слово «плагин» (от англ. plugin) присутствует в самом языке и означает чтото, что может быть подключено к некоей электрической схеме.
164  Глава 4 Состояние 4.6.7. RTK Query RTK Query — библиотека для упрощения управления состоянием данных при работе с сервером. Она предоставляет удобный способ делать запросы к серверу и автоматически управляет кэшированием данных, обновлением, отменой, а также повторными запросами. Вот основные преимущества RTK Query. y Простота использования — RTK Query предлагает простой и интуитивно понятный API для написания запросов. Вместо того чтобы писать сложный код с использованием fetch или axios, RTK Query использует декларативный подход, в котором запросы и сущности данных объявляются в виде объектов. y Автоматическое кэширование данных — RTK Query автоматически кэширует полученные данные, что позволяет избежать повторных запросов на сервер. Она также предоставляет механизмы для автоматической инвалидации кэша при обновлении данных на сервере. y Легкость интеграции с Redux — RTK Query был разработан командой Redux Toolkit и интегрируется непосредственно с Redux. Она позволяет легко объединить в одном месте управление состоянием данных и состоянием приложения. y Встроенная поддержка пагинации и сортировки — RTK Query обеспечивает удобный способ работы с пагинацией и сортировкой данных, предлагает разные стратегии кэширования, такие как стандартный кэш по ключу, пагинационный кэш и т. д. По моему мнению, это одна из самых удобных библиотек для работы с сетевыми запросами для Redux. Библиотека RTKQuery применяется с Redux Toolkit, поэтому примеры работы с ней будут рассмотрены в соответствующем разделе. 4.6.8. normalizr Как и autodux, эта библиотека больше не поддерживается. Однако разработчики Redux Toolkit переняли идею, для которой она была создана. normalizr предназначена для нормализации1 данных в JavaScript-приложениях. Основная цель — упростить и улучшить обработку и хранение данных с помощью нормализованной структуры. Что делает библиотека normalizr? y Упрощает и оптимизирует хранение данных. Благодаря нормализации данных normalizr позволяет избежать дублирования объектов и минимизировать размер хранилища данных. Это особенно полезно, когда мы имеем дело с большими объемами данных, такими как список пользователей или записей. 1 Нормализация — процесс структурирования данных с использованием ссылочной модели данных.
4.7. Установка Redux Toolkit и настройка стора  165 y Упрощает манипуляцию данными. normalizr дает простой способ организации сложных структур данных и управления связями между ними, который позволяет разбить данные на отдельные сущности и установить связи между ними с помощью идентификаторов. Это делает манипуляцию данными более простой и интуитивной. y Повышает производительность. При использовании нормализованной структуры данных доступ к ним становится более эффективным. Вместо поиска по списку объектов можно использовать прямой доступ к конкретной сущности с помощью идентификатора. Это улучшает производительность приложения и ускоряет обработку данных. При использовании идей этой библиотеки и в случае совместного использования Redux Toolkit и React повышается производительность приложения для работы с большими списками любого вида элементов. Мы рассмотрим пример, где будет показан подход из normalizr. 4.7. Установка Redux Toolkit и настройка стора 4.7.1. Установка Redux Toolkit Установка включает в себя в том числе RTKQuery (листинг 4.24). Листинг 4.24. Установка Redux Toolkit npm install @reduxjs/toolkit Redux Toolkit содержит следующие функции. y configureStore() — применяется для создания и настройки стора. Использует createStore с упрощенными параметрами конфигурации и значениями по умолчанию. Может автоматически комбинировать редьюсеры для слайсов, добавляет middleware, включает redux-thunk по умолчанию и позволяет использовать расширение Redux DevTools. y createReducer() — позволяет применять таблицу поиска типов экшенов для редьюсеров, вместо того чтобы писать инструкции switch. Автоматически использует библиотеку Immer, позволяющую создавать более простые неизменяемые обновления с обычным изменяемым кодом, как показано в листинге 4.22. y createAction() — генерирует функцию action creator по строке, задающей тип экшена. y createSlice() — принимает объект с редьюсерами, именем слайса, значением начального состояния и автоматически генерирует редьюсер слайса с соответствующими action creators и типами экшенов. y combineSlices() — объединяет несколько слайсов в один редьюсер и позволяет выполнять «ленивую загрузку» слайсов после инициализации.
166  Глава 4 Состояние y createAsyncThunk() — принимает строку «Тип экшена» и функцию, воз­ вращающую промис, а также генерирует thunk, который вызывает метод стора dispatch с экшенами pending/fullfilled/rejected на основе этого промиса. y createEntityAdapter() — генерирует набор повторно используемых редьюсеров и селекторов для управления нормализованными данными в хранилище. y createSelector() — реэкспортируется из библиотеки Reselect для использования в Redux Toolkit. Библиотека RTKQuery содержит следующие функции. y createApi() — позволяет определить набор эндпоинтов1 и описать, как извлекать данные, включая настройку их преобразования. y fetchBaseQuery() — обертка над функцией браузера fetch для упрощенного выполнения запросов. Рекомендуется большинству пользователей для использования в createApi. y setupListeners() — используется для повторной выборки при монтировании и повторной выборки при повторном подключении. Нам надо рассмотреть основные из названных функций, остальные при необходимости можно изучить самостоятельно. 4.7.2. Создание и настройка стора Для начала стор нужно создать. Главной функцией для этого является configureStore, которая пришла на замену привычного createStore (листинг 4.25). Кстати, в Redux функция createStore помечена как deprecated, то есть как идентификатор, более не рекомендуемый для использования. Но Redux Toolkit на самом деле использует createStore, добавляя некоторые настройки по умолчанию. Поскольку библиотека новая и необходимо понять ее отличия от Redux, новые функции можно вводить постепенно, и, таким образом, станут видны отличия. Листинг 4.25. Вызов функции configureStore import { configureStore } from '@reduxjs/toolkit'; const actionTypeList = ['INCREMENT', 'DECREMENT']; const [INCREMENT, DECREMENT] = actionTypeList; const counterReducer = (state = 20, action) => { if (action.type === INCREMENT || action.type === DECREMENT){ return state + (action.type === INCREMENT ? 1 : -1); } 1 Эндпоинт (от англ. endpoint). При создании API какого-либо сервиса определяется набор URL, которые включают в себя метод и путь. Обращаясь к ним, можно отправлять запросы к этому сервису, чтобы получить или обновить данные. При этом обновление может предусматривать создание нового элемента данных, удаление элемента или обновление данных элемента.
4.7. Установка Redux Toolkit и настройка стора  167 }; return state; const [increment, decrement] = actionTypeList.map(type => () => ({ type })); const rootReducer = { counter: counterReducer, }; const store = configureStore({ reducer: rootReducer, preloadedState: { counter: 5 }, }); Вы можете скачать данный пример из репозитория и протестировать для себя порядок работы. Как видите, в данном примере стор конфигурируется двумя свойствами — reducer и preloadedState. Напомню, что в листинге 4.7 для объединения редьюсеров использовалась вспомогательная функция combineReducers. Когда применяется configureStore, она вызывается автоматически — достаточно создать объект с названиями областей состояния и назначить им редьюсер, который изменяет эту область. 4.7.3. Объект настроек configureStore Настройка стора вызовом configureStore осуществляет несколько действий. 1. Вызов функции combineReducer (см. раздел 4.3) для комбинирования редьюсеров, каждый из которых работает со своей областью дерева состояний. 2. Добавление middleware из Redux Thunk и вызов функции applyMiddleware. 3. Автоматическое добавление при разработке дополнительных middleware — для проверки на наличие распространенных ошибок вроде случайного изменения состояния. 4. Автоматическая настройка соединения с расширением Redux DevTools. 5. Вызов функции createStore для создания стора с использованием корневого редьюсера и настроек, описанных в предыдущих пунктах. Функция configureStore принимает всего один параметр — объект со следующими свойствами: y reducer; y preloadedState; y middleware; y devTools; y enhancers. reducer. В свойство reducer обычно помещается объект с редьюсерами. Однако можно передать и функцию. Если передается функция, то она будет использована
168  Глава 4 Состояние как корневой редьюсер (root reducer). Напомню, что корневой редьюсер — функция, которой передается все дерево состояний. Например, если видоизменить листинг 4.25 и вместо объекта передать саму функцию-редьюсер, а свойство preloadedState расширить дополнительными свойствами, то в качестве аргумента state такой редьюсер будет получать все начальное состояние. Как и в библиотеке Redux, в объекте rootReducer названия свойств определяют области состояния. То есть если поместить второй редьюсер, допустим, в свойстве users, а затем вывести состояние store.getState() в консоль, то в консоли окажется объект с двумя свойствами — counter и users. preloadedState. Свойство preloadedState определяет предварительное состоя­ ние стора. Это необязательное свойство, которое можно получить из хранилища localStorage, где оно хранится в сериализованном виде. Если поместить изначальное состояние в preloadedState и редьюсер одновременно, то состояние в конечном итоге установится в переданное в preloadedState. middleware. Колбэк, который получает аргументом функцию getDefaultMiddleware и возвращает массив middleware. Если данное свойство присутствует в объекте настройки, то оно должно содержать все middleware-функции, которые вы хотите добавить в стор. При этом configureStore отправит их в applyMiddleware. Если данное свойство не представлено, то будут применены middleware по умолчанию. О том, что такое middleware, и о функции applyMiddleware шла речь в разделе 4.5, а разъяснения про getDefaultMiddleware и middleware по умолчанию будут даны далее в текущем разделе. devTools. Если для данного свойства представлена переменная типа булеан, то configureStore определяет, следует ли включить поддержку расширения браузера Redux DevTools. Если представлен объект, то будет включено расширение браузера, а данный объект передан аргументом в энхансер composeWithDevTools. По умолчанию свойство установлено в true. Про Redux DevTools будет рассказано в разделе 4.10. enhancers. Необязательный массив энхансеров стора или колбэк-функция для настройки массива энхансеров в соответствии со своими потребностями. Если значение свойства — массив, то он будет передан в функцию compose, а результат передан аргументом в createStore. При настройке данного свойства есть несколько моментов, которые необходимо знать, иначе вы не получите ожидаемого поведения. Свойство enhancers связано со свойством middleware. 4.7.4. Middleware в configureStore В Redux Toolkit middleware разделены с энхансерами, поэтому и те и другие определяются более явно. По умолчанию configureStore добавляет middleware, их может быть вполне достаточно. Для этого нужно только вызвать configureStore, как в листинге 4.25. Однако вам может понадобиться добавить свои middleware. Тогда нужно знать, как в точности это делать. Если добавить свои middleware способом, показанным в листинге 4.26, то вы добавите исключительно их и middleware по умолчанию будут опущены.
4.7. Установка Redux Toolkit и настройка стора  169 Листинг 4.26. Задание в стор исключительно своих middleware import { configureStore, Tuple } from '@reduxjs/toolkit'; /** другой код */ const store = configureStore({ reducer: rootReducer, preloadedState: { counter: 5 }, middleware: () => new Tuple(delayedActionMiddleware), }); DelayedActionMiddleware — все та же middleware, впервые показанная в листингах 4.18–4.19, когда речь шла о middleware для библиотеки Redux. То есть middleware, созданные до появления Redux Toolkit, применимы и в ней. Но в листинге присутствует конструктор Tuple, название которого переводится как «кортеж»1. Технически это класс, расширяющий класс Array, который переопределяет метод concat и добавляет новый метод prepend для работы непосредственно и с middleware, и с энхансерами. concat — метод, принимающий или список middleware, то есть от одного до нескольких аргументов, разделенный оператором «запятая», или массив middleware. Ранее имевшиеся в кортеже middleware добавляются в начало, а middleware, переданные аргументом (-ами), добавляются к этому списку в конец. Он делает то же самое, что и нативный2 метод concat, реализованный для массивов. prepend — метод, принимающий или список middleware, то есть от одного до нескольких аргументов, разделенный оператором «запятая», или массив middleware. Ранее имевшиеся в кортеже middleware добавляются в конец, а middleware, переданные аргументом (-ами), следуют в начале. Чтобы и оставить middleware по умолчанию, и добавить свои, необходимо использовать функцию getDefaultMiddleware, как показано в листинге 4.27. Листинг 4.27. Добавление собственных middleware к middleware по умолчанию const store = configureStore({ reducer: rootReducer, preloadedState: { counter: 5 }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(delayedActionMiddleware), }); 1 2 Кортеж в программировании — это компактная структура данных, чаще всего неизменяемая, содержащая упорядоченную коллекцию объектов. Примерами кортежей служат: 1) координаты точки на карте (широта, долгота); 2) адрес (страна, город, улица, дом); 3) информация о книге (название, автор, издательство, год выпуска). Нативный — происходит от английского слова native, что означает «составляющее исходное вещество или источник». Конечно, у этого слова есть другие значения. На русский язык оно переводится как «родной», «собственный». В сфере разработки данное слово употребляют применительно к встроенным в язык средствам, классам и их методам.
170  Глава 4 Состояние Вот таким вот образом можно объединить middleware. Кстати, функция getDefaultMiddleware возвращает объект типа Tuple, то есть массив, у которого доступны методы concat, prepend. Одна из целей Redux Toolkit — предоставить однозначные значения по умолчанию и предотвратить распространенные ошибки. Для этого getDefaultMiddleware включает в себя middleware, которые добавляются в сборки приложения для режима разработки — только для обеспечения проверки во время выполнения трех распространенных проблем. y Проверка неизменяемости. Middleware сравнивает значения состояний для мутации. Он может обнаруживать изменения в редьюсерах во время вызова dispatch, а также изменения, которые происходят между отправками (например, в компоненте или селекторе). При обнаружении мутации выдается сообщение об ошибке и указывается путь к ключу, по которому в дереве состояний было обнаружено измененное значение. y Проверка сериализуемости. Middleware, созданный специально для использования в Redux Toolkit. Проверяет дерево состояний и экшены на наличие несериализуемых значений, таких как функции, промисы, символы и другие значения, отличные от обычных JS-данных. При обнаружении несериализуе­ мого значения в консоли будет выведено сообщение об ошибке с указанием пути к ключу, по которому обнаружено такое значение. y Проверка action creator. Middleware, также созданный специально для использования в Redux Toolkit. Определяет, когда action creator был ошибочно передан в dispatch без вызова, и предупреждает об этом в консоли. В дополнение к этим middleware getDefaultMiddleware добавляет redux-thunk по умолчанию, поскольку thunks — основная middleware для Redux. Все названные middleware работают в режиме разработки. Однако в продакшене из них попадет только лишь thunk. 4.7.5. Энхансеры в configureStore По умолчанию configureStore добавляет некоторые энхансеры. Для этого ничего не нужно делать дополнительно — достаточно вызвать configureStore, как в листинге 4.25. Однако вам может понадобиться добавить свои энхансеры. Для добавления к энхансерам по умолчанию — допустим, вы добавляете логгер — нужно использовать аргумент-функцию getDefaultEnhancers, как показано в листинге 4.28. Листинг 4.28. Добавление своих энхансеров к энхансерам по умолчанию const store = configureStore({ reducer: rootReducer, preloadedState: { counter: 5 }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(delayedActionMiddleware),
4.8. createSlice  171 enhancers: (getDefaultEnhancers) => getDefaultEnhancers().concat(loggerEnhancer), }); Полный пример дан в репозитории, где представлен код энхансера loggerEnhacner. Функция getDefaultEnhancers возвращает объект типа Tuple, поэтому доступны методы concat, prepend. 4.8. createSlice 4.8.1. Что делает createSlice В разделе 4.6 речь шла в том числе об autodux и Immer. Идеями autodux вдохно­ вились разработчики Redux.js/Toolkit и воплотили их в своей библиотеке. Мы воспользуемся этими возможностями и тоже будем использовать библиотеку Immer. Функция createSlice принимает несколько параметров (мы все их разберем), но чаще всего передает исходное состояние, объект с редьюсерами и наименование слайса. Под слайсом понимается область стора, как показано на рис. 4.3. Функция createSlice автоматически генерирует: y action creators; y типы экшенов, которые соответствуют редьюсерам и состоянию. На самом деле в коде приложения можно обойтись без использования типов экшенов. Редьюсеры, которые переданы в качестве параметра, будут доступны в свойстве reducer возвращаемого объекта функцией createSlice, а action creators — в свойстве actions. Далее мы рассмотрим, как работать с селекторами. Листинг 4.29. Пример создания редьюсеров, задания начального состояния, названия области состояния (слайса) и action creators с помощью createSlice import { configureStore, createSlice } from '@reduxjs/toolkit'; const initialState = 5; const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => state + 1, decrement: (state) => state - 1, incrementByAmount(state, action) { return state + action.payload; }, }); },
172  Глава 4 Состояние const { increment, decrement, incrementByAmount } = counterSlice.actions; const { reducer } = counterSlice; const store = configureStore({ reducer: { counter: reducer, // другие редьюсеры }, preloadedState: { counter: 11, // другие области состояния }, }); Полный листинг находится в репозитории к книге. 4.8.2. Свойства конфигурационного объекта createSlice Рассмотрим, какие свойства могут быть у конфигурационного объекта: y name — строковое имя для слайса состояния. Типы экшенов будут содержать это имя как префикс; y initialState — изначальное состояние редьюсера; y reducers — объект, содержащий так называемые case-редьюсеры (что это, поясняется в подразделе 4.8.4 далее). Имена ключей будут использованы для генерации экшенов; y extraReducers — функция — создатель колбэка для добавления дополнительных редьюсеров (зачем это необходимо, поясняется в подразделах 4.8.4 и 4.8.5 далее); y reducerPath — предпочтения по расположению редьюсера слайса, используемые функциями combineSlice и slice.selectors. По умолчанию располагается в свойстве со значением name; y selectors — объект селекторов, являющихся функциями, первый параметр которых — состояние слайса. 4.8.3. initialState Обратите внимание на то, как задано начальное состояние. Как и в предыдущих листингах, где оно задавалось в двух местах, начальное состояние примет значение из configureStore. Значит ли это, что его можно не задавать для редьюсера в createSlice? Нет. Если не задать начальное состояние, в редьюсере возникнет ошибка. В configureStore вы можете задавать начальное состояние динамически, например сохранять его в localStorage и впоследствии извлекать его оттуда. Кроме того, можно не задавать состояние для какого-то конкретного слайса. Если не задано состояние редьюсера в configureStore, то будет применено состояние из createSlice.
4.8. createSlice  173 4.8.4. reducers При работе с Redux обычно пишут один редьюсер на несколько типов экшенов и, как правило, разделяют их внутри редьюсера с помощью инструкции case внутри инструкции switch. В объекте reducers в Redux Toolkit вы пишете редьюсер для каждого экшена отдельно, что намного удобнее и понятнее. Ключи, используемые внутри объекта reducers, применяются для генерации типов экшенов и, когда они отправляются в стор, отображаются в инструменте Redux DevTools. Наш код листинга включает несколько кнопок, нажатие на которые ведет к вызову метода dispatch с передачей ему результата вызова action creator. На рис. 4.8 показано, как будет выглядеть инструмент разработки Redux DevTools после того, как нажать кнопку Прибавить 1, затем ввести цифру 3 и нажать Прибавить количество и далее нажать Отнять 1. Объект reducers будет передан во вспомогательную функцию createReducer (рассматривается далее), поэтому редьюсеры могут безопасно мутировать1 состоя­ ние, которое им передается. В данном примере, поскольку состояние является обычным числом, мутирование не показать. Но далее мы рассмотрим такой пример (рис. 4.8). Рис. 4.8. Экшены в Redux Toolkit 1 Мутировать — применительно к разработке означает видоизменять структуру данных или переменную, создавая так называемый побочный эффект, в противоположность созданию измененной копии, как это делает чистая функция. Например, если изменить свойство таким образом — const obj = { prop: 5 }; obj.prop++, — то это будет называться «мутировать свойство».
174  Глава 4 Состояние Обратите внимание, как написаны функции внутри объекта reducers. Редьюсеры increment и decrement написаны как обычные свойства и являются стрелочными функциями, а редьюсер incrementByAmount — как сокращенное свойство литерала объекта. Настройка генерируемого action creator. В листинге 4.29 кейс-редьюсеры increment и decrement не принимают аргументов при вызове, а кейс-редьюсер incrementByAmount принимает. Принятый аргумент становится значением свойства payload, но его можно настраивать более удобным образом и делать это в объекте reducers. Вместо функции в кейс-редьюсер передается объект с двумя свойствами — reducer и prepare. Оба являются функциями. Свойство reducer содержит кейс-редьюсер, а свойство prepare — колбэк, осуществляющий подготовку данных для payload. Данная ситуация продемонстрирована в листинге 4.30. Листинг 4.30. Настройка генерируемого экшена в объекте reducers const initialState = []; const shoppingCartSlice = createSlice({ name: 'shoppingCart', initialState, reducers: { addToCart: { reducer: (state, action) => { if (!state.find(productId => productId === action.payload)){ state.push(action.payload); } }, prepare: (productId) => { return { payload: productId, }; }, }, removeFromCart: { reducer: (state, action) => { const index = state.indexOf(action.payload); if (index !== -1) { state.splice(index, 1); } }, prepare: (productId) => { return { payload: productId, } } } }, }); В листинге представлены два кейс-редьюсера — addToCart и removeFromCart, от которых впоследствии генерируются action creators.
4.8. createSlice  175 4.8.5. extraReducers Фраза extra reducers переводится как «дополнительные редьюсеры». Зачем они могут понадобиться? Каждый редьюсер владеет состоянием своей области. Очевидна взаимосвязь между обновлением состояния в редьюсере и типом экшена, который генерируется на его основе. Во многих случаях слайсам Redux может потребоваться обновить собственное состояние в ответ на типы экшенов, которые были определены где-либо еще в приложении. Например, в стор отправляется экшен, указывающий на то, что пользователь вышел из системы (разлогинился). Такой экшен может быть определен вызовом другого createSlice или может быть сгенерирован createAsyncThunk. В основе Redux есть идея, что множество редьюсеров слайсов могут независимо реагировать на конкретный тип экшена. extraReducers позволяют функции createSlice реагировать и обновлять собственное состояние в ответ на типы экшенов — помимо тех, которые они сгенерировали. В отличие от кейс-редьюсеров, определенных в reducers, кейс-редьюсеры внутри extraReducers не будут генерировать новые типы экшенов или action creators. Как задействовать extraReducers, будет рассмотрено далее, в подразделе 4.8.12. 4.8.6. reducerPath В этом свойстве указывается предпочтение, где слайс должен располагаться. По умолчанию оно равняется свойству name. Используется функцией combineSlice, роль которой поясняется далее в этом разделе. 4.8.7. selectors В этом свойстве можно задать набор селекторов. Объект, наименования свойств которого выступают именами селекторов. Данные селекторы появятся в свойстве selector возвращаемого объекта вызова createSlice. Теперь самое время посмотреть, как они используются в Redux Toolkit. Нам необходимо видоизменить листинг 4.30 так, чтобы можно было получать количество товаров в корзине с помощью селектора, а товар в корзину добавлять в том количестве, в каком пожелает пользователь (листинг 4.31). Листинг 4.31. Объект selector selectors: { getTotalPrice: (state) => { const totalPrice = state.reduce((total, productId) => { return total + books.find(book => book.productId === productId).price; }, 0); return totalPrice; } }
176  Глава 4 Состояние Полный пример листинга приведен в репозитории. Кроме селекторов, в нем добавлены дополнительные кейс-редьюсеры, так как необходимо в том числе увеличивать и уменьшать количество конкретных товаров в корзине. Впоследствии селекторы, заданные в createSlice, будут использованы в разных местах кода. В коде примера 4.31 они применяются для извлечения из состояния количества книг, помещенных в корзину. 4.8.8. Возвращаемое значение из createSlice Функция createSlice возвращает объект со следующими свойствами. y name — имя слайса, являющееся строкой. y reducer — метод, который включает в себя создание нового состояния на основе всех типов экшенов, объявленных для данного слайса. Напрямую вам не нужно создавать типы экшенов, так как они генерируются автоматически, на основе кейс-редьюсеров. Данный метод является объединением всех кейс-редьюсеров, которые вы определили в свойстве reducers объекта конфигурации. y actions — объект, содержащий action creators для одноименных кейсредьюсеров. y caseReducers — кейс-редьюсеры, которые вы определили в свойстве reducers объекта конфигурации. y getInitialState — метод, возвращающий изначальное состояние, которое было определено в свойстве initialState объекта конфигурации. y reducerPath — указывает ключ (имя), под которым данный слайс в дереве состояний сохраняет свое состояние. Может отличаться от name. y selectSlice — метод, позволяющий извлечь состояние всего слайса. Принимает аргументом состояние стора. Рассмотреть работу можно в репозитории кода листингов (см. листинг 4.36 далее). y selectors — объект, содержащий селекторы, которые определены вами в объекте конфигурации. y getSelectors — метод, возвращающий объект с селекторами, которые определены вами в объекте конфигурации. y injectInto — метод позволяет добавить текущий слайс к корневому редьюсеру после того, как произведена инсталляция стора (см. подраздел 4.8.11). 4.8.9. Функция в свойстве reducers Помимо объекта, в свойство reducers вы можете передать функцию. Эта функция содержит единственный аргумент, и авторы библиотеки его назвали create. Аргумент create — это объект. Взглянем на листинг 4.32.
4.8. createSlice  177 Листинг 4.32. Применение функции с аргументом create для задания редьюсеров в объекте конфигурации createSlice const testCreateReducers = createSlice({ name: 'testCreateReducer', initialState, reducers: (create) => ({ deleteFromState: create.reducer((state, action) => { const index = state.findIndex((phraseObject) => phraseObject.id === action.payload); if (typeof index === 'number') { state.splice(index, 1); } }), saveToState: create.preparedReducer((phrase) => { return { payload: { id: nanoid(), title: phrase, } } }, (state, action) => { state.push(action.payload); }), }), }); Объект create содержит всего три метода: 1) reducer; 2) preparedReducer; 3) asyncThunk. Метод asyncThunk мы рассматривать не будем, а два других мы уже задействовали. Функция, назначаемая свойству reducers объекта конфигурации, возвращает объект с редьюсерами. Первый из них — deleteFromState — реализован с помощью метода reducer объекта create, второй — с помощью preparedReducer. Метод reducer принимает в качестве аргумента одну функцию с двумя параметрами, state и action. Метод preparedReducer принимает две функции, первая из которых принимает один аргумент, а вторая — два. Когда в тексте программы вы будете задействовать автоматически генерируемые action creators для экшена, созданного с помощью метода preparedReducer, аргумент будет передан аргументом в первую функцию, из которой нужно возвратить объект экшена. Если взглянуть на код листинга 4.32, то видно, что мы возвращаем из нее объект со свойством payload. То есть сами подготавливаем экшен. Во вторую функцию уже будет передан экшен, который мы подготовим в первой. А в action creator, созданном на основе редьюсера с помощью create, переданный аргумент будет назначен свойству payload экшена.
178  Глава 4 Состояние 4.8.10. combineSlices Функция, которая комбинирует слайсы и возвращает единый редьюсер. До сих пор мы рассматривали листинги, где присутствовал всего один слайс, или, другими словами, одна область с состоянием. Добавив второй слайс, вы будете вынуждены каким-то образом объединить слайсы и передать их в configureStore. Для этих целей предназначена функция combineSlices (листинг 4.33). Листинг 4.33. Применение combineSlices const events = createSlice({ name: 'events', initialState: { current: null, happendEventList: [], }, reducers: (create) => ({ addEvent: create.reducer((state, action) => { const {payload} = action; if (payload !== state.current) { state.current = payload; state.happendEventList.push(payload); } }), }), }); const notes = createSlice({ name: 'notes', initialState: [], reducers: (create) => ({ addNote: create.preparedReducer((text) => { return { payload: { id: nanoid(), text, } } }, (state, action) => { state.push(action.payload); }), deleteNote: create.reducer((state, action) => { const index = state.findIndex(item => item.id === action.payload); const isNotInArray = -1; if (index !== isNotInArray){ state.splice(index, 1); } }), }) });
4.8. createSlice  179 Подробности всего примера опущены. Видно, что функция применяется достаточно просто, но у нее есть и другие возможности для объединения слайсов. В листинге 4.33 мы видим статический способ объединения слайсов. То есть, когда вы объявили слайсы, в приложении будете это делать в отдельных модулях, а не как в листинге — в одном. Соответственно, нужно импортировать все слайсы в какой-то один модуль, где их впоследствии объединить. Но может возникнуть потребность добавлять и удалять слайсы динамически. Функция combineSlices возвращает редьюсер со следующими двумя методами: y inject; y withLazyLoadedSlices и объектом: y selectors. 4.8.11. inject и injectInto Для динамического добавления слайса предназначен метод inject. Мы рассмотрим его и только его. Дополним наш пример третьим слайсом, добавив и удалив его динамически (листинг 4.34). Листинг 4.34. Использование метода inject для динамического добавления слайса const picassoMode = createSlice({ name: 'picassoMode', initialState: false, reducers: (create) => ({ togglePicassoMode: create.reducer((state) => state = !state), }), }); const rootReducer = combineSlices(events, notes); const store = configureStore({ reducer: rootReducer, }); rootReducer.inject(picassoMode); Это сокращенный листинг, полный пример находится в репозитории. В этом листинге добавлен новый слайс picassoMode, в котором всего один редьюсер, переключающий состояние всякий раз, как в стор отправляется экшен picassoMode/ togglePicassoMode. Что включает/отключает возможность рисовать на специально добавленной доске на странице? Для этого достаточно щелкнуть в зоне доски. Обратите внимание, что в combineSlices добавлены всего два слайса — events и notes. Третий новый слайс добавляется с помощью метода inject уже после того,
180  Глава 4 Состояние как инициализирован стор. Это делается для того, чтобы вы могли осуществлять добавление и удаление слайсов. Но в возвращаемом редьюсере вроде как нет метода наподобие delete или extract. По умолчанию замена редьюсера не разрешается. Если вы попытаетесь внедрить редьюсер по reducerPath, который уже существует, появится предупреждение. Однако это можно сделать с помощью метода inject, но придется в метод вторым аргументом передать конфигурационный объект со свойством, как показано в листинге 4.35. Листинг 4.35. Внедрение слайса с пустым редьюсером rootReducer.inject(picassoMode); const emptyReducer = () => null; rootReducer.inject( { reducerPath: 'picassoMode', reducer: emptyReducer }, { overrideExisting: true, }, ); Фактически такой редьюсер прекращает обновлять слайс. А конкретно в данном примере вы не сможете рисовать на доске. Методы injectInto отличаются от метода inject лишь «точкой зрения» внедрения (что во что внедряется) и возвращаемым результатом. Метод injectInto — метод слайса (createSlice). Метод inject — метод rootReducer (combineSlice). Задача одна — внедрить слайс, точнее, лишь некоторые его компоненты в корневой редьюсер. Использовать с одинаковым результатом можно как один, так и второй. В листинге 4.36 наглядно показано, как происходит внедрение. Листинг 4.36. Использование injectInto для внедрения слайса const picassoMode = createSlice({ name: 'paintDesk', initialState: false, reducers: (create) => ({ togglePicassoMode: create.reducer((state) => state = !state), }), }); const rootReducer = combineSlices(events, notes); const store = configureStore({ reducer: rootReducer, }); picassoMode.injectInto(rootReducer, { reducerPath: 'picassoMode', });
4.8. createSlice  181 Здесь метод injectInto использует конфигурационный объект и указывает свойство reducerPath, что фактически означает название области состояния в дереве состояний. 4.8.12. extraReducers Периодически требуется, чтобы в текущем слайсе обновлялось состояние в ответ на типы экшенов, которые объявлены в других местах, а именно — в других слайсах. Для реализации этой возможности необходимо в свойстве extraReducers объекта конфигурации createSlice описать поведение для конкретных внешних экшенов (листинг 4.37). Листинг 4.37. extraReducers const rawText = createSlice({ name: 'raw', initialState: [], reducers: (create) => ({ addRawText: create.preparedReducer((text) => { return { payload: { id: nanoid(), text, } } }, (state, action) => { state.push(action.payload); }), /* deleteRawText */ }), }); const {addRawText} = rawText.actions; const sanitizedText = createSlice({ name: 'sanitized', initialState: [], extraReducers: (builder) => { builder.addCase(addRawText, (state, action) => { const {id, text} = action.payload; state.push({ sanitizedId: nanoid(), rawId: id, text: text.replace(/</g, '&lt;').replace(/>/g, '&gt;'), }) }) /* .addCase(deleteRawText, (state, action) => {...}) */ }, });
182  Глава 4 Состояние Полный пример приведен в репозитории. В листинге 4.37 представлены два слайса: один сохраняет введенный в поле текст в неизменном виде, второй — измененный текст, где угловые скобки заменены их HTML-кодами. Первый слайс содержит редьюсеры, второй — экстраредьюсеры1. По этой причине первый слайс генерирует action creators, второй — нет. Экстраредьюсеры не генерируют action creators. Как действуют экстраредьюсеры? В качестве значения в extraReducers передана функция, у которой есть именованный параметр builder. У этого параметра есть три метода: y addCase; y addMatcher; y addDefaultCase. Мы рассмотрим только addCase. Остальные можно изучить самостоятельно, это несложно. Параметр builder выстраивает цепочку из методов, например: builder.addCase(…).addMatcher(…).addDefaultCase() addCase — метод с двумя аргументами. Первым должен быть тип экшена в виде строки (например, raw/addRawText) или функция action creator. Вторым аргументом является редьюсер, изменяющий состояние слайса, в котором находится данный метод addCase. Экстраредьюсер, являющийся вторым аргументом у метода addCase, принимает два аргумента, как большинство других редьюсеров в ReduxToolkit. В момент вызова экстраредьюсера передается состояние текущего слайса (в листинге 4.37 это состояние слайса sanitizedText), а экшен с данными — тот, для которого собственно и добавлен данный экстраредьюсер. В нашем листинге 4.37 это экшен типа raw/addRawText, а данные у него — id и text. Текст будет тот, который пользователь ввел в поле. При нажатии кнопки в стор передается этот экшен со всем содержимым. Другие методы builder слегка отличаются в плане использования, но суть у них такая же. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 4.8. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 4.8. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 4.8 и ознакомьтесь с моим решением. 1 Экстраредьюсер — это английское слово, написанное русскими буквами, означает «дополнительный редьюсер». Он является дополнительным, а не основным потому, что для него нельзя сгенерировать action creator.
4.9. RTK Query  183 4.9. RTK Query 4.9.1. Для чего предназначен RTK Query RTK Query — инструмент в составе Redux Toolkit, позволяющий делать запросы к эндпоинтам и кэшировать данные. Он создан для использования с Redux Toolkit и избавляет от самостоятельного написания кода приложения там, где требуется получение, реакция на события и кэширование. Веб-приложениям нужно получать данные и отображать их. Конечно, не­ обходимо изменять данные и отправлять обратно на сервер, синхронизировать кэши­рованные данные на клиенте с данными на сервере. При этом нужно предусмотреть: y отслеживание состояния загрузки, чтобы информировать об этом пользователя; y предотвращение дублирования запросов к одним и тем же данным; y оптимистичные1 обновления для ускорения работы пользовательского интерфейса; y управление временем жизни кэша при взаимодействии пользователя с пользовательским интерфейсом. При сравнении может показаться странным, но извлечение и кэширование данных — другой набор задач, нежели управление состоянием. Хотя вы можете использовать библиотеку управления состоянием, такую как Redux, для кэширования данных способы использования отличаются, поэтому лучше пользоваться инструментами, специально разработанными для выборки данных, а именно RTK Query. Что представляет собой API RTK Query: y извлечение и кэширование данных построено поверх API createSlice и createAsyncThunk Redux Toolkit; y Redux Toolkit не зависит от пользовательского интерфейса, поэтому функциональность RTK Query можно использовать на любом его уровне; y конечные точки API (те, которые создаются на клиенте для запросов) определяются заранее, включая то, как генерировать параметры запроса из аргументов и преобразовывать ответы для кэширования; y RTK Query может генерировать хуки React, которые инкапсулируют весь процесс извлечения данных, предоставляя компонентам поля data и isLoading, и управляют временем жизни кэшированных данных по мере подключения и размонтирования компонентов; 1 Обычное обновление — это когда пользователь в веб-интерфейсе обновил данные и они отправляются на сервер, чтобы записаться в базу данных. В этот момент интерфейс ожидает ответа от сервера об успешном выполнении операции. Затем приходит ответ и пользователя уведомляют о результате. В случае с оптимистичными обновлениями пользователя уведомляют об обновлении сразу, как только он изменил данные. Данные кэшируются и параллельно отправляются на сервер.
184  Глава 4 Состояние y RTK Query предоставляет параметры «жизненного цикла записи в кэш», которые позволяют использовать такие варианты, как потоковое обновление кэша через сообщения websocket после получения исходных данных. 4.9.2. Использование RTK Query С RTK Query работают как с ReduxToolkit. Подобно тому как в ReduxToolkit с помощью createSlice создается слайс для состояния, в RTK Query создается слайс для работы с запросами и кэшированием. Вот что обычно делают с помощью RTK Query. y Создают запросы для получения данных. y Отправляют запросы на изменения данных. y Автоматически обновляют ранее полученные данные. y Управляют кэшем. Мы познакомимся с запросами для получения данных. Вполне закономерно, что это самый частый вид запросов в веб-приложениях. Для примера листинга 4.38 у нас будет запущен локальный простой сервер, который выдает всегда один и тот же JSON и запускается на порте 4000 на localhost. Листинг 4.38. Создание слайса для RTK Query import { configureStore, createSlice, combineSlices } from '@reduxjs/toolkit'; import { createApi, fetchBaseQuery, setupListeners } from '@reduxjs/toolkit/query'; export const products = createApi({ reducerPath: 'products', baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:4000/' }), endpoints: (build) => ({ getProducts: build.query({ query: (name) => `${name}`, }), }), }); const counter = createSlice({ name: 'counter', initialState: 0, reducers: (create) => ({ increment: create.reducer((state) => { return state += 1; }), decrement: create.reducer((state) => { if(state === 0) { return state; }
4.9. RTK Query  185 }) }); return state -= 1; }) const rootReducer = combineSlices(counter, products); const {getProducts} = products.endpoints; const store = configureStore({ reducer: rootReducer, middleware: (buildGetDefaultMiddleware) => buildGetDefaultMiddleware().concat(products.middleware), }); setupListeners(store.dispatch); // Сокращение листинга requestB.addEventListener('click', () => { store.dispatch(getProducts.initiate('products')); }, false); store.subscribe(() => { console.log('state', store.getState()); console.log('products', products); }); Вы можете просмотреть пример, если запустите приложение npm run build, предварительно открыв папку с примером 4.38. Рассмотрим некоторые строки листинга. 4.9.3. Внедрение в код для совместного использования с другим состоянием Redux Toolkit В листинге 4.38 намеренно создан дополнительный слайс counter — чтобы показать, как слайс для работы с RTK Query работает совместно со слайсами для работы с состоянием, не связанным с получением данных. Для объединения слайсов, как и в ранее рассмотренных примерах, используется функция combineSlices из Redux Toolkit в строке 32. Для корректной работы RTK Query необходимо добавлять в стор middleware. Что и сделано в листинге в строке 38. Middleware — свойство возвращаемого объекта функцией createApi, которая рассматривается далее. Сами middleware мы уже изучили в разделе 4.5. 4.9.4. createApi Создание слайса для запросов — любых, не только GET-запросов — осуществляется с помощью функции createApi. Это ядро RTK Query. Она позволяет определить набор эндпоинтов, где каждый эндпоинт описывает, как извлечь данные из API
186  Глава 4 Состояние бэкенда и других асинхронных источников. Можно также конфигурировать запросы и указывать, как извлекать данные и преобразовывать их. Эндпоинт генерирует структуру API слайса, содержащую логику Redux, которая инкапсулирует получение данных и процесс кэширования. При вызове createApi необходимо передать единственный аргумент — объект конфигурации. После этого вызов данной функции возвращает значение, ряд свойств которого задействуются до осуществления запросов для конфигурирования стора, и в том числе объекты для создания экшенов, при чьей помощи осуществляется запрос. 4.9.5. Объект конфигурации createApi Объект конфигурации может содержать следующие свойства: y baseQuery; y endpoints; y reducerPath; y tagTypes; y extraRehydrationInfo; y serializeQueryArgs; y keepUnusedDataFor; y refetchOnMountOrArgChange; y refetchOnFocus; y refetchOnReconnect. 4.9.6. baseQuery и queryFn baseQuery — базовый запрос. Надо взглянуть на листинг 4.38, на строки 8, 11 и 51. В строке 8 baseQuery задействует функцию fetchBaseQuery из RTK Query. Однако инициализация функции createApi на baseQuery никак не влияет. baseQuery задей- ствуется, только когда осуществляется вызов, подобный показанному в листинге, в строке 51. По сути, мы отправляем экшен в стор, как если бы хотели изменить состояние. Это то же самое, но немного сложнее. Предварительно делается запрос на сервер по URL, который определяется базовым URL, путем и дополнительными параметрами. В листинге 4.38 используется только базовый URL, переданный как свойство baseUrl объекта конфигурации функции fetchBaseQuery. Путь передается аргументом в выражение getProducts.initiate("products"). В строке 51 и строке 11 листинга этот аргумент становится значением параметра name. В листинге запрос производится на URL http://localhost:4000/products. Помимо связки baseQuery и отправки экшена, для передачи аргументом пути можно использовать queryFn, связку baseQuery — queryFn и отправку экшена для передачи аргументом пути. В листинге 4.39 приведен пример того, как применяется queryFn. Запрос RTK экспортирует утилиту fetchBaseQuery, которая использует fetch. Эта функция должна задействоваться для baseQuery.
4.9. RTK Query  187 Листинг 4.39. Использование queryFn export const products = createApi({ reducerPath: 'products', endpoints: (build) => ({ getProducts: build.query({ queryFn(arg, queryApi, extraOptions, baseQuery) { return { data: [{ // свойства и значения объекта },] } }, }), }), }); В листинге 4.39 приведен сокращенный код примера. В нем не используется baseQuery — вместо этого в builder.query, в объекте конфигурации, применена функция queryFn. Вы видите, что она напрямую возвращает объект со свойством data, значением которого является массив объектов. Значение может быть любым. В данном случае эта функция используется для непосредственного возвращения значения, то есть в качестве заглушки, но внутри нее можно делать запросы. Функция queryFn получает четыре аргумента, последним из которых является функция baseQuery, с чьей помощью можно сделать асинхронный запрос. Таким образом, можно реализовать использование связки baseQuery — queryFn и отправку экшена в стор для передачи пути аргументом, как показано в листинге 4.40. Листинг 4.40. Использование baseQuery внутри функции queryFn export const products = createApi({ reducerPath: 'products', baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:4000/' }), endpoints: (build) => ({ getProducts: build.query({ async queryFn(arg, queryApi, extraOptions, baseQuery){ try { return await baseQuery(arg, queryApi); } catch(error){ return { error }; } }, }), }), }); Отправка экшена производится точно так же, как в листинге 4.38. Теперь путь становится значением первого аргумента arg.
188  Глава 4 Состояние 4.9.7. endpoints Это свойство настраивает запросы разных видов к эндпоинтам на сервере. Мы уже рассмотрели несколько листингов с ними, но обошли стороной их детальное изучение. Для начала займемся методами query и mutation, чтобы создать эндпоинты, затем перейдем к объектам конфигурации самих методов. Если посмотреть на листинг 4.41, то станет видно, что для эндпоинта getProducts используется метод query параметра build, а для эндпоинта createProduct — метод mutation. Листинг 4.41. Методы query и mutation для создания эндпоинтов export const products = createApi({ reducerPath: 'products', baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:4000/' }), endpoints: (build) => ({ getProducts: build.query({ query: (name) => `${name}`, }), createProduct: build.mutation({ query: (body) => { const headers = new Headers(); headers.append("Content-Type", "application/json"); return ({ url: 'createProduct', method: 'post', headers, body, }) }, }) }), }); y query — метод, используемый для создания запросов только на получение данных. y mutation — метод, используемый для отправки обновленных данных на сервер и применения изменений к локальному кэшу. С его помощью также можно сделать данные, хранимые в кэше, недействительными и заставить выполнить новый запрос к эндпоинту. И первый, и второй методы принимают в качестве аргумента объект конфигурации. В нем обязательно должно быть определено свойство — query или queryFn. Второе мы уже рассмотрели. Свойство query — функция. Если посмотреть на листинг 4.41, то видно, что в случае с методом mutation возвращаемым значением служит объект со свойствами url, method, headers, body. Аргумент body передается при отправке экшена в стор. При этом, если вам необходимо передать больше значений, просто передавайте объект и осуществляйте деструктуризацию. В качестве метода можно использовать не только POST, но и PATCH, PUT и др.
4.9. RTK Query  189 4.9.8. reducerPath Это наименование области хранилища, в которой будут сохраняться данные с эндпоинтов для конкретного API какого-либо сервиса. Вы можете создать несколько областей, например одну — для аутентификации, другую — для разделов каталога, третью — для товаров. В наших листингах использовалось имя products. Свойство reducerPath является строкой и выполняет ту же роль, что и reducerPath в функции createSlice, рассмотренной в предыдущем разделе. 4.9.9. tagTypes Это массив имен строковых типов тегов. Использование тегов в данном случае необязательно. Имена нужны для того, чтобы присвоить их определенному набору данных для управления кэшированием и аннулированием с целью повторного запроса. Это можно рассматривать как «метку», прикрепленную к кэшированным данным, которые считываются после запросов типа mutation, чтобы решить, должен ли запрос повлиять на данные. Теги используются при конфигурировании эндпоинтов. Определяя запросы с помощью query, вы можете указать теги в свойстве providesTags, а делая это с помощью mutation, теги нужно указать в свойстве invalidatesTags, что приведет к их аннулированию, когда на такой эндпоинт будет сделан запрос. В листинге 4.42 показано использование тега Products. Листинг 4.42. Использование свойства tagTypes export const products = createApi({ reducerPath: 'products', baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:4000/' }), tagTypes: ['Products'], endpoints: (build) => ({ getProducts: build.query({ query: (name) => `${name}`, providesTags: ['Products'] }), createProduct: build.mutation({ invalidatesTags: ['Products'], query: (body) => { const headers = new Headers(); headers.append("Content-Type", "application/json"); return { url: 'createProduct', method: 'post', headers, body, }; }), }); }, }),
190  Глава 4 Состояние Попробуйте выполнить код этого примера. В самом начале получите список продуктов. Затем добавьте новый продукт, и Redux Toolkit автоматически произведет новый запрос к эндпоинту getProducts. Почему? Благодаря механизму тегов. В запросах типа query вы назначаете данным тег. В запросах типа mutation — указываете теги, которые будут аннулированы в кэше, если будет произведен запрос по данному эндпоинту. Когда запрос отправится к эндпоинту типа mutation, при условии, что были указаны теги в invalidatesTags, такие данные в кэше аннулируются. В этом случае производится новый запрос к серверу на те эндпоинты, данные которых в кэше аннулировались. Очень удобно. В случае с листингом 4.42 данные, получаемые с эндпоинта getProducts, подписаны тегом Products. А запросы на createProducts аннулируют кэш для данных, помеченных тегом Products, поэтому Redux Toolkit производит новый запрос для получения обновленных данных. Надо помнить важный момент: позаботиться об обновлении данных на странице должны вы сами. Обновления страницы нужно осуществлять из метода стора subscribe. Посмотрите полный код листинга 4.42, строки 69–80, и у вас сложится вся картинка. 4.9.10. Оставшиеся свойства Здесь приведено только назначение оставшихся свойств. y serializeQueryArgs — принимает пользовательскую функцию, если по какойлибо причине вам необходимо изменить создание ключей кэша. y keepUnusedDataFor — то, как долго запрос RTK будет сохранять ваши данные в кэше после того, как последний компонент откажется от подписки. Например, если вы запрашиваете конечную точку, потом отключаете компонент и затем подключаете другой компонент, который выполняет тот же запрос в течение заданного периода времени, то из кэша будет отправлено самое последнее значение. По умолчанию — 60 секунд. y refetchOnMountOrArgChange — параметр позволяет управлять поведением RTK Query относительно кэширования результатов запросов. По умолчанию установлено значение false. Если кэшированный результат уже доступен, RTK Query вернет именно этот результат, не отправляя новый запрос. Однако если параметр установлен в true, то прошло достаточное количество времени с момента последнего успешного результата запроса и данные будут перезагружены заново. y refetchOnFocus — этот параметр позволяет контролировать, будет ли RTK Query пытаться повторно выполнить выборку всех подписанных запросов после того, как окно приложения восстановит фокусировку. По умолчанию — false. y refetchOnReconnect — параметр, который позволяет контролировать, будет ли RTK Query пытаться повторно выполнить выборку всех подписанных запросов после восстановления сетевого подключения. По умолчанию — false.
4.9. RTK Query  191 4.9.11. setupListeners Это утилита, которая находится в RTK Query. В коде листинга 4.42 она задействована в строке 55. Позволяет получать автоматически данные повторно при наступлении таких событий, как фокус и переподключение после потери интернет-­ соединения. Обычно достаточно настроек, применяемых по умолчанию. Но для более точного контроля можно отправить второй аргумент в качестве колбэка и задать необходимое поведение для указанных событий. 4.9.12. Возвращаемое значение функцией createApi В листингах мы уже использовали возвращаемое значение вызова функции createApi, но не рассматривали его подробно (рис. 4.9). Рис. 4.9. Объект конфигурации и объект, возвращаемый функцией createApi Если рассмотреть листинг 4.42 и дополнить его несколькими выводами в консоль, то можно получить все сведения о возвращаемом объекте. Если изобразить
192  Глава 4 Состояние возвращаемый объект в виде графа, то получится достаточно объемное дерево, в основном состоящее из методов, а также свойств. Мы сосредоточимся на самых главных, остальные можно изучить самостоятельно. Возвращаемый объект содержит следующие свойства и методы: y middleware; y endpoints; y reducer; y util; y enhanceEndpoint; y internalAction; y injectEndpoints; y reducerPath. 4.9.13. middleware В листинге 4.42, в строке 52, middleware применяется без каких-либо допол­ нительных действий со стороны разработчика. С целью создания области стора для текущего API обязательно нужно передать в него возвращаемую middle­ ware. 4.9.14. endpoints Объект enpdoints содержит одноименные с объявленными в объекте конфигурации эндпоинтами свойства. Так, в листинге, в объекте конфигурации, объявлены два эндпоинта — getProducts и createProduct. Соответственно, объект endpoints содержит свойства с аналогичными именами, которые являются объектами. Каждый такой объект содержит пять методов и одно свойство name, являющееся именем эндпоинта. В листинге 4.42 мы задействовали один метод — initiate, чтобы создать экшен, который передается в метод dispatch. Таким образом инициируется запрос к эндпоинту, возвращаемый результат которого сохранится в стор. В упражнениях мы с вами пройдем негативный сценарий и узнаем, как справляться с подобными ситуациями. 4.9.15. reducer В листингах этого раздела свойство reducer не используется. Вместо прямого использования редьюсера применялась функция combineSlices, в которую был передан весь объект, возвращаемый функцией createApi. 4.9.16. Другие свойства и методы Оставшиеся свойства вам предстоит изучить самостоятельно. Я только скажу, что свойства utils и internalActions являются объектами и содержат методы. В utils их 12, в internalActions — 14.
4.10. Redux DevTools  193 Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 4.9. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 4.9. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 4.9 и ознакомьтесь с решением приводимым автором. 4.10. Redux DevTools 4.10.1. Redux DevTools и установка DevTools — инструмент, дающий возможность наглядно показывать состояние, отправленные в стор экшены и историю изменения состояния, а также позволяющий возвращаться по истории экшенов к предыдущим состояниям и даже управлять приложением. Он улучшает наглядность работы приложения. Redux DevTools существует в виде расширения (extension)1 для браузера, специального компонента для React-приложений и как отдельное приложение. Мы рассмотрим расширение. Установка осуществляется для каждого браузера отдельно из специального хранилища расширений, предназначенного только для конкретного браузера. Поскольку ссылки, ведущие на страницу расширения в таких хранилищах, имеют длинные URL, я дам ссылку на официальный GitHub-репозиторий DevTools, где собраны ссылки для конкретных браузеров: https://github.com/reduxjs/redux-devtools/. Первым делом зайдите на эту страницу и установите DevTools для нужного вам браузера. Чтобы это сделать, достаточно нажать Установить или что-то подобное. 4.10.2. Подключение к приложению Установив, например, в браузер Edge или «Ян­ декс.Браузер» и открыв веб-приложение, для чего нужно запустить пример 26.1, вы должны увидеть в правом углу значок в виде концентрических кругов с точкой посредине, как показано Рис. 4.10. Redux DevTools в браузере на рис. 4.10. Если страница, на которой вы находитесь в данный момент, содержит стор Redux Toolkit, то значок расширения Redux DevTools отобразится с кругами светло-зеленого цвета. В противном случае круги будут темно-серые. Щелкнув на значке, вы увидите инструмент (рис. 4.11). 1 Расширение — дополнительная программа к какой-либо другой, основной программе, дополняющая основную узкоспециализированную функциональность. Redux DevTools — расширение, браузер — основная программа.
194  Глава 4 Состояние Рис. 4.11. Инструмент DevTools Подключение DevTools к приложению осуществляется автоматически. Посколь­ ку мы используем Redux Toolkit, достаточно создать стор с помощью функции configureStore. Библиотека сама сделает все необходимое. 4.10.3. Интерфейс DevTools Поскольку инструмент полностью графический, нам нужно в первую очередь сориентироваться в его панелях и областях. На рис. 4.12, в левом верхнем углу, есть «Экшены/настройки». На представленном снимке экрана показан вид при Рис. 4.12. Панели и области DevTools
4.10. Redux DevTools  195 выборе вкладки Actions. Вся работа делается на этой вкладке. Попрактиковаться в применении инструмента вы сможете, когда перейдете к упражнениям. В разделе дано описание всех действий, доступных на вкладке Actions. 4.10.4. Режимы экшенов Существует три режима: y записи; y сохранения истории; y блокировки изменений. Режим записи позволяет включать и отключать запись экшенов. Режим сохранения истории дает возможность при перезагрузке страницы (F5 или кнопка обновления в браузере) и сохранить состояние в DevTools, и оставить приложение в таком виде, будто перезагрузка не происходила. Режим блокировки изменений позволяет заблокировать возможность изменять состояние. Ваше приложение не сможет этого сделать, если данный режим активен. При перезагрузке он неактивен. 4.10.5. Действия с экшенами Реализовано в виде текстовых кнопок. Всего представлено четыре кнопки. y Reset — позволяет сбросить состояние стора до такого, которое было в момент создания приложения. y Revert — возвращает к последнему состоянию, зафиксированному с помощью кнопки Commit. Например, в приложении 26.1 вы получили товары, затем счетчик был 5, и если вы нажали Commit, то зафиксируете это состояние. Далее вы можете изменить состояние, например довести счетчик до 10 — тогда при нажатии на Revert состояние вернется к последнему, которое было зафиксировано, то есть полученные товары и счетчик 5. y Sweep — удаляет все неактивные экшены в области экшенов. Когда дойдем до этой области, рассмотрим пример. y Commit — фиксирует текущее состояние приложения и стирает все экшены из области экшенов. 4.10.6. Выбор приложения В браузере можно открыть несколько вкладок разных или одного и того же приложения, которое работает с состоянием с помощью библиотеки Redux Toolkit. В данном окне, представляющем собой выпадающий список, отображаются title тех HTML-страниц, у которых подключено хранилище Redux Toolkit. Сейчас выбрано JavaScript-приложение, подключенное к HTML-странице с заголовком «Современная фронтенд-разработка». Это как раз пример 4.43. Если вы его еще не запустили, то самое время сделать это. Перейдите в папку, содержащую листинг (папка 4/4.43), и выполните инструкции в файле README.md.
196  Глава 4 Состояние 4.10.7. Выбор инструмента обзора данных Выбор инструмента обзора данных определяет внешний вид области инструмента обзора данных. Всего есть четыре инструмента: y Inspector1; y Log monitor; y Chart; y RTK Query. Рассмотрим Inspector, представленный на рис. 4.13. Это инструмент, который позволяет: y при работе с хронологией2 экшенов (область 2 на рисунке) — просматривать ее, изменять, отключать экшены или же откатывать состояние приложения до состояния, которое было в момент отправки определенного экшена, а также фильтровать (область 1 на рисунке) для быстрого доступа к экшенам определенного типа; y когда в хронологии выбран экшен (или все сразу) — выбирать, что просматривать: источник данных (область 4 на рисунке), например экшен (Action), состояние (State), разницу с предыдущим экшеном (Diff), трассировку (Trace) или тесты (Test); Рис. 4.13. Области инструмента Inspector 1 2 Inspector (по-русски — «инспектор»). Это слово, которое переводится как «просматривать», состоит из английского глагола и приставки, используемой для образования существительного. В итоге получается существительное, означающее того или то, кто или что просматривает или позволяет это делать. В русском обиходе есть такая профессия — инспектор. Это человек, осуществляющий надзор и контроль за правильностью действий некоторых сотрудников или организаций. А вот устройства нет. Хронология — последовательность событий во времени.
4.10. Redux DevTools  197 y выбирать, в каком виде представлять данные (область 3 на рисунке), напри- мер в виде дерева (Tree), диаграммы (Chart) или в том виде, как если бы вы определили такие данные в коде сами (Raw); y просматривать детально все данные (область 5 на рисунке), относящиеся к источнику данных. Когда пользователь взаимодействует с приложением, определенные события ведут к отправке экшенов (dispatch) в стор, что приводит к изменению состояния. Экшен как минимум содержит тип, но может содержать и дополнительные данные payload. Инспектор — главный инструмент в DevTools. Его основная рабочая зона — хронология экшенов (2), детальный просмотр данных (5) и выбор источника (4). 4.10.8. Хронология экшенов На рис. 4.14 показана хронология экшенов и на третий снизу экшен наведен указатель мыши. В этом случае справа вместо указания времени отображаются две кнопки — Jump и Skip. Если выбрать Jump, то состояние приложения примет значения, которые появились в тот момент, когда в стор был отправлен данный экшен (см. рис. 4.14, 4.15). Рис. 4.14. Хронология экшенов Рис. 4.15. Использование кнопки Jump Если выбрать Skip, то состояние приложения примет значения, кроме значений выбранного экшена (рис. 4.16). Кроме того, необходимо отметить, как указывается хронология в самих экшенах (рис. 4.17). Напротив типа экшена указывается время. В самом первом экшене — время инициализации приложения в формате ЧЧ:ММ:СС:МС, где: y ЧЧ — часы, два знака; y ММ — минуты, два знака; y СС — секунды, два знака; y МС — миллисекунды, два знака.
198  Глава 4 Состояние Рис. 4.16. Использование кнопки Skip Рис. 4.17. Время, указываемое в экшенах Напротив остальных типов указывается время в таком же формате со знаком плюс. Однако вы видите, что значения не содержат часов и минут. В остальных экшенах показано, через какое время после инициализации приложения произошел тот или иной экшен. Поэтому перед значениями, отражающими время, там стоит символ «+». 4.10.9. Выбор источника Вы можете выбрать источник данных (область 4 на рис. 4.13), которые будут отображены в области детального просмотра (область 5). Всего доступны пять источников. y Action — позволяет посмотреть данные экшена. y State — позволяет посмотреть состояние, а также значения переменных, хранимых в состоянии. y Diff — позволяет посмотреть значение (-я) измененной переменной (-ых) в результате отправки экшена в стор. Показывает состояние до и после отправки экшена. y Trace — позволяет посмотреть, какие были задействованы модули JavaScript, в коде которых вызывается экшен. y Test — предоставляет код теста для конкретного экшена. 4.10.10. Вид представления данных и их детальный просмотр Количество выбранных экшенов в хронологии влияет на количество отображаемых данных в конкретном источнике. На вид данных влияет вид их представления. y Action. Если в области хронологии не выбран конкретный экшен, то будут отображены данные по последнему отправленному экшену. В противном случае — по выбранному вами из списка.
4.10. Redux DevTools  199 y State. Если в области хронологии не выбран конкретный экшен, то состояние будет отображено для всего состояния. Если же выбран конкретный экшен, то это все равно что ничего не выбрано. Однако если выбрать у экшена Skip, то данные будут исключены из состояния, а если Jump — состояние будет иметь значение, которое было на момент отправки этого экшена в стор. y Diff. Если в области хронологии не выбран конкретный экшен, то будут показаны предыдущее значение переменной состояния и новое значение переменной, на которую данный экшен влияет. То же самое — для конкретного выбранного экшена. Но если выбрать несколько экшенов, а хронология позволяет это сделать, то будет показана совокупная разница. y Trace. Трассировка показывается только для конкретного экшена либо для последнего, если не выбран ни один из области хронологии. y Test. Тест пишется для той библиотеки (фреймворка), которая выбрана в этой вкладке. Он составляется для последнего отправленного в стор экшена. Но можно выбрать конкретный экшен в хронологии, и тогда тест примет данные в утверждающем выражении, которые будут равны тому состоянию, которое было на момент отправки конкретного выбранного экшена в стор. 4.10.11. Диспатчер, слайдер Для использования слайдера нужно его отобразить, щелкнув на значке выглядит так, как показано на рис. 4.18. . Слайдер Рис. 4.18. Слайдер Слайдер содержит кнопки Play, Go back и Go forward. Использование Go back позволяет сделать то же действие, как если бы вы выбрали Jump на конкретном экшене в хронологии. По сути, если воспользоваться , то вы
200  Глава 4 Состояние увидите, что с каждым нажатием экшены в хронологии становятся неактивными. Также это влияет на состояние самого приложения на странице, которое будет изменять свои значения. Кнопка Play может быть применена, если в хронологии с помощью Jump выбран какой-либо экшен, кроме последнего, или действия в хронологии прошли с помощью Go back. Она ведет приложение вперед по хронологии через равные промежутки времени до последнего отправленного в стор экшена. Другой инструмент — диспатчер1 (рис. 4.19). Для его отображения необходимо нажать . Рис. 4.19. Диспатчер Диспатчер позволяет отправлять в стор экшены, как если бы они были отправлены туда вашим приложением вследствие действий пользователя. На рис. 4.19 видно, что при отправке экшенов посредством диспатчера состояние приложения изменилось. Более того, на странице, где действует приложение, изменилось значение счетчика. Для отправки экшена нужно задать объект, у которого есть единственное обязательное свойство type, остальные данные — по необходимости. На рисунке в стор отправляется экшен типа counter/increment. Далее нужно нажать кнопку Dispatch. 1 Диспатчер — происходит от английского глагола dispatch и приставки er, используемой для образования существительного. Означает что-то или кого-то, отправляющего (отсылающего) нечто без промедления.
4.11. Как разрабатывать приложение с использованием Redux Toolkit  201 Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 2.3. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 2.3. Выполните практическое задание. 4.11. Как разрабатывать приложение с использованием Redux Toolkit 4.11.1. Что позволяет делать Redux Toolkit (RTK) RTK позволяет сохранять состояние и работать с ним как с отдельной подсистемой. Благодаря этому у вас нет необходимости думать, где в иерархии приложения сохранять состояние, и в дальнейшем обращаться к этим частям для извлечения или изменения. Для выполнения обеих операций нужны методы. Более того, в комплекте с RTK поставляется RTK Query, с помощью которого можно кэшировать результаты запроса. Чаще всего RTK используется с библиотекой React, но мы ее пока не изучали, поэтому не будем задействовать понятия из нее. Однако RTK можно использовать и с Web Api, и с Node.js. В любом случае, что бы вы ни использовали, изменяются только технические детали, процедура разработки приложения — одна и та же. 4.11.2. Процедура разработки Процедура разработки такова. 1. Принять решение о необходимости использовать RTK. Если в приложении вам требуется отобразить изменяющиеся значения, на которые влияет пользователь, то понадобится состояние. 2. Определить области стора. Правило следующее: одна область для одной порции данных. Назовите эти области. 3. Реализуйте состояние в коде приложения с помощью RTK. Если вы только начали создавать приложение с нуля, то все области состояния можно поместить в одном файле (в модуле JS, TS) либо в отдельных модулях. Создайте для каждой области состояния слайс с обязательным значением по умолчанию. В зависимости от источника данных вы задаете или примитив либо структуру, используя createSlice, или же эндпоинты, используя createApi. Этот шаг на самом деле очень объемный и содержит семь действий, которые описываются далее в подразделе 4.11.4.
202  Глава 4 Состояние 4.11.3. Как выделить состояние и примеры Несмотря на то что по описанию самый большой пункт — третий, в действительности больше всего трудностей вызовет пункт первый. Шаг два — уточнение первого шага, а шаг три — реализация в коде. Нужно учесть, что состояние — свойство приложения во время его работы. Какое состояние нужно выделять? Если какое-то значение должно изменяться со временем в результате действия пользователя, то такое действие может быть состоянием (в том числе прямые действия пользователя). Приведу примеры состояний. y Добавление товаров в корзину. Значит, товары в корзине — состояние. y Викторина1 или игра. Баллы, полученные за что-то в игре, викторине, или выборы пользователя в игре (викторине), которые были запомнены, — состояние. y Получение списка товаров с сервера, например, при переходе в раздел или персональные предложения. Полученные товары — состояние. Также это могут быть действия относительно пользователя, но и в этом случае их активирует непосредственно пользователь. Вот другие примеры состояний. y Аутентификация. Пользователь аутентифицирован — состояние. y Разделы, которые посещались. y Действия пользователя на странице. И еще примеры состояний. y Выбор настроек. y Выбор фильтра (-ов) столбца таблицы. y Выбор разделов. y Введенный поисковый запрос. y Выбранный товар в разделе. y Промотка страницы на определенное количество пикселей. y Выбранный размер элемента. Все приведенные примеры взяты из моей деятельности. Хранить состояние можно для любого элемента, компонента, страницы или данных, которые могут изменяться в ответ на действия пользователя. 4.11.4. Шаг 3 процедуры разработки приложения Действия при реализации шага 3 следующие. 1. Реализуйте слайсы, вызывая функцию createSlice (createApi), для которой в объекте конфигурации задайте свойства: name (или reducerPath), initialState и reducers. С целью реализации третьего свойства можно задавать 1 Викторина — игра в ответы на вопросы (устные или письменные) из разных областей знания.
4.12. Что нужно изучить самостоятельно  203 2. 3. 4. 5. 6. 7. редьюсеры с помощью объекта или функции (с аргументом create , как описано в подразделе 4.8.9). Для createApi в объекте конфигурации задайте reducerPath, baseQuery и endpoints. Экспортируйте action creators, редьюсеры и селекторы. Передайте редьюсеры или объект, возвращенный вызовами createSlice либо createApi в configureStore. Извлеките из стора функции subscribe, getState, dispatch. Далее они используются по назначению в коде приложения. Подпишите на изменения колбэки. Передайте их функции subscribe. Создавая колбэки, объединяйте их с функцией getState и с селекторами, чтобы извлекать текущее нужное состояние. Это условный шаг. Вызов функции subscribe возвращает функцию, которую часто именуют unsubscribe. Вызывайте unsubscribe, чтобы прекратить вызов колбэка, подписанного на изменения в пункте 5. Объедините action creators с функцией dispatch и обработчиками событий, которые вызываются в ответ на действия пользователя или на другие события (сетевые запросы или setTimeout). Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 4.11. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 4.11. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 4.11 и ознакомьтесь с моим решением. 4.12. Что нужно изучить самостоятельно 4.12.1. Уже изучено Мы изучили достаточно, чтобы создавать приложения, но не изучили всего. В библио­теке Redux Toolkit есть дополнительная функциональность. В этом разделе дан список дополнительных функций, а также ссылки на страницы с руководством по ним на официальном сайте. 4.12.2. Redux Toolkit. Редьюсеры и экшены createReducer — функция, необходимая для создания функции редьюсера, которая внутренне использует библиотеку Immer, чтобы можно было писать код в мутабельном стиле. Кроме того, редьюсер, созданный на основе этой функции, может задействовать «builder колбэк». URL: https://redux-toolkit.js.org/api/createReducer.
204  Глава 4 Состояние createAction — вспомогательная функция для создания action creator и типа экшена. URL: https://redux-toolkit.js.org/api/createAction. createAsyncThunk — функция, которая принимает строку с наименованием типа экшена и колбэк, который возвращает промис. Генерирует типы экшенов жизненного цикла промиса на основе переданного вами префикса типа экшена и возвращает action creator thunk, запускающий обратный вызов промиса и делающий диспатч экшенов жизненного цикла на основе возвращенного промиса. Это обобщает стандартный рекомендуемый подход к обработке жизненных циклов асинхронных запросов. С данной функцией нужно хорошенько разобраться, чтобы она не вызывала умственного дискомфорта. URL: https://redux-toolkit.js.org/api/createAsyncThunk. createEntityAdapter — функция, которая генерирует набор предварительно встроенных редьюсеров и селекторов для выполнения операций CRUD над нормализованной структурой состояний, содержащей экземпляры объекта данных определенного типа. Эти функции-редьюсеры могут передаваться в createReducer и createSlice, а также использоваться в качестве «мутирующих» вспомогательных функций внутри createReducer и createSlice. URL: https://redux-toolkit.js.org/api/createEntityAdapter. 4.12.3. Redux Toolkit. Middleware createActionCreatorInvariantMiddleware — настраиваемая middleware, которая определяет, что action creator ошибочно отправлен в стор не как вызов функции, а была передана ссылка на данный action creator. URL: https://redux-toolkit.js.org/api/actionCreatorMiddleware. createListenerMiddleware — middleware, которая позволяет определить записи «прослушивателя», содержащие обратный вызов «эффекта» с дополнительной логикой, и способ указать, когда этот обратный вызов должен выполняться на основе отправленных действий или изменений состояния. Задумана как легкая альтернатива более широко используемому асинхронному промежуточному программному обеспечению Redux, такому как saga и observables. Хотя по уровню сложности и концепции она похожа на thunks, ее можно применять для воспроизведения некоторых распространенных шаблонов использования saga. URL: https://redux-toolkit.js.org/api/createListenerMiddleware. createDynamicMiddleware — middleware, позволяющая добавить middleware в цепочку после того, как стор инициализирован. URL: https://redux-toolkit.js.org/api/createDynamicMiddleware. 4.12.4. RTK Query Оптимистичные обновления — когда вы хотите обновить кэш данных немедленно, как только был вызван эндпоинт (mutation). Это бывает полезно при желании дать пользователю ощущение, будто обновленные им данные уже обновились, хотя на самом деле запрос может все еще выполняться. URL: https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#optimistic-updates.
4.12. Что нужно изучить самостоятельно  205 Пессимистичные обновления — когда вы желаете обновлять кэш данных только на основе ответа об обновлении от сервера. URL: https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates. Библиотеку React мы не изучали. Библиотека RTK Query предоставляет автогенерируемые хуки, как и экшены при использовании Redux Toolkit. Подобные хуки можно импортировать в компонент, вызвать его в теле и получить переменные состояния запроса к данным или их мутации. Это существенно экономит время. Рекомендую изучить самостоятельно после освоения библиотеки React. URL: https://redux-toolkit.js.org/rtk-query/usage/queries#performing-queries-with-react-hooks.
Глава 5 UI 5.1. О библиотеке React.js 5.1.1. Для чего нужна React Библиотека необходима для создания пользовательского интерфейса. 5.1.2. Необходимость библиотеки С деревом DOM браузера можно работать только через API. Если вы разрабатывали приложения, используя лишь DOM API, то знаете, что периодически приходится создавать фабрики, чтобы не дублировать код. При этом все равно нужно работать с каждым отдельным элементом и частенько находить какой-либо узел с помощью do­cument.querySelector(), а затем создавать элемент с помощью метода document.createElement или текстовый узел с помощью метода document.createTextNode и вставлять его в начало, конец либо в другую позицию. Операции вставки медленные, поэтому каждый раз необходимо писать приложение, оптимизируя эти операции. Библиотека React скрывает от вас многие ручные (назовем их так) операции и предлагает с помощью другого синтаксиса выполнять подобные шаги как бы между прочим. Она заменяет многие рутинные действия, которые становятся ненужными. Например, вместо количества кода, приведенного в листинге 5.1, вам достаточно написать количество кода как в листинге 5.2. Листинг 5.1. Код, задающий элемент span с текстом при использовании DOM API const span = document.createElement('span'); const text = document.createTextNode('Текст внутри span'); span.appendChild(text); Листинг 5.2. Код, задающий элемент span с текстом при использовании JSX <span>Текст внутри span</span> Теперь подумайте: сколько кода нужно написать с помощью DOM API, чтобы, например, создать структуру как в листинге 5.3? Листинг 5.3. Пример кода React <section> <form onSubmit={handleSubmit}> <label htmlFor="user-name">Имя пользователя: </label>
5.1. О библиотеке React.js  207 <input type="text" id="user-name" value={userNameValue} onChange={handleChangeName} /> <label htmlFor="user-password">Пароль: </label> <input type="password" id="user-password" value={passwordValue} onChange={handleChangePassword} /> <input type="submit" value="Отправить" /> </form> </section> Разумеется, ради упрощенной возможности нужно использовать библиотеку, которая поддерживает данный синтаксис, то есть React, создающую отношения между элементами. Вы просто пишете то, что рендерит компонент с помощью синтаксиса HTML, даже не задумываясь о том, что между тегами необходимо создавать отношения. React сама определяет, что делать со всем этим, позволяя сильно экономить время. Как это работает в самой React? Скорее всего, она не вставляет элементы друг в друга, а использует свойство родителя innerHTML1, которое представляет собой инструмент рендеринга, реализованный в браузере. Возможно, последние версии React используют другие механизмы, так как innerHTML небезопасен. Но нам что до этого? Нас интересует, как быстрее и понятнее описать компонент. Стоит ли использовать библиотеку React? Конечно. Более того, ее надо хорошенько изучить. Стоит ли изучать, как работает DOM API? Однозначно да. Для этого есть несколько причин. Одна из них состоит в том, что вы будете намного лучше понимать происходящее. Другая — вам периодически придется вызывать этот API из библиотеки React. 5.1.3. Ответственность библиотеки Когда речь идет о библиотеке React, обычно не углубляются в детали. Стоит отметить, что она подразумевают не одну, а две библиотеки: одна — React, другая — React-Dom. Библиотека React ответственна за создание модели дерева DOM со всеми необходимыми данными. DOM должна быть воплощена в браузере. Библиотека ReactDom дает указания непосредственно через WEB API (API браузера) воплотить это дерево DOM в документе. На заре создания React действительно была одна библиотека, а потом ее разделили на две вышеназванные. Причина очевидна: одна библиотека создает модель дерева в виде дерева функций, другая воплощает ее в браузере. 1 Свойство объекта DOM innerHTML — свойство, присваивая которому значение вы задействуете механизм браузера, который осуществляет рендеринг того, что будет указано значением данного свойства. Оно должно содержать html-разметку — точно такую же, как если бы вы писали просто HTML-файл. Если там присутствует элемент, например, img, у которого указан атрибут src, то будет произведен GET-запрос.
208  Глава 5 UI 5.1.4. Как устроено взаимодействие библиотек с браузером На рис. 5.1 показано, как код, использующий библиотеки React и React-Dom, подается программе-сборщику, например Webpack. Сборщик разделяет ваш код по типу файлов, а именно JavaScript, HTML и CSS. Далее файлы попадают в браузер и создают внешний вид страницы (см. рис. 5.1). Однако в браузере страница создается с помощью DOM API. Поэтому, когда вы пишете приложение, используя React, DOM API не касаетесь, но React и React-Dom в конечном счете преобразуют ваш код в программу, которая задействует DOM API. По-другому никак. Поэтому React и React-Dom — ваши посредники, упрощающие разработку и скрывающие от вас необходимость создавать приложение с помощью DOM API. Рис. 5.1. React и React-Dom — посредники между вашей программой и DOM API
5.2. Элемент, компонент, приложение  209 5.1.5. Немного о компоненте Чтобы приложение действительно было приложением, то есть системой, разработчики ввели идею компонента. Как мы уже выяснили, система всегда состоит из элементов. В библиотеке React таковым является компонент. Компонент может быть написан один раз и впоследствии использован множество раз. В нем объединены разметка (структура) и порядок ее обработки (данные, последовательность и др.). Очень удобно и практично. 5.2. Элемент, компонент, приложение Создавая веб-приложение, мы формируем его из «строительных блоков». Мы уже начали рассматривать UI приложения, за которое отвечает библиотека React. Давайте посмотрим на рис. 5.2. Рис. 5.2. «Строительные блоки» приложения
210  Глава 5 UI Как видно, веб-приложение — это целый набор библиотек, отвечающих за разные функциональные стороны. React задает и управляет структурой UI всего приложения, при этом UI состоит из компонентов. В данном случае React — подсистема, создающая структуру приложения. Другими подсистемами являются: y библиотека управления состоянием, например, только что изученная Redux Toolkit; y библиотека отправки запросов RestAPI; y библиотека управления поддеревьями; y другие библиотеки, например, для создания GET-запросов в формате OData1. Вернемся к библиотеке React. Элементом в ней является JSX-тег, который представляет HTML-тег. Элементы нужны для описания структуры UI. Компонент — единица интерфейса в библиотеке React и элемент структуры приложения. Он строится из элементов и является переиспользуемым. 5.3. Вывод компонента: JSX 5.3.1. Чем является компонент Компонент — это обычная функция. В нем можно выделить три важные составляющие, такие как: y вывод компонента; y обработка данных; y эффекты. Вывод компонента — обязательная часть, остальные — по необходимости. Схематично компонент можно представить так, как показано на рис. 5.3. В коде программы компонент может выглядеть как в листинге 5.4. Рис. 5.3. Схематичное изображение компонента Листинг 5.4. Возможный код компонента import React, { useState, useEffect } from 'react'; export const ExampleComponent = ({ title }) => { const [count, setCount] = useState(0); const handleClick = () => setCount(count => count + 1); useEffect(() => { document.title = `Вы нажали на кнопку ${count} раз`; }); 1 OData — набор правил, устанавливающий наименования параметров и их значений. Применяется не только к GET-запросам. Позволяет не придумывать каждый раз параметры и значения, а использовать стандартизированный формат.
5.3. Вывод компонента: JSX  211 } return ( <section> <h3>Добрый день {title}!</h3> <p> Вы нажали на кнопку {count} раз. </p> <button onClick={handleClick}>Нажать</button> </section> ); В браузере это будет выглядеть так (рис. 5.4). Рис. 5.4. Компонент ExampleComponent в браузере Далее в этой главе будут часто использоваться и схематическое изображение компонента, и листинги компонентов, и снимки результатов их вывода. Приступаем к внимательному изучению кода компонента. На рис. 5.5 приведен код компонента, набранный в IDE1 VisualStudioCode2. 1 2 IDE (Integrated Development Environment) — в дословном переводе «интегрированная среда разработки» (ИСР). Имеется в виду программа, в которой есть текстовый редактор кода, функция подсветки синтаксиса многих языков программирования и интегрированный терминал для взаимодействия с операционной системой. А кроме того, встроенный графический клиент для системы контроля версий — например, Git, Mercurial. Коммерческие версии IDE также предусматривают меню для работы с компиляторами и системами сборки проектов и отладчик (debugger). IDE объединяет множество отдельных программ, которые требуются на разных этапах создания вашей программы в рамках того или иного проекта. При взаимодействии с ней вы переходите от одного этапа к другому, практически не замечая, что на самом деле взаимодействуете с разными программами. Сокращение ИСР разработчики не используют, только английскую аббревиатуру IDE. IDE VisualStudioCode (IDE) от Microsoft. Бесплатная программа. Существуют версии для разных операционных систем. Я использую версию для ОС Windows. Скачать IDE VisualStudioCode можно по адресу https://code.visualstudio.com.
212  Глава 5 UI Рис. 5.5. Код компонента ExampleComponent в IDE VisualStudioCode Как видите, ExampleComponent — обычная функция. Компонентом является лишь код, расположенный в строках 3–21. Строка 1 содержит статический импорт используемых в компоненте зависимостей и сама компонентом не является. Это модуль JS. Строки с 12-й по 20-ю содержат код, обозначенный на рисунке как «Вывод». Строки с 4-й по 6-ю содержат код «Обработка данных», а строки с 8-й по 10-ю — «Эффекты». В следующих разделах мы детально изучим составляющие «Обработка данных» и «Эффекты», а «Вывод» рассмотрим в этом разделе. 5.3.2. История развития компонентов в React С момента появления библиотек компоненты в React создавались не на основе функций, а на основе классов и обладали гораздо большими возможностями по сравнению с компонентами на основе функций. Так, в них изначально предусматривалась возможность хранить состояние и методы жизненного цикла компонентов. При этом компоненты на основе функций могли лишь принимать свойства от родителя и выводить то, что передано через свойства, либо выражения на их основе, а также константные значения, объявленные внутри компонента. Пример компонента на основе класса приведен в листинге 5.5.
5.3. Вывод компонента: JSX  213 Листинг 5.5. Компонент на основе класса, обладающий той же функциональностью, что и компонент в листинге 5.4 import React from 'react'; export class ClassComponent extends React.Component { constructor(props){ super(props); } this.state = { count: 0, } handleClick(){ this.setState(state => ({ count: state.count + 1, })); } componentDidUpdate(){ if (prevState.count !== this.state.count) { document.title = `Вы нажали на кнопку ${this.state.count} раз`; } } } render(){ return ( <section> <h3>Добрый день {this.props.title}!</h3> <p> Вы нажали на кнопку {this.state.count} раз. </p> <button onClick={() => this.handleClick()}>Нажать</button> </section> ); } В самом начале, чтобы создать компонент с такой же функциональностью, как в листинге 5.4, нужно было использовать класс и писать компонент как в листинге 5.5. Практически максимум того, что мог компонент, созданный на основе функции, показан в листинге 5.6. Листинг 5.6. Возможности компонента, созданного на основе функции export const FunctionComponent = ({ title, count, onClick }) => { return ( <section>
214  Глава 5 UI } ); <h3>Добрый день {title}!</h3> <p> Вы нажали на кнопку {count} раз. </p> <button onClick={onClick}>Нажать</button> </section> Поскольку такой компонент не хранил состояние, все изменяющиеся данные в него передавали из родителя, в том числе функцию обратного вызова. Компонент на основе класса обладал методами жизненного цикла, выполняемыми в определенные моменты его жизни. Компонент мог принимать пропсы и хранить состояние. На официальном сайте библиотеки React так и писали: компонент на основе классов может принимать свойства и хранить внутреннее состояние. То есть на это обращали внимание. А про компоненты на основе функций было сказано: могут делать только вывод. Вся нужная функциональность в классовом компоненте размещалась в методах жизненного цикла и в методе render, ответственном за вывод компонента. 5.3.3. Обработка данных современного компонента на основе функции Некоторое время назад все изменилось. В React добавили кое-что, что помогло компонентам на основе функций значительно расширить свои возможности. С этого момента библиотека сильно изменилась. Сегодня компоненты на основе классов не пишут, хотя React позволяет их использовать исключительно для обратной совместимости. Почему так? Потому что компоненты на основе функций уже обладают бо́льшими возможностями и лучшей читаемостью, чем прежде. Если раньше в классовом компоненте отдельно выделялось состояние, то сегодня в функциональном компоненте этого не делают, поскольку возможностей у компонента на основе функций больше, а управление внутренним состоянием — просто одна из возможностей. А самая первая возможность — обработка данных (назовем ее так). Вы можете использовать ссылки на DOM-элементы, управлять действиями пользователя и сохранять историю, хранить изменяющиеся данные и изменять их, получать уникальные значения, вызывать разные функции для обработки данных, оптимизировать число рендеров компонента и его дочерних элементов — и все это прямо в коде функционального компонента. На рис. 5.5 обработка данных показана в строках 4–6. 5.3.4. Эффекты компонента Среди возможностей компонента есть одна, которую я выделил отдельно. Речь об эффекте. Это что-то вызывающее и создающее какие-либо побочные эффекты, например изменение DOM-элемента, такого как title, HTML-страницы. Другим примером здесь может быть сетевой запрос с помощью функции fetch. На рис. 5.5 эффект показан в строках 8–10.
5.3. Вывод компонента: JSX  215 5.3.5. Вывод компонента и JSX Обработка данных и эффекты — встроенные возможности в React, которые будут рассмотрены в следующих главах, а сейчас мы детально изучим вывод. Разработчики React совместили HTML и JavaScript, так получился JSX. Зачем понадобилось их объединять? Это сделали для того, чтобы было удобно одновременно задавать структуру пользовательского интерфейса и порядок ее обработки. Но главный плюс — скорость. JSX может располагаться только в выводе или в присвоении переменной. React-элемент — простой объект, описывающий какой-либо HTML-тег или React-компонент. Под простым объектом понимается JavaScript-объект. Помните, ранее мы говорили, что есть две библиотеки: React и React-Dom. Так вот в React элемент — это объект, содержащий сведения (например, о DOM-элементе), но пока данный объект не передан в библиотеку React-Dom, он продолжает оставаться простым JavaScript-объектом. А когда будет передан, на его основе с помощью DOM API будет создан настоящий DOM-элемент (например, div или span). Почему тогда приложение рассматривается как система, состоящая не из Reactэлементов, а из компонентов? Ведь React-элемент — самая неделимая часть. Причина проста и заключается в том, что компонент содержит не только элементы. Элементы — это то, что возвращает компонент, но он также содержит кое-что другое. В следующих разделах мы детально рассмотрим компонент, а сейчас продолжаем разговор о выводе. Как объект элемент содержит информацию о типе (например, div, span), его атрибутах и дочерних элементах или других компонентах, в свою очередь состоящих из элементов? Для создания элементов лучше всего подходит синтаксис JSX. Данная аббревиа­ тура — сокращение JavaScript XML. Относительно JavaScript, я думаю, понятно. Но при чем здесь XML? HTML, помещенный в JavaScript, выглядит как XML — отсюда и наименование. Данный синтаксис упрощает создание React-элементов, позволяя использовать HTML-подобный код в JavaScript-коде. Сегодня уже не надо никому объяснять, что это более быстрый способ создания компонентов. Как было сказано ранее, JSX нельзя использовать везде, где можно использовать JavaScript, — только со специальным обработчиком синтаксиса. Иначе при попытке прочитать JSX-код браузер выдаст ошибку. Внутри React-компонентов JSX тоже используется не везде. Компонент может или возвращать JSX-разметку, как в листинге 5.1, или присваивать ее переменной, как в листинге 5.2. JSX-код преобразуется в JavaScript и HTML. Специальный преобразователь, используемый программой типа сборщика, например webpack, при чтении файла с JSX отделяет JavaScript-код от HTML и помещает их в разные файлы. После этого данные файлы могут быть использованы браузером. Приведу фрагмент кода компонента, где используется элемент, чтобы у вас было наглядное представление и понимание того, чем является элемент (листинг 5.7).
216  Глава 5 UI Листинг 5.7. Элемент div //... return ( <div> Это элемент, который будет превращен в HTML-элемент div </div> ); Такой вот простой фрагмент, показывающий, что div — элемент. Элементы могут быть вложены друг в друга и образовывать любую иерархию тегов, как показано в листинге 5.8. Листинг 5.8. Вложенные элементы, образующие иерархию // ... const greeting = <span>Привет </span>; return ( <main> <h1>{greeting}, Мир!</h1> <blockquote> О сколько нам открытий чудных готовит просвещенья дух... </blockquote> </main> ); 5.3.6. Что возвращает компонент Из компонента можно вернуть JSX, как, например, в листинге 5.8. А также null, undefined, строку либо массив из null, строк или JSX-элементов. Далее в листингах показаны примеры возврата таких значений. А чтобы увидеть, как в браузере отображается то или иное возвращаемое значение, запустите коды примеров (листинги 5.9–5.14). Листинг 5.9. Возврат из компонента значения null // ... return null; Листинг 5.10. Возврат из компонента значения undefined // ... return undefined; Листинг 5.11. Возврат из компонента строки // ... return "Это просто строка"; Листинг 5.12. Возврат из компонента массива строк // ... return ["Первая", "Вторая"];
5.4. Вывод компонента: композиция  217 Листинг 5.13. Возврат из компонента массива JSX-элементов // ... return ( [<div>Первый div</div>, <div>Второй div</div>] ); Листинг 5.14. Возврат из компонента массива со значениями null // ... return ( [null, null, null] ); Листинг 5.14 может показаться бессмысленным, однако станет таковым, только если возвращать самостоятельно вписанные значения. Если же вы возвращаете переменные, которые могут принимать значение null, то, запустив код листинга, сможете понять, что вас ждет. Кроме того, следует заметить, что, возвращая массив JSX-элементов, вы увидите в консоли предупреждение от React. Этот момент мы рассмотрим позже. 5.4. Вывод компонента: композиция 5.4.1. Композиция компонентов В предыдущем разделе вы узнали, как компонент выводит элементы. Однако сила React в переиспользовании компонентов. Один раз написав компонент, в дальнейшем вы можете использовать его столько раз, сколько посчитаете нужным. Делается это с помощью композиции. Композиция компонентов в библиотеке React означает создание сложных компонентов путем комбинирования того, что выводит один компонент, с тем, что выводит другой. Чтобы использовать ранее созданный компонент, его нужно импортировать в модуль, где объявлен другой компонент и предполагается использовать ранее созданный, или объявить в том же модуле, в котором предстоит использование, и поместить тег с именем ранее созданного компонента в вывод (в инструкцию return), как показано в листинге 5.15. Листинг 5.15. Повторное использование ранее объявленного компонента import React, { useState, useEffect } from 'react'; export const ClickCounter = ({ limit }) => { const [count, setCount] = useState(0); const handleClick = () => setCount(count => count === limit ? count : count + 1); useEffect(() => { document.title = `Вы нажали на кнопку ${count} раз`; }, [count]);
218  Глава 5 UI return ( <div> <p> } ); Вы нажали на кнопку {count} раз. </p> <button onClick={handleClick}>Нажать</button> </div> export const Greeting = ({ title }) => { return ( <section> <h3>Добрый день {title}!</h3> <ClickCounter limit={5} /> </section> ); } Схематично показать эту композицию можно следующим образом (рис. 5.6). С помощью графа данное отношение можно изобразить так (рис. 5.7). Рис. 5.6. Отношение композиции между двумя компонентами Рис. 5.7. Граф композиции между двумя компонентами На графе видно, что компонент ClickCounter имеет направленное отношение с Greeting. Таким образом мы показываем, что ClickCounter передает свой вывод, и не более. При этом на схеме показаны стрелки в обе стороны. Дело в том, что на схеме мы показываем движение данных в обе стороны (туда и обратно), а на графе — не движение данных, а отношение использования. То есть
5.4. Вывод компонента: композиция  219 Greeting использует ClickCounter. Если на графе изобразить не дугу, а ребро, то будет неясно, какой компонент использует другой у себя в выводе. На графе показан основной способ композиции компонентов. На практике сочетание компонентов частенько гораздо сложнее. Компонент, использующий в своем выводе другой компонент, называется родительским, а компонент, который используют, — дочерним. 5.4.2. Более сложная композиция Теперь рассмотрим более сложные композиции, а на практических занятиях потренируемся в их применении. Первая композиция — пара абсолютно одинаковых элементов (могут быть и разные), как показано на рис. 5.8 и 5.9 и в листинге 5.16. ({ title, pause, onClick }) ({ title, pause, onClick }) Рис. 5.8. Композиция нескольких дочерних компонентов Рис. 5.9. Граф композиции нескольких дочерних компонентов Листинг 5.16. Композиция компонентов ChessClock и Clock import React, {useState} from 'react'; import { Clock } from './Clock'; export const ChessClock = () => { const [playersClockState, setPlayersClockState] = useState([true, false]); const [player1ClockState, player2ClockState] = playersClockState; const togglePlayerClockState = () => setPlayersClockState(prev => [...prev].reverse()); return ( <section className="chess-clock">
220  Глава 5 UI } ); <Clock onClick={togglePlayerClockState} pause={player1ClockState} title={'Игрок 1'} /> <Clock onClick={togglePlayerClockState} pause={player2ClockState} title={'Игрок 2'} /> </section> В данном случае у нас есть композиция двух компонентов Clock в компоненте ChessClock. Рассмотрим ситуацию, когда осуществляется композиция большего числа компонентов (рис. 5.10). Их количество будет меняться программно, а не задаваться вручную. В этом примере компонент ChessClock является родительским, а два компонента Clock — дочерними (листинг 5.17). ({ player1State, player2State}) ({ title, pause, onClick }) ({ title, pause, onClick }) Рис. 5.10. Композиция компонентов, где один дочерний компонент является родителем другого Еще одна распространенная ситуация — когда дочерний компонент является родительским для другого компонента (рис. 5.11).
5.4. Вывод компонента: композиция  221 Пример в листинге 5.17 и на рис. 5.11 показывает типичную для приложений композицию. Здесь есть высота дерева, равная 3: App — ChessClock — Clock. В реальном приложении высота дерева от корня обычно достигает 6–7. Больше встречается редко. А вот количество дочерних элементов может сильно варьироваться. Например, компонент, где выводятся строки таблицы. В нем количество дочерних элементов будет равно как минимум количеству строк плюс другие компоненты интерфейса таблицы. И таких строк иногда бывает очень много. Листинг 5.17. Композиция компонентов App, ChessClock и Clock export function App() { return ( <div className="App"> <ChessClock player1State={true} player2State={false} /> </div> ); } Рис. 5.11. Граф вертикальной композиции, где один дочерний компонент является родителем другого export const ChessClock = ({player1State, player2State}) => { /* Реализация обработки данных*/ } return ( <section className="chess-clock"> <Clock onClick={togglePlayerClockState} pause={player1ClockState} title={'Игрок 1'} /> <Clock onClick={togglePlayerClockState} pause={player2ClockState} title={'Игрок 2'} /> </section> ); export const Clock = ({title, onClick, pause}) => { /* Реализация обработки данных и эффектов */ }; return ( <div className="clock"> <h3 className="clock__title">{title}</h3> <button className="clock__btn" onClick={handleClick}>{actionTitle} </button> { time.min } : { time.sec } </div> ); Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.4. Выполните упражнения по порядку.
222  Глава 5 UI y Затем откройте папку problems и в ней — подпапку 5.4. Выполните практи- ческое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.4 и ознакомьтесь с моим решением. 5.5. Обработка данных и их источники: свойства 5.5.1. Источники данных компонента Компонент может работать с данными из пяти разных источников. 1. Свойства самого компонента, так называемые пропсы (props). 2. Состояние, хранимое внутри компонента, так называемый стейт (state). 3. Данные, загруженные с помощью запроса. 4. Данные, хранимые в хранилищах и доступные с помощью API. 5. Константы, объявленные внутри компонента. В этом разделе мы рассмотрим свойства самого компонента — первый и пятый пункты (константы). В следующем разделе — второй пункт (состояние). Загруженные с помощью запроса данные изучим в разделе 5.7, а глобальные данные — в разделе 5.18. 5.5.2. Что такое свойства Компонент является функцией. Как у любой функции, у него могут быть объявлены параметры1. Компонент React имеет единственный параметр, имя которого может быть любым, но по негласному соглашению обычно используют props. Рассмотрим листинг 5.18. Листинг 5.18. Объявление и использование props export const Component = (props) => { } 1 return ( <React.Fragment> <h3>Привет { props.name }!</h3> <p> У вас { props.messageCount } непрочитанных сообщений </p> </React.Fragment> ); Параметр — переменная (-ые), не имеющая (-ие) конкретного значения в момент объявления функции. Параметры приводятся в круглых скобках, через запятую, в специально отведенном для них разделе параметров функции. В теле функции они используются как переменные. Впоследствии при вызове функции ей передаются аргументы, которые становятся значениями параметров. Таким образом, очевидно, что значение параметра определяется в момент вызова функции и от вызова к вызову может принимать любое допустимое значение.
5.5. Обработка данных и их источники: свойства  223 Как видно из кода, props — объект, который содержит свойства. Это и есть свойства компонента. Подход, описанный выше, предусматривающий доступ к свойствам с помощью параметра props, мало кто применяет. Принято использовать деструктуризацию1 параметра props, что сильно облегчает чтение, так как в данном случае сразу перечислены все свойства компонента. Свойства в компоненте нужно объявлять так, как показано на примере Component в листинге 5.19. Листинг 5.19. Объявление свойств с помощью деструктуризации export const Component = ({ name, messageCount }) => { } return ( <React.Fragment> <h3>Привет { name }!</h3> <p> У вас { messageCount } непрочитанных сообщений </p> </React.Fragment> ); 5.5.3. Константы Константы имеет смысл объявлять в самом компоненте и использовать там же. Обычно они применяются в месте объявления и не передаются в дочерние компоненты или компоненты-потомки. Но есть минимум один случай, когда необходимо разделить изначальное (инициализирующее) состояние, объявленное в одном компоненте, и дочернее, или потомка, — в таких обстоятельствах константа может быть передана. Листинг 5.20 дает пример использования константы. Листинг 5.20. Объявление и использование константы export const LengthCm = ({ inchLength }) => { const inchCoefficient = 2.54; } 1 return ( <div> Длина в дюймах { inchLength } в сантиметрах составляет { inchCoefficient * inchLength } </div> ); Деструктуризация — это специальное действие по получению значения переменной, которая расположена в структуре данных. Для извлечения значения используется способ доступа не с помощью точки, как в объектах, и не с помощью индекса, как в массивах, а с помощью указания устройства структуры данных вплоть до расположения нужного значения, которому присваивается имя. Это имя становится идентификатором в той области видимости, где производится деструктуризация.
224  Глава 5 UI Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.5. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.5. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.5 и ознакомьтесь с моим решением. 5.6. Обработка данных и их источники: состояние 5.6.1. Состояние Состояние компонента — это значение всех его переменных в конкретный момент времени. Под конкретным моментом времени понимается момент, когда приложение запущено и находится в ожидании следующего обновления. Состояние — это то, что управляет внешним видом приложения. Изменение состояния приводит к изменению внешнего вида приложения. Каждый компонент может иметь собственное состояние. Но приложение может включать и состояние, которое хранится отдельно, но влияет на него. Об использовании такого состояния речь пойдет в разделе 5.18. Почему вероятностное «может иметь», а не твердое «имеет»? Дело в том, что это не является обязательным условием компонента. Состояние тоже источник данных компонента. Компонент может передавать свое состояние другим компонентам. По большей части это касается дочерних компонентов, но возможна и передача родительскому. И главное, что вы должны уяснить: состояние не идея, придуманная разработчиками библиотек React, Redux или какой-либо другой. Это, как говорилось в разделе 3.4, одна из характеристик системы, существующая ровно столько, сколько существует Вселенная. Фронтенд-приложение тоже является системой. Поэтому разработчики библиотек обеспечивают ему поддержку, чтобы оно могло иметь место, а вы — взаимодействовать и получать результаты, которые в том числе основываются на ваших предыдущих действиях. Поэтому изучаемый раздел насыщен данными. 5.6.2. Как создается состояние компонента Состояние касается функционирования всего приложения, поэтому оно изменяется (потенциально может измениться) с начала запуска и до конца работы. Состояние может быть как у всего приложения, так и у компонента. Это значит, что для хранения состояния нужен способ, с помощью которого можно контролировать изменения и реагировать на них нужным образом.
5.6. Обработка данных и их источники: состояние  225 Такой способ, для работы с состоянием в компонентах на основе функций, существует. Это хук useState. Поскольку не все знакомы с хуками, хотя их в React, начиная с версии 16.8, добавлено немало, мы сначала выясним, что это такое, а затем приступим к useState. Впоследствии мы изучим другие хуки, дающие много нужных функциональных возможностей компонентам. 5.6.3. Хук Слово «хук» (от англ. hook), переводимое как «крючок» или «зацеплять», в IT-сфере означает «специальную точку входа, присоединения; зацепку. Место в программе, куда сторонние разработчики могут подсоединить дополнительный код (обычно для расширения ее функциональных возможностей). Аналогичные точки могут существовать и в аппаратуре, однако там это более строго определено на уровне разъемов, перемычек и интерфейсов». Это определение взято из словаря Э. Пройдакова и Л. Теплицкого1. Таким образом, хук давно существует в IT, и создатели React внедрили его в библиотеку. Давайте посмотрим, где еще можно встретить хуки. Они относятся к механизму или функциональности, позволяющей программисту расширять либо изменять поведение программы или системы. В данном случае хук является неким открытым интерфейсом, позволяющим встраивать пользовательский код для выполнения определенных действий или реагирования на события. Вот примеры в информационных технологиях. y Хуки в системах контроля версий, такие как Git или Subversion, позволяют программистам выполнять определенные действия перед или после выполнения операций с файлами или репозиториями. y Хуки в сетевых системах позволяют программистам встраивать свой код для обработки определенных событий сети, например приема и передачи данных или управления соединениями. y Хуки в веб-разработке позволяют разработчикам изменять поведение вебстраницы или взаимодействовать с определенными событиями, такими как щелчок кнопкой мыши или отправка формы. В целом определение и функциональность хука зависят от конкретных технологий или языков программирования, в которых он применяется. 5.6.4. Хуки в React В React хуки позволяют расширять возможности компонентов конкретной функциональностью. Например, хук useState позволяет управлять состоянием, useContext — передавать данные от родительского компонента по всему дереву дочерних элементов, а хук useRef способен помочь сразу в двух случаях: сохранить 1 Пройдаков Э. М., Теплицкий Л. А. Большой англо-русский словарь по вычислительной технике и информационным технологиям (ВТ/ИТ). — М.: РТСофт, 2015. — 1600 с.
226  Глава 5 UI значения данных и предоставить ссылку на DOM-элемент. Можно утверждать, что это дополнительная функциональность, которая требуется чаще всего. Библиотека React содержит хуки, необходимые в компонентах. Однако главное в хуках не специализированность. Ее вы можете создать и самостоятельно. Главное то, что хуки осуществляют мемоизацию. Мемоизация1 — способ оптимизации, применяемый преимущественно для ускорения выполнения программ (за счет дополнительных затрат памяти). Он предусматривает запоминание результатов вычисления функций, с тем чтобы при последующем вызове функции при тех же входных данных можно было не повторять вычисления, а сразу выбирать результаты. Вместо слова «мемоизация» допустимо использовать термин «запоминание (сохранение) результатов». Мы с вами изучим хуки useState, useEffect, useContext, useCallback, useMemo, useReducer, useRef и useId, а также как создать свой собственный. Другие — такие как useImperativeHandle, useDebugValue, useDefferedValue, useInsertionEffect, useLayoutEffect, useSyncExternalStore и useTransition — вы сможете изучить самостоятельно. 5.6.5. Хук useState Состоянием компонента можно управлять с помощью трех хуков: useState , useReducer и useRef. Второй и третий хуки мы рассмотрим в разделах 5.16 и 5.17. Третий хук, useRef, отличается в плане работы с состоянием. Когда мы дойдем до нужного раздела, вы об этом узнаете. К трем названным хукам можно смело добавить работу с контекстом, но, поскольку это целый механизм, я не стал его добавлять, а выделил в отдельную область. Ему посвящен раздел 5.11. Начнем с примера листинга 5.21, где компонент хранит состояние. Листинг 5.21. Код компонента Score с состоянием import React, { useState } from 'react'; export const Score = () => { const [score, setScore] = useState(0); } 1 return ( <div> <button title='-' onClick={() => setScore(prevState => prevState === 0 ? 0 : prevState - 1)}>-</button> <span>{ score }</span> <button title='+' onClick={() => setScore(prevState => prevState + 1)}>+</button> </div> ); Там же.
5.6. Обработка данных и их источники: состояние  227 Компонент Score позволяет, нажимая кнопки, прибавлять и отнимать некие очки за что-либо, значение которых не может быть ниже нуля. Для управления очками у нас есть константа score и функция setScore. Сначала изучим составляющие, с которыми вам придется иметь дело при разработке, затем разберем механизм работы. Ведь score — это константа, а состояние должно изменяться (рис. 5.12). Рис. 5.12. Возвращаемые хуком useState значения и инициализация константы score Вызов хука возвращает массив, состоящий из двух элементов — переменной, содержащей текущее состояние (первый элемент массива), и функции (второй элемент массива). Функция нужна для изменения значения состояния. При вызове хука useState ему нужно передать начальное значение, которое будет присвоено переменной, в нашем случае — константе. В листинге 5.21 видно, что score используется исключительно для вывода текущего состояния. Для его изменения применяется функция setScore — в колбэках, вызываемых при нажатии на кнопки. Обратите внимание на аргумент функции setScore — это колбэк-функция. Колбэк prevState => prevState + 1 при вызове получит в переменную prevState текущее значение константы score, а затем с этим текущим значением будет произведено сложение с единицей. Тело функции колбэка, которая является аргументом в функции, изменяющей состояние, в нашем случае setScore, может быть более сложным. Для этого его надо поместить в блок {}. Функция, изменяющая состояние, не обязательно должна принимать колбэк. Можно просто помещать конкретное значение или выражение, как показано в листинге 5.22. Листинг 5.22. Разные выражения в setScore для вычисления нового состояния export const Score = () => { const [score, setScore] = useState(0); return ( <React.Fragment> <div>
228  Глава 5 UI } ); <h3>Изменение состояния с помощью score</h3> <button title='-' onClick={() => setScore(score === 0 ? 0 : score - 1)}>-</button> <span>{ score }</span> <button title='+' onClick={() => setScore(score + 1)}>+</button> </div> <div> <h3>Изменение состояния с помощью функции, тело которой содержит блок кода</h3> <button title='-' onClick={() => { setScore(prevState => { return prevState === 0 ? 0 : prevState - 1; }); }}>-</button> <span>{ score }</span> <button title='+' onClick={prevState => { setScore(prevState => { return prevState + 1; }); }}>+</button> </div> </React.Fragment> Каким бы способом вы ни воспользовались для передачи нового состояния в setScore, главное, чтобы выражение правильно считало следующее состояние. В данном примере нет ничего хитрого — он более чем нагляден. В практических заданиях мы рассмотрим способы изменения состояний, хранящихся в массивах и объектах. Здесь же приведен материал для понимания работы хука. Важное замечание! Изменение состояния возможно только с помощью функции, возвращаемой хуком useState. Возвращаемые хуком значения объявлены с помощью ключевого слова const. С функцией все понятно. А вот с константой score — не так очевидно, скорее даже наоборот. Есть вопрос: почему состояние, которое будет изменяться, помещено в константу? 5.6.6. Изменение состояния и рендер Когда изменяется состояние компонента, React осуществляет рендер компонента, в котором изменилось состояние. Чаще говорят «перерендер» или «ререндер». Рендер (он же — перерендер или ререндер) — это вызов функций библиотекой React. Разумеется, функцией является компонент. Английское слово render переводится как «выводить», «отображать». Эта функциональность встроена в библиотеку React, а причиной для рендера является изменение состояния. Если состояние изменилось, произойдет рендер. Рассмотрим, как меняется состояние в компоненте с помощью вызова функции, возвращаемой хуком useState, а затем — как это влияет на рендер.
5.6. Обработка данных и их источники: состояние  229 5.6.7. Изменение состояния в компоненте Уже очевидно, что вызов функции, изменяющей состояние, ведет к его изменению. Стоит, однако, заметить, что React не будет ничего предпринимать, если вызвать функцию, изменяющую состояние, но передать в качестве аргумента предыдущее состояние, то есть неизмененное. Однако если вы передали новое значение состоя­ ния, то произойдет то, что показано на рис. 5.13. Рис. 5.13. Порядок действий при вызове setScore Как видно из рисунка, компонент Score, который является функцией, повторно вызывается библиотекой. Соответственно, все внутренние функции, располагающиеся в компоненте, также вызываются повторно — иначе и быть не может. На шаге, показанном на рисунке, хук useState возвращает массив. Получается, у нас как бы заново инициализируется константа score, будто это происходит в первый раз. Мы не присваиваем новое значение существующей переменной. Вместо этого создается новый объект с новым состоянием, имеющим идентификаторы, как и в предыдущем компоненте Score, который находился в прошлом состоянии. Поэтому при деструктуризации значения, возвращаемого массивом, можно использовать const, а не let. 5.6.8. Рендеры и их отображение на веб-странице Исходя из информации, представленной выше, можно сказать, что контроль поведения приложения (той части, с которой взаимодействует пользователь) — это контроль рендеров с помощью кода. Пояснить сказанное можно на основе
230  Глава 5 UI нашего компонента, который при нажатии на кнопки «плюс» и «минус» позволяет увеличить или уменьшить цифру между кнопками, как показано на рис. 5.14. Рис. 5.14. Компонент и изменение страницы при нажатиях на кнопки Поведение вполне ожидаемое, число между кнопками изменяется. Чтобы цифра изменялась и чтобы после изменения состояния в компоненте данные могли быть обновлены на самой веб-странице, после рендера реализуются следующие этапы, которые и создают изменения на странице. Это уже не касается хука useState, но именно он инициирует цепочку действий. Изобразим это графически. Для упрощения понимания примем за круг компонент, за стрелку (дуга в терминах графов) — использование компонента в выводе другого компонента (рис. 5.15). Рис. 5.15. Компонент и изменение страницы при нажатиях кнопки На рис. 5.15 наглядно показаны три шага. Первый — вызов useState и изменение состояния. На втором шаге React обновляет компонент. На третьем библиотека React передает дерево компонентов с измененным состоянием в библиотеку
5.6. Обработка данных и их источники: состояние  231 React-Dom, которая, используя API DOM, вносит изменения в страницу — и пользователь видит новое состояние. Следует добавить, что React оптимизирует изменения. Таким образом, на странице изменяется не все приложение, начиная с первого компонента, а лишь та его часть, которая подверглась корректировке. В нашем случае это участок <span>1</span>. Все остальное — без изменений. 5.6.9. Хуки React и рендеры Самое важное отличие хуков от функций заключается в следующем. Обычная функция при перерендерах не будет сохранять состояние, если вы целенаправленно не реализуете сохранение. Хук сохраняет состояние и меняет его, когда вызывается метод, специально предназначенный для изменения состояния, как, например, это сделано в хуке useState. Как реализовать свой хук, вы узнаете из разделов 5.16 и 5.17. 5.6.10. Если не изменилось состояние Если, например, поместить вызов функции, изменяющей состояние, в обработчик события и при этом, скажем, при щелчке произвести вычисления, но не изменить состояние, то React не будет производить ререндер. Это очень важно — перерендеры происходят не на любое вычисление, что, безусловно, положительно сказывается на работе всего приложения. 5.6.11. Общепринятый способ именования В листинге 5.22 константа, содержащая состояние, названа score, а функция, его изменяющая, — setScore. Это общепринятое правило, когда переменную, хранящую состояние, называют по имени хранимого состояния с помощью существительного. Как правило, для этого используется приставка set. В нашем случае хранятся очки score — соответственно, функция setScore. Вот другие примеры: y isExtensible — setIsExtensible; y hasName — setHasName; y value — setValue; y title — setTitle. И так далее. 5.6.12. Важные выводы К рендеру приводит только изменение состояния, произведенное через функцию, возвращаемую хуком useState. Если произошло изменение состояния, это ведет к изменению страницы приложения через React-Dom и API DOM.
232  Глава 5 UI Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.6. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.6. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.6 и ознакомьтесь с моим решением. 5.7. Эффекты 5.7.1. Что такое эффект Эффект — это какое-либо действие, производимое компьютером, помимо вычисления значений, в ответ на вызов методов или функций. Поскольку компоненты React являются чистыми функциями, а в компонентах требуется обращаться к сторонним API, то для этих целей существует специальный способ. Это третья важная составляющая компонента, наряду с обработкой данных и выводом. Обращение к сторонним API, синхронизация со сторонними API или обращение к другим библиотекам осуществляется в отдельном хуке — useEffect. 5.7.2. Хук useEffect Допустим, мы хотим использовать платформенную функцию setInterval для отображения текущего времени. Рассмотрим листинг 5.23. Листинг 5.23. Использование стороннего API в компоненте
5.7. Эффекты  233 Компонент Clock возвращает JSX с текущим временем. Вернемся к useEffect и детально изучим строки с 8-й по 11-ю, используя рис. 5.16. Рис. 5.16. Разбор хука useEffect на составные части Рассмотрим составляющие, а также их необходимость. Хук. Представляет собой вызов функции useEffect, импортированной из библио­ теки React. Хук useEffect принимает два аргумента — функцию и массив зависимостей. Обязательным аргументом является только функция. Функция. Разработчики React называют данный параметр установкой (от англ. se­ tup). Это анонимная функция1, как на рис. 5.16 и в листинге 5.24. Листинг 5.24. Установка () => { } const intervalId = setInterval(() => setTime(getLocalTime()), 1000); return () => clearInterval(intervalId); Когда вызывается функция, выполняется код, задействующий или стороннюю библиотеку, или платформенную функциональность. В примере, приведенном 1 Анонимная функция — функция, у которой отсутствует имя, но присутствует тело и блок параметров. Функцию без имени выделяют отдельно из-за определения понятия «функция», которое гласит, что это именованный блок инструкций. Действительно, в теле функции содержится набор инструкций, которые программист заранее предписывает к выполнению. Чтобы не переписывать несколько раз одно и то же, была придумана функция. Для повторного выполнения того же самого блока инструкций нужно просто обратиться к имени и вызвать этот блок с помощью вызова (...). Поскольку у анонимной функции нет имени, она может использоваться только в месте определения.
234  Глава 5 UI выше, это setInterval. Можно также сделать запрос с помощью fetch. Или, например, работать напрямую с DOM API браузера или local(session)Storage API либо задействовать стороннюю библиотеку, допустим, для сбора метрик или рисования диаграмм, графиков, а кроме того, для взаимодействия с API других систем, вне React, которые могут находиться и на клиенте, и в сети и представлять собой сервис. Установка может возвращать функцию, которая будет выполнена при размонтировании компонента из DOM, а также всякий раз, когда изменяются зависимости. Еще ее называют функцией очистки. В листинге 5.24 она находится после инструкции return. Зависимости или список зависимостей, которые используются в вычислениях в установке, передаются в массиве. В случае изменения любой зависимости, что может произойти при перерендере, установка вызывается снова уже с новым значением одной из зависимостей. Перед тем как будет вызвана установка, вызывается функция очистки с предыдущим значением одной из зависимостей и неизмененными значениями остальных (если больше одной). В листинге 5.23, как и на рис. 5.16, массив зависимостей пуст, но второй аргумент как таковой не отсутствует. 5.7.3. Зависимости Зависимости в хуке — то, от чего зависит, сколько раз и когда будет вызвана функция очистки и установка при перерендерах компонента. Возможны следующие ситуации: y отсутствие массива зависимостей (второго аргумента); y массив зависимостей пуст; y массив зависимостей содержит элементы. Отсутствие массива зависимостей. Когда отсутствует второй аргумент, установка будет вызываться при каждом перерендере компонента. Компонент Clock с вызовом setInterval в качестве платформенного API не подходит для рассмотрения отличий всех трех способов. Нагляднее компонент Person, представленный в листинге 5.25. Листинг 5.25. Компонент Person и отсутствие зависимостей export const Person = ({ id }) => { const [person, setPerson] = useState(null); useEffect(() => { console.log('Вызов установки'); const controller = new AbortController(); const url = `http://localhost:4000/persons/${id}`; fetch(url, { signal: controller.signal, person.json()).then(person => { const { data } = person; }).then(person =>
5.7. Эффекты  235 setPerson(data); }); return () => { controller.abort() }; }); if (!person) { return <div>Загрузка...</div> } const { name, address } = person; } return ( <div> <span>{name}</span>, <span>{address}</span> </div> ) При использовании данного компонента запрос по URL будет производиться при каждом перерендере. Следует также помнить про функцию очистки. Перед каждым новым запуском установки она будет выполнять вызов метода abort, который отменит сделанный ранее вызов функцией fetch, если ответ еще не успел прийти. О свойстве signal объекта AbortController, передаваемом функции fetch, можно прочитать на сайте MDN1. Массив зависимостей пуст. Если мы разместим пустой массив вторым аргументом, как показано в листинге 5.26, то количество вызовов установки в useEffect сократится до одного. Листинг 5.26. Пустой массив зависимостей /* весь предыдущий код, как в листинге 5.25 */ .then(person => setPerson(person)); return () => { controller.abort() }; }, []); Поскольку массив зависимостей пуст, никакая из них не предполагает изменений. Массив зависимостей содержит элементы. Последний случай — когда массив зависимостей содержит элементы, как показано в листинге 5.27. Листинг 5.27. Массив зависимостей содержит идентификатор ID /* весь предыдущий, код как в листинге 5.25 */ .then(person => setPerson(person)); return () => { controller.abort() }; }, [id]); 1 https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal.
236  Глава 5 UI Если массив зависимостей содержит элементы, которые являются переменными, то при их изменении установка будет выполнена вновь. Предварительно будет запущена функция очистки. Изменения, как вы уже знаете, происходят строго при перерендерах. Зависимостей может быть больше одной. Но надо понимать, что повторный вызов установки в useEffect происходит последовательно: сначала — с одной измененной зависимостью, затем — с другой. 5.7.4. Сторонние API и события Мы еще не рассматривали события, но у тех, кто с ними знаком, может возникнуть вопрос: «Если мне нужно задействовать стороннее API, когда произойдет какое-либо событие, например, по щелчку?» Скажу больше, вам действительно потребуется такое поведение. Есть желание рассмотреть это сейчас, но лучше отложить данную тему до раздела 5.10. К тому моменту вы изучите события и будете иметь представление о группе компонентов. 5.7.5. Зависимости, не принимающие участия ни в установке, ни в функции очистки При работе с хуком может возникнуть необходимость, или, правильнее сказать, соблазн, установить зависимость, которая не принимает участия ни в установке, ни в функции очистки. Эту ситуацию, как и предыдущую, мы рассмотрим в том же разделе. 5.7.6. Порядок вызова Когда компонент рендерится или перерендеривается, сначала выполняется код компонента, а затем вызывается useEffect. Простой пример подтверждения этого представлен в листинге 5.28. Листинг 5.28. Компонент с выводами в консоль const getOuterFnCallTime = () => Date.now(); export const UseEffectCallSequence = () => { console.log('В теле компонента', getOuterFnCallTime()); useEffect(() => { console.log('В useEffect', Date.now()); }, []); }; return (<div>Вывод UseEffectCallSequence {console.log('Вывод из UseEffectCallSequence', getOuterFnCallTime())} </div>);
5.7. Эффекты  237 Порядок вывода в консоль показан на рис. 5.17. Рис. 5.17. Порядок вывода в консоль Как мы узнали ранее, дерево компонентов реального приложения может иметь и шесть компонентов в высоту. По умолчанию, если не мемоизировать дочерние компоненты с помощью конкретных функций, которые будут рассмотрены далее, то, если родительский компонент изменит состояние, все дерево дочерних компонентов будет перерендерено. Дочерние компоненты могут использовать хук useEffect. Листинг 5.29 и рис. 5.18 демонстрируют порядок вызова хуков useEffect в такой ситуации. Листинг 5.29. Порядок вызова хуков useEffect import { useEffect } from 'react'; const getOuterFnCallTime = () => Date.now(); export const FirstUseEffectComponent = () => { console.log('1. FirstUseEffectComponent', getOuterFnCallTime()); useEffect(() => { console.log('В useEffect FirstUseEffectComponent', Date.now()); }, []); } return (<div> Вывод FirstUseEffectComponent {console.log('2. FirstUseEffectComponent', getOuterFnCallTime())} </div>); export const SecondUseEffectComponent = () => { console.log('1. SecondUseEffectComponent', getOuterFnCallTime()); useEffect(() => { console.log('В useEffect SecondUseEffectComponent', Date.now()); }, []); }; return (<div> <FirstUseEffectComponent /> Вывод SecondUseEffectComponent {console.log('2. SecondUseEffectComponent', getOuterFnCallTime())} </div>);
238  Глава 5 UI Рис. 5.18. Снимок экрана из браузера и консоли вывода компонента SecondUseEffectComponent Уверен, что данный вывод стал для вас полной неожиданностью. Не обращайте внимания на дублирование сообщений. Из-за настройки React это не для промышленной эксплуатации. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.7. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.7. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.7 и ознакомьтесь с моим решением. 5.8. Дополнительно о компонентах 5.8.1. «Виды» компонентов Слово «виды» в заглавии подраздела специально взято в кавычки, потому что видового деления компонентов нет. Однако понятие взято не с потолка. Раньше, когда компоненты на основе функций могли только выводить JSX, их называли «глупыми», а компоненты на основе классов, содержащие состояние, — «умными». Однако с появлением хуков различие между ними себя изжило, больше его нет. Это не значит, что вы не можете сами для себя выделить в вашем приложении компоненты, которые будут лишь выводить JSX и больше ничего не делать, а все, так сказать, решения будет принимать родитель. Конечно, вы можете называть такой компонент «глупым», но это ваш индивидуальный взгляд на компоненты.
5.8. Дополнительно о компонентах  239 Кроме того, ранее — до внедрения хуков — тем, кто использовал библиотеку Redux совместно с React, предлагалось делить компоненты на два вида — контейнеры и презентационные компоненты. Контейнеры передавали данные от хранилища Redux в дочерние компоненты, сами ничего не добавляли в качестве вывода, а презентационные использовали как обычные компоненты. Сегодня вышеперечисленное разделение — анахронизм1. Мы еще не дошли до совмещения библиотек React и Redux, но вскоре приступим к этому. 5.8.2. Свойство children Это свойство доступно в любом компоненте. До текущего момента мы изучали те из них, где данное свойство не было задействовано. JSX рассмотренных компонентов хоть и содержал другие компоненты, но мы их не использовали так, чтобы понадобилось свойство children. В листинге 5.30 показано объявление двух компонентов, один из которых задействует свойство children. Листинг 5.30. Использование компонента со свойством children const ClockImitator = () => <div>13:15</div>; const Header = ({ title }) => <h3>{title}</h3>; const Paragraph = ({ text }) => <p>{text}</p>; const ComponentWithChildren = ({ children }) => ( <React.Fragment> <div>Контент, который всегда содержится в компоненте</div> { children } </React.Fragment> ); export const Composition = () => ( <ComponentWithChildren> <Header title="Недвигающиеся часы" /> <ClockImitator /> <Paragraph text="Нам важно понять свойство children" /> </ComponentWithChildren> ); В каком случае применяется свойство children? Если еще раз взглянуть на листинг 5.30, то станет очевидно, что компоненты, которые выводятся с помощью этого свойства, отличаются друг от друга своим выводом. Например, Header выводит лишь заголовок в теге h3, а Paragraph — текст в теге p. Чаще всего это свойство необходимо в компонентах, которые предполагается использовать в нескольких местах. Количество его дочерних элементов не определено. 1 Анахронизм — явление или понятие отжившее, устаревшее, не соответствующее условиям современности; пережиток.
240  Глава 5 UI Компоненты, которые будут выводиться с помощью свойства children, могут быть помещены в качестве дочерних как статически, с помощью JSX, так и динамически. Статически — имеется в виду, что вы вручную вписываете дочерние компоненты, как в листинге 5.30. Динамически — это когда выводимые компоненты управляются данными, пример чего мы сейчас рассмотрим. 5.8.3. Использование метода массивов map для вывода Из раздела 5.3 нам известно, что React может возвращать массив. Выше было рассмотрено, как, заранее не задавая разметку, воспользоваться свойством children и вывести возвращаемый JSX из других компонентов, причем разных. Есть и второй распространенный способ вывода заранее не определенного количества элементов с помощью массива — когда вы управляете выводом с помощью данных. Пример этого показан в листинге 5.31. Листинг 5.31. Вывод заранее не известного числа компонентов на основе данных const WriterList = ({ list }) => ( <ul> { list.map(({ id, name, lastName }) => <li key={id}>{name} {lastName}</li>) } </ul> ); export const WritersPresentation = () => { /** другой код */ const writers = [ { id: 'zX34c', name: 'Айзек', lastName: 'Азимов'}, { id: 'V5gHq', name: 'Роберт', lastName: 'Шекли'}, { id: 'wEr45', name: 'Герберт', lastName: 'Уэлс'}]; } return <WriterList list={writers} />; В компоненте WritersPresentation есть массив объектов писателей-фантастов, который передается в дочерний компонент пропсом. Дочерний компонент Writer­List выводит массив элементов li. Так можно управлять выводом с помощью данных. Что нужно знать еще, так это то, что в каждом компоненте или элементе, который выводится в качестве возвращаемого значения из метода map, содержится свойство key. Листинг 5.32. Специальное свойство key <li key={id}>{name} {lastName}</li> Это специальное свойство, которое необходимо React, чтобы при изменениях данных управлять списком. «При изменениях данных» — имеется в виду момент,
5.8. Дополнительно о компонентах  241 когда изменится массив. Ключ указывает React, какому компоненту (или элементу, в нашем случае li) он соответствует. При изменении данных (например, вы отфильтруете массив или добавите в него элемент) React поймет по ключу, какой это компонент (элемент), и правильно изменит DOM при перерендере. Если не поместить в выводимые с помощью массива компоненты или элементы свойство key, то React выдаст в консоли ошибку, как показано на рис. 5.19. Рис. 5.19. Сообщение в консоли о том, что каждый компонент/элемент списка должен содержать уникальный ключ Источниками данных для ключей, по рекомендации создателей React, могут быть: y базы данных. Если данные поступают из них, этот же источник обеспечит и уникальный ID; y пакеты генераторов случайных чисел. Важно уточнить, что данные нельзя генерировать на лету. Например, как показано в листинге 5.33. Листинг 5.33. Как бессмысленно задавать ключи <li key={Math.random()}>{name} {lastName}</li> 5.8.4. Слот1 для JSX В свойства можно передавать не только данные, но и JSX. Раз так, допустимо передавать компоненты, а не только элементы (листинг 5.34). Листинг 5.34. Компонент, содержащий слоты export const LayoutPage = ({ header, footer }) => { return ( <Fragment> {header} <main> Здесь располагается другой JSX </main> 1 Слот — свойство компонента, которое предназначено для присвоения ему другого компонента в виде JSX.
242  Глава 5 UI } ); {footer} </Fragment> export const Header = ({ title }) => ( <header> {title} </header> ); export const Footer = ({ text }) => ( <footer> {text} </footer> ) function App() { } return ( <div className="App"> <LayoutPage header={<Header title="Заголовок" />} footer={<Footer text="Текст как текст..." />} /> </div> ); export default App; Выглядеть это будет так, как показано на рис. 5.20. Рис. 5.20. Компонент со слотами
5.8. Дополнительно о компонентах  243 Этот способ передачи JSX в другой компонент весьма полезен. Не стоит забывать, что JSX вы можете присвоить переменной внутри компонента и в слоты передать переменную. Кроме того, можно передавать не только объявленный где-либо компонент, но и элементы, как показано в листинге 5.35. Листинг 5.35. Передача в слоты переменных, а не JSX const header = <Header title="Заголовок" />; return ( <div className="App"> <LayoutPage header={header} footer={<footer>Текст как текст...</footer>} /> </div> ); 5.8.5. Передача компонентов по ссылке Компонент можно передать в другой компонент не только как JSX, но и по ссылке (листинг 5.36). Листинг 5.36. Компоненты, переданные по ссылке /** компоненты Header, Footer */ export const LayoutPage = ({ HeaderComponent, FooterComponent }) => { }; return ( <Fragment> <HeaderComponent title="Заголовок" /> <main> Здесь располагается другой JSX </main> <FooterComponent text="Текст футера" /> </Fragment> ); function App() { }; return ( <div className="App"> <LayoutPage HeaderComponent={Header} FooterComponent={Footer} /> </div> ); Результат данного листинга точно такой же, как и результат листинга 5.34. На первый взгляд, выгоды от этого никакой. Да, если писать код таким образом,
244  Глава 5 UI то проще использовать компоненты внутри как JSX. Случай, рассмотренный в листинге 5.36, практически не используется. Он показан скорее как возможность. Итак, способы объединения одних компонентов с другими: y свойство children; y массив и метод map; y передача JSX через слот; y передача компонента по ссылке. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.8. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.8. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.8 и ознакомьтесь с моим решением. 5.9. Обработка событий 5.9.1. Как назначить обработчик событий Из раздела 2.6 вы знаете, что для того, чтобы получить данные о произошедшем событии и каким-либо образом отреагировать на него, в момент свершения события нужно установить определенному DOM-элементу обработчик события с помощью метода addEventListener. В React это делается прямо в JSX, с помощью атрибута с именем, состоящим из приставки on и имени события, для которого назначается обработчик. Например, Click, как показано в листинге 5.37. Листинг 5.37. Назначение обработчика в компоненте TodoItem export const TodoItem = ({ title }) => { const [checked, setChecked] = useState(false); const handleClick = () => setChecked(checked => !checked); } return ( <li onClick={handleClick}> <input type="checkbox" checked={checked} /> { title } </li> );
5.9. Обработка событий  245 Обработчик назначен на элемент li. Где бы ни нажал пользователь в пределах элемента li, сработает обработчик события handleClick, установленный в атрибуте onClick. Обработчик вызывает функцию setChecked, вследствие чего возвращается новое состояние, присваиваемое константе checked. И конечно, происходит рендер. Для пользователя визуально изменяется флажок — он становится, например, установленным, если до этого не был. Назначать обработчик события можно только на конкретный элемент (рис. 5.21). Несмотря на то что в React мы создаем компоненты, реакция на события принадлежит элементам. Рис. 5.21. Назначение обработчика события на элементе компонента Таким образом, очевидно, что один компонент может включать столько назначенных обработчиков, сколько требуется. Разработчик не стеснен никакими рамками. Более того, даже на один элемент может быть назначено больше одного вида события. Например, как показано в листинге 5.38. Листинг 5.38. Два вида событий, назначенных на элемент li /** Код обработчика handleMouseMove и хуки состояний */ return ( <li onClick={handleClick} onMouseMove={handleMouseMove}> <input type="checkbox" checked={checked} /> { title } <div> Текущие координаты мыши на элементе: <div> <span>X: {xCoord}</span> <span>Y: {yCoord}</span> </div> </div> </li> );
246  Глава 5 UI 5.9.2. Порядок действий для назначения обработчика Чтобы назначить обработчик события на элемент, нужно: 1) выбрать элемент компонента, на котором должна происходить обработка события; 2) указать в этом элементе атрибут с именем, состоящим из приставки on и имени события, например Click; 3) в качестве значения атрибута или передать ссылку на обработчик, или написать его непосредственно в фигурных скобках. 5.9.3. Виды событий в React События в React ничем не отличаются от событий в DOM. Имеется в виду, что React реализует обработку все тех же событий, которые есть в DOM. Другими словами, он служит посредником между вашим кодом и браузером в том, что касается назначения элементам обработчиков событий. Все виды событий, которые могут быть отловлены (назовем это так) в элементах DOM с помощью обработчиков событий, тоже могут быть назначены и отловлены в элементах JSX, в компонентах. Примеры имен атрибутов, с помощью которых на определенные события назначаются обработчики: onChange, onFocus, onBlur, onMouseOver, onMouseOut, onMouseMove, onKeyPress, onKeyDown, onKeyUp, onReset, onSubmit, onCut, onCopy, onPaste, onClick, onDoubleClick, onContextMenu, onScroll, onSelect и т. д. 5.9.4. Именование обработчиков событий Как вы успели заметить, обработчики событий именуются, начиная со слова handle. Если вы создаете обработчик события doubleClick, то он получит имя handleDoubleClick. Это лишь соглашение об именовании. Как правило, его придерживаются, чтобы другие быстрее читали и понимали ваш код. И конечно, понятно, что, как бы вы ни назвали свои обработчики, они все равно будут работать. Как именовать, если, например, у вас два обработчика одного и того же события? Такое возможно, если вы сами раздуваете компонент до больших размеров, а не делите его на несколько. В таком случае можно прибегнуть к третьему слову, именующему или JSX-тип, такой как div, input, или другую отличительную особенность, например атрибут name, type, как в листинге 5.39, либо данные или структуру, например handleChangeUserList. Листинг 5.39. Именование нескольких обработчиков одного события /** код компонента */ const handleChangeEmail = (e) => {/** код обработчика */}; const handleChangePassword = (e) => {/** код обработчика */};
5.9. Обработка событий  247 } return ( <Fragment> <input type="email" name="email" onChange={handleChangeEmail} /> <input type="password" name="password" onChange={handleChangePassword} /> </Fragment> ); В листинге и type, и name имеют одно имя. Но лишь в этом примере, чаще будет совсем по-другому. Поэтому лучше использовать что-то одно — или type, или name. В любом случае читающему будет понятно. 5.9.5. Объект события React Объект события React — источник данных о произошедшем событии, который получает каждый обработчик события (в качестве аргумента), когда он вызывается. В разделе 2.6 вы уже ознакомились с объектом события DOM-элемента. Объект события React представляет собой объект, составленный из свойств объекта события, создаваемого браузером. Для ссылки на него у объекта события React есть свойство nativeEvent. Разницу между двумя этими объектами события мы увидим на практических занятиях. Несмотря на то что объект события содержит множество свойств и несколько методов, большинство из них не будут вами использоваться. Однако несколько свойств и методов задействуются практически во всех случаях — это свойства target и currentTarget и метод preventDefault. Свойство target содержит ссылку на элемент в DOM, где произошло событие, а свойство currentTarget — ссылку на элемент в DOM, которому назначен обработчик события. Метод preventDefault предотвращает действие по умолчанию. Рассмотрим пример в листинге 5.40. Листинг 5.40. Использование свойств target, currentTarget и метода preventDefault export const PropertyMethodDemo = () => { const handleClick = (e) => { e.preventDefault(); console.log(e.target); console.log(e.currentTarget); } } return ( <div onClick={handleClick}> <a href="https://piter.com">Сайт издательского дома "Питер"</a> </div> ); Если встроить этот компонент в приложение и посмотреть на результаты вывода на рис. 5.22, то вы увидите, что выводят свойства в консоль, а действие,
248  Глава 5 UI по умолчанию установленное для тега <a> (переход по указанной ссылке), не работает, так как мы по-прежнему на текущей странице. Рис. 5.22. Демонстрация вывода значений свойств target, currentTarget и действие метода preventDefault Поскольку события, как и их источники, отличаются друг от друга, это сказывается и на объекте события. Для разных событий объекты события могут содержать дополнительные свойства. Все события содержат некий единый базовый набор свойств. Приводить их полностью нет никакого смысла, так как большинство из них вы не будете применять. В зависимости от события у вас появятся дополнительные свойства относительно базовых. Посудите сами, зачем в объекте, например, для события Click свойство, содержащее код клавиши, которую нажали. Ведь ее значение в этом случае будет undefined. Все свойства, которые содержит объект события React, в том числе существующие для конкретных событий, перечислены на этой странице1. 5.9.6. Назначение событий за пределами React Если вы захотите обрабатывать события, которые назначаются на объекты window, document, то вам необходимо использовать непосредственное назначение событий этим объектам — для React их не существует. Сделать это нужно в хуке useEffect. На рис. 5.23 показано, как назначать обработчики событий элементам, расположенным в React и за его пределами, то есть в DOM-элементах, которые React не включает в дерево компонентов приложения. Рассмотрим листинг 5.41. 1 https://react.dev/reference/react-dom/components/common.
5.9. Обработка событий  249 Рис. 5.23. Назначение обработчиков событий элементам, расположенным в разных местах приложения Листинг 5.41. Назначение обработчиков события элементам DOM, не находящимся в компонентах React import { useEffect } from 'react'; export const EventResizeDOMElement = () => { const handleResize = () => {/** код обработчика */} useEffect(() => { window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); } return ( <main>{/** JSX-содержимое вывода */}</main> ); Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.9. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.9. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.9 и ознакомьтесь с моим решением.
250  Глава 5 UI 5.10. Группа компонентов, работающих в связке 5.10.1. Обмен данными между компонентами Как вы уже знаете, главное, что создает структуру, — это связи. Когда один компонент импортирует к себе другой и использует его в своем выводе, то он всего лишь использует его как структуру. Если родительский компонент не будет передавать дочернему компоненту никаких данных, то таким образом вы можете создать лишь структуру приложения, причем статичную. То есть это даже не будет приложением — просто структура, которая ничего не делает. Данные и обмен ими между компонентами дают приложению возможность меняться. В приложении может быть глобальное состояние или локальное. Как построить обмен данными между компонентами, если эти данные хранятся исключительно в состоянии компонента? На рис. 5.24 видно, что родительский и дочерний компоненты могут легко обмениваться данными. Рис. 5.24. Обмен данными между родительским и дочерним компонентами и наоборот Родительский компонент передает пропсы дочернему. Чтобы дочерний компонент мог передать обратно родительскому какие-то данные, последний должен передать дочернему функцию, вызов которой изменит состояние в родительском компоненте тем новым значением, которое будет передано из дочернего компонента. Код компонентов для такой ситуации приведен в листинге 5.42. Листинг 5.42. Код родительского и дочернего компонентов, обменивающихся данными export const MonthsChanger = ({ months, startFrom = 0 }) => { const [currentMonth, setCurrentMonth] = useState(startFrom); const [isReset, setIsReset] = useState(false);
5.10. Группа компонентов, работающих в связке  251 const handleChangeMonth = (count) => { setIsReset(count === months.length - 1); setCurrentMonth(count); } } return ( <Fragment> <div>{months[currentMonth]}</div> <ButtonChanger onClick={handleChangeMonth} reset={isReset} /> </Fragment> ); export const ButtonChanger = ({ onClick, reset, startFrom = 0, decrement = false }) => { const [count, setCount] = useState(startFrom); const changeCount = (count) => decrement ? count - 1 : count + 1; const handleClick = () => { const nextCount = reset ? startFrom : changeCount(count); } } setCount(nextCount); onClick(nextCount); return (<button onClick={handleClick}>{count}</button>); Небольшое разъяснение. Мы часто употребляем фразу «родительский компонент передает пропсы дочернему». В этих пропсах содержатся значения, то есть дочерний компонент получает через свои параметры конкретные значения. Происходит это в момент вызова, которым является первый рендер на экране и перерендер, происходящий, когда в родительском компоненте изменилось состояние. То есть для того, чтобы родительский компонент передал дочернему пропсы, в родительском должно измениться состояние. В листинге 5.42 дочерний компонент изменяет состояние родителя, так как родитель передает дочернему функцию, меняющую состояние. Поэтому если из родительского компонента передать в дочерний какуюлибо функцию, не изменяющую состояние, то перерендера не будет. 5.10.2. Обмен данными между компонентами одного уровня Родительский компонент может передать пропсы и колбэк, а дочерний — вызвать колбэк, чтобы передать данные родительскому компоненту. Но что делать, если нужно, чтобы состояние одного компонента каким-то образом было доступно другому компоненту, находящемуся на одном уровне с текущим? Такая необходимость часто возникает, когда несколько компонентов одного уровня должны взаимодействовать. Например, знать о состоянии друг друга или при изменении состояния одного компонента изменить свое состояние. Это требуется при работе с формами, когда при вводе конкретных данных в поле типа input должно стать доступным еще
252  Глава 5 UI одно поле или кнопка либо провалидировано1 другое данное в другом поле. Как обеспечить взаимодействие для ситуации, представленной на рис. 5.25, когда для изменения состояния в одном из компонентов нужно знать состояние в другом? Рис. 5.25. Как обеспечить взаимодействие двух компонентов одного уровня Например, выбор двух дат — вылета и прилета. Если бы мы имели дело с DOMэлементами, то могли бы сослаться на соседний компонент по ссылке nextElementSibling/previousElementSibling. Но мы имеем дело с компонентами React. Когда надо обеспечить взаимодействие компонентов одного уровня, это можно организовать минимум пятью способами, если не привлекать другие библиотеки. Однако, какой бы способ мы ни выбрали, напрямую компоненты одного уровня взаимодействовать в React не могут, поэтому нужен третий компонент, с которым каждый компонент мог бы обмениваться состоянием или просто принимал бы от него пропсы. Поскольку мы имеем дело с деревом компонентов, данный компонент будет родительским. 5.10.3. Подъем состояния Периодически возникает ситуация, когда есть компонент, который может находиться в двух или более состояниях, и его использование не доставляет никаких проблем. Однако, когда мы собираем несколько таких компонентов вместе, того, что компонент может находиться только в этих состояниях, недостаточно. Нужно, чтобы одни компоненты как-то узнали о состоянии других. Например, у нас есть компонент Star. Он может находиться в двух состояниях — незакрашенном и закрашенном, как показано на рис. 5.26. Рис. 5.26. Визуальные Данный компонент будет иметь код, как показано в лисостояния компонента Star стинге 5.43. Листинг 5.43. Код компонента Star, способного принимать два состояния import React, {useState} from 'react'; import star_empty from './star_empty.svg'; import star_filled from './star_filled.svg'; export const Star = () => { const [isFilled, setIsFilled] = useState(false); 1 Валидировать — действие по слову «валидация», что означает процесс проверки введенных данных на соответствие определенным правилам, условиям и/или ограничениям.
5.10. Группа компонентов, работающих в связке  253 const handleClick = () => setIsFilled(!isFilled); return (<img onClick={handleClick} src={isFilled ? star_filled : star_empty} />); } Теперь представьте, что у вас есть компонент Star, на основе которого нужно создать другой компонент — StarRating, состоящий, например, из пяти Star. Если вы соедините пять компонентов Star, то не получите ожидаемого Рис. 5.27. Компонент поведения: допустим, при выборе пользователем звезStarRating, состоящий дочки, стоящей третьей слева, две предыдущие звездочки из пяти компонентов Star должны тоже стать закрашенными. Однако в текущем моменте мы имеем ситуацию, показанную на рис. 5.27. В таких ситуациях применяется способ «Подъем состояния». Он заключается в том, что состояние, которое необходимо во всех компонентах, перемещается в родительский компонент. Дочерние его более не хранят, а получают от родителя через пропсы свойства. Это не значит, что дочерние компоненты не могут работать с каким-либо другим состоянием. Речь идет о подъеме состояния, общего для всех компонентов. Таковым является состояние в isFilled. Однако управлять им для каждого компонента Star будет StarRating. Подняв состояние в StarRating, мы получим листинг 5.44. Листинг 5.44. Перенос состояния из компонента Star в StarRating export const StarRating = ({ amount = 1 }) => { const starList = useMemo(() => new Array(amount).fill(Star), []); const [isRatingPutDown, setIsRatingPutDown] = useState(false); const [rateStar, setRateStar] = useState(null); // реализация компонента } return ( <> { starList.map((Star, idx) => ( <Star key={`${idx}-${idx}`} isFilled={rateStar !== null ? rateStar >= idx : false} mouseOver={() => handleMouseOver(idx)} mouseOut={handleMouseOut} onClick={() => handleStarAction(idx)} /> )) } </> ); В листинге 5.44 видно, что состояние из компонента Star было перенесено в компонент-родитель StarRating и теперь он им управляет. Компонент StarRating нуждается в доработке, чтобы он работал, как, например, рейтинг в соцсетях.
254  Глава 5 UI Однако мы с вами увидели, как применяется способ «Подъем состояния», а значит, главная цель достигнута. 5.10.4. Несколько компонентов, которые должны знать состояние друг друга На практике часто необходимо, чтобы одни компоненты знали о состоянии других. Например, компонент курса валюты, кнопки проигрывателя видео или форма для ввода логина и пароля, где кнопка Отправить должна быть доступна только после правильного заполнения всех полей. «Подъем состояния» не обязательно применять во всех случаях, да и его не всегда можно применить, но он важен, поэтому вы должны уметь им пользоваться. В основном он подходит для переноса состояния из одинаковых компонентов. 5.10.5. Если компоненты импортируются из сторонней библиотеки Это достаточно распространенная ситуация. Со сторонними компонентами вы не сможете использовать «Подъем состояния». Но это и не нужно. Например, вы импортировали компоненты Date Pickers и Time Pickers из UI-библиотеки Reactкомпонентов Material UI. Здесь вы не сможете поменять компонент — придется организовать обратную связь в родителе с каждым компонентом и, в зависимости от состояния, которое возвращает какой-либо дочерний компонент, через колбэк осуществлять действие. 5.10.6. Если ближайший общий родитель не является непосредственным родителем обоих компонентов Ближайший общий родитель может находиться через компонент вверх, но это не отменяет необходимость перемещать в него состояние. Если нужно поднять состояние в родителя, не являющегося непосредственным родителем обоих компонентов (отстоит хотя бы через один уровень), то перемещение состояния осуществляется точно так же, как если бы он был непосредственным родителем. А вот способ передачи значений от родителя к таким дочерним компонентам изменится. Для этих целей придется применить хук useContext. Почему? Об этом вы узнаете в следующем разделе и сможете сами ответить на вопрос. 5.11. Контекст и хук useContext 5.11.1. Что такое контекст и зачем нужно с ним работать Чтобы сразу разобраться, о каком контексте идет речь, нужно правильно понимать значение слова. В JavaScript присутствует понятие «контекст», есть и схожее с ним по смыслу — «область видимости». О контексте может идти речь и в обычном разговоре. Что касается React, то здесь контекст схож, по сути, с понятием, которое используется в JavaScript, но это не одно и то же.
5.11. Контекст и хук useContext  255 В обычном языке слово «контекст» означает «относительно законченный отрывок письменной или устной речи (текста), в пределах которого наиболее точно выявляется значение отдельных входящих в него слов, выражений и т. п.» (Большой энциклопедический словарь). Контекст в JavaScript — объект, содержащий информацию о том, как была вызвана функция и с какими аргументами. Он включает в себя значение ключевого слова this, указывающего на объект, по отношению к которому выполняется код. Область видимости определяет, где и какие переменные, а также функции доступны в том или ином блоке кода. В JavaScript она может быть глобальной или локальной. Глобальная область видимости означает, что переменная доступна во всем коде, локальная — что переменная ограничена определенным блоком кода, таким как функция. Оба понятия — и контекст, и область видимости — влияют на доступность переменных и функций, но контекст, кроме того, добавляет информацию о текущем объекте, который используется для доступа к свойствам и методам, через ключевое слово this. В React, когда идет речь о хуке useContext, контекст не имеет того же значения, что и в JavaScript, а означает какие-либо данные, которые могут быть как примитивным значением, так и объектом, в том числе функцией, и которые доступны всему поддереву компонентов, начиная с какого-то компонента. Используется целый ряд составляющих, и хук useContext — лишь одна из них. Поддеревом компонентов, напомню, называется любая часть дерева, которая начинается с текущего рассматриваемого компонента, называемого корнем поддерева, затем следуют дочерние элементы — и так до конечных компонентов. На рис. 5.28 показан пример дерева компонентов, являющегося поддеревом. Рис. 5.28. Поддерево компонентов
256  Глава 5 UI С помощью контекста, допустим созданного в самом верхнем компоненте (на рис. 5.28 — компонент с контекстом), легко передать любые данные на любой из дочерних уровней. На рис. 5.28 их показано всего три, а может быть гораздо больше. Однако, как правило, слишком много не бывает. Какой другой способ для передачи данных на второй и третий уровни существует? Только с помощью пропсов, и на второй уровень данные нужно передавать через первый, а на третий — через первый и второй. Какая здесь проблема? Допустим, свойства, которые должны быть использованы на втором или третьем уровне, не нужны на первом. Тогда компонент первого уровня примет от родителя свойства и просто передаст компоненту второго уровня, который, в свою очередь, передаст их далее, на третий. Ни на первом, ни на втором эти свойства не будут использованы. Именно такая ситуация показана на рис. 5.29, где пропс передается транзитом из родительского компонента через первый дочерний уровень на второй. Это называется prop drilling. Рис. 5.29. Передача пропсов с помощью prop drilling
5.11. Контекст и хук useContext  257 И вот каким образом с помощью контекста передаются свойства от родителя любому из компонентов, находящихся на любом уровне (рис. 5.30). Рис. 5.30. Передача пропсов с помощью контекста На рисунке схематично показано, что с помощью контекста данные передаются не через свойства, а доступны прямо в компоненте. Именно потому, что данные доступны всему поддереву какого-либо из родительских компонентов, причем в компоненте, а не через пропсы, разработчики React назвали данный способ «Контекст». Часть поддерева, начиная с родителя, вместе с дочерними компонентами и составляют контекст. 5.11.2. Как использовать контекст в коде Как использовать контекст в коде, показано в листинге 5.45. Код листинга будто бы помещен в одном модуле, но в репозитории с примерами из книги каждый модуль компонента располагается в отдельном файле.
258  Глава 5 UI Листинг 5.45. Использование контекста для передачи свойств Если сделать рендер компонента ParentContext, то на экран будет выведена строка «Привет от современной фронтенд-разработки». Рассмотрим шаги, которые нужно выполнить, чтобы создать и использовать контекст. 1. Создать переменную контекста с помощью функции createContext из React (строка 3 листинга). Переменная контекста должна начинаться с прописной буквы. 2. Переменную, созданную в пункте 1, импортировать в компонент, являющийся корнем поддерева, для которого должен быть доступен контекст. В листинге это переменная ExampleContext, а компонент — ParentContextComponent. Поскольку все компоненты объявлены в одном модуле, эта переменная доступна тем из них, что находятся в одной области видимости, и дополнительно ее импортировать нет необходимости. 3. В выводе компонента с помощью JSX, который является корневым для поддерева, где должен быть доступен контекст, разместить компонент-провайдер1. В листинге это строки 6 и 8. Компонент-провайдер — это свойство переменной контекста ExampleContext.Provider. 4. Задать в компоненте-провайдере свойство value. Ему надо присвоить необходимое значение, которое и будет представлять собой значение контекста во всем поддереве. В листинге это показано в строке 6. В нашем случае мы задаем значение в виде строки «Привет от современной фронтенд-разработки». 1 Провайдер — то, что делает доступным какие-либо данные. В рассматриваемом случае это контекст (от англ. provide — «предоставлять»).
5.11. Контекст и хук useContext  259 5. В компоненте, где нужно получить значение контекста, импортировать из библиотеки React хук useContext и переменную контекста. В листинге переменная контекста ExampleContext доступна в области видимости, но, когда она объявлена вне области видимости, ее нужно импортировать. Хук useContext в листинге импортируется в строке 1. 6. Внутри компонента, где нужно получить значение контекста, необходимо вызвать хук useContext и в качестве аргумента в нем передать переменную контекста ExampleContext. Возвращаемое хуком значение следует присвоить переменной. В листинге это строка 17. 7. Возвращаемое в пункте 5 значение из хука useContext и есть значение контекста. Теперь его нужно использовать. Поскольку контекст — обычная строка текста, в листинге используется переменная напрямую в выводе, в строке 19. 5.11.3. Изменение контекста Если бы контекст нельзя было изменять в процессе работы приложения, он стал бы бессмысленным. Самый распространенный способ использования контекста — с хуком useState. В предыдущем подразделе, в шаге 4, в свойстве value, мы задали значение контекста. Естественно предположить, что если мы хотим, чтобы его значение менялось при наступлении определенных событий, то можем поместить контекст (часть контекста) в состояние, и при изменении его обновленное значение будет доступно по всему поддереву компонентов. Изменим пример в листинге 5.45 и поместим контекст в состояние, как показано в листинге 5.46. Листинг 5.46. Контекст, помещенный в состояние export const ParentContextComponent = () => { const [greetings, setGreetings] = useState([ "Привет от современной фронтенд-разработки", "Hello World!", "Наше вам с кисточкой!", ]); const handleClick = () => { const [firstGreeting, ...restGreetings] = greetings; setGreetings([...restGreetings, firstGreeting]); }; const [nextGreeting] = greetings; } return ( <ExampleContext.Provider value={nextGreeting}> <div><button onClick={handleClick}>Следующее приветствие</button></div> <ChildFirstLevel /> </ExampleContext.Provider> ); Как следует из листинга, при нажатии кнопки будет изменяться приветствие. Таким образом мы можем динамически изменять контекст.
260  Глава 5 UI 5.11.4. Обратная связь от дочерних компонентов в корневой компонент При использовании контекста иногда требуется получать состояние дочерних компонентов. В контекст можно помещать не только числа, строки, массивы или объекты, но и функции и таким образом передавать любому из дочерних компонентов функцию, изменяющую состояние либо колбэк. В этом случае дочерний компонент, неважно насколько далеко отстоящий в поддереве от родительского компонента, передающего контекст, способен вызывать функцию, изменяющую состояние или колбэк. Очевидно, что при вызове дочерний компонент может передать аргументы — так налаживается обратная связь дочернего компонента с родительским. Рассмотрим пример — изменим листинг 5.45 и все компоненты. Получившийся результат представлен в листинге 5.47. Листинг 5.47. Обратная связь дочернего компонента в поддереве контекста export const GreetingUpdateContext = createContext(null); export const ParentContextComponent = () => { const [greetings, setGreetings] = useState([ "Привет от современной фронтенд-разработки", "Hello World!", "Наше вам с кисточкой!", ]); const currentComponentId = "parent"; const [id, setId] = useState(currentComponentId); const [nextGreeting] = greetings; const handleUpdateState = (e) => { const [firstGreeting, ...restGreetings] = greetings; } } setId(e.target.id); setGreetings([...restGreetings, firstGreeting]); return ( <GreetingUpdateContext.Provider value={handleUpdateState}> <div>{nextGreeting}</div> <div>Состояние обновил: {id}</div> <button id={currentComponentId} onClick={handleUpdateState}>{currentComponentId}</button> <ChildFirstLevel /> </GreetingUpdateContext.Provider> ); export const ChildFirstLevel = () => ( <Fragment> <ChildSecondLevel id="first" />
5.11. Контекст и хук useContext  261 <ChildSecondLevel id="second" /> </Fragment> ); export const ChildSecondLevel = ({ id }) => { const handleUpdateState = useContext(GreetingUpdateContext); } return (<button id={id} onClick={handleUpdateState}>{id}</button>); Если отрендерить эти компоненты, то вы сможете менять приветствие любой из трех кнопок. При этом будет выводиться, каким именно компонентом обновлено приветствие. Если бы не данная возможность, представьте, чего бы стоило наладить обратную связь при ее передаче непосредственному родителю и так далее, вверх до компонента, где задается контекст. Это будет очень сложно поддерживать. 5.11.5. Несколько контекстов В поддереве может быть сколько угодно контекстов. Их количество определяется вашими запросами и ничем иным. На рис. 5.31 показаны три контекста и видно, сколько всего контекстов доступно в конкретном дочернем компоненте. Рис. 5.31. Несколько контекстов Например, крайним справа дочерним компонентам доступно целых три контекста, а крайнему левому — всего один. Использование такой возможности определяется исключительно необходимостью. Очевидно, что использовать в дочернем компоненте три контекста будет сложнее, чем один.
262  Глава 5 UI Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.11. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.11. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.11 и ознакомьтесь с моим решением. 5.12. Формы 5.12.1. Что такое формы Слово «форма» для веб заимствовано из обычного языка, где одно из его значений — распечатанный на принтере или печатный документ с пустыми пространствами для вставки требуемой или запрашиваемой информации. Например, вы приходите получать паспорт в МФЦ1. Если вы заранее не заполнили заявление на выдачу в электронном виде на сайте Госуслуг, то в МФЦ вам предложат заполнить форму «Заявление о выдаче паспорта». Таким образом, подходящее слово уже существовало, поэтому создатели HTML просто его переняли. Под формами в HTML-стандарте понимаются такие элементы, как текстовые поля, которые можно заполнять данными, флажки, переключатели, диапазоны и др. Или кнопки, позволяющие управлять действиями с этими данными. Это такие HTML-теги, как input, textarea, select и option, datalist, output, progress, meter. К ним же относят группирующие и уточняющие элементы form, fieldset, legend, label, optgroup. 5.12.2. Входные данные Данные, которые пользователь передает программе, называются входными данными. Единственный способ передать пользователю данные во фронтенд-приложение — это формы. Вы можете возразить, сказав, что можно повесить обработчик события на любую кнопку или даже элемент типа div и он будет, например, передавать ранее заданные, случайные либо инкрементируемые данные, — и будете правы. Но без форм невозможно получить произвольные данные. Поэтому, если пренебречь заранее ограниченным набором заранее известных данных, то формы — единственный способ получить данные от пользователя. Поэтому пользоваться ими нужно хорошо. 1 МФЦ — многофункциональный центр. Это учреждение, где предоставляют государственные услуги, такие как выдача паспорта, регистрация прав на недвижимость, регистрация по месту жительства и др.
5.12. Формы  263 5.12.3. Формы и React React имеет дело с формами, как и с любыми другими HTML-элементами. Совсем недавно в нем появились экспериментальные хуки useFormState и useFormStatus. Поэтому, рассматривая формы, мы рассматриваем HTML-теги, относящиеся к формам. Однако на тегах работа с ними не заканчивается. В HTML есть API для работы с данными форм — FormData и ValidityState. Вы ознакомитесь с тегами и API, а также узнаете, как работать с формами в React. 5.12.4. Теги для создания форм Необходимо рассмотреть, какие теги для создания форм существуют. Для этого взглянем на листинг 5.48, который является обычным HTML. Листинг 5.48. Теги HTML для создания форм <form> <fieldset> <legend>Теги для получения данных от пользователя</legend> <p><label for="text">Текст</label> <input type="text" id="text" name="text" inputmode="numeric" /></p> <p><label for="textarea">Текстовое поле</label> <textarea id="textarea" name="textarea" maxlength=1000></textarea></p> <p><label for="select">Поле для выбора</label> <select id="select" name="select"> <optgroup label="Все селекты"> <option selected name="select1" value="select1">Селект 1</option> <option name="select2" value="select2">Селект 2</option> </optgroup> </select></p> </fieldset> <button type="submit">Отправить</button> </form> В браузере этот HTML-код выводит то, что показано на рис. 5.32. Это все теги, которые применяются для получения данных от пользователя и вообще для построения форм. Возможно, у вас возник закономерный вопрос: «Но ведь в подразделе 5.12.1 перечислено больше тегов. Флажков и переключателей я тоже не вижу. Почему здесь не все?» Дело в том, что приведены все теги, которые могут понадобиться для создания форм применительно к получению данных от пользователя. Некоторые данные, которые нужно получить от пользователя, например, выбор определенных, заранее заданных пунктов, создаются с помощью переключателей и флажков, а выбор файлов — с помощью тега input. Изменяется лишь атрибут type. Далее мы подробно рассмотрим данный тег.
264  Глава 5 UI Рис. 5.32. Представление тегов в браузере Такие теги, как output, progress, meter, не являются тегами для получения данных от пользователя. Они отображают состояние, хотя стандартом HTML причислены к тегам, относящимся к созданию форм. А такой тег, как datalist, применяется вместе с тегом input, который уже приведен в листинге 5.48. Он задает лишь разновидность поведения и будет рассмотрен в том же подразделе про input. 5.12.5. Виды тегов, используемых для создания форм Как вы наверняка успели заметить в листинге, теги для работы с формами предназначены не только для получения данных от пользователя. Все теги можно разделить на несколько групп: y ввод данных от пользователя: input, select и option, textarea; y вывод данных пользователю: output, progress, meter; y группировка: form, fieldset, optgroup; y действие: button; y пояснение: label, legend; y дополнение: datalist. 5.12.6. Управляемые компоненты Любой из элементов input, textarea или select, помещенный в вывод JSX, является неуправляемым. Это значит, что компонент не контролирует значение поля для ввода данных. Чтобы управлять элементом input из программы, необходима переменная, которая будет устанавливать значение атрибута value. Для этого используют хук useState и обработчик события onChange. В этом случае состояние элемента можно будет контролировать.
5.12. Формы  265 Пример того, как создается контролируемый компонент, приведен в листинге 5.49. Листинг 5.49. Управляемый компонент export const ControlledComponents = () => { const options = ['A', 'B', 'C']; const [, defaultSelectValue] = options; const [valueInput, setValueInput] = useState(''); const [valueTextarea, setValueTextarea] = useState(''); const [valueSelect, setValueSelect] = useState(defaultSelectValue); const handleChangeInput = (e) => setValueInput(e.target.value); const handleChangeTextarea = (e) => setValueTextarea(e.target.value); const handleChangeSelect = (e) => setValueSelect(e.target.value); } return( <Fragment> <input type="text" value={valueInput} onChange={handleChangeInput} /> <textarea value={valueTextarea} onChange={handleChangeTextarea}></textarea> <select onChange={handleChangeSelect} value={valueSelect}> { options.map(title => <option key={title} value={title}>{title}</option>) } </select> </Fragment> ); При таком подходе данные полностью доступны компонентам, а они, в свою очередь, могут обмениваться своими данными с другими компонентами. Значит, можно строить необходимую обработку полученных от пользователя данных. 5.12.7. input Внешний вид на странице тега input, а также формат данных, которые пользователь может ввести, полностью определяются его атрибутом type. Не будем далеко ходить и сразу посмотрим на листинг 5.50, рис. 5.33 и 5.34, чтобы узнать, какие данные пользователь может передать. Листинг 5.50. Значения атрибута type тега input <form> <p>hidden <input type="hidden" name="hidden" value="hidden" /></p> <p>search <input type="search" name="search" value="search" /></p> <p>tel <input type="tel" name="tel" value="tel" /></p> <p>url <input type="url" name="url" value="url" /></p> <p>email <input type="email" name="email" value=""/></p> <p>password <input type="password" name="password" value="password" /></p> <p>date <input type="date" name="date" value="" /></p> <p>month <input type="month" name="month" value="" /></p>
266  Глава 5 UI <p>week <input type="week" name="week" value="" /></p> <p>time <input type="time" name="time" value="" /></p> <p>datetime-local <input type="datetime-local" value="" /></p> <p>number <input type="number" name="number" value="" /></p> <p>range <input type="range" name="range" value="" /></p> <p>color <input type="color" name="color" value="" /></p> <p>checkbox <input type="checkbox" name="checkbox" value="checkbox" /></p> <p>radio <input type="radio" name="radio" value="radio" /></p> <p>file <input type="file" name="file" value="" /></p> <p>submit <input type="submit" name="submit" value="Submit" /></p> <p>image <input type="image" src="imagebutton.svg" value="" /></p> <p>reset <input type="reset" name="reset" value="reset" /></p> <p>button <input type="button" name="button" value="Button" /></p> </form> Рис. 5.33. input с разными значениями type, часть 1 Рис. 5.34. input с разными значениями type, часть 2
5.12. Формы  267 Как видно на двух рисунках, визуально можно создать все необходимые поля для получения данных от пользователя. Слева на каждом из полей подписано значение его атрибута type. Hidden нужен при POST-запросах, когда в форме требуется передать какое-либо значение в виде пары «ключ — значение». Это значение не вводится пользователем, а задается программно. Например, вы хотите передать через форму ID пользователя или сгенерированный UUID на клиенте. Ряд элементов, внешне напоминающих кнопки, со значениями submit, image и reset, выполняют действия отправки и очистки. Кнопка с типом reset будет очищать форму, только если в ней представлены неуправляемые компоненты. Если компоненты управляются React, то эффекта она не даст. А вот кнопки submit и image будут работать по-прежнему, то есть осуществлять действие отправки. При их нажатии генерируется событие submit. Button не осуществляет никаких действий по умолчанию. Со значениями остальных атрибутов предлагаю вам ознакомиться самостоятельно. 5.12.8. События События происходят в формах ввода данных пользователем input, textarea, select, а также в инструменте управления button и группирующем элементе form. Они происходят и в элементах вывода данных пользователю output, progress, meter, но нас это не интересует. Нам необходимо рассмотреть события, которые нужно обрабатывать в каждом случае создания форм. В элементах input, textarea и select происходит событие change, когда пользователь изменяет значение, например печатает символ или выбирает из select другой пункт. В элементе button происходит событие click, когда пользователь нажимает на кнопку. Если кнопка имеет тип submit или reset, оно возникнет в элементе form. Собственно, эти события и возникают в элементе form, когда кто-либо нажимает кнопку с соответствующим типом. Однако если кнопка просто как кнопка, то только событие click. Рассмотрим листинг 5.51. Листинг 5.51. События, происходящие в формах export const FormEvents = ({ defaultInputValue = '' }) => { const [inputValue, setInputValue] = useState(defaultInputValue); const handleChange = (e) => { setInputValue(e.target.value); console.log('change'); }; const handleSubmit = (e) => { e.preventDefault(); console.log('submit'); }; const handleReset = () => { setInputValue(defaultInputValue); console.log('reset'); };
268  Глава 5 UI } return ( <form onSubmit={handleSubmit} onReset={handleReset}> <p> <input type="text" value={inputValue} onChange={handleChange} /> </p> <button type="reset">Сбросить</button> <button type="submit">Отправить</button> </form> ); Как вы можете видеть, основной группирующий тег — form. 5.12.9. form Тег form является группирующим. Ранее, когда не было никаких фреймворков, он играл ключевую роль в создании форм на HTML-странице. Сегодня можно обойтись без него и отправить из управляемого компонента на сервер значение, введенное пользователем, которое уже хранится в переменной. Однако с ним понятнее, где начинается та или иная форма. Кроме того, этот элемент содержит ссылки на все включенные в него формы и предоставляет к ним доступ как к элементам массива. Если получить доступ к тегу form как к DOM-элементу, то в своем свойстве elements он будет содержать ссылку на все включенные в него элементы форм. Такой доступ можно получить с помощью хука useRef. Его мы еще не проходили. Однако в таком подходе нет необходимости. Просто знайте о данной возможности — использовать ее, скорее всего, никогда не придется. Тег form включает в себя несколько атрибутов, такие как method, enctype , action. Они имеют смысл, когда задействуется поведение по умолчанию. Например, если не задействовать никакой из API для отправки HTTP-запросов — ни fetch, ни XMLHttpRequest, — то можно использовать отправку данных из формы браузером так, как показано в листинге 5.52. Листинг 5.52. Отправка данных на сервер браузером без использования XMLHttpRequest, fetch import React, { useState } from 'react'; export const DefaultBehaviourForm = () => { const [value, setValue] = useState(''); const handleChange = (e) => setValue(e.target.value); }; return ( <form method="GET" action="http://localhost:3000/some_path" encType="text/plain"> <input value={value} onChange={handleChange} name="some_name" /> <button type="submit">Отправить</button> </form> );
5.12. Формы  269 При этом способе отправки запроса, метод которого указан в атрибуте method, браузер делает перезагрузку страницы и переходит по адресу, указанному в атрибуте action. Данные предоставляются типом MIME, указываемым в атрибуте enctype (encType для React). Поскольку один из замыслов SPA — отсутствие перезагрузки страницы браузера, то подход на основе редиректа по пути, указанному в атрибуте action, не применяется. Не применяется в том числе и потому, что осуществление запросов с помощью fetch и XMLHttpRequest не требует перезагрузки страницы или редиректа, как происходит в случае запросов с помощью браузера. Сегодня в теге form используют исключительно атрибуты для назначения обработчиков событий, например onSubmit, onReset. Форм может быть несколько на одной странице (раздел SPA, который в данный момент отрендерен в браузере). Когда вы создаете форму, которая импортирует другие компоненты, представляющие собой элементы для ввода данных, разделять их тегом form — самая разумная идея. К тому же обработчики событий submit и reset проще назначить именно тому тегу form, с которым пользователь в данный момент имеет дело, нежели делать это в объекте document. 5.12.10. FormData Интерфейс FormData предоставляет нам способ для создания множества пар «ключ — значение», представляющих поля формы и их значения. С помощью FormData очень удобно отправлять эти пары через fetch. Посмотрим на листинг 5.53. Листинг 5.53. Использование FormData для получения и отправки данных export const Form = () => { const handleSubmit = (e) => { e.preventDefault(); fetch('/request', { method: 'POST', body: new FormData(e.target), }) .then(response => response.json()) .then(data => console.log(data)); }; } return ( <form onSubmit={handleSubmit}> <input type="search" name="request" /> <select name="case"> <option value="sensitive">Учитывать регистр</option> <option value="not_sensitive">Не учитывать регистр</option> </select> <button type="submit">Отправить</button> </form> );
270  Глава 5 UI Если отрендерить этот компонент и сделать запросы, не обращая внимания на то, что нет запрашиваемого ресурса, сам запрос будет выполнен браузером. Нужно посмотреть тело запроса в браузере — на вкладке Сеть инструментов разработчика. Сделанный запрос надо выделить и открыть Полезные данные в браузере Edge или Полезная нагрузка в «Яндексе». Результаты этого запроса можно увидеть на рис. 5.35. Рис. 5.35. Тело POST-запроса в инструментах разработчика в браузере Использование FormData напрямую в запросах с помощью fetch возможно, только если подразумевается тело запроса. Такими методами являются POST, PATCH, PUT. Вот эти запросы тела не имеют. А, например, запрос DELETE может его иметь или не иметь. Для запросов, не содержащих тела, нужны параметры. Часть url, где передаются параметры запроса, выглядит так: ?key1=value1&key2=value2. В запросах типа GET, HEAD, OPTIONS параметры передаются только в url. Однако их можно передать и в других видах запросов. Для этого нужно создать конструктор параметров запроса. Сделать это можно из данных, находящихся в FormData, как показано в листинге 5.54.
5.12. Формы  271 Листинг 5.54. Функция для создания строки запроса const createRequestParams = (formData) => '?' + [...formData] .reduce((params, [key, value]) => `${params}${params ? '&' : ''}${key}=${value}`, ''); Код листинга достаточно компактен, некоторые читатели не смогут понять его с первого раза. Параметр FormData — итерируемый объект, а значит, его значения можно смело поместить в массив с помощью оператора spread. В итоге у нас получится массив с теми же парами «ключ — значение». Но теперь мы можем применить методы массива для создания строки с параметрами запроса. Здесь используется метод reduce, который на каждой итерации преобразует исходный массив в строку запроса, состоящую из пар «ключ — значение», разделяемую символом & (амперсанд). Первым параметром функции обработанного вызова метода reduce является аккумулятор, содержащий результат. Второй параметр — это деструктуризация пары «ключ — значение». В теле функции обратного вызова осуществляется сборка подстроки (см. рис. 5.35). Необходимо добавить, что при использовании fetch1 можно, но не обязательно использовать конструкторы Request, Headers, AbortContoller, Response и др. Изучению глобальной функции fetch стоит посвятить время, чтобы лучше понимать, как ее использовать в своем коде. 5.12.11. Валидация2 Валидацию можно организовать несколькими способами. Один из них — вручную, то есть создавать функции на JavaScript, которые перед отправкой будут проверять содержимое полей и при необходимости отменять отправку и показывать сообщения рядом с DOM-элементами, которые не прошли проверку. Проверять можно не только поля с данными, но и сами данные в FormData и при необходимости отменять отправку и выдавать сообщение о непройденной валидации. В HTML 5 есть встроенный способ валидации полей. Каждый элемент формы содержит свойство validity, которое реализует интерфейс ValidityState3. Чтобы осуществлять валидацию с помощью HTML 5, нужно задать правила этой самой валидации. Задаются они с помощью атрибутов type, required, pattern, min, max, minlength и maxlength, которые есть у элементов форм input, select, textarea, output, button (табл. 5.1). 1 2 3 https://developer.mozilla.org/en-US/docs/Web/API/fetch. Валидация — во фронтенд-приложении это процесс проверки введенных данных на соответствие определенным правилам, условиям и/или ограничениям. https://developer.mozilla.org/en-US/docs/Web/API/ValidityState.
272  Глава 5 UI Таблица 5.1. Предназначение атрибутов Атрибут Тип данных Для чего предназначен type string Для задания типа элемента формы, который предназначен для ввода конкретных типов данных или способа ввода значений. Например, text, search, url, tel и др. required boolean Когда указан, данный элемент является обязательным pattern regexp Для указания шаблона, которому должно соответствовать введенное значение min number, time, date Для указания минимально возможного значения какого-либо диапазона значений, которое может представлять число, дату, время max number, time, date Для указания максимально возможного значения какого-либо диапазона значений, которое может представлять число, дату, время minlength number Для указания минимальной длины строки (в символах), которую может ввести пользователь maxlength number Для указания максимальной длины строки (в символах), которую может ввести пользователь Использовать встроенную валидацию вместе с правилами в React можно так, как показано в листинге 5.55. Листинг 5.55. Пример использования встроенной валидации export const ValidateAttributes = () => ( <form> <p>reuired <input required name="simpletext" /></p> <p>type=url <input type="url" name="url" /></p> <p>pattern <input name="text" pattern="[A-Z]{3}" /></p> <p>min max <input type="number" name="number" min={2} max={12} /></p> <p>minlength maxlength <input name="lastname" minLength={5} maxLength={20} /></p> <button type="submit">Отправить</button> </form> ); Попробуйте отрендерить данный компонент, чтобы проверить, как браузер выдает сообщения при попытке отправить форму при незаполненных или неправильно заполненных полях. Данный компонент является хорошим обучающим примером. Стоит, конечно, упомянуть, что в элементе form надо указать атрибут action, чтобы отправка происходила по какому-либо URL. Обычно указывают путь. При работе с примером следует обратить внимание на то, как себя ведет валидация и когда происходит отправка формы. Если какое-либо поле, то есть элемент формы, не проходит валидацию, то событие submit не происходит. Однако это ограничение можно обойти, указав атрибут noValidate. Кроме того, при необходимости
5.12. Формы  273 вы можете совместить данный пример с JavaScript, допустим присвоить обработчик событию submit, который будет отменять действие по умолчанию, отправку данных браузером и отправлять данные через функцию fetch, например. В листинге 5.56 дан пример возможного использования, где задействуется метод checkValidity, вызванный на форме. Листинг 5.56. Проверка валидации всей формы export const ValidateAttributes = () => { const handleSubmit = (e) => { e.preventDefault(); const isFormValid = e.target.checkValidity(); /** Код в зависимости от состояния формы * Если isFormValid == false, * то какой-либо элемент формы не прошел валидацию. */ }; } return ( <form onSubmit={handleSubmit} noValidate> <p>reuired <input required name="simpletext" /></p> <p>type=url <input type="url" name="url" /></p> <p>pattern <input name="text" pattern="[A-Z]{3}" /></p> <p>min max <input type="number" name="number" min={2} max={12} /></p> <p>minlength maxlength <input name="lastname" minLength={5} maxLength={20} /></p> <button type="submit">Отправить</button> </form> ); Напоследок — о контроле за валидацией с помощью HTML5 и JavaScript. Интерфейс ValidityState содержит несколько свойств, по которым принимается решение о соответствии правилам валидации. Допустим, у нас есть форма всего с одним полем input, которое содержит атрибут required, а тег form — атрибут noValidate. Тогда валидация по умолчанию производиться не будет, но если поле не заполнять данными, а оставить пустым, то его состояние валидации будет отрицательным. Например, у нас есть код, как показано в листинге 5.57. Листинг 5.57. Проверка состояния валидации конкретного поля export const ValidateAttributes = () => { const handleSubmit = (e) => { e.preventDefault(); const [{ validity }] = e.target.elements; console.dir(validity); }; return ( <form onSubmit={handleSubmit} noValidate>
274  Глава 5 UI } ); <input required name="simpletext" /> <button type="submit">Отправить</button> </form> Видно, что поле — обязательное для заполнения. Если отрендерить этот компонент и нажать кнопку Отправить, то в консоль будет выведено состояние валидации поля input. Как вы можете видеть на рис. 5.36, свойство valueMissing равно true, а это значит, что оно не заполнено. Рис. 5.36. Состояние свойств валидации Вот таким нехитрым способом вы можете задействовать валидацию в компонентах. 5.12.12. Дополнительные источники Область форм более обширная, чем может охватить один раздел книги. Я постарался коснуться всех возможных тем, а для более глубокого изучения предлагаю следующие источники: y https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation; y https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation; y https://html.spec.whatwg.org/multipage/forms.html#forms. Когда будем изучать библиотеку React Router в разделе 6.6, вы познакомитесь с удобным способом организации отправки и обработки данных форм.
5.13. НОС  275 5.13. НОС 5.13.1. Что такое НОС HOC (произносится «хок»), или High Order Component, переводится как «компонент высшего порядка». Представляет собой функцию, которая принимает на вход какой-либо компонент и возвращает другой, обогащенный или дополнительным состоянием, или свойствами. В программировании существует паттерн декоратор1, а High Order Component — его реализация. HOC не является элементом React, но React содержит HOC memo, который можно с успехом использовать для оптимизации перерендеров. Помимо этого, мы можем создать HOC самостоятельно. High Order Component, содержащий условие авторизации пользователя приложения, уже даже стал популярным. Он приведен в листинге 5.58. Листинг 5.58. НОС, проверяющий авторизацию import React from 'react'; import { LoginForm } from './LoginForm'; function withAuthCheck(Component) { return function(props) { if (!localStorage.getItem('token')) { return <LoginForm />; } return <Component {...props} />; } }; function App() { return ( <div> <h1>Hello, world!</h1> <button onClick={() => alert('Clicked')}>Click me</button> </div> ); } export default withAuthCheck(App); Обращаю внимание на то, что функция withAuthCheck — НОС. Хотя название довольно оригинальное — «компонент», по сути, это функция-декоратор, создающая компоненты с дополнительной функциональностью. К этому нужно привыкнуть. Обычно создаваемый разработчиком НОС именуется, начиная с приставки with, далее следует название добавляемой функциональности. 1 Декоратор — паттерн, который существует в объектно-ориентированном программировании и добавляет объекту новую функциональность. Он также может применяться с функциями.
276  Глава 5 UI 5.13.2. Перерендеры и измерение производительности Рассмотрим компонент, куда входит список каких-либо других компонентов, а также переменная, значение которой хранится в состоянии и которая не связана со списком. Код данного компонента приведен в листинге 5.59. Листинг 5.59. Компонент, совмещающий и состояние, и список import React, { Fragment, useState } from 'react'; const list = [{id: 1, value: 'Первый'}, {id: 2, value: 'Второй'}, {id: 3, value: 'Третий'}, {id: 4, value: 'Четвертый'}, {id: 5, value: 'Пятый'}, {id: 6, value: 'Шестой'}, {id: 7, value: 'Седьмой'}, {id: 8, value: 'Восьмой'}]; const ListItem = ({ value }) => <div>{value}</div>; export const ListAndStateComponent = () => { const [property, setProperty] = useState(false); const handleClick = () => setProperty(prop => !prop); } return (<Fragment> <div>Сейчас состояние: {`${property}`}</div> <button onClick={handleClick}>Сменить состояние</button> { list.map(({id, value}) => <ListItem key={id} value={value} />) } </Fragment>); Чтобы понять, почему на подобную ситуацию следует обратить внимание, в брау­ зере нам понадобится дополнительный инструмент разработчика — React Deve­ loper Tools1. Я уверен, что с такими компонентами вы сталкивались и столкнетесь еще не раз — это распространенная компоновка. Прежде чем мы двинемся далее, необходимо установить инструмент. В сноске дана ссылка для каждого браузера. Установите этот инструмент, и у вас в браузере, в инструментах разработчика, появятся две дополнительные вкладки, Components и Profiler2, как на рис. 5.37. Наша задача — увидеть, какие компоненты осуществляют перерендер при смене состояния переменной property . Еще я хочу отметить, что замерять 1 2 React Developer Tools — Chrome https://chrome.google.com/webstore/detail/react-developertools/fmkadmapgofadopljbjfkapdkoienihi. Edge https://microsoftedge.microsoft.com/addons/ detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil?hl=en-US. Firefox https:// addons.mozilla.org/en-US/firefox/addon/react-devtools/. Profiler переводится с английского как «профилировщик». Что это? Это программа сбора информации, например, об использовании ресурсов, работе модулей, частоте вызова функций и т. д. Анализ собранных данных позволяет выделить и оптимизировать участки кода, занимающие значительное время. Это основной инструмент для программ, оптимизирующих код.
5.13. НОС  277 производительность можно и программно. В React есть компонент Profiler, который легко импортировать и использовать в компоненте. Мы рассмотрим наглядный способ, а с программным вы можете ознакомиться самостоятельно на странице компонента Profiler1. Мы будем использовать вкладку Profiler в инструментах разработчика браузера, то есть профилировщик в виде программы. Она представлена на рис. 5.37. Рис. 5.37. Инструменты разработчика React Developer Tools в браузере Edge Сейчас нужно нажать кнопку в профилировщике, отображающуюся как синий кружок (рис. 5.38), — и она примет красный цвет. Затем надо нажать на странице браузера кнопку Сменить состояние и снова нажать кнопку в профилировщике, чтобы остановить запись. Рис. 5.38. Профайлер, запущенный на запись В профилировщике я выбрал способ отображения Ranked chart. Это позволяет увидеть, что компонент ListAndStateComponent и его дочерние компоненты ListItem перерендерились (рис. 5.39). Профилировщик также показывает время, которое потребовалось для перерендера. Время 0,4 и 0,1 миллисекунды — очень незначительное. На него можно не обращать внимания. Правда, и компонент ListAndStateComponent, и его дочерние компоненты тоже небольшие по размерам. ListItem — совсем крошечные, содержат всего одну текстовую строку и тег. В реальном приложении дочерние компоненты 1 https://react.dev/reference/react/Profiler.
278  Глава 5 UI будут не столь маленькими, как, например, ProductCategory на рис. 5.40, который возможен и в интернет-магазине, а каждый его дочерний компонент будет представлять карточку товара. Рис. 5.39. Результат рендера ListAndStateComponent и всех его дочерних компонентов Рис. 5.40. Упрощенное дерево компонентов интернет-магазина Компонент ProductList в этом дереве компонентов интернет-магазина может содержать большое количество дочерних компонентов ProductListItem. По этой причине данный компонент помечен звездочкой, что означает «ноль и более». Каждый такой компонент, в свою очередь, будет состоять из других дочерних компонентов. И уже перерендер всех дочерних компонентов, если в ProductCategory изменится состояние, будет не таким же быстрым, как в случае с компонентом ListAndStateComponent на рис. 5.39. React по умолчанию работает так. Если в компоненте ProductCategories изменяется состояние, то все дочерние компоненты, типа CategoriesMenu, PageShoppingCart, SpecialOffers и ProductList, будут перерендерены. Однако ProductList представляет собой список компонентов ProductListItem некоторой длины. Поскольку
5.13. НОС  279 перерендеривается родитель ProductList, перерендериваются и все его дочерние компоненты. То есть поведение React по умолчанию такое, будто изменилось состояние, но не были предприняты специальные меры, и перерендеривается все поддерево, начиная с компонента, в котором изменилось состояние. 5.13.3. Оптимизация перерендеров Именно для ситуаций, когда в одном компоненте есть одно или несколько состояний и дочерние компоненты могут перерендериваться вследствие изменения «чужого» состояния, существует встроенный НОС React.memo. НОС memo (React.memo) получает на вход компонент и необязательную функцию, осуществляющую сравнение параметров, и возвращает мемоизированный компонент (листинг 5.60). Листинг 5.60. Использование НОС memo import { memo } from 'react'; const MemoizedComponent = memo(function Component(props) { // реализация мемоизируемого компонента }); В листинге не показана функция, сравнивающая параметры. Ее мы разберем далее. Наш листинг можно изменить двумя способами. Первый — вместо компонента ListItem в методе массива map подставить мемоизированную версию. Второй — вынести в отдельный компонент (разумеется, мемоизированный) рендеринг списка и передавать ему в качестве параметра список (листинг 5.61). У обоих подходов есть плюсы и минусы. Листинг 5.61. Код компонента, выводящий мемоизированные компоненты MemoizedListItem const ListItem = ({ value }) => <div>{value}</div>; const MemoizedListItem = memo(ListItem); export const ListAndStateComponent = () => { const [property, setProperty] = useState(false); const handleClick = () => setProperty(prop => !prop); />) } return (<Fragment> <div>Сейчас состояние: {`${property}`}</div> <button onClick={handleClick}>Сменить состояние</button> { list.map(({id, value}) => <MemoizedListItem key={id} value={value} } </Fragment>);
280  Глава 5 UI Мемоизированный компонент принимает те же параметры, что и компонент, который был до мемоизации. Теперь проверим нашу мемоизацию с помощью профилировщика. Включим запись, изменим состояние и отключим запись (рис. 5.41). Рис. 5.41. Результат рендера ListAndStateComponent На рис. 5.41 видно, что перерендер был только у компонента ListAndStateComponent. А вот среди компонентов MemoizedListItem ни один не перерендеривался. Более того, если сделать инструмент масштабнее, то даже будет указано, что данные компоненты мемоизированы. Если входные параметры не изменялись, то и мемоизированные компоненты не перерендериваются. 5.13.4. Второй параметр НОС memo Второй параметр является необязательным. Если он опущен, то параметры, переведенные в текущий рендер, сравниваются с предыдущими с помощью метода Object.is 1. Пара замечаний на этот счет. Например, если в мемоизированный компонент передают примитив, то они сравниваются по значению. Если передают объект, то по ссылке. То есть изменение отдельного элемента или изменение длины массива с помощью мутации будет означать, что это тот же самый массив, поскольку ссылка не изменилась. Поэтому, даже если вы полностью удалили все элементы из массива, но ссылка на него та же, на экране ничего не изменится. И наоборот. В данный момент нам нужно рассмотреть функцию, сравнивающую аргументы. Какие аргументы получает функция сравнения, видно в листинге 5.62. Листинг 5.62. Функция сравнения function arePropsEquals(oldProps, newProps){ //... return oldProps.someProp === newProps.someProp; } 1 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ Object/is.
5.14. useReducer — второй хук для работы с состоянием  281 Если вернуть true из функции, это будет значить, что предыдущие свойства не изменились. И наоборот. Если имеем дело с массивом, то сравнивать нужно каждый элемент нового массива со старым. 5.14. useReducer — второй хук для работы с состоянием 5.14.1. Хук useReducer Второй хук — useReducer — предоставляет возможность управлять состоянием и рендером, но делает это иначе. Как и после изменения состояния с помощью хука useState, происходит перерендер. Прежде чем приступить к работе с хуком, необходимо понять его идею. Хук useReducer лишь позволяет нам использовать механизм, лежащий в основе. Если с ним не разобраться, то использовать этот хук самостоятельно не получится. Только если кто-то напишет, как, вы сможете подправлять некоторые элементы. Начнем с составляющих, задаваемых в коде (для сравнения на рис. 5.42 приведен хук useState). Рис. 5.42. Составляющие хука useReducer в коде Для работы с состоянием с помощью хука useState используются: y переменная, содержащая текущее состояние. В англоязычной литературе для обозначения всей совокупности переменных, содержащих состояние компонента, используют слово snapshot1; y функция, с помощью которой изменяется значение переменной состояния. В качестве аргумента она должна получить новое значение состояния. Если вновь переданное состояние не отличается от предыдущего, то перерендер не произойдет; 1 Snapshot — совокупность значений переменных, в которых содержится состояние в конкретный момент времени работы фронтенд-приложения и до следующего перерендера (или после предыдущего). Snapshot означает «моментальный снимок», «мгновенный снимок». В IT это слово используется не только применительно к библиотекам для работы с фронтендом, но и к базам данных, файловым системам. Кроме того, так обозначают копию экрана. Но фактически под этим понимается моментальная копия видеопамяти компьютера (редко — ОЗУ), получаемая с помощью специальной программы.
282  Глава 5 UI y хук React, возвращающий новое значение переменной состояния и в библио­ теке React создающий экземпляр состояния и функции для его изменения. Такая пара располагается в каком-либо компоненте. За счет объявления хука вне компонента переменная state, сколько бы раз ни был вызван компонент и сколько бы раз в нем ни был вызван хук useState, не изменит свое состоя­ ние, если не будет вызвана функция setState; y изначально инициализируемое состояние. Слово initial переводится с английского в том числе как «изначальный». Для работы с состоянием с помощью хука useReducer используются: y переменная, содержащая текущее состояние; y функция dispatch, запускающая механизм обновления состояния. При вызове она должна получить какое-либо значение, называемое action (читается «экшен»). Обычно экшен — объект. По сути, это абстракция, содержащая сведения о произошедшем действии, то есть описывающая его. Далее при рассмотрении механизма мы изучим экшен подробно; y хук useReducer, который при вызове получает три аргумента, описанные далее. Возвращает массив с двумя элементами — переменной, содержащей текущее значение состояния, и функцией dispatch. Данный хук, как и useState, реализован вне компонента, поэтому сохранение состояния возможно независимо от того, сколько раз вызывается компонент, в котором он используется; y reducer — функция, которая при вызове вычисляет новое состояние. Подроб­ ности приведены далее, при рассмотрении механизма работы; y initialArg — значение, начиная с которого вычисляется текущее состояние; y init — инициализирующая функция, которой следует возвратить начальное состояние. Она необязательная, и если не предоставлена, то начальное состояние задается равным предыдущему аргументу, то есть initialArg. В противном случае начальное состояние устанавливается равным вызову init(initialArg). 5.14.2. Механизм работы составляющих Состояние хранится вне компонента, в самой библиотеке React, а получает его компонент за счет вызова хука, поэтому одним из элементов механизма является сам React как хранитель состояния. Также будет добавлен экшен. Изучить механизм нам поможет конкретный пример (листинг 5.63). Листинг 5.63. Работа с состоянием в компоненте ClickCounter import React, {Fragment, useReducer} from 'react'; const reducer = (state, action) => { const { type } = action; if (type === 'reset') { return { count: 0 }; }
5.14. useReducer — второй хук для работы с состоянием  283 return { count: state.count + (type === 'increment' ? 1 : -1) }; }; export const ClickCounter = () => { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <Fragment> <div>{state.count}</div> <button onClick={() => dispatch({ type: 'increment'})}>Плюс 1</button> <button onClick={() => dispatch({ type: 'decrement'})}>Минус 1</button> <button onClick={() => dispatch({ type: 'reset'})}>Сбросить к нулю</button> </Fragment> ); }; Три главные составляющие механизма в листинге 5.63: y reducer — функция, объявленная в строках 3–11; y dispatch — возвращается из хука в строке 14, задействуется в строках 18, 19 и 20; y action — экшен, передается как аргумент функции dispatch в строках 18–20. Вот как работает механизм. 1. Пользователь осуществляет действие с элементом интерфейса, и происходит вызов функции dispatch, аргументом в которую при вызове передается объект, называемый «экшен», например { type: 'increment' }. 2. Происходит вызов редьюсера, который получает текущее состояние первым аргументом и экшен — вторым: reducer = (state, action) {.... 3. В редьюсере на основе экшена вычисляется новое состояние, возвращается его новая копия. 4. React производит рендер компонента ClickCounter. 5. В компоненте ClickCounter вызывается хук useReducer, что возвращает массив с двумя элементами: первый — переменная состояния, второй — функция dispatch. Переменная состояния теперь содержит новое состояние, которое было вычислено на шаге 3. 6. Вывод компонента ClickCounter уже с новым состоянием передается в ReactDom, и новое состояние рендерится на экране. Разберем несколько моментов. Первый момент — функция dispatch. Она просто вызывает редьюсер и передает ему первым аргументом текущее состояние, вторым — экшен. Затем, после того как редьюсер вернет новое состояние, React вызывает перерендер компонента. Второй момент — функция-редьюсер. Как вы видели, она объявляется вне компонента. Сама функция должна быть чистой, то есть не мутировать состояние, а возвращать его новый экземпляр. За счет того, что редьюсер объявлен вне компонента, код самого компонента сильно сокращается, а редьюсер можно поместить в отдельный JavaScript-модуль (то есть в отдельный файл) и затем импортировать его в компонент.
284  Глава 5 UI Третий момент связан с экшеном, который может быть произвольным значением. Но существует некое соглашение, и согласно ему экшен должен быть объектом, с единственным обязательным свойством type. Если в экшене должны присутствовать другие данные, то их можно размещать как угодно, но чаще всего помещают в свойство payload1, что переводится как «полезная нагрузка». Четвертый момент — надо учитывать, что редьюсер и экшен связаны. То есть редьюсер должен быть построен на основе структуры экшена. Взгляните на лис­ тинг 5.63. В строке 4 редьюсер обращается к свойству type, в строке 7 — возвращает объект со свойством count, а в строке 10, помимо возвращения объекта со свойством count, изменяет его значение на основании уже хранящегося состояния. Таким образом, когда вы задаете состояние, первым делом нужно определить структуру экшена, а затем на ее основе создавать редьюсер. 5.14.3. Задание начального состояния Задать начальное состояние можно двумя способами. Во-первых, передать вторым аргументом, как в листинге 5.63. Во-вторых, передать вдобавок третий аргумент, который должен быть функцией. Эта функция вычислит начальное состояние на основе второго аргумента. Пример использования приведен в листинге 5.64. Листинг 5.64. Начальное состояние, задаваемое функцией /** реализация до, как в листинге 5.63 */ const initializer = (initialValue) => ({ count: initialValue }); export const ClickCounter = () => { const [state, dispatch] = useReducer(reducer, 3, initializer); /** реализация после, как в листинге 5.63 */ Начальное состояние задается только при первом рендере компонента. Поэтому вторым аргументом лучше передавать структуру данных или примитивное значение, а не вызов функции, как показано в листинге 5.65. Листинг 5.65. Начальное состояние, задаваемое вызовом функции /** реализация до, как в листинге 5.63 */ const initializer = (initialValue) => { console.log('Будет вызываться при каждом перерендере'); }; return ({ count: initialValue }); export const ClickCounter = () => { const [state, dispatch] = useReducer(reducer, initializer(3)); /** реализация после, как в листинге 5.63 */ 1 На самом деле это слово перекочевало из работы сетевых протоколов, где оно означает данные, передаваемые по сети. Для таких случаев стало применяться по аналогии.
5.14. useReducer — второй хук для работы с состоянием  285 Функция initializer будет вызываться при каждом перерендере, но все вызовы, кроме первого, не имеют смысла, так как возвращаемое значение игнорируется. Поэтому, если из такого вызова возвращается большая структура данных или долгое вычисление, это тормозит ваш интерфейс. 5.14.4. Порядок действий для создания состояния в компоненте с помощью useReducer Помните: состояние может быть объединено и обрабатываться единым редьюсером. Соответственно, в компоненте нужна всего одна переменная, через которую можно получить доступ к значениям всех сохраняемых состояний. Но это не обязательно. Итак, порядок действий. 1. Реализовать вывод компонента, то есть задать структуру. 2. Придумать, какие действия ведут к изменению состояния. Дать им наименования. 3. В практических упражнениях мы потренируем данную инструкцию. 4. Разработать экшены для каждого изменяемого состояния и payload для них. 5. Создать редьюсер на основе экшенов. 6. Внедрить хук useReducer в компонент и задать в хуке начальное состояние. 7. Реализовать вызовы функции dispatch с передачей им соответствующих экшенов. 5.14.5. Использование useReducer В редьюсере можно объединить все состояния, которые необходимы компоненту. Хотя если быть точным, то в редьюсере можно объединить все вычисление нового состояния. Поскольку редьюсер должен быть чистой функцией, его можно спокойно выносить за пределы компонента. А значит, и вычисление нового состояния. Представляете, насколько проще будет читать компонент? Взгляните еще раз на листинг 5.63. Если бы вы использовали для хранения состояния useState, то вам пришлось бы весь код, связанный с вычислением нового состояния, разместить внутри компонента. По этой причине, когда требуется изменять в компоненте несколько состояний или делать это по разным причинам, имеет смысл использовать useReducer. Когда надо управлять состоянием одной переменной, лучше использовать useState. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.14. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.14. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.14 и ознакомьтесь с моим решением.
286  Глава 5 UI 5.15. useRef 5.15.1. Хук useRef Хук useRef тоже управляет состоянием, но он не инициирует перерендер. Соот­ ветственно, у него нет такого способа смены состояния, как у хуков useState и useReducer. Значит, изменить состояние, хранимое в этом хуке, можно только путем прямого изменения значения. В листинге 5.66 дан пример назначения начального состояния, а также его изменения. Листинг 5.66. Хранение состояния с помощью хука useRef export const ComponentWithRef = () => { const refTest = useRef(0); const [count, setCount] = useState(0); const handleIncrementCount = () => setCount(count => count + 1); const handleIncrementRefTest = () => refTest.current += 1; } return (<Fragment> <button onClick={handleIncrementCount}>count: {count}</button> <button onClick={handleIncrementRefTest}> ref: {refTest.current} </button> </Fragment>); Отрендерите этот компонент. Теперь несколько раз нажмите кнопку с надписью ref: 0. Ничего не происходит. Даже если перед инструкцией return в компонент добавить вывод в консоль console.log(refTest.current), там все равно ничего не будет выводиться. Все потому, что изменение значения состояния, хранимого с помощью хука useRef, не ведет к перерендеру. Однако, как только вы нажмете кнопку с надписью count: 0, цифра на обеих кнопках изменится. Теперь понажимайте кнопку с надписью count: 0 (уже с другой цифрой) и увидите, что при перерендерах значение кнопки с надписью ref: 0 не изменяется. Хук возвращает объект со свойством current, поэтому переменная refTest и содержит свойство, значение которого мы изменяем напрямую. В функции handleIncrementRefTest свойству current напрямую присваивается новое значение. Поскольку значение очень легко изменить, а заметить это можно, лишь начав работать с хранящимся значением, менять его в компоненте нельзя. Почему? Потому что при каждом перерендере вы будете изменять состояние. Но, скорее всего, это не то, чего вы ждете, если, конечно, специально не подсчитываете число перерендеров компонента. Обычно значение свойства current меняют в колбэках обработчиков событий или других хуков. В листинге 5.66 это колбэк — обработчик события Click.
5.15. useRef  287 5.15.2. Где чаще всего применяется хук Две основные области применения: y сохранение состояний данных, которые не используются в рендере, то есть тех, что не предназначены для вывода; y управление DOM-узлами. Листинг 5.67 демонстрирует первый случай использования. Листинг 5.67. Хранение состояния, не используемого в рендере export const SilentStopWatch = () => { const [timeLeft, setTimeLeft] = useState(0); const timeRef = useRef(0); const isStopWatchRun = useRef(false); const handleStartStop = () => { isStopWatchRun.current = !isStopWatchRun.current; if (isStopWatchRun.current) { timeRef.current = Date.now(); } else { setTimeLeft(Date.now() - timeRef.current); } } }; return <button onClick={handleStartStop}>{timeLeft}</button>; Данный код перерендеривается только при втором щелчке на кнопке, на которой выводится количество миллисекунд, прошедших с момента первого щелчка. Кроме того, с помощью хука useRef часто сохраняют ID, возвращаемый функциями setInterval, setTimeout. Второй случай использования — управление DOM-узлами. Вы не сможете обойтись без хука useRef при установлении фокуса на поле ввода (элементы input, textarea, select), прокрутке до определенного элемента на экране, подсчете прокрученного расстояния и др. Подробно об этом будет рассказано в следующем подразделе. 5.15.3. Совместная работа хука useRef и атрибута элемента ref Разработчики React позаботились о том, чтобы можно было из React получить доступ к DOM-узлам. У любого элемента, который в JSX представляет HTML-тег, есть свойство ref. Если присвоить этому атрибуту возвращенный объект из хука useRef, то после первого рендера в свойстве current будет ссылка на узел DOM. Листинг 5.68 показывает это. Листинг 5.68. Ссылка на DOM-узел с использованием useRef export const DOMElementRef = () => { const domRef = useRef(null); const [count, setCount] = useState(0);
288  Глава 5 UI const handleClick = () => { console.log(domRef.current); setCount(count => count + 1); }; }; return <button ref={domRef} onClick={handleClick}>Нажать: {count}</button>; При первом рендере на кнопке будет ноль. Но если нажать, отобразится единица и в консоли появится ссылка DOM-элемента. Это можно увидеть на снимке экрана (рис. 5.43). Дополнительные данные — то, что в атрибут ref достаточно передать ссылку на объект, возвращенный из хука useRef; присвоение осуществляет React. Поэтому два названных элемента — атрибут ref и хук useRef — работают вместе. Рис. 5.43. Ссылка на DOM-узел, выведенная в консоль Если в консоли навести на ссылку, то браузер подсветит кнопку на странице. Это говорит о том, что мы вывели в консоль ссылку на элемент, которая хранится с помощью хука useRef. Имея ссылку на узел, можно выполнять разные действия непосредственно с самим узлом, используя API браузера. 5.15.4. Практика применения useRef в DOM-узлах Рассмотрим, как можно устанавливать фокус на поле ввода input (листинг 5.69). Это наиболее распространенная форма использования атрибута ref и хука useRef. Листинг 5.69. Использование ref, useRef для установления фокуса у input export const FocusInput = () => { const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); }, []); }; return (<form> <input ref={inputRef} /> </form>);
5.15. useRef  289 В листинге видно, что, как только загрузится страница, на поле сразу устанавливается фокус. Это можно делать и нажатием кнопки — просто поместить код установки фокуса в обработчик события. Обратите внимание, что если планируется использовать хук useRef вместе с атрибутом ref, то его инициализируют значением null. 5.15.5. Хранение ссылки на DOM-узел в родителе Если вы хотите позволить управлять родителю DOM-элементом какого-либо компонента, то, поступив так, как показано в листинге 5.70, доступ к DOM-узлам не получить. Листинг 5.70. Передача объекта, возвращаемого из useRef в компонент export const ParentRef = () => { const domRef = useRef(null); const handleClick = () => { console.log('domRef', domRef); } } return <Button ref={domRef} onClick={handleClick} /> export const Button = ({ ref, onClick }) => { return <button ref={ref} onClick={onClick}>Нажать</button> } Код выглядит вполне оправданным, но работать ожидаемым образом не будет и вызовет сообщения об ошибках. Это показано на рис. 5.44. Как видите, React выдал аж две ошибки. Первая, по поводу свойства ref, сообщает, что попытка получить к нему доступ приведет к тому, что будет возвращен undefined. Вторая — что функциональному компоненту нельзя передавать свойство ref и что попытка получить доступ к нему провалится. Кроме того, данное сообщение об ошибке содержит вопрос: «Быть может, вы имели в виду использовать React.forwardRef()?» Что ж, воспользуемся этим советом. Ранее говорилось, что ref и useRef имеют в React целенаправленное применение. Но при попытке передать через атрибут ref в свойстве current будет null. Можно «обхитрить» React и передать переменную domRef через какой-нибудь пропс, отличный от ref, как показано в листинге 5.71. Таким образом вы сможете получить доступ к DOM-узлу, но для передачи ref в компонент существует специальный способ. Листинг 5.71. Передача свойства domRef в дочерний компонент через пропс, отличный от ref /** */ return <Button tricky={domRef} onClick={handleClick} /> } export const Button = ({ tricky, onClick }) => { /** */
290  Глава 5 UI Рис. 5.44. Вывод ошибок в консоль при передаче переменной domRef При манипулировании DOM-элементами нужно помнить, что атрибут ref по умолчанию есть только у элемента. К чему приводит попытка указать свойство у компонента, вы уже видели. Чтобы у компонента появился пропс ref, надо использовать функцию forwardRef из React так, как показано в листинге 5.72. Листинг 5.72. Использование функции forwardRef для передачи ref в DOM-узел export const ParentComponent = () => { const domRef = useRef(null); const handleClick = () => { console.log('domRef', domRef); } } return <Button ref={domRef} onClick={handleClick} /> export const Button = forwardRef(({ onClick }, ref) => { }); return <button ref={ref} onClick={onClick}>Нажать</button> Если отрендерить компонент ParentComponent, то отрендерится кнопка, а в консоли не будет ошибок. Если нажать кнопку, то в консоль будет выведен объект со свойством current, где содержится ссылка на DOM-узел.
5.16. Другие хуки React  291 Помните: ref должен получить исключительно элемент, который вы выводите в JSX. Думаю, это очевидно, ведь любой тег, который вы размещаете в JSX, в конечном счете становится DOM-элементом. Просто передать ref куда-то и надеяться получить доступ к DOM-элементу означает не знать, как должен работать ref (рис. 5.45). Рис. 5.45. Передача ref через forwardRef Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.15. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.15. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.15 и ознакомьтесь с моим решением. 5.16. Другие хуки React 5.16.1. useMemo Данный хук предназначен для мемоизации результатов вычислений между рендерами. Это может быть список или объект, но не функция. Обратите внимание, что речь идет именно о результатах вычислений. Какие вычисления обычно пытаются мемоизировать? Если есть список, то, как правило, производят его сортировку или фильтрацию и мемоизируют полученный результат, чтобы не повторять это действие в следующий рендер. Если есть объект, то обычно его дополняют данными или преобразуют их. Если вычислений нет, а вы хотите мемоизировать данные, то
292  Глава 5 UI нужно применить НОС memo, описанный в предыдущем разделе, или поместить данные в состояние. Чтобы использовать хук useMemo, необходимо сделать следующее. 1. Импортировать его из React. 2. Передать в хук функцию, осуществляющую преобразование (то есть вычисление). 3. В зависимостях указать данные, которые участвуют в функции и от изменения которых функция должна быть выполнена вновь. Если оставить массив зависимостей пустым, то вычисленное значение останется таким, каким было вычислено при первом рендере. Можно не использовать массив зависимостей вовсе — тогда вычисление будет происходить при каждом перерендере, но это не имеет никакого смысла; все равно что хук для мемоизации не применялся вовсе. 4. Результат, возвращаемый хуком, присвоить переменной. 5. Задействовать переменную далее в коде, как правило в выводе компонента. Рассмотрим листинг 5.73. Листинг 5.73. Использование хука useMemo export const ComponentWithUseMemo = ({ productList }) => { const [isAscDirection, setIsAscDirection] = useState(false); const [state, setState] = useState(false); const memoizedProductList = useMemo(() => productList.sort(createSortFunction(isAscDirection)), [isAscDirection, productList]); const handleClick = (isAsc) => setIsAscDirection(isAsc); } return (<Fragment> <button onClick={() => setState(!state)}>state {state.toString()} </button> <button onClick={() => handleClick(false)}>По возрастанию</button> <button onClick={() => handleClick(true)}>По убыванию</button> { memoizedProductList.map(item => <ListItem key={item.id} title={item.title} />) } </Fragment>); Если посмотреть файл с листингом, то там тоже будет реализация функции createSortFunction, а также генератор списка, с помощью которого можно сгенерировать список и передать его в компонент ComponentWithUseMemo в качестве параметра.
5.16. Другие хуки React  293 5.16.2. Когда имеет смысл использовать useMemo Несмотря на то что можно мемоизировать вычисления, их большая часть не всегда в этом нуждается. Основной критерий для мемоизации — время, затрачиваемое на вычисление. Когда оно занимает порядка 1 миллисекунды, его можно причислить к затратным. Но не только время определяет необходимость мемоизации вычислений. Вот ситуации, когда необходимо применять хук useMemo. y Если, как уже было сказано, вычисления заметно медленные. y Когда нужно поместить результат вычислений в НОС memo. y Когда значение, вычисленное в хуке useMemo, будет использоваться далее как зависимость в другом хуке. Это может быть зависимость, далее применяемая в хуке useEffect, или в другом хуке useMemo используется результат предыдущей мемоизации. В остальных случаях использовать useMemo бессмысленно. Например, список, состоящий из одной тысячи элементов, не имеет смысла мемоизировать. Хочу также обратить внимание на то, что, поскольку мы работаем в IT-сфере, должны расширять свой словарный запас. Когда вы читаете «мемоизация вычислений», должны понимать, что это. 5.16.3. Хук useCallback Хук useCallback позволяет мемоизировать определение функции между рендерами. Известно, что объявление функции внутри другой функции при каждом следующем вызове создает новый объект функции. И что функция в JavaScript является объектом, содержащим код, который может быть вызван впоследствии сколько угодно раз, а также содержать свойства и методы, как и обычный объект. Это легко проверить (листинг 5.74). Листинг 5.74. Проверка того, что функции, объявленные внутри компонента при каждом рендере, не являются теми же самыми функциями import React, { Fragment, useState } from 'react'; const fns = []; export const ComponentWithFn = () => { const [count, setCount] = useState(0); const innerFn = () => {}; fns.push(innerFn); return (<Fragment> <div>{count}</div> <button onClick={() => setCount(count => count + 1)}>+1</button>
294  Глава 5 UI { }; fns.map((fn, idx) => <div key={idx}>{(fn === innerFn).toString()}</div>) } </Fragment>); Нужно отрендерить компонент, и если нажимать кнопку +1, то на экран будет выводиться false, кроме последнего сравнения, равного true. Ведь последняя помещенная в массив функция является той, что была объявлена в текущем рендере. А вот функция, образовавшаяся при текущем рендере, не равна всем функциям, которые были помещены в массив при предыдущих рендерах. Причина этого описана ранее. Однако мы можем задействовать хук useCallback. Если применить его к текущей ситуации, то получим код как в листинге 5.75. Листинг 5.75. Мемоизированная функция с помощью useCallback import React, { Fragment, useState, useCallback } from 'react'; /** все тоже до, что и в листинге 5.67 */ const innerFn = useCallback(() => {}, []); fns.push(innerFn); /** все тоже после, что и в листинге 5.67 */ Листинг 5.75 практически не изменился, по сравнению с листингом 5.74. Функция помещена в хук useCallback, а в остальном все то же самое. Однако если отрендерить компонент, то все предыдущие функции будут равны функции в текущем рендере. Это достигается благодаря мемоизации. 5.16.4. В каких случаях использовать хук useCallback Хук можно использовать в таких случаях: y при работе с НОС memo; y при оптимизации перерендеров кастомного хука; y когда функция является зависимостью в другом хуке. При использовании НОС memo может потребоваться передача обработчика события, как в листинге 5.76. Листинг 5.76. Использование мемоизированного колбэка const ListItem = ({ id, value, onClick }) => <button onClick={() => onClick(id)}>{value}</button>; const MemoizedListItem = memo(ListItem); export const MemoizedCallback = () => { const [pressedButtons, setPressedButton] = useState([]); const [count, setCount] = useState(0); const handleClick = useCallback((id) => setPressedButton(pressedButtons => [...pressedButtons, id]), []);
5.16. Другие хуки React  295 const handleChangeCount = () => setCount(count => count + 1); } return (<Fragment> <div>Вы нажали кнопки: { pressedButtons.map((pressed, key) => <span key={key}>{pressed}, </span>) }</div> Сменить состояние: <button onClick={handleChangeCount}>{count}</button> { list.map(({id, value}) => <MemoizedListItem key={id} id={id} onClick={handleClick} value={value} />) } </Fragment>); Если отрендерить компонент MemoizedCallback , поставить на запись профилировщик, понажимать на кнопки и потом прекратить запись профилировщиком, то будет видно, что ни один из дочерних компонентов не перерендеривается. Но как убедиться, что это происходит благодаря оптимизации? Да очень просто — надо убрать мемоизацию из handleClick и передать колбэк в мемоизированный компонент MemoizedListItem. При таком подходе использование НОС memo бессмысленно, так как при каждом перерендере создается новый объект функции. Ситуация с оптимизацией кастомного хука будет рассмотрена в следующем разделе. 5.16.5. Как использовать хук useCallback 1. Получить функцию для мемоизации либо написать реализацию как первый параметр вызова хука useCallback. Если вы реализуете функцию в качестве первого параметра, то также должны объявить параметры, если они используются при вычислении. 2. Передать зависимости в массив зависимостей. Зависимости должны быть использованы в теле мемоизированной функции. Массив зависимостей может быть пустым либо содержать переменные, но не быть пустым. 3. Присвоить возвращаемое хуком значение переменной. 4. Использовать переменную как ссылку на функцию там, где требуется. Есть пара моментов, которые необходимо знать. Первое — массив зависимостей в хуке useCallback нельзя опускать, так как при каждом перерендере будет создаваться новый объект функции, а значит, в использовании хука нет никакого смысла. Это показано в листинге 5.77. Второе — нельзя использовать вызов useCallback в цикле. Это показано в листинге 5.78. Листинг 5.77. Бессмысленное использование хука useCallback export const ComponentMemoizedFn = () => { const [count, setCount] = useState(0); const memoizedFn = useCallback(() => {
296  Глава 5 UI } return setCount(count => count + 1); }); /** реализация компонента */ Листинг 5.78. Использование хука useCallback в цикле { } list.map(({id, value}) => { const handleClick = useCallback((id) => setPressedButton(pressedButtons => [...pressedButtons, id]), []); }) return <MemoizedListItem key={id} id={id} onClick={handleClick} value={value} /> Листинг 5.78 — это измененный листинг 5.76. Так использовать хук не получится: хук не может быть вызван внутри колбэка в цикле. Для реализации функционала нужно вынести этот колбэк и использовать как в листинге 5.76 или сформировать отдельный компонент, где создать мемоизированный колбэк и передать его в возвращаемый компонент MemoizedListItem. Такой компонент можно помещать в цикл. 5.16.6. Разница между useMemo и useCallback При использовании хуков useMemo и useCallback часто возникает путаница. Может появиться идея, что раз useMemo возвращает мемоизированное вычисление, то почему бы не вернуть из хука функцию. И действительно, useMemo можно использовать для возвращения мемоизированной функции. Но для этих целей существует useCallback, способный мемоизировать функции без дополнительных вызовов. Вот, например, какая разница между этими двумя хуками для мемоизации функции (листинг 5.79). Листинг 5.79. Мемоизация функции хуками useMemo, useCallback import React, { useMemo, useCallback } from 'react'; export const OurComponent = () => { const byUseMemo = useMemo(() => () => {/** */}, [dependency]); const byUseCallback = useCallback(() => {/** */}, [dependency]); } return (/** */); Мемоизация функции возможна с использованием как useMemo, так и useCallback. Однако мемоизация вычислений возможна только с помощью хука useMemo. С помощью хука useCallback можно напрямую мемоизировать функцию, но если ее передать в этот хук, она не будет вызвана. В useMemo передается функция, которая вызывается и уже мемоизирует результат, возвращаемый вызванной функции.
5.16. Другие хуки React  297 5.16.7. Хук useId Хук useId генерирует уникальный ID, который может быть передан в атрибут доступности. Помимо основного назначения — использования в атрибуте доступности, — его можно применять для генерации ID, задействуемого в атрибуте id DOM-элементов. Чтобы использовать хук, его нужно вызвать без параметров и возвращаемое значение присвоить переменной, а затем переменную использовать в атрибуте доступности или в атрибуте id. Примеры использования приведены в листингах 5.80 и 5.81. Листинг 5.80. Использование сгенерированного ID в атрибуте доступности export const PasswordField = () => { const passwordHintId = useId(); return (<Fragment> <label> Password: <input type="password" aria-describedby={passwordHintId} /> </label> <p id={passwordHintId}> Пароль должен содержать по крайней мере 7 символов </p> </Fragment>); } Листинг 5.81. Использование сгенерированного ID в атрибутах id, htmlFor export const const id = return ( <form> <label <input <hr /> <label <input </form> ); } Form = () => { useId(); htmlFor={id + '-firstName'}>Имя:</label> id={id + '-firstName'} type="text" /> htmlFor={id + '-lastName'}>Фамилия:</label> id={id + '-lastName'} type="text" /> Хук useId нельзя использовать для генерации ключей списка, как показано, например, в листинге 5.82. Листинг 5.82. Некорректное использование useId для генерации ключей списка const productList = [{title: 'Булка'}, {title: 'Хлеб'}, {title: 'Рогалик'}]; export const ProductList = ({ productList }) => { return (<Fragment> { productList.map(product => {
298  Глава 5 UI const key = useId(); const { title } = product; }) } return <div key={key}>{title}</div>; } </Fragment>); Несмотря на большой соблазн, для генерации ключей списка нужно использовать данные. 5.17. Кастомные хуки 5.17.1. Что такое кастомный хук Кастомные хуки служат конкретным целям. Словом «кастомный» обычно обозначают то, что является пользовательским, заказным, специальным или сделанным на заказ. Каждое из этих определений подходит и к хукам, которые вы можете разработать для своих нужд, а не взять из библиотеки React. Однако не только она предоставляет хуки, а также библиотеки @ReduxJS/Toolkit и React-Router-Dom. Эти хуки не называют кастомными. Кастомные лишь те, которые изготавливаете вы сами для текущего приложения. 5.17.2. Переиспользование обработки данных Кастомные хуки нужны для того, чтобы переиспользовать способы обработки данных так, как нужно именно вам. При этом можно сохранять состояние или задействовать сторонние API, WebAPI или делать сетевые запросы. Например, вы делаете запрос данных в компоненте. Аналогичное действие потребуется и в другом компоненте. Значит, нужно вынести это в кастомный хук. Несмотря на то что кастомный хук располагается в отдельном месте, в функции, это то же самое, что написать код такого хука внутри компонента. Для приложения нет разницы, где располагается код — в отдельной функции, именуемой кастомным хуком, или внутри компонента. Разница появляется для разработчика, так как в дальнейшем хук, вынесенный в отдельную функцию, вы можете переиспользовать в любых других компонентах. 5.17.3. Составляющие кастомного хука и некоторые правила y Имя кастомного хука должно начинаться с use. y Хук должен вызываться на верхнем уровне компонента. y Кастомный хук должен содержать внутри себя вызов хотя бы одного хука из имеющихся в React или из других библиотек, предназначенных для React. y Если кастомный хук возвращает функцию, то ее необходимо мемоизировать с помощью хука useCallback.
5.17. Кастомные хуки  299 y Если хук подразумевает параметры, то они должны стать зависимостью в ис- пользуемых в хуках React в теле кастомного хука. Хук нельзя вызывать: y в условии (if, case), цикле (for, while) или методах обработки списков (forEach, map); y после выражения с условным рендерингом. Например, когда есть условие if, за которым следует инструкция return, содержащая вывод; y в обработчиках события. Это частный случай первого пункта текущего списка; y внутри функций, переданных в хуки useCallback, useMemo и useEffect; y внутри блока try/catch/finally. Пояснение по поводу вызова хука на верхнем уровне. Вы не можете вызывать хук в условии (рис. 5.46). Допустим, вы написали кастомный хук useLocation, который может перенаправить из кода компонента на другой URL; это необходимо сделать по условию, и вы решаете сделать вызов хука в блоке кода, расположенного за инструкцией if (листинг 5.83). Листинг 5.83. Расположение хука в блоке кода после инструкции if export const ConditionHook = () => { const [authenticated, setAuthenticated] = useState(true); if (!authenticated) { const { assign } = useLocation(); assign('/signin'); } const handleClick = () => setAuthenticated(authenticated => !authenticated); }; return (<Fragment> <p>Тест переадресации. Сейчас пользователь аутентифицирован. Сделаем тест.</p> <button onClick={handleClick}> {authenticated ? 'Разаутентифицироваться' : 'Аутентифицироваться'} </button> </Fragment>); Рис. 5.46. Предупреждение о наличии хука в условии
300  Глава 5 UI Если проигнорировать это предупреждение и нажать кнопку, то выполнится код компонента в блоке, следующем за условием if, а в консоль будет выведена ошибка (рис. 5.47). В итоге приложение, если в нем не предусмотрена обработка ошибок, перестанет работать. Поэтому самое правильное — перенести вызов хука на верхний уровень компонента. Рис. 5.47. Сообщение об ошибке в консоли Пояснение по поводу использования в кастомном хуке хуков из React. На самом деле кастомный хук можно реализовать и на основе хуков из других библиотек, но, разумеется, они должны быть созданы на основе хуков из React. Почему? Потому что хуки из библиотеки React или другие, созданные на их основе, осуществляют мемоизацию или сохраняют значение между перерендерами, а также при изменении состояния инициируют перерендер. Если вы можете реализовать данный функцио­ нал, то сумеете использовать и собственную реализацию. Если создать кастомный хук, который не использует хуки из React или других библиотек, предназначенных для работы в React, это будет обычная функция, а не хук. То, что название функции начинается с use, не означает, что она становится хуком. Вот пример функции, которая является обычной функцией, а не хуком (листинг 5.84). Листинг 5.84. Пример функции, не являющейся хуком export const useFetch = (url, cb) => { fetch(url) .then(response => response.json()) .then(data => { cb && cb(data); }); }; Однако если немного видоизменить эту функцию, у вас появится хук (листинг 5.85).
5.17. Кастомные хуки  301 Листинг 5.85. Хук, мемоизирующий данные запроса import { useEffect, useState } from 'react'; export const useFetch = (url) => { const [data, setData] = useState([]); useEffect(() => { fetch(url) .then(response => response.json()) .then(data => { setData(data); }); }, [url]); }; return data; Еще одна важная особенность кастомного хука состоит в том, что если вы используете в хуке useState, то при изменении состояния компонент, применяющий такой кастомный хук, перерендерится. Выше было сказано, что код кастомного хука — то же самое, что расположение кода хука в самом компоненте. Стоит упомянуть, что хук, использующий fetch, должен обладать несколько большей функциональностью и использовать чуть больше платформенных объектов (Request, Headers, Response). Однако версия, представленная в листинге 5.85, может работать с GET-запросами к эндпоинтам, которые будут возвращать ответ в формате JSON. Во многих случаях такого хука достаточно. 5.17.4. Как написать кастомный хук Существует два способа написания. Первый способ — перенести код, который был написан в компоненте, в отдельную функцию. При этом придется внести некоторые изменения в то, что касается передачи параметров и возвращаемых значений. Второй способ — написать хук с нуля. Для этого нужно выполнить следующие шаги. 1. Определить результат хука. 2. Определить API, подходящий для результата, данные и структуры, хранящие эти данные, или UI, которые будут задействованы для реализации хука. 3. Задать имя, начинающееся с use и использующее имя задействованных API, данных, структур или UI, UI-эффектов. 4. Определить возвращаемые значения и/или функции. Хук также может ничего не возвращать. 5. Определить параметры хука, которые будут использованы в вычислениях в его теле. 6. Написать реализацию хука.
302  Глава 5 UI Шаг 6 требует пояснения. При реализации кастомного хука следует использовать имеющиеся в React хуки, или написанные ранее вами же кастомные хуки, либо хуки из других библиотек, предназначенных для работы с React. Если хук подразумевает асинхронность, то вам необходимо или использовать хуки для хранения состояния, например useState, useReducer, или в параметре определять колбэки, которые будут вызваны при окончании асинхронного действия. Примером является листинг 5.85. Обратите внимание: если вместо useState использовать колбэк в параметрах, то этот хук станет обычной функцией, как в листинге 5.84. Нужно также понимать, что возвращаемые значения и параметры могут уточниться при написании реализации. Возвращаемые из кастомного хука функции следует мемоизировать с помощью useCallback. 5.17.5. Хуки React, задействуемые в кастомных хуках Конкретной статистики по хукам из React, используемым в кастомных хуках (табл. 5.2), я не собирал. Могу лишь высказать свои предположения, что они располагаются в таком порядке (от наиболее часто используемого к менее часто): useState, useEffect, useCallback, useRef, useMemo, useContext. Остальные примерно одинаково или вообще не задействуются. Таблица 5.2. Основное назначение React-хуков в кастомном хуке Название Для чего использовать useEffect Когда необходимо выйти за пределы React и задействовать или стороннюю библиотеку, или WebAPI useCallback Когда нужно мемоизировать функцию или осуществить оптимизацию перерендеров, то есть мемоизировать возвращаемую хуком функцию useMemo Когда необходимо мемоизировать данные вычислений, которые могут возвращаться из хука или использоваться в качестве зависимости в другом хуке, используемом в теле кастомного хука useRef Когда нужно мемоизировать данные или ссылку на DOM-элемент, которые не должны изменяться при перерендерах, но которые можно изменить при необходимости useState Когда необходимо мемоизировать любые произвольные данные. Чаще используется для простых данных useReducer Когда необходимо мемоизировать любые произвольные данные. Чаще используется для целого ряда связанных между собой данных или данных всего компонента в целом
5.17. Кастомные хуки  303 Название Для чего использовать useContext Когда необходимо в кастомном хуке получать какие-либо значения из заранее установленного контекста1. При этом неважно, где в поддереве контекста будет располагаться кастомный хук, использующий данные контекста useId Когда необходимо сгенерировать уникальный ID в пределах компонента, где будет использоваться кастомный хук useSyncExternalStore Когда необходимо синхронизироваться с хранилищем данных, расположенных вне React useImperativeHandle Когда необходимо ограничить возможности или просто сделать кастомным использование ссылки на DOM-элемент вместо предоставления самой ссылки на DOM-элемент. Аналог Proxy для DOM-элементов 5.17.6. Как приспособиться писать кастомный хук или просто облегчить его создание Как было сказано в самом начале, кастомный хук — то же самое, что код хука, написанный в теле компонента. Раз так, значит, это можно использовать при написании хука. Поэтому первое действие — написать код хука внутри компонента, чтобы добиться его работоспособности. Под «добиться» я имею в виду вашу привычку писать код. Кто-то любит сначала придумать и написать, лишь затем выполнить. При этом вычисления выполняются в уме, а код — реализация задумки. Другой разработчик может идти строго последовательно, на каждом шаге используя console.log. Написав один шаг, проверять выводы в консоли. В любом случае главное, чтобы вы получили от кода нужный результат. Когда вы добились необходимой работоспособности, нужно вынести код хука в отдельную функцию и вновь использовать этот хук в теле того компонента, откуда вы только что вынесли его код. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.17. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.17. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.17 и ознакомьтесь с моим решением. 1 Контекст в React — любые данные, доступные целому поддереву компонентов. Чтобы данные стали именоваться контекстом, используется функция из react createContext и специальный компонент Provider, являющийся свойством созданного объекта контекста (см. раздел 5.11).
304  Глава 5 UI 5.18. Объединение библиотек React и Redux 5.18.1. Библиотека React-Redux Библиотеки React и Redux Toolkit не будут вместе работать, если не дописать определенный код. Он должен включать в себя компоненты и хуки React. Для этих целей создана библиотека React-Redux. Установка библиотеки обычная, как показано в листинге 5.86. Листинг 5.86. Установка React-Redux npm install react-redux Подразумевается, что перед установкой React-Redux у вас уже были установлены React и Redux Toolkit. Для примера мы рассмотрим одностраничное приложение с возможностью добавления товара в корзину. Чтобы называться полноценным интернет-магазином, у данного приложения отсутствуют некоторые составные части. Из основного — отсутствие аутентификации, маршрутизации (соотнесение набора компонентов, которые должны быть отрендерены при совпадении пути в URL, с определенным шаблоном, заданным в коде), а также списка разделов (обычно он есть в интернет-магазине). 5.18.2. Provider Материал по контексту мы прошли в разделе 5.11. Компонент Provider для работы в React задействует описанный подход. Углубляться в детали здесь не будем, но сказать об этом нужно, чтобы вы понимали, зачем в код приложения импортировать данный компонент. В качестве свойства ему необходимо передать стор, как показано в листинге 5.87. Листинг 5.87. Внедрение Redux через Provider в React-приложение // src/app/store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer, }, }); // index.js import React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { store } from './app/store'; import App from './App';
5.18. Объединение библиотек React и Redux  305 import './index.css'; const container = document.getElementById('root'); const root = createRoot(container); root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> ); Таким образом, всему поддереву доступно содержимое стора. Правда, для его извлечения нужно применить хук, о чем будет рассказано далее. Компонент Provider делает состояние доступным всему поддереву. При этом вы можете сделать состояние стора доступным меньшей части дерева приложения — точно так же, как было с контекстом. 5.18.3. Хуки Библиотека React-Redux тоже предоставляет два хука — useSelector и useDispatch, о назначении которых несложно догадаться, исходя из их названий. Хук useSelector применяется, чтобы извлечь часть дерева состояния из стора, используя в качестве аргумента ссылку на функцию-селектор. Хук useDispatch применяется, чтобы получить в компоненте метод стора dispatch. Впоследствии он применяется, чтобы отправить в стор экшен. В качестве аргумента dispatch принимает или объект экшен, или action creator. Пример использования показан в листинге 5.88. Листинг 5.88. Использование хуков useSelector, useDispatch // src/features/counter/Counter.js import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; // import {} from "./counterSlice"; export function Counter() { const count = useSelector(selectCount); const dispatch = useDispatch(); const [incrementAmount, setIncrementAmount] = useState('2'); const incrementValue = Number(incrementAmount) || 0; return ( <div> <div className={styles.row}> <button className={styles.button} aria-label="Decrement value"
306  Глава 5 UI > } ); onClick={() => dispatch(decrement())} </button> // часть компонента не показана, так как содержит повторение dispatch В третьей строке листинга из библиотеки React-Redux импортируются два хука. Хук useSelector применяется для того, чтобы селектор из библиотеки ReduxJS/Toolkit можно было использовать в коде компонента. Как это делается, показано в строке 7. Метод useDispatch возвращает метод dispatch стора (строка 8 листинга), и впоследствии этот метод используется для отправки экшена в стор (строка 18 листинга). 5.19. Новый взгляд на React 5.19.1. Что здесь освещено Для понимания этого раздела требуется знать изложенный выше материал по React. Здесь вы изучите некоторые идеи, которые сможете применять при разработке. Также в этом разделе представлены некоторые обобщения, чтобы было легче сделать вывод из пройденного ранее. Подавляющая часть материала больше нигде не освещена, в том числе в других публикациях, имеющихся на сегодняшний день. 5.19.2. На что направлять внимание Когда читаешь описание библиотеки React, везде повторяется, что она позволяет описать интерфейс пользователя с помощью переиспользуемых компонентов, содержащих JSX. Все так и есть, но компонент, который ставится во главу, немного переоценен по части значимости. Если быть точным, то на нем заострили внимание и многие разработчики отталкиваются от него при разработке. Скорее всего, вы скажете: «Ну и ладно. А что в этом такого?» В свое время выделение такого модуля, как компонент, стало революционным решением. Когда появился он и JSX, существенно ускорилась разработка: больше не приходилось думать о каждом теге, представленном в DOM браузера. С помощью компонентов действительно легко описать интерфейс. Однако это сыграло с нами злую шутку, и вот почему. Создатели библиотеки направили внимание на компонент, но не упомянули о разработке фронтендприложения и не указали, в какой момент разработки данный компонент играет важную роль. Вероятно, поэтому про приложения мало кто пишет. А ведь, начиная с раздела 3.4, мы знаем, что фронтенд-приложение — целая система. Раз так, одного компонента маловато. Поэтому я хочу внести в эту область свой посильный вклад, а именно: в момент проектирования или доработок нужно направлять внимание на структуру, которая состоит из компонентов и связей между ними, а на компонент и используемые в нем зависимости1 — в момент реализации. 1 Под зависимостями подразумеваются функции, хуки, HOC, обработчики событий.
5.19. Новый взгляд на React  307 При проектировании ваша первая задача — задать структуру приложения и определить, из каких компонентов оно будет состоять, с какими внешними системами взаимодействовать. При реализации вы продумываете непосредственно код компонентов, чтобы наладить нужные связи между ними, а также с внешними системами, внутренними хранилищами данных. Структура — это определенные связи, взаиморасположение составных частей приложения, ее устройство, строение. Она создается с помощью React-компонентов. Компоненты связываются между собой с помощью связей (пропсы, возвращаемые значения или JSX). Обмен данными — это ответ на какое-то событие в виде передачи новых значений, как между React-компонентами, так и между хранилищами и React-компонентами. Теперь настает очередь компонентов и JSX. На заре становления библиотеки React бок о бок с ней развивалась библиотека Redux для работы с состоянием. На тот момент для их совместной работы Дэн Абрамов ввел понятия «презентационный компонент» и «компонент-контейнер». Сегодня в них нет необходимости, надо забыть про такое разделение. Во-первых, потому, что это устаревший подход: в библиотеку React добавлены хуки, позволяющие без труда добавлять состояние в приложение без библиотеки Redux. Во-вторых, в React есть хуки, помогающие наладить обмен данными между компонентами. Сегодня нужно забыть прежний подход и принять новую точку зрения: «компонент играет роль» в зависимости от того, где в иерархии компонентов, составляющих структуру приложения, он находится. Компонент может обмениваться данными с хранилищем и другими компонентами. Он должен принимать данные, отображать или изменять их, отправлять родителю или своим дочерним компонентам. Связывая компоненты в структуру, вы получаете приложение. 5.19.3. Структура приложения и роли компонентов в ней Структура строится на основе компонентов. Но, говоря про структуру приложения и применяя для ее описания такую структуру данных, как дерево, частенько упускают из виду, что структура приложения — не просто дерево (в подразделе 5.19.7 мы уточним этот момент), а иерархия компонентов. Это ключевой момент, на который я обращаю ваше внимание. Иерархия, повторюсь, представляет собой естественное соподчинение частей одного целого. Соподчинение в данном случае — это последовательное подчинение частей или элементов одного уровня частям, элементам более высокого уровня. Когда мы работаем в коде с такой структурой данных, как дерево, объекты, называемые узлами (от англ. node), какого-то уровня, взятого наугад, являются абсолютно точно такими же, как и те, что расположены на одном уровне, ниже или выше. Разнятся только значения свойств наподобие parent, children и др. При этом у каждого уровня структуры данных нет качественного изменения самих узлов. Если, допустим, родительский узел поменять местами со средним узлом из первого дочернего уровня (рис. 5.48) и поменять значение свойств children, parent, next и previous, чтобы узел как бы был на своем месте, то не изменится ничего, кроме, например, уникального идентификатора. А если заменить и его, то ничего от слова «совсем».
308  Глава 5 UI Рис. 5.48. Иерархия узлов в структуре данных «дерево» Однако если применить аналогичную замену к структуре данных дерева вебприложения, то вы все сломаете. Почему? Компоненты структуры веб-приложения, расположенные на разных уровнях, отличаются друг от друга качественным образом. Общее у структуры приложения и дерева — только структура (расположение узлов, которыми являются компоненты, и наличие связей между ними). Виды связей и сами компоненты будут отличаться. Компоненты веб-приложения качественно меняются в зависимости от уровня расположения. По этой причине данная структура приложения называется иерархией. Компоненты более низкого уровня подчиняются компоненту более высокого уровня, и наоборот — компонент более высокого уровня управляет компонентами более низкого. На рис. 5.49 видно, что на разных уровнях иерархии располагаются разные по типу компоненты. Одно многостраничное веб-приложение будет отличаться от другого составом компонентов и их реализацией, но уровни от одного приложения к другому — примерно одинаковые. Говоря «примерно», я имею в виду, что уровни от «Компонент приложения» до «Уровень простых (конечных) компонентов» присутствуют всегда. Провайдеры — уровень, как правило выставляемый до уровня приложения, чтобы предоставить какие-либо данные и/или функциональность для приложения в целом. Провайдеры могут располагаться и ниже уровня приложения, но тогда эти данные будут доступны только части дерева, а не всем компонентам. Обычно используются библиотеки, в которых задействован провайдер. Примерами здесь являются такие компоненты, как Provider из библиотеки ReactRedux и RouterProvider из библиотеки React-Router-Dom. В самой библиотеке React компонент-провайдер является свойством контекста, задаваемого вызовом функции createContext. Эта роль чисто технически (с помощью внедрения в компонент провайдера контекста React) может быть совмещена с другими ролями, кроме простого конечного компонента.
5.19. Новый взгляд на React  309 Рис. 5.49. Иерархическая структура компонентов приложения Основное назначение — предоставить какие-либо данные поддереву компонентов с помощью возможности React, такой как контекст. Шаблон верстки, или лэйаут, — уровень, применяемый для создания шаблона, которому будут соответствовать все страницы приложения. Он также может располагаться ниже и с теми же следствиями — влияние не на все приложение, а только на его часть. Основное назначение — рендерить дочерние компоненты, передаваемые в качестве дочерних, включая компоненты, которые определены в самом лэйауте. Приложение — уровень, собирающий воедино все страницы, как родительский узел. Основное назначение — группировка разных компонентов, таких как провайдеры, страницы. Компонент призван служить корневым узлом. Страницы — уровень, который представляется пользователю в виде разделов приложения. Группирует бизнес-компоненты и простые конечные компоненты, может включать в себя провайдеры. Кроме того, на страницах может располагаться код, создающий запросы к серверу. Полученные данные передаются далее бизнес-компонентам. Основное назначение — группировка компонентов для создания одного раздела, представленные данные и функциональность в котором отличаются от других разделов приложения. Бизнес-компоненты — уровень основных действующих компонентов, то, ради чего пользователь использует сервис или приложение. Обычно они или делают запросы к серверу за данными, или принимают пропсами данные от других, ради чего и существует это приложение. Основное назначение — отобразить пользователю данные, ради которых он использует приложение. Уровень простых (конечных) компонентов (в React-приложении) — компоненты, состоящие не из других, переиспользуемых компонентов (такой компонент может состоять только из компонентов, импортируемых из дизайн-системы, или других конечных компонентов, которые более не переиспользуются сами по себе),
310  Глава 5 UI а исключительно из JSX, в виде тегов. Могут быть переиспользованы в разных местах приложения, а не только в бизнес-компонентах. Простой конечный компонент может использоваться не только в бизнес-компонентах, но чаще всего именно там. Основное назначение — предоставить часто переиспользуемый JSX. Когда создатели библиотеки React говорят про переиспользуемые компоненты, они имеют в виду компонент с ролью «Уровень простых (конечных) компонентов». Важный вывод из подраздела: управляя положением компонента в иерархии, вы повышаете или понижаете его влияние на приложение и таким образом качественно изменяете его смысл. Несмотря на то что реальное приложение гораздо разветвленнее, основная структура именно такая, как описано выше, а роли компонентов — исключительно такие. Чтобы увидеть разные виды компонентов и узнать, где они располагаются в приложении, какой код содержат, посмотрите код приложения в репозитории, в папках 5.89–5.93. Начните читать файл README.md, чтобы понять, где и какие компоненты располагаются. 5.19.4. Чтение компонента Во многих источниках много говорится о создании компонентов, но мало — об их чтении. Ниже данная процедура рассмотрена. Как реализовать компонент, описано в разделе 8.3. Если придерживаться идеи о роли компонента, то легко понять, правильно ли написан компонент. Однако забудем про роли и сосредоточимся на выражениях JavaScript, которые есть в компоненте. В самом его верху располагаются импорты, затем следует реализация самого компонента. Каждый разработчик склонен по-своему организовывать связи между компонентами приложения и компоновать сами файлы, в которых компоненты реализованы, то есть написан код. Это отличительная особенность, она не повторяется. Иногда в одном файле компонента, как в зоне до начала его реализации, присутствует реализация функций и других компонентов. А в пространстве после основного компонента порой обнаруживается немало реализаций функций или компонентов. Такое решение мешает чтению. На самом деле чтение — одна из главных задач разработчика. Поэтому нужно приобрести способность к беглому обзору компонента, не вникая в детали (если, конечно, вашей задачей не является детальное изучение). Вот на что следует обратить внимание. y Импорты. Сперва нужно сосредоточиться на абсолютных импортах из главных библиотек React, Redux, React-Router-Dom. Импортированные хуки из React подскажут, как и с чем работает компонент:  useState и useReducer — что компонент содержит состояние, а значит, может перерендериваться;  useEffect — что используется синхронизация со сторонним API или WebAPI;  useContext — что используется источник данных какого-то родительского компонента;
5.19. Новый взгляд на React  311  y y y y y useCallback — что колбэк, переданный в дочерний компонент в JSX, мемоизируется, чтобы не вызывать лишний перерендер, а значит, вам нужно посмотреть, используется ли НОС (High Order Component) React.memo (другой НОС, созданный на его основе), чтобы действительно предотвращать перерендеры;  useMemo — что какие-либо данные мемоизируются. По всей видимости, компонент принимает большой список с данными, которые рендерит в своем выводе;  useSelector из библиотеки React-Redux — укажет на извлечение состоя­ ния из стора, а useDispatch — на то, что данный компонент отправляет экшены в стор. Другие хуки React пока серьезного значения не имеют, поэтому нужно переходить к самому компоненту. Количество импортов вообще — не только абсолютных, но и относительных — укажет на объем компонента. Если много импортов компонентов, значит, у него сложный вывод (интерфейс). Если много импортов функций, значит, много вычислений. Большое количество вычислений, как правило, увеличивает количество используемых условий, а значит, растет и умственная сложность чтения. Что выводит компонент. Сложность добавит условный рендеринг, тогда в компоненте будет присутствовать несколько инструкций return и условий, выполнение которых приводит к выводу. Состояние, которое хранит компонент. Как работает с дочерними компонентами и передает им данные. Какое API задействуется в эффектах. Если необходимо выяснить что-то дополнительно, можно посмотреть оставшиеся выражения в теле компонента. Если он включает в себя большое количество строк, значит, в его теле реализовано множество колбэков, обработчиков событий, просто функций и компонентов, черпающих значения из его области видимости, а затем они используются в коде реализации и выводе. 5.19.5. Обмен данными между компонентами Создав структуру, вы можете легко задать состояние и обмен данными. Зачем об этом говорить? Распространена ситуация, когда одно значение должно быть доступно в двух и более компонентах. На первый взгляд, здесь нет ничего, что могло бы создавать проблемы. Однако некое значение может быть состоянием компонента, а его значение — быть доступно другому компоненту, например родителю, предку1 или компоненту, расположенному далеко в дереве. 1 Предок — узел, который является родителем, отстоящим от текущего рассматриваемого узла на несколько уровней вверх. Иначе говоря, это родитель одного или нескольких родителей какого-либо узла. Если посмотреть на рис. 5.50 и найти компоненты с циф­рами 2, то верхний компонент с цифрой 2 на уровне страниц — предок для компонента с цифрой 2 внизу.
312  Глава 5 UI Обмен данными — типичное динамическое проявление системы, то есть поведение. Как известно, поведение — это переход из одного состояния в другое. Или, если взглянуть с другой стороны, изменение состояния ведет к изменению поведения системы, и она оказывается в другом состоянии. Можно прибегнуть к разным способам обмена данными. Выбор зависит от того, где в структуре (точнее, в иерархии приложения) расположены компоненты относительно друг друга. Они могут быть связаны непосредственно отношением «родитель — дочерний», находиться в одном поддереве, начиная с какого-то родительского компонента, или же в разных поддеревьях. Все три вида отношений показаны на рис. 5.50. Рис. 5.50. Расположение в структуре приложения компонентов, которым необходимо обмениваться данными Компоненты, обозначенные цифрами 1, имеют непосредственную связь, а цифрами 2 и 3 — нет. Компоненты, обозначенные цифрами 2, располагаются в одном поддереве; обозначенные цифрой 3 — в разных. Указав на компоненты с цифрой 3, можно возразить, что они, в принципе, тоже имеют общего родителя и, по сути, располагаются в одном поддереве. Это будет верно. Поэтому лучше обговорить, с какого уровня выделять поддеревья. Поддеревом в данном случае и в данной ситуации называется то поддерево дерева приложения, которое начинается с уровня страницы, включая ее компонент. Поэтому если вы еще раз взглянете на рис. 5.50, то увидите, что первый компонент с цифрой 3 находится во втором поддереве (считаем слева направо), а второй — в пятом. Теперь нужно внести ясность, что такое обмен данными между компонентами с технической точки зрения. Компоненты фронтенд-приложения не являются активными сущностями, как процессы (программы, которые вы запустили на исполнение) в операционной системе. Поэтому все происходит исключительно через
5.19. Новый взгляд на React  313 события, которые активирует пользователь или задает разработчик приложения через setTimeout, setInterval или через события из сети, например по веб-сокету. Веб-сокет-соединение устанавливается один раз, после чего сервер и клиент могут обмениваться данными в обе стороны. События по веб-сокету могут быть инициированы сервером, а на клиенте установлена обработка такого события. По своему желанию компонент не может ничего инициировать. Но после того, как происходит событие и состояние обновляется, обмен данными возможен. Теперь можно дать определение. Обмен данными между компонентами — это изменение состояния в источнике и получение значения его нового состояния в приемнике. Получение значения нового состояния в приемнике происходит только после перерендера или размонтирования источника и монтирования приемника, которым предшествовало изменение состояния. Для получения значения состояния приемник должен иметь с источником какую-либо связь. Изучим детально определение обмена данными для всех случаев. При этом ситуации с цифрами 1 и 2 будем рассматривать с двух сторон, когда источником является родительский компонент и когда источником является дочерний компонент. Для компонентов с цифрами 3 иерархических отношений нет, поэтому они равнозначны, независимо от того, на каком уровне иерархии в своем поддереве каждый из них расположен. Для демонстрации того, как это реализуется в коде, создано React-приложение, расположенное в репозитории кода, Рис. 5.51. Передача в папках 5.89–5.93. Все листинги для этого подраздела взяты данных от родительского из него. компонента дочернему Первый способ называется «Родитель — дочерний» и показан на рис. 5.51. На всех рисунках с обменом данными стрелка означает направление передачи данных. Способ «Родитель — дочерний» используется для непосредственной передачи данных между двумя уровнями иерархии: родитель передает данные дочернему компоненту (листинг 5.89). Это способ, который сами разработчики React объявляют как возможности самого компонента. Один компонент связан с другим и может передавать ему данные через пропсы. Листинг 5.89. «Родитель — дочерний» // ParentChild.js export const ParentChild = () => { const [text, setText] = useState('Измените текст'); const handleChange = (e) => {setText(e.target.value);}; return ( <Fragment> <div className='row bgGray'> <div className='cell'>Это родительский:</div> <div className='cell'>
314  Глава 5 UI }; ); <input type='text' value={text} onChange={handleChange} /> </div> </div> <div className='row'> <div className='cell fontGray'>Это дочерний:</div> <div className='cell'><A text={text} /></div> </div> </Fragment> // A.js export const A = ({ text }) => <div>{text}</div>; Второй способ называется «Дочерний — родитель» и показан на рис. 5.52. Не существует прямого способа передать данные от дочернего компонента родителю. Чтобы это произошло, родитель должен передать колбэк в дочерний компонент через пропсы. Такой колбэк должен принимать аргументом данные, которые дочерний компонент должен передавать родителю. А при вызове такого колбэка в родительском компоненте должно меняться состояние, иначе нет никакого смысла. Рис. 5.52. Передача По этой причине необходимо организовывать передачу данных от дочернего состояния от дочернего компонента к родительскому или компонента родителю через обновление состояния, или через вызов обработчика события в дочернем компоненте. В любом из этих случаев нужно вызвать колбэк родительского компонента и передать ему аргументом новое состояние или данные (листинг 5.90). При этом родительский колбэк должен содержать функцию, изменяющую переменную состояния, или сам быть такой функцией. Листинг 5.90. «Дочерний — родитель» // ChildParentVariant1.js export const ChildParentVariant1 = () => { const [text, setText] = useState('Измените текст'); const handleChange = (e) => { setText(e.target.value); }; return ( <Fragment> <div className='row bgGray'> <div className='cell'>Это родительский:</div> <div className='cell'>{text}</div> </div> <div className='row'> <div className='cell fontGray'>Это дочерний:</div> <div className='cell'>
5.19. Новый взгляд на React  315 }; ); <B onChange={handleChange} text={text} /> </div> </div> </Fragment> // B.js export const B = ({ onChange, text }) => { return <input type='text' value={text} onChange={onChange} />; }; Другой способ называется «Предок — потомок» и показан на рис. 5.53. Этот способ обмена данными применяется, когда родительский и дочерний компоненты не связаны непосредственно, то есть когда нужно обеспечить динамическую связность между предком и нижележащими компонентами, не прибегая к prop drilling1. Технически такой обмен реализуется через контекст, что для потомка означает обязательность использования хука useContext, а для предка — задействование в JSX свойстваРис. 5.53. Передача провайдера из созданного функцией createContext контекста. данных от предка Таким образом, какая бы роль ни была у предка, он тоже напотомку чинает играть роль провайдера. На рис. 5.53 показано, что обмен данными происходит между предком и компонентом-потомком второго уровня. А в листинге 5.91 видно, что возвращаемое значение — то, что будет отрендерено, из потомка возвращается компоненту, являющемуся дочерним первого уровня по отношению к тому же предку. Листинг 5.91. «Предок — потомок» // AncestorDescendant.js export const AncestorDescendant = () => { const [text, setText] = useState('Измените текст'); const handleChange = (e) => {setText(e.target.value);}; return ( <DataChangeContext.Provider value={{text}}> <div className='row bgGray'> <div className='cell'>Это предок: </div> <div className='cell'> <input type='text' value={text} onChange={handleChange} /> 1 Prop drilling — способ передачи данных от компонента-предка его потомкам. Предок и потомок связаны друг с другом через один компонент и более. При этом подходе данные и колбэки передаются сначала одному дочернему компоненту, в котором они не нужны, и затем из этого дочернего компонента передаются следующему, целевому, дочернему компоненту.
316  Глава 5 UI }; ); </div> </div> <div className='row'> <div className='cell fontGray'>Это потомок:</div> <div className='cell'> <MiddleBetweenAncestorDescendant1 /> </div> </div> </DataChangeContext.Provider> // MiddleBetweenAncestorDescendant1.js export const MiddleBetweenAncestorDescendant1 = () => <D />; // D.js export const D = () => { const { text } = useContext(DataChangeContext); return <div>{text}</div>; }; Другой способ называется «Потомок — предок» и показан на рис. 5.54. Как и в случае со способом «Дочерний — родитель», здесь предок должен через контекст передать потомку колбэк. Компонент-потомок, чтобы передать данные родительскому компоненту, должен вызывать колбэк родителя (разумеется, при наступлении какого-либо события или изменении состояния) и передавать нужные данные аргументом, что показано в листинге 5.92. Рис. 5.54. Передача данных от потомка предку Листинг 5.92. «Потомок — предок» // DescendantAncestorVariant1.js export const DescendantAncestorVariant1 = () => { const [text, setText] = useState('Измените текст'); const handleChange = (value) => {setText(value);}; return ( <DataChangeContext.Provider value={{text, onChange: handleChange}}> <div className='row bgGray'> <div className='cell'>Это предок:</div> <div className='cell'>{text}</div> </div> <div className='row'> <div className='cell fontGray'>Это потомок:</div> <div className='cell'> <MiddleBetweenAncestorDescendant2 /> </div>
5.19. Новый взгляд на React  317 } ); </div> </DataChangeContext.Provider> // MiddleBetweenAncestorDescendant2.js export const MiddleBetweenAncestorDescendant2 = () => <E />; // E.js export const E = () => { const { text, onChange } = useContext(DataChangeContext); const handleChange = (e) =>onChange(e.target.value); }; return <input type='text' value={text} onChange={handleChange} />; Третий способ называется «Компоненты из разных поддеревьев» и показан на рис. 5.55. Рис. 5.55. Передача данных между компонентами, расположенными в разных поддеревьях Показанный на рис. 5.55 способ обмена данными применяется, когда необходимо организовать обмен данными между разными компонентами, расположенными в разных поддеревьях, а также когда по всему приложению нужен единый источник данных, к которому обращаются из разных компонентов. Технически это можно реализовать двумя способами. Первый заключается в использовании локального хранилища, такого как IndexedDB или localStorage. Для IndexedDB можно применять специальные библиотеки или самостоятельно реализовать хук useIndexedDB для чтения и записи. Для localStorage тоже можно применять готовые библиотеки или самостоятельно реализовать хук useLocalStorage. Второй способ заключается в использовании библиотеки Redux Toolkit, но это будет сбрасывать сохраненные состояния при перезагрузке страницы. При использовании IndexedDB или localStorage подобное не происходит. Поэтому если вы используете Redux Toolkit и вам необходимо запоминать состояние между сессиями пользователя, то можете рассмотреть применение двух названных способов. Поскольку во втором способе используется некий посредник между компонентами, чтобы они оба могли получать одинаковые данные, то два компонента или более будут содержать одинаковый код. В листинге 5.93 показан код одного из таких компонентов. В другом (-их) компоненте (-ах) должны содержаться аналогичные выражения.
318  Глава 5 UI Листинг 5.93. Компоненты из разных поддеревьев import {Fragment} from'react'; import {useSelector, useDispatch} from "react-redux"; import {change} from '../store/inputText/inputTextSlice'; export const ComponentsFromDifferentSubtreesFirst = () => { const dispatch = useDispatch(); const inputText = useSelector(state => state.inputText); const handleChange = (e) => { dispatch(change(e.target.value)); } } return ( <Fragment> <div className='row bgGray'> <div className='cell'> <input type='text' value={inputText} onChange={handleChange} /> </div> </div> </Fragment> ) Дополнительно отмечу, что, исходя из определения обмена данными между компонентами, легко сделать вывод, что даже если между двумя компонентами налажен обмен данными в обе стороны, то обновление состояния всегда одностороннее. Имеется в виду, что сначала происходит какое-либо событие, затем — обновление состояния в компоненте 1, перерендеринг и далее новое значение получает компонент 2. Следом еще какое-либо событие, обновление состояния компонента 2, перерендеринг и новое значение получает компонент 1. Кроме того, важно помнить, что все способы обмена данными для одного конкретного компонента могут сочетаться. 5.19.6. Все есть функция, но роли разные В подразделе 5.19.3 я описал роли компонентов. Компонент — это функция, но в React используется не только компонент, но и несколько других элементов, тоже являющихся функциями и различающихся ролями, которые они играют в приложении. Вот эти роли. y Хук. y Обработчик события. y Компонент. y Обычная функция JavaScript. y HOC (High Order Component). y Функция высшего порядка.
5.19. Новый взгляд на React  319 Хук — функция, имеющая целевое назначение и осуществляющая мемоизацию. Хуки специальным образом обрабатываются в React, за счет чего хранится состояние. Эти функции могут задавать поведение компонента, обрабатывать побочные эффекты, например обращаться к сторонним API за данными, менять содержимое страницы, работать с DOM непосредственно или обращаться к хранилищам браузера. Обработчик события — функция, которая передается в качестве колбэка в свойства элементов, чьи имена содержат название события, при наступлении которого будет вызвана функция-обработчик. Этот колбэк может содержать любой JavaScript-код и в том числе обращения к WebAPI браузера или запросы к сторонним API, то есть создавать контролируемые побочные эффекты. Компонент — функция, которая задает и вывод в DOM, и обработку поведения. В компонент могут быть импортированы другие компоненты, хуки и простые функции. В теле могут быть объявлены обработчики событий. Компонент специально обрабатывается библиотекой React. Помещая один компонент в другой, вы создаете дерево вызовов функций. Каждая вызванная функция в таком дереве вернет результат (набор элементов с данными и поведением, а также обработчиками событий), и каждый результат будет передан родительской функции, а в конечном итоге вызовы будут доведены до корневой функции. Она возвращает структуру, состоящую из элементов, которые будут превращены в DOM и отрендерены в браузере. Обычная функция JavaScirpt — функция, содержащая выражения для обработки данных, такие как преобразования данных или вычисления. Как правило, это выражения, тесно связанные с предметной областью решаемой задачи. Часто такие функции называют утилитами. Обычно утилиты определяются за пределами компонента. HOC (High Order Component) — по-русски это компонент высшего порядка. С помощью НОС можно создать мемоизированный компонент. Непосредственного участия в структуре приложения он не принимает, но дает возможность воспользоваться результатом его работы, а именно создать компонент, встроенный в структуру приложения. НОС можно рассматривать как фабрику компонентов, что, собственно, и представляет собой данный тип функции. Функция высшего порядка (ФВП) — функция, которая позволяет создавать другие функции. Обычно ФВП или предоставляет возвращаемой функции область видимости, чтобы использовать возможность сохранения значений между перерендерами, или задает определенный алгоритм. При ее помощи можно создать множество одинаковых функций, каждая — со своей областью видимости. То есть она будет выступать в роли фабрики. Если ФВП имеет параметром функцию, то возвращаемая из нее функция может содержать дополнительную функциональность, которой не было в переданной в ФВП функции. В этом случае она выступает как декоратор. Непосредственно в компоненте ФВП участия не принимает, а возвращаемые функции принимают. Все вышеперечисленное — функции и их названия — это роли. Обработчики события, компоненты и утилиты имеют множество способов реализации. Хуки в React ограничены своим количеством. Но вы можете реализовать любое
320  Глава 5 UI количество кастомных хуков, где каждый из них должен содержать минимум один хук из библиотеки React, иначе он ничем не будет отличаться от обычной функции и не будет приводить к желаемым результатам. Либо вы можете самостоятельно реализовать мемоизацию значения при перерендерах и реализовать возможность явного изменения значения. 5.19.7. DAG В подразделе 3.3.8 было упомянуто, что именно отличительные свойства дают наименования графам. Одним из широко известных видов графов является так называемый DAG — Directed Acyclic Graph. На русский переводится как ориентированный граф без циклов. Чтобы лучше понять, какой это вид графа, рассмотрим пример. На рис. 5.56 показаны примеры графа DAG (слева) и ориентированного графа «цикл» (справа). Рис. 5.56. DAG и ориентированный граф «цикл» Выглядят они на первый взгляд одинаково, однако есть разница в ориентации дуг. Так, в графе справа образуется цикл, тогда как слева его нет. 5.19.8. DAG React-компонентов На рис. 5.49 должен был бы быть DAG React-компонентов, но вместо дуг там показаны ребра. Зато сейчас самое время наверстать упущенное. Когда вы разрабатываете код компонентов, то один компонент затем импортируется в родительский и используется в нем для вывода части интерфейса. Можно сказать, что React-компонент поставляет родительскому компоненту JSX. Компонент может переиспользоваться сколько угодно раз, разумеется, в зависимости от роли, и поэтому для примера DAG React-компонентов какого-нибудь простого приложения можно изобразить так, как показано на рис. 5.57. Если вы возьмете сейчас любое фронтенд-приложение и зарисуете импорты компонентов, то получите DAG. Обратите внимание, что стрелка направлена от дочернего объекта к родительскому. Это означает, что дочерний компонент направляет JSX родительскому.
5.19. Новый взгляд на React  321 Рис. 5.57. DAG React-компонентов Таким графом, как на рис. 5.57, изображают статические импорты компонентов приложения (инструкция import в JavaScript/TypeScript) или их композицию с JSX родительского компонента. 5.19.9. DAG вызовов → DOM При загрузке React-приложения в браузер код каждого компонента будет вызван. Вызов начинается с корневого компонента. Что можно изобразить также при помощи DAG, но с противоположной ориентацией дуг, как на рис. 5.58. Рис. 5.58. DAG вызовов React-компонентов На рис. 5.58, помимо вызова, также изображена передача пропсов от родительского компонента дочернему. Это само собой разумеющееся неотъемлемое свойство вызова функции. При разработке приложения мы создаем DAG из React-компонентов, но, когда наше приложение дойдет до браузера и начнет в нем выполнение, все наши компоненты превратятся в дерево DOM. Вместо DAG, показанного на рис. 5.58, будет дерево, как на рис. 5.59. Сравнивая рис. 5.58 и 5.59, вы можете увидеть, какой будет конечный результат. А для большей наглядности запустите React-приложение, часть которого представлена в листинге 5.94. Вы сможете проследить череду вызовов и количество компонентов в дереве DOM.
322  Глава 5 UI Рис. 5.59. DOM-дерево Листинг 5.94. Как граф вызовов React-компонентов превращается в DOM-дерево // src/App.js function App() { queueComponentName.queue("App"); useEffect(() => { for (const [order, componentName] of queueComponentName.dequeue().entries()){ console.log(`%c${order} - ${componentName}`, 'background-color:rgb(202, 235, 202); color: #364a4e;'); } }, []); } return ( <div className="App"> <h1>Как работает DAG вызовов React-компонентов</h1> <p>... и как это соотносится с DAG React-компонентов и DOM-деревом</p> <Business2 /> <Business1 /> </div> ); //src/business/Business2.js export const Business2 = () => { queueComponentName.queue("Business2"); return ( <div> <h2>Бизнес-компонент 2</h2> <SimpleComponent1 className="Business2" title="из Business2" /> <SimpleComponent2 className="Business2" title="из Business2" /> <SimpleComponent3 className="Business2" title="из Business2" /> </div> ); };
5.20. Что нужно изучить самостоятельно  323 Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 5.19. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 5.19. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 5.19 и ознакомьтесь с моим решением. 5.20. Что нужно изучить самостоятельно 5.20.1. Хуки Мы рассмотрели не все хуки, но основные, с помощью которых создается фронтенд-­ приложение. Однако React постоянно развивается, и есть ряд хуков, которые незачем добавлять в книгу — их вы можете изучить самостоятельно. y useImperativeHandle Хук, позволяющий настроить обработку, выставленную как ref (имеется в виду ref, создаваемый хуком useRef и переданный в свойство ref элемента). URL: https://react.dev/reference/react/useImperativeHandle y useDebugValue Хук, позволяющий добавить метку в кастомный хук в ReactDevTools. URL: https://react.dev/reference/react/useDebugValue y useDefferedValue Хук, позволяющий отложить обновление части UI. URL: https://react.dev/reference/react/useDeferredValue y useInsertionEffect Этот хук предназначен для авторов библиотек CSS-in-JS. Если вы с ними работаете и нужно место для вставки стилей, то, вероятно, вы захотите использовать useEffect или useLayoutEffect вместо этого хука. Он позволяет вставить элементы в DOM до того, как произойдет какой-либо эффект лэйаута. URL: https://react.dev/reference/react/useInsertionEffect y useLayoutEffect Версия хука useEffect, который срабатывает до того, как браузер перерисует отображаемую страницу. URL: https://react.dev/reference/react/useLayoutEffect y useSyncExternalStore Хук, который позволяет подписаться на хранилище, расположенное вне React. URL: https://react.dev/reference/react/useSyncExternalStore
324  Глава 5 UI 5.20.2. lazy Функция lazy позволяет откладывать загрузку кода компонента до момента, пока не возникнет необходимость в его первой загрузке. URL: https://react.dev/reference/react/lazy 5.20.3. startTransition и useTransition Хук useTransition позволяет обновить состояние без блокировки пользовательского интерфейса. URL: https://react.dev/reference/react/useTransition Используется в паре с функцией startTransition, импортируемой из React, которая предназначена для того, чтобы пометить обновление состояния как перемену (transition). URL startTransition: https://react.dev/reference/react/startTransition
Глава 6 УПРАВЛЕНИЕ РЕНДЕРИНГОМ КОМПОНЕНТОВ С ПОМОЩЬЮ ИЗМЕНЯЮЩЕГОСЯ URL 6.1. О библиотеке React-Router-Dom Эта библиотека не является основной, но без нее никак не обойтись. Мне кажется, что ей не уделяется достаточное внимание. Именно эта библиотека, по моей практике, вызывает больше всего непонимания, и по вполне конкретным причинам. Я работал со многими людьми и проводил собеседования со многими разработчиками, и большинство из них не понимало, как ее использовать. Что? Зачем? Почему? Если не освоить идеи этой библиотеки, то возникнут большие сложности с простыми, казалось бы, вещами. Непонимание кроется не в сложности идей, заложенных в библиотеке, а раз так, вы легко их усвоите. В этом поможет данная глава. Библиотеку React-Router-Dom называют React Router1. Далее в тексте будет использоваться именно это имя. 6.1.1. Назначение библиотеки Основное назначение React Router, то, ради чего ее все применяют: 1) связывание в коде приложения путей2 в URL с React-компонентами, которые будут отрендерены в DOM; 2) подписка и манипулирование стеком истории браузера. Приведу примеры применения. Допустим, у нас есть интернет-магазин. Пользователь входит на главную страницу и видит один тип страницы. Затем он 1 2 Будьте внимательны: название React Router — зарегистрированная торговая марка. Приводя его в книге, я подразумеваю библиотеку React-Router-Dom. Ее официальный сайт (reactrouter.com) и бренд принадлежат компании Remix Software Inc. Заявление по поводу торговой марки находится здесь: https://reactrouter.com/brand. Под словом «путь» здесь подразумевается та часть URL, которая следует после номера порта и до знака «?», после чего следуют параметры. Пример: https://somedomain.ru/part/ of/path?param1=value1, где /part/of/path — путь.
326  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL останавливает свой выбор на конкретном товаре, щелкает на нем и переходит на совершенно другой тип страницы, где представлен конкретный товар. Ознакомившись с ним, он в браузере жмет кнопку Назад и переходит на первую страницу, с которой начал свое знакомство. Теперь на минуту остановитесь и подумайте, как бы вы организовали интернетмагазин, чтобы он работал аналогичным образом — куда бы ни зашел пользователь, он всегда может вернуться на предыдущую страницу по кнопке Назад. Разумеется, страницы должны представать перед ним в том же виде, как и до ухода с них. Приступайте. Получилось? На самом деле React Router может делать чуть больше. Совершенно нормально организовывать очень простым решением такие подсистемы приложений, как аутентификация; модальное окно, где пользователь должен совершить несколько шагов, например, для заполнения форм. Разумеется, при использовании кнопки Назад браузера и переходе на предыдущий шаг будет восстанавливаться предыдущее состояние. Возможно, этим никого не удивишь, но начните реализовывать это с помощью других средств и сразу ощутите нехватку чего-то более подходящего. Глава 4 книги посвящена состоянию приложения, но там дана неполная картина. Вам будет мало изложенного для создания полноценного приложения. Библиотекой Redux Toolkit вы можете охватить только состояние, создаваемое пользователем. Чтобы полноценно управлять приложением, нужно использовать React Router. Свое основное назначение, указанное выше, библиотека React Router реализует с помощью управления состоянием дерева DOM. Но помните, что на этапе разработки мы работаем с графом компонентов приложения. И ко­ гда далее в тексте речь пойдет о дереве DOM, будет так и указано: дерево DOM. А когда нужно указать на компоненты и их связи, будет использоваться граф компонентов. Изменяя конфигурацию, вы принимаете участие в настройке будущего дерева DOM. Как это осуществляется мы разберем далее. 6.1.2. React Router позволяет создавать SPA Как можно обойтись без библиотеки Redux Toolkit, так можно и создать SPA без React Router. Но потребуется решение, которое будет выполнять функцию, схожую с той, которая возложена на React Router. Почему эта библиотека способствует созданию SPA? Говоря коротко, она создана специально для этой цели. Действительно, если в приложении осуществлена настройка рендеринга компонентов, которые будут отображаться в ответ на изменения пути в URL в адресной строке, то браузер не станет запрашивать новый HTML-документ, чтобы отобразить желаемую пользователем страницу. Вместо этого React Router составит нужные компоненты в React, которые ее представляют. React, в свою очередь, передаст эти компоненты в React-Dom, а тот — браузеру. В итоге перед пользователем отобразится новая страница без перезагрузки HTMLдокумента. Это и есть SPA.
6.2. Визуализация работы React Router  327 6.2. Визуализация работы React Router 6.2.1. Граф компонентов приложения без библиотеки React Router В подразделе 6.1.1 было заявлено два основных назначения React Router: 1) связывание в коде приложения путей в URL с React-компонентами, которые будут отрендерены в DOM; 2) подписка и манипулирование стеком истории браузера. В этом разделе будет визуализирован первый пункт, так как именно его можно сделать очевидным. Второй пункт является внутренней реализацией самой библио­ теки. Как это происходит, мы будем разбираться последовательно. Допустим, вы создали мини-веб-приложение, состоящее из трех разделов: Книги, Форма заказа и Контакты. Вы решили, что каждый из них должен отображаться как отдельный раздел, то есть это не сплошной длинный лендинг1, и создали структуру компонентов как на рис. 6.1. Рис. 6.1. Структура компонентов мини-приложения Поскольку вы еще не используете библиотеку React Router, вам нужен способ, который позволит щелкать на разделе и переключать внешний вид страниц, например, с раздела с книгами на форму заказа. Одно из популярных ранее средств — табы2. На самом деле можно все организовать по-другому, но мне нужно показать вам именно этот способ. Очевидно, что вывод компонента App будет выглядеть примерно так, как показано в листинге 6.1. 1 2 Лендинг (другое название — «одностраничник») — наименование вида веб-страницы, на которую зазывают пользователя и с которой никуда переходить не нужно: здесь он может и оформить заказ на товар, и/или получить всю необходимую информацию. Табы (от англ. слова tab — «вкладка», «закладка») — это общепринятый способ отображения содержимого, когда его можно делить на разделы и отображать в одном и том же пространстве разные части, переключаясь между разделами. В таком инструменте всегда отображается какой-то один раздел, остальные скрыты. Реализован практически во всех UI-библиотеках или дизайн-системах.
328  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL Листинг 6.1. Когда все компоненты в DOM, но скрываются стилями import import import import {useState} from'react'; { Contacts, Books, Order } from "./business"; { Header } from './components'; './App.css'; function App() { const [currentIndex, setCurrentIndex] = useState(0); return ( <div className="App"> <Header list={["Книги", "Заказ", "Контакты"]} cb={setCurrentIndex} /> { [Books, Order, Contacts].map((Page, index) => <Page key={index} className={currentIndex !== index ? 'hidden' : ''} />) } </div> } ); Если в инструментах разработчика вы откроете вкладку Элементы, то увидите, что все компоненты находятся в DOM. При выполнении данное приложение запускается по пути /. Когда вы переключаетесь между разделами, а по умолчанию отображен всего один раздел Книги, путь в адресной строке не изменяется. Это значит, что если вы запустили приложение на новой вкладке, то при переключении между разделами кнопка Назад будет недоступна, так как не происходит изменения пути в URL. Нам нужно еще раз взглянуть на схему компонентов на рис. 6.1. Компонент App рендерит все три дочерних компонента Books, Order и Contacts. Все элементы, принадлежащие им, находятся в DOM (React передаст все компоненты в ReactDom), а неактивные компоненты скрыты с помощью стилей. Теперь посмотрим, как ведет себя дерево компонентов при использовании библиотеки React Router, когда переключением разделов управляет она. 6.2.2. Граф компонентов приложения, управляемый с помощью библиотеки React Router Приложение, управляемое с помощью React Router, обязательно включает настройку соответствия компонента, который будет отрендерен, и пути в URL. Такая настройка называется конфигурацией (листинг 6.2). Она импортируется в специальный компонент-провайдер RouterProvider или BrowserRouter. Листинг 6.2. Настройка переключения компонентов в будущем DOM в соответствии с изменяющимся путем в URL с использованием библиотеки React Router // index.js import React from 'react';
6.2. Визуализация работы React Router  329 import ReactDOM from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; import {routes} from "./routes"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <RouterProvider router={routes} /> </React.StrictMode> ); // routes.js import { createBrowserRouter } from "react-router-dom"; import { Books, Order, Contacts, Cart } from "./business"; import { Layout } from "./layout"; export const routes = createBrowserRouter([{ element: <Layout />, children: [{ path: "/", element: <Books />, children: [{ path: "cart", element: <Cart />, }] }, { path: "order", element: <Order />, children: [{ path: "cart", element: <Cart />, }], }, { path: "contacts", element: <Contacts />, children: [{ path: "cart", element: <Cart />, }], }] }]); Компонент App исключен. По умолчанию открывается раздел с книгами, то есть по пути / React Router в React построит дерево компонентов, как на рис. 6.2. Как видите, не все компоненты из React передаются в React-Dom, лишь их часть. Те из них, что не будут отрендерены, помечены серым цветом и пунктирной линией. Но это еще не все. Теперь при переходе в раздел Заказ, находящийся по пути /order, во-первых, изменится путь в адресной строке браузера, а во-вторых, изменится дерево компонентов, составляющее React Router в React, которое будет передано в React-Dom. Дерево компонентов показано на рис. 6.3. И разумеется, при переходе в раздел Заказ в браузере станет активной кнопка Назад. Если вы нажмете ее, то перейдете в раздел Книги.
330  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL Рис. 6.2. React Router строит дерево в React для раздела Книги Рис. 6.3. React Router строит дерево в React для раздела Заказ
6.3. Конструирование графа приложения с помощью конфигурации  331 6.2.3. Заключение С библиотекой React Router контроль отображаемого раздела приложения становится более наглядным и очевидным. Для ее использования нужны две главные составляющие: y конфигурация путей и отображаемых компонентов; y компонент RouterProvider или BrowserRouter. 6.3. Конструирование графа приложения с помощью конфигурации1 6.3.1. Конфигурация — главный настраиваемый элемент Конфигурация — главный настраиваемый разработчиком элемент в приложении. Без нее приложения просто не будет. Конфигурацию можно задать тремя способами, которые отличаются используемыми библиотечными составляющими. В этом разделе мы изучим все три. Каждый отдельный элемент в конфигурации может содержать детали, которые будут рассмотрены в следующих разделах. Основная идея настройки приложения с помощью конфигурации проста (листинг 6.3). В библиотеке очень много деталей (даже больше, чем в Redux Toolkit), которые нужно знать, чтобы заниматься разработкой профессионально. 6.3.2. Способы задания конфигурации приложения Итак, существует три способа задания конфигурации страниц. Все перечисленные функции и компоненты находятся в библиотеке. Первый задействует функцию createBrowserRouter, массив маршрутов в форме объектов и React-компонент RouterProvider. Этот способ показан в листинге 6.3. Второй задействует сочетания функций createBrowserRouter, createRoutesFromElements и React-компоненты Router, RouterProvider. Он показан в листинге 6.4. Третий задействует React-компоненты Routes, Route и BrowserRouter. Этот способ показан в листинге 6.5. Я лично предпочитаю первый. Листинг 6.3. Первый способ конфигурации приложения import {createBrowserRouter} from "react-router-dom"; import { Layout } from "./Layout"; export const router = createBrowserRouter([ { element: <Layout />, 1 Конфигурация — набор программных установок (например, значений переменных, управляющих последовательностей), которые задают состав системы или приложения, функциональные возможности и/или режимы функционирования программы. Это также фактически установленный состав системы.
332  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL children: [{ path: '/', element: <div>Книги</div>, }, { path: 'order', element: <div>Заказы</div>, }, { path: 'contacts', element: <div>Контакты</div>, }] }, ]); Основной единицей конфигурации является так называемый Route. В текущем способе конфигурации он представлен объектом. Например, в листинге 6.3 это: { } path: '/', element: <div>Книги</div>, Далее мы увидим его использование в качестве React-компонента. Имеет смысл узнать, что сами авторы библиотеки пишут про Route: «Route (маршрут) — объект или Элемент Маршрута (Route Element) обычно такой формы { path, element } или <Route path element>. Свойство path — это шаблон пути. Когда шаблон пути совпадает с текущим URL, элемент (element) будет отрендерен». В листинге 6.3 использована библиотечная функция createBrowserRouter. Для понимания того, что она создает, привожу определение, данное авторами: «Router (маршрутизатор) — содержащий состояние, верхнеуровневый компонент, который делает все компоненты и хуки работающими». Глядя в листинг, вы видите, что в компоненте RouterProvider есть свойство router, куда мы передаем ссылку на нашу переменную router, являющуюся результатом вызова функции createBrowserRouter. Получается, главная задача — создать маршрутизатор. Если попытаться объяснить простыми словами, то можно сказать следующее. Список элементов, который мы передаем для создания маршрутизатора, — это конфигурация. Получается, мы задаем конфигурацию маршрутизатора. Маршрутизатором обычно называют то, что сравнивает имеющиеся записи с тем, что затребовано. Например, если наш маршрутизатор построен на основе конфигурации, как в листинге 6.3, то при запросе в адресной строке URL, в котором путь совпадает с /contacts, такой компонент-маршрутизатор в библиотеке React передаст на рендеринг соответствующий компонент, в нашем случае — JSX <div>Контакты</div>. Маршрутизатор создается на основе элементов, именуемых Route. На русский это слово переводится как «маршрут». В привычном понимании это слово означает перемещение от одного пункта к другому, например, при ходьбе. Вы можете посетить несколько пунктов или переместиться из одной точки в другую. Независимо от количества точек в вашем движении это все будут примеры маршрута.
6.3. Конструирование графа приложения с помощью конфигурации  333 Если углубиться, то маршрутом можно назвать и стояние на месте, нулевую длину, без перемещения. Авторы библиотеки называют маршрутом совокупность пути и компонента, передаваемого на рендеринг. Приложение представляет собой дерево компонентов — с точки зрения двух библиотек, React и React Router. Но рендерится всегда одна конкретная ветка компонентов, начиная с родителя, указанного в маршруте. Получается, что библиотека React Router сопоставляет путь в URL в адресной строке с теми, что описаны в конфигурационном файле, и находит совпадение. Компонент, привязанный к данному пути, становится родителем. React Router проходит от родителя к листам и передает их на рендеринг в React, а он, в свою очередь, — в ReactDOM. Теперь рассмотрим пример. Вы набрали адрес сайта, разработанного с помощью React Router. Значит, вам будут отрендерены компоненты, расположенные по маршруту /. Далее вы переместились в раздел Книги. Это значит, что рендериться будут компоненты, расположенные по маршруту /books. И так далее. Каждый новый путь в URL является новым маршрутом. Видимо, от такого понимания отталкивались авторы библиотеки, давая название компоненту, где связаны путь и компонент, который рендерится в ответ на совпадение с данным путем. Листинг 6.4. Второй способ конфигурации приложения import { createBrowserRouter, createRoutesFromElements, Route, } from "react-router-dom"; import { Layout } from "./Layout"; export const router = createBrowserRouter( createRoutesFromElements( <Route element={<Layout />} > <Route path="/" element={<div>Книги</div>} /> <Route path="/contacts" element={<div>Контакты</div>} /> <Route path="/order" element={<div>Заказы</div>} /> </Route> ) ); Здесь маршрутизатор (Router) создается посредством функции createBrowserRouter, а список маршрутов преобразуется из JSX в массив с помощью createRoutesFromElements. Результат аналогичен полученному с использованием первого способа. Листинг 6.5. Третий способ конфигурации приложения import {BrowserRouter, Routes, Route} from'react-router-dom'; import { Layout } from './Layout'; export default function App() { return ( <BrowserRouter> <Routes>
334  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL } ) <Route element={<Layout />} > <Route path="/" element={<div>Книги</div>} /> <Route path="/contacts" element={<div>Контакты</div>} /> <Route path="/order" element={<div>Заказы</div>} /> </Route> </Routes> </BrowserRouter> // index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <App /> </React.StrictMode> ); Здесь маршрутизатор создается с помощью React-компонента BrowserRouter, а конфигурация — компонентами Routes и Route. Все три способа работают одинаково, в чем вы можете убедиться, запустив примеры. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 6.3. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 6.3. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 6.3 и ознакомьтесь с моим решением. 6.4. Настройка Route Скорее всего, у вас появились вопросы: как сделать так, чтобы можно было: y задавать отображаемые компоненты, когда в адресной строке будет, например, такой путь /part1/part2; y задавать отображаемые компоненты на заранее не известные участки пути (например, для каталога, состоящего из 1000 книг, нереально перечислять все пути);
6.4. Настройка Route  335 y при переходе на конкретную страницу с помощью средств библиотеки понять, по какому пути перешел пользователь, чтобы получить нужные данные. Ответы на эти и многие другие вопросы вы получите в данном разделе. 6.4.1. Указание более одного сегмента в пути Авторы библиотеки называют словом «сегмент» следующее: «Segment (сегмент) — часть URL или шаблон пути, это символы между символами /. Для примера, /books/9786010835146 содержит два сегмента». Действительно, до сих пор мы рассматривали примеры, где в URL присутствовал всего один сегмент, например /books. Вполне оправданно спросить, что будет, если задать несколько сегментов? Здесь есть два варианта развития событий. Первый, когда вы, например, задаете в маршрутизаторе путь, состоящий из нескольких сегментов, но на рендеринг отправляете один компонент. Второй, когда в маршрутизаторе для одного сегмента вы задаете один компонент, но у данного сегмента будут вложенные сегменты. Как это выглядит, увидим далее. Путь, составленный из таких вложенных сегментов, может выглядеть как при первом варианте, но поведение будет разное. Второй случай вызывает сильное непонимание — его мы разберем детально. 6.4.2. Случай первый, когда указано более одного сегмента в пути Рассмотрим листинг 6.6, где задан такой маршрут на самом верхнем уровне. Изучив второй случай, вы научитесь управлять им иным образом. Листинг 6.6. Маршрутизатор с указанием нескольких сегментов в пути import {createBrowserRouter} from "react-router-dom"; import { Books, Order, Contacts } from "./pages"; export const routes = createBrowserRouter([ { path: "/", element: <Books /> }, { path: "order", element: <Order /> }, { path: "contacts/has/some/address", element: <Contacts />, } ]); Как видите, в конфигурации маршрутизатора на путь / задан компонент Books, на /order — компонент Order, а компонент Contacts отрендерится, только если в адресной строке будет URL, содержащий путь /contacts/has/some/address. Если набрать в адресной строке путь, например /contacts/has, то появится сообщение об ошибке 404.
336  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL 6.4.3. Случай второй, когда маршрут содержит вложенные маршруты Рассмотрим листинг 6.7, в котором есть вложенные маршруты. Листинг 6.7. Маршрутизатор с конфигурацией, имеющей вложенные маршруты import {createBrowserRouter} from "react-router-dom"; import {Main, Books, Order, Contacts} from "./pages"; import {BookAmfibiya, BookPodYujnyimiNebesami} from "./business"; export const routes = createBrowserRouter([ { path: "/", element: <Main />, }, { path: "books", element: <Books />, children: [ { path: "belyaev-chelovek-amfibiya", element: <BookAmfibiya /> }, { path: "leykin-pod-yujnyimi-nebesami", element: <BookPodYujnyimiNebesami /> } ] }, { path: "order", element: <Order /> }, { path: "contacts", element: <Contacts /> } ]); Маршрут books включает в себя дочерние маршруты, или, говоря по-другому, вложенные. Возьмем первый вложенный маршрут belyaev-chelovek-amfibiya. При такой вложенности компонент, указанный в свойстве element, рендерится, если в адресной строке стоит путь /books/belyaev-chelovek-amfibiya. Похожий на предыдущий случай — когда при совпадении пути в URL /contacts/has/some/ address отображался компонент Contacts. Однако между этими способами есть существенная разница. В первом случае (см. листинг 6.6), когда мы указали на весь путь /contacts/ has/some/address компонента Contacts, рендериться будет только этот компонент и он не будет вложенным в другой компонент (то есть будет единственным на всю страницу).
6.4. Настройка Route  337 Во втором случае (см. листинг 6.7) второй сегмент belyaev-chelovek-amfibiya является вложенным в родительский сегмент books, поэтому и компонент Book­ Amfibiya (связанный с путем belyaev-chelovek-amfibiya) будет рендериться внутри компонента Books (связанного с путем books). Как обеспечить точность вывода содержимого компонента BookAmfibiya, чтобы он рендерился в нужном вам месте? Для этих целей в библиотеке предусмотрен специальный компонент Outlet. Вот что о нем говорят авторы библиотеки: «Outlet (выход) — компонент, который рендерит следующее совпадение во множестве совпадений». В листинге 6.7 приведен код компонента Books. Там присутствует компонент Outlet. Значит, в этом месте и будут рендериться компоненты вложенных маршрутов. Может возникнуть другой вопрос: «А если у маршрута belyaev-chelovek-amfibiya или leykin-pod-yujnyimi-nebesami тоже есть вложенный маршрут, где в этом случае будет рендериться связанный с ним компонент?» Ответ: в родительском компоненте. Например, у маршрута leykin-pod-yujnyimi-nebesami есть вложенный маршрут prices-in-shops и с ним связан компонент PricesShops. Это значит, что если в адресной строке браузера стоит URL, содержащий путь /books/leykin-podyujnyimi-nebesami/prices-in-shops, то компонент PricesShops будет рендериться внутри компонента BookPodYujnyimiNebesami. А он, в свою очередь, рендерится внутри компонента Books. Само собой, внутри каждого родительского компонента должен присутствовать компонент Outlet. Получается схема как на рис. 6.4. Рис. 6.4. Схема рендеринга вложенных маршрутов Из данной ситуации вытекает еще один вопрос: «Как быть, пока в адресной строке нет совпадения, например, с путем /books/leykin-pod-yujnyimi-nebesami
338  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL или с /books/leykin-pod-yujnyimi-nebesami/prices-in-shops и на месте компонента Outlet в каждом случае ничего не рендерится? Не оставлять же для пользователя пустоту?» Верно. Способ, с помощью которого нужно этим управлять, мы рассмотрим далее, в разделе 6.7. 6.4.4. Динамический сегмент и параметры URL В листинге 6.7 заданы вложенные сегменты, но построить настоящий магазин таким способом не получится. Ведь, допустим, книг у нас может быть превеликое множество плюс наверняка более глубокая вложенность. Мы не будем перечислять в конфигурации маршрутизатора весь каталог руками. Для этих целей предусмотрен способ «Динамический сегмент» (листинг 6.8). Вот как его определяют сами авторы: «Dynamic Segment (динамический сегмент) — сегмент шаблона пути, который является динамическим, означающий, что он может совпадать с каким-либо значением в сегменте. Для примера, шаблон /books/:bookId будет совпадать с URL наподобие /books/9786010835146, или с /books/leykin-pod-yujnyimi-nebesami, или с /books/B987». Листинг 6.8. Конфигурация маршрутизатора с динамическим сегментом /** другой код маршрутизатора*/ { path: "books", element: <Books />, children: [ { path: ":bookId", element: <BookDetails /> }, ] }, /** другой код маршрутизатора*/ Из листинга видно, что количество компонентов сократилось c двух до одного, а сам сегмент теперь содержит символ «:». Он ставится перед каким-либо словом в пути и означает, что данный сегмент является динамическим. При этом само слово — по сути, имя — становится идентификатором. Теперь bookId — идентификатор, и его значение всякий раз будет изменяться. В какой момент какое значение он будет принимать и где его можно извлечь? Когда путь в URL совпадет с шаблоном /books/:bookId, идентификатор bookId будет принимать значение, которое является значением сегмента. Например, пользователь выбрал книгу и в адресной строке появился путь /books/leykin-podyujnyimi-nebesami. Значением идентификатора bookId будет строка "leykin-podyujnyimi-nebesami". Но в каком месте можно получить доступ к значению переменной? В компоненте, в маршруте, соединенном с динамическим сегментом. Для этого в библиотеке предусмотрен специальный хук useParams, который возвращает параметры URL. Авторы библиотеки определяют параметры URL следующим образом: «URL Params
6.4. Настройка Route  339 (параметры URL) — разобранные значения из URL, которые совпадают с динамическим сегментом». Поскольку мы сделали сегмент конкретной книги динамическим, вызвав в компоненте хук useParams, получим значение из сегмента пути, сможем задействовать этот сегмент и сделать запрос, чтобы получить данные по конкретной книге. Именно это осуществляется в компоненте BookDetails (листинг 6.9). Листинг 6.9. Чтение значения параметра URL (динамического сегмента) export const BookDetails = () => { const [bookData, setBookData] = useState(null); const [error, setError] = useState(null); const {bookId} = useParams(); useEffect(() => { fetch(`/books/${bookId}`) .then(response => response.json()) .then(data => { if (error) { setError(null); } setBookData(data); }) .catch(error => setError(error)); }, [bookId]); /** другой код компонента */ Вполне возможна ситуация, когда динамический параметр не один. Как в этом случае получать доступ? Точно так же, как и в случае с динамическим параметром bookId. Предположим, вы добавили еще один динамический сегмент :pageNumber, чтобы пользователь мог увидеть конкретную страницу, доступную для свободного просмотра. Тогда для сегмента :bookId нужно заполнить свойство children маршрутом наподобие { path: ":pageNumber", element: <PageNumber /> }. А чтобы получить значения динамических сегментов, в компоненте PageNumber следует использовать хук useParams, который возвратит значения для обоих сегментов — и pageNumber, и bookId. При этом в родительском компоненте BookDetails данный хук также вернет значения для двух параметров. Правда, такое возможно, если в URL будет путь, содержащий значения для обоих динамических сегментов, например /books/kniga/23. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 6.4. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 6.4. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 6.4 и ознакомьтесь с моим решением.
340  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL 6.5. Загрузка данных и обработка ошибок 6.5.1. Что такое загрузка данных У маршрутов, задаваемых с помощью createBrowswerRouter, есть свойство loader. Каждый маршрут может определить функцию-загрузчик (свойство loader), чтобы загрузить данные в связанный компонент, и они будут загружены до того, как он отрендерится. 6.5.2. Динамический сегмент и загрузка данных Загрузчик данных получает несколько аргументов, и одним из них является params, содержащий значения динамических сегментов пути, если они есть в URL. В предыдущем примере с книгами вместо помещения запроса о книге в компоненте BookDetails загрузку можно сделать прямо в конфигурации маршрутизатора, а в компоненте использовать специальный хук useLoaderData, работающий в связке с загрузчиком из маршрута (листинг 6.10). Листинг 6.10. Использование загрузчика и хука useLoaderData /** другой код маршрутизатора*/ children: [ { path: ":bookId", element: <BookDetails />, loader: async ({ params }) => { const response = await fetch(`/books/${params.bookId}`); const json = await response.json(); ] }, }, return json; }, /** другой код маршрутизатора*/ // BookDetails.js import { Link, useLoaderData } from "react-router-dom"; export const BookDetails = () => { const bookData = useLoaderData(); const [error, setError] = useState(null); if (!bookData) { setError(bookData.error); } if (error){ return (<div>Ошибка получения данных! Попробуйте обновить страницу</div>); } const { description, author, price, title, year, } = bookData; /** другой код компонента */
6.5. Загрузка данных и обработка ошибок  341 Теперь все, что касалось загрузки данных, перенесено в загрузчик, а компонент просто их рендерит. Как видно, параметры динамического сегмента доступны в загрузчике. Данные, загруженные загрузчиком, доступны в компоненте с помощью хука useLoaderData. 6.5.3. Параметр загрузчика request Данный параметр — экземпляр fetch Request , создаваемый для приложения. Наиболее частый способ использования состоит в создании URL и чтении оттуда URLSearchParams (листинг 6.11). Листинг 6.11. Использование значения параметра request export async function loader({ request }) { const url = new URL(request.url); const searchName = url.searchParams.get("search"); } return searchName; 6.5.4. Возврат ответов Поскольку можно вернуть что-либо из загрузчика и получить доступ к этим данным в компоненте через хук useLoaderData, можно и просто вернуть объект типа Response1. Поскольку возвращаемое значение из функции fetch — это Response, большинство загрузчиков просто возвращают сам вызов fetch, как, например, в листинге 6.12. Листинг 6.12. Возврат вызова fetch из загрузчика function loader({ request, params }) { return fetch("/books", { signal: request.signal, method: "post", body: JSON.stringify({ params: params, }), }); } Ответ можно создать самостоятельно (листинг 6.13). Листинг 6.13. Самостоятельно созданный ответ с помощью Response function loader({ request, params }) { const data = { book: { author: "Айзек Азимов", title: "Академия. Основание", 1 https://developer.mozilla.org/en-US/docs/Web/API/Response.
342  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL } /** другие свойства */ } }; return new Response(JSON.stringify(data), { status: 200, headers: { "Content-Type": "application/json; utf-8", }, }); В таком случае React Router автоматически применит response.json(), и в компоненте не придется делать парсинг. Хук useLoaderData в компоненте вернет объект со свойством book, являющийся объектом. 6.5.5. Утилита json Для еще большего удобства в библиотеке предусмотрена утилита json, которую можно использовать для упрощения кода листинга 6.14. Листинг 6.14. Использование утилиты json import { json } from "react-router-dom"; function loader({ request, params }) { const data = { book: { author: "Айзек Азимов", title: "Академия. Основание", /** другие свойства */ }}; return json(data, { status: 200 }); } 6.5.6. Возврат ошибки Чтобы вернуть ошибку, необходимо использовать инструкцию throw (листинг 6.15). Листинг 6.15. Выброс ошибки с ответом async function loader({ request, params }) { const res = await fetch(`/api/books/${params.bookId}`); if (res.status === 404) { throw new Response("Not Found", { status: 404 }); } return res.json(); } 6.5.7. errorElement Когда исключения выбрасываются в загрузчике, экшене или при рендеринге компонента вместо нормального пути рендеринга (<Route element>), путь ошибки будет отрендерен через (<Route errorElement>) и ошибка будет доступна через
6.5. Загрузка данных и обработка ошибок  343 хук useRouteError. Посмотрим, как работает свойство маршрута errorElement, на примере принудительного возврата исключения из загрузчика, то есть применим листинг 6.15 в листинге 6.16. Листинг 6.16. Использование свойства errorElement в маршруте // routes.js /** другой код маршрутизатора */ { path: "books", element: <Books />, children: [ { path: ":bookId", element: <BookDetails />, loader: async () => { throw new Response("Not Found", { status: 404 }); }, errorElement: <ErrorBoundary />, }, ] }, /** другой код маршрутизатора */ // business/ErrorBoundary.js import { useRouteError } from "react-router-dom"; export const ErrorBoundary = () => { const error = useRouteError(); const {data, status} = error; if (status === 404){ return <div>Неверный запрос, {data}</div> } if (status === 500){ return <div>Ошибка на сервере, {data}</div> } } return <div>{status}, {data}</div>; В конфигурации маршрутизатора в маршруте :bookId задан загрузчик, который выбрасывает исключения. Когда пользователь перейдет в раздел Книги по пути в адресной строке /books, ему на выбор будет предложен некий список. Выбрав какую-либо книгу, он перейдет по пути наподобие /books/leykin-pod-yujnyiminebesami, и загрузчик (свойство loader) выбросит исключение. Возникшая ошибка будет обработана, и в компоненте Outlet (который расположен в компоненте Books) отрендерится не компонент BookDetails, как задано маршрутом, а компонент ErrorBoundary. Его можно оформить по собственному желанию, тогда пользователь увидит не странные сообщения, с которыми непонятно что делать, а что-то заданное вами.
344  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL 6.5.8. Всплытие ошибки А если во вложенном маршруте :bookId не указано свойство errorElement, а в его родителе оно есть? Тогда произойдет всплытие ошибки, и компонент, указанный в свойстве errorElement, обработает эту ошибку. Выглядеть он будет иначе, чем если бы ошибку обрабатывал errorElement вложенного маршрута. А именно, так, как если бы ошибка произошла в родительском маршруте /book. 6.5.9. Заключение Когда мы начали изучать динамические сегменты, был рассмотрен листинг 6.8. В нем компонент BookDetails содержал несколько условий. Первое связано с наличием данных. Используя загрузчик, мы можем смело убрать необходимость в этом условии. Поскольку мы убрали загрузку данных из компонента, условие, связанное с ошибочной загрузкой данных, потеряло свою актуальность. Однако необходимость обрабатывать ошибочную загрузку и ошибки рендеринга автоматически не пропала. Свойство маршрута errorElement решает эту проблему. Поэтому использование загрузчика и errorElement является правильным действием. 6.6. Компонент Form 6.6.1. Что представляет собой компонент Form Компонент Form является оберткой HTML-тегом form, который эмулирует1 браузер для маршрутизации на клиентской стороне и изменения данных. Когда пользователь заполняет форму и нажимает кнопку Отправить, библиотека не отправляет данные на сервер, как это сделал бы браузер. Вместо этого пользователю рендерится определенный компонент. Сейчас мы рассмотрим материалы, необходимые для работы с формами в React Router. Это не библиотека для валидации форм или управления состоянием, подобная тем, что вы могли использовать в экосистеме React. Для работы с формами в библиотеке предусмотрено два типа запросов: 1) GET-запросы; 2) изменяющие запросы. В компоненте Form предусмотрены следующие свойства: y action; y method; y navigate; y fetcherKey; y replace; 1 Эмулировать — имитировать программными средствами работу какого-либо физического устройства, процесса, другого программного средства или действий.
6.6. Компонент Form  345 y y y y relative; reloadDocument; state; preventScrollReset. Стоит обратить внимание на свойство action. В библиотеке, в маршруте конфигурации маршрутизатора (Route или объект), может быть определено свойство action, которое мы будем активно использовать далее. Иногда возникает путаница при определении разницы между свойством формы action и свойством маршрута action. А так как action маршрута использует API браузера для запросов и функцию fetch, также появляется вопрос о различиях loader (загрузчика) и action (действия). 6.6.2. action Свойство action — это URL-адрес, на который будет отправлена форма, аналогичный действию HTML-формы. Единственное отличие — действие по умолчанию. В HTML-формах по умолчанию используется полный URL-адрес, в <Form> — относительный URL-адрес ближайшего маршрута в контексте (листинг 6.17). Листинг 6.17. Использование Form и action по умолчанию import { Form, useLoaderData } from "react-router-dom"; export const Main = () => { const data = useLoaderData(); }; return (<main> <Form action="/"> <input aria-label="search products" type="text" name="search" /> <button type="submit">Найти</button> </Form> <div>Вы ввели поисковый запрос: {data}</div> </main>); // routes.js import {createBrowserRouter} from "react-router-dom"; import {Main, ErrorBoundary} from "./pages"; export const routes = createBrowserRouter([ { path: "/", element: <Main />, action: Main.action,
346  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL loader: async ({ request }) => { const url = new URL(request.url); const searchTerm = url.searchParams.get("search"); ]); }, return searchTerm; }, errorElement: <ErrorBoundary />, А каков метод? По умолчанию, если вы сами не указали метод, будет использоваться GET. В листинге, в конфигурации маршрутизатора, в свойстве action маршрута, используется значение Main.action. Объяснение этого свойства дано в следующем подразделе. 6.6.3. method Это свойство определяет метод, которым отправляется форма. Компонент Form поддерживает методы HTTP — GET, POST, PUT, PATCH и DELETE (табл. 6.1). Таблица 6.1. Назначение методов HTTP, предусмотренное RFC1 9112 Метод Назначение Мутирует GET Запрашивает представление указанного ресурса и только получает данные Нет POST Отправляет объект в указанный ресурс, часто изменяя состояние или создавая побочный эффект на сервере Да PUT Заменяет все текущие представления целевого ресурса полезной нагрузкой, которая отправляется с запросом Да PATCH Применяет частичные изменения к ресурсу Да DELETE Удаляет указанный ресурс Да Разумеется, на сервере должна быть реализована поддержка этих методов и они должны выполнять именно те действия, для которых обычно предназначены методы HTTP. Метод GET. Предоставлять пользователю форму для ввода запроса и отправлять запрос методом GET — то же самое, что предоставлять пользователю ссылку с тем же URL с параметром, где указан поисковый запрос. Например, если пользователь вводит запрос notebook, то атрибут name поля input имеет значение 1 RFC, или Request For Comments, — собрание документов, в которых описаны спецификации разных технологий Интернета, например протоколов. RFC 9112 описывает HTTP 1/1, RFC 9113 HTPP 2, RFC 9114 HTTP 3.
6.6. Компонент Form  347 search, а если запросы отправляются по адресу /products, то URL GET-запроса будет выглядеть так: /products?search=notebook. Это то же самое, как если бы вы внедрили в HTML-страницу тег с атрибутом href. Выглядеть подобная ссылка будет следующим образом: <a href="/products?search=notebook">Notebook</a>. Пример использования этого метода приведен в листинге 6.17. Мутирующие методы. Мутирующие методы предназначены для создания, изменения и удаления данных на сервере. Это такие методы, как POST, PUT, PATCH и DELETE. Когда пользователь на клиенте отправляет форму (имеется в виду событие Submit), React Router будет искать совпадение адреса, указанного в свойстве action компонента Form, с маршрутами приложения и вызывать <Route action> с сериализованными данными FormData1. Имеет смысл дать пояснения по поводу <Route action>. Как было сказано ранее, свойство action есть в двух местах: в компоненте Form и в компоненте маршрута Route (или в объекте маршрута). Исполь­зование в компоненте Form известно и понятно. В маршруте данное свойство используется в качестве функции, которая осуществляет отправку данных одним из мутирующих методов. Посмотрим на листинг 6.18, где свойство action показано в маршруте для мутирующего метода. Листинг 6.18. Использование мутирующих методов и свойства action маршрута для их обработки export const routes = createBrowserRouter([ { path: "/", element: <Main />, loader: async () => { const const const const responseProducts = await fetch('/products'); responseOrders = await fetch('/orders'); products = await responseProducts.json(); orders = await responseOrders.json(); return {products, orders}; }, action: async ({ request }) => { const {method} = request; const formDataBody = await request.formData(); const headers = new Headers(); headers.append("Content-Type", "application/json"); 1 FormData — это интерфейс, предоставляющий способ для конструирования множества пар «ключ — значение», которые представляют поля форм и их значения, отправляемые с использованием функции браузера fetch(), XMLHttpRequest() или navigation.sendBeacon(). Применяет тот же формат, который использовала бы форма, если бы ее тип кодирования был установлен в multipart/form-data.
348  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL const body = fromFormDatatoJSON(formDataBody); const response = await fetch('/products', {method, headers, body}); const json = await response.json(); ]); } return json; }, errorElement: <ErrorBoundary /> В листинге приведен код настройки маршрутизатора, состоящий из одногоединственного маршрута. Остальной код можно посмотреть в репозитории кода. В компоненте Main реализовано три формы, у которых свойство method имеет следующие значения: y POST — чтобы оформить заказ на товар; y DELETE — чтобы удалить заказ на товар; y PATCH — чтобы изменить информацию о товаре. Все формы различаются методами, и ни одна не содержит свойства action. Поэтому применяется путь по умолчанию, а именно тот, по которому отрендерен компонент. В нашем случае компонент Main рендерится по пути /, что видно из листинга. Любая форма содержит мутирующие методы, поэтому, когда произойдет событие submit, в действие вступит свойство action маршрута, представленное в листинге 6.18. Видно, что action содержит параметр request, из которого извлекается HTTP-метод отправки (POST, DELETE или PATCH). Метод передается в fetch без оценки, какой именно метод извлечен. Это неважно: псевдосервер реализован таким образом, что, получая запросы на один и тот же URL, он определяет, что с ним делать по методу. Реализацию можно посмотреть в коде репозитория, в папке 6/6.18. После обработки запроса сервер возвращает ответ, который доступен в коде компонента Main с помощью хука useActionData. Как только закончил работу action (действие) маршрута, вслед за ним запросы делает loader маршрута. С loader (загрузчиком) мы уже разобрались в предыдущем разделе. На всякий случай отмечу: action (действие) вступает в работу, когда пользователь отправляет форму одним из мутирующих методов, например POST, DELETE, PUT или PATCH. loader (загрузчик) загружает данные, при этом не обязательно делает запрос к серверу, когда компонент переходит по пути. Но, когда в одном маршруте, как, например, в листинге 6.18, присутствует и действие, и экшен, если экшен осуществил работу, то загрузчик будет вызван следом. Размышление здесь такое: данные на сервере обновились мутирующими методами — значит, нужно получить их новое состояние. Действие (action) маршрута, как и загрузчик (loader), содержит второй параметр params. Его можно включить в листинг, но он содержал бы пустой объект, так как маршруты не содержат динамических сегментов. Если бы мы направляли по событию submit форму по пути, содержащему динамический сегмент, то данный параметр был бы инициализирован не пустым объектом.
6.6. Компонент Form  349 6.6.4. state Свойство state может быть использовано, чтобы передать полноценное состояние для нового местоположения. Это состояние хранится в состоянии истории. Доступ к нему можно получить с помощью хука useLocation. Пример использования приведен в листинге 6.19. Листинг 6.19. Использование свойства state // routes.js export const routes = createBrowserRouter([ { path: "/", element: <Main />, errorElement: <ErrorBoundary /> }, { path: "/result", element: <Result />, errorElement: <ErrorBoundary />, action: async () => ({success: true}), } ]); // Main.js export const Main = () => { const [name, setName] = useState(''); }; return (<main> <Form method="post" action="result" state={{ name }}> <fieldset> <legend>Проверяем работу свойства state:</legend> <label htmlFor="name"> Ваше имя: <input type="text" name="name" id="name" onChange={(e) => setName(e.target.value)} /> </label> <input type="submit" value="Заказать" /> </fieldset> </Form> </main>); // Result.js export const Result = () => { const {state} = useLocation(); const {name} = state; } return (<main> {name} </main>);
350  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL При взгляде на листинг первое, что бросается в глаза, — наличие экшена в маршруте, который просто всегда возвращает объект и не делает реальных запросов. Для примера этого достаточно. Но если он не нужен, то его можно вообще не писать. Однако нет. Внимательно посмотрите на реализацию компонента Main. В компоненте Form определено свойство action. Когда пользователь нажмет кнопку и инициируется событие submit, форма будет направлена по пути /result, и, поскольку свойство method равно post (то есть мутирующий метод), в целевом маршруте должен быть реализован экшен (action). Целевой компонент Result содержит хук, который извлекает данные и содержит свойство state, являющееся объектом. Мы инициализировали свойство name, идя по пути /, и теперь оно доступно в совершенно другом компоненте, когда мы движемся по пути /result. 6.6.5. Заключение Библиотека упрощает работу с формами. Как вы успели убедиться, мы разобрали не все свойства. Оставшиеся предлагаются для самостоятельного изучения. Здесь были рассмотрены лишь самые важные для приложений. 6.7. Лэйаут1 Библиотека React Router не содержит специальный компонент или хук, являющийся лэйаутом. Однако его удобно применять, особым образом используя маршрут. Данный способ задумывался именно с такой целью. Здесь мы рассмотрим этот прием, а также компоненты, которые часто в него помещают (листинг 6.20). Листинг 6.20. Использование маршрута в качестве лэйаута export const routes = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorBoundary />, }, 1 Лэйаут (от англ. layout — «макет», а также «расположение», «размещение») — в вебразработке обозначает повторяемый набор компонентов от страницы к странице и от раздела к разделу, составляющий основу внешнего вида приложения. Повторяемый набор компонентов — это компоненты, которые не изменяются от раздела к разделу. Как правило, это хэдер, футер, компонент разделов и общий компонент, внутри которого располагаются все другие компоненты. Но лэйаут может быть и меньшего размера, чем все приложение. Например, когда существует ряд повторяемых компонентов для одного раздела. Лэйаут определяется дизайном. У этого слова есть и другие значения. Например, в дизайне и типо­графике лэйаутом называют сам процесс расположения элементов.
6.7. Лэйаут  351 { path: "mathematics", element: <Mathematics />, errorElement: <ErrorBoundary />, children: [ { index: true, element: <DisciplineIndex />, }, { path: ":discipline", element: <Discipline />, children: [ { index: true, element: <DisciplineAttributesIndex />, }, { path: ":attribute", element: <DisciplineAttributes />, } ] } ] }, { path: "technical", element: <LayoutTechnical />, errorElement: <ErrorBoundary />, }, { { ]); } element: <LayoutWithMenu />, children: [ { path: "descriptionlayout", element: <DescriptionLayout />, }, ] } path: "otherpageusesamelayout", element: <OtherPageUseSameLayout /> В листинге определены девять маршрутов. Однако родительских всего четыре. Первый из них можно смело отбросить, так как это просто стартовая страница — путь /, а компонент Root добавлен для удобства. Поэтому нам нужно рассмотреть три оставшихся. Здесь представлены все возможности для вставки лэйаута.
352  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL 6.7.1. Псевдолэйаут Путь mathematica содержит дочерние маршруты. Один из них не включает в себя свойство path. Соответственно, компонент <Mathematics> содержит компонент <Outlet>, в котором рендерятся дочерние компоненты. Так, <Discipline­Index> рендерится, если путь совпадает только с путем /mathematics. А если путь содержит что-то наподобие /mathematics/combinatorics, то рендерится компонент <Discipline>. Почему так? Потому что дочерний компонент <Discipline> может быть отрендерен, лишь когда в URL есть совпадение с путем /mathematics/nabor-simvolov. А до этого момента данный компонент отрендерен не будет. В родительском компоненте <Mathematics> присутствует <Outlet>. Он заполняется содержимым компонента дочернего маршрута только при совпадении в URL пути. В нашем случае это /mathematics/nabor-simvolov. Пока нет совпадения, на месте <Outlet> будет пусто. Чтобы этого не было, существует дочерний маршрут без свойства path, но содержащий свойство index со значением true. В разделе 6.4 мы уже рассматривали компонент Outlet. Здесь он упоминается вновь, потому что родительский компонент, куда входит Outlet, как бы выступает лэйаутом. Поэтому его нужно привести для сравнения. Наша конфигурация маршрутизатора для маршрута /mathematics содержит следующий уровень вложенности. Компонент <Discipline> тоже содержит <Outlet> и будет действовать точно так же, но в компоненте <Discipline> на месте компонента <Outlet> будет рендериться компонент <DisciplineAttributeIndex>, если URL будет совпадать с путем, например, /mathematics/combinatorics. А если в URL появится путь /mathematics/combinatorics/read, то на месте <Outlet> в <Discipline> отрендерится <DisciplineAttribute>. Поскольку в этом маршруте используется иной прием, нежели лэйаут, то я позволил себе назвать его псевдолэйаутом. Любой родительский компонент, содержащий <Outlet>, становится как бы вынужденным лэйаутом. 6.7.2. Лэйаут для одного маршрута В листинге маршрут /technical использует лэйаут, отличный от других. На него никак не влияют соседние маршруты. Совершенно очевидно, что компоненты одного уровня не влияют на рендеринг друг друга. 6.7.3. Маршрут с лэйаутом Наконец мы добрались до лэйаута, который задумали разработчики библиотеки. Обратите внимание, что последний маршрут не содержит свойства path. Однако у него есть дочерние маршруты: descriptionlayout и otherpageusesamelayout. Родительский маршрут содержит только компонент <LayoutWithMenu>. Разумеется, он содержит компонент <Outlet>.
6.7. Лэйаут  353 Поскольку родительский маршрут не содержит свойства path, его дочерние маршруты будут рендериться при наличии в URL пути /descriptionlayout или /otherpageusesamelayout. В первом случае в родительском компоненте <Layout­ WithMenu> (являющемся лэйаутом) в месте, где располагается <Outlet>, будет рендериться <DescriptionLayout>, во втором — <OtherPageUseSameLayout>. Прием, когда родительский компонент не содержит path , а только дочерние маршруты, и является способом предоставить лэйаут всем дочерним маршрутам. Альтернативой данному приему является способ оборачивать каждый дочерний компонент компонентом, содержащим лэйаут. То есть пришлось бы вручную в нескольких местах дублировать оборотку лэйаутом. 6.7.4. Link В библиотеке определен компонент Link, с помощью которого создают возможность переключаться между разделами (листинг 6.21). Листинг 6.21. Использование компонента Link в компоненте MenuLeft <nav> { menu.map(({id, path, title}) => <Link key={id} to={path}>{title}</Link>) } </nav> Компонент MenuLeft содержит участок кода, показанный в листинге. Данный компонент Link используется для создания переходов между маршрутами. Путь указывается относительный. Здесь должен быть указан конкретный путь. То есть, если подразумевается совпадение с динамическим сегментом, здесь указывается конкретный путь, который с ним совпадает. Его используют для создания меню, чтобы переключаться между разделами приложения, а также внутри раздела, чтобы отобразить дочерние компоненты, которые рендерятся в компоненте Outlet, как в примере с переключением между подподразделами Где применяется, Где спрашивают, Что читать (для уточнения деталей см. подраздел 6.4.3). Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 6.7. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 6.7. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 6.7 и ознакомьтесь с моим решением.
354  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL 6.8. Составляющие библиотеки Поскольку библиотека предназначена для работы с React, она содержит функции, хуки и компоненты. Рассмотрим их. 6.8.1. Функции Маршрутизаторы. Все функции-маршрутизаторы создают конфигурацию для компонентов провайдеров маршрутизаторов (табл. 6.2–6.4). Но, по сути, они и являются маршрутизаторами. Таблица 6.2. Функции, создающие роутеры Функция Описание createBrowserRouter Создает маршрутизатор на основе полноценных URL. Используется DOM History API, чтобы обновлять URL и управлять стеком истории createHashRouter Создает маршрутизатор для использования с частью URL, называемой хешем (#), чтобы управлять URL приложения. В остальном работает так же, как createBrowserRouter createMemoryRouter Создает маршрутизатор, использующий память (как если бы вы хранили обычный объект), и управляет собственным стеком истории вместо того, чтобы использовать стек истории браузера. Предназначается для тестирования или для такого инструмента разработки компонентов, как StoryBook createStaticRouter Используется, когда вы хотите управлять роутером данных для рендеринга на вашем сервере createStaticHandler Используется для выполнения запросов данных и их отправки на сервер перед рендерингом на серверной стороне вашего приложения, через компонент <StaticRouterProvider> Таблица 6.3. Утилиты, используемые при получении данных Функция Описание json Позволяет сократить выражение: new Response(JSON.stringify(someValue), { headers: { "Content-Type": "application/json; utf-8", }, }); до используемого в загрузчике выражения: const loader = async () => { const data = getSomeData(); return json(data); };
6.8. Составляющие библиотеки  355 Функция Описание redirect Поскольку в загрузчиках и действиях можно возвращать значения или выбрасывать исключения, там же можно использовать функцию redirect, чтобы перенаправить на другой маршрут redicrectDocument Обертка над функцией redirect, которая переключит перенаправление уровня document в новое местоположение навигации на клиентской стороне Таблица 6.4. Утилиты Функция Описание createRoutesFromChildren Алиас для createRoutesFromElements createRoutesFromElements Вспомогательная функция, которая создает объекты маршрутов из элементов <Route>. Полезна, если вы предпочитаете создавать ваши маршруты, используя JSX, а не объекты createSearchParams Обертка над new URLSearchParams, которая добавляет поддержку для использования объекта со значениями массива defer Позволяет отложить возвращаемые значения из загрузчиков, отправляя промисы вместо разрешаемого значения generatePath Интерполирует множество параметров в строку пути маршрута с плейсхолдерами :id и * isRouteReturnResponse Возвращает true, если ошибка маршрута является ошибкой route error response matchPath Сопоставляет шаблон пути маршрута с путем URL и возвращает сведения о совпадении matchRoutes Запускает алгоритм сопоставления маршрута для множества маршрутов с местоположением (location), чтобы определить, какие из них совпадают renderMatches Рендерит результат matchRoutes() в React-элемент resolvePath Разрешает предоставленное значение То в действительный объект Path с абсолютным pathname. Это полезно, когда необходимо знать точный путь для относительного значения То 6.8.2. Хуки Рекомендуется составить собственные примеры использования каждого хука (табл. 6.5). Действуя таким образом, вы получите полное представление об их функциональности. Таблица 6.5. Хуки библиотеки Хук Описание useActionData Хук предоставляет возвращаемое значение результата действия (action) с предыдущего пути или undefined, если не было отправки формы Продолжение 
356  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL Таблица 6.5 (продолжение) Хук Описание useAsyncError Возвращает значение отклонения из ближайшего компонента Await useAsyncValue Возвращает данные из ближайшего компонента-предка Await useBeforeUnload Вспомогательный хук — обертка над window.onbeforeunload. Может быть полезен для сохранения важного состояния приложения на странице (например, в local storage) до того, как пользователь уйдет со страницы useBlocker Позволяет запретить пользователю перемещаться от текущего местоположения и предоставляет ему пользовательский интерфейс, позволяющий подтвердить навигацию useFetcher В HTML/HTTP изменение данных и загрузка смоделированы с помощью переходов: <a href> и <form action>. Оба вызывают переход в браузере. Подобные компоненты в React Router — <Link> и <Form>. Но иногда вам необходимо вызывать загрузчик (loader) вне перехода или вызывать действие (action) (и получить данные на странице для повторной валидации) без изменения URL. Или вам могут понадобиться множественные изменения при выполнении запросов в то же самое время. Множественные взаимодействия с сервером не являются событиями перехода. Этот хук позволяет поместить UI в действие (action) и загрузчик (loader) без перехода useFetchers Возвращает массив всех фетчеров (fetchers), которые выполняются без их свойств load, submit или Form. Применяется в так называемом оптимистичном UI useFormAction Используется внутренне в компоненте <Form>, чтобы автоматически разрешать действия по умолчанию и относительные действия для текущего маршрута в контексте useHref Возвращает URL, который может быть использован для перехода к указанному местоположению в To даже за пределами React Router useInRouterContext Возвращает true, если компонент отрендерен в контексте <Router>, иначе — false. Необходим для расширений сторонних библиотек, которым нужно знать, если они рендерятся в контексте приложения React Router useLinkClickHandler Возвращает обработчик события Click для переходов, когда создается на­ страиваемый <Link> в React-Router-Dom useLinkPressHandler react-router-native — аналог useLinkClickHandler. Этот хук возвращает обработчик события нажатия для настраиваемого перехода по <Link> useLoaderData Предоставляет возвращаемое значение из загрузчика (loader) маршрута useLocation Возвращает объект текущего местоположения location. Это полезно, если вы желаете выполнить некоторые побочные эффекты, когда изменяется местоположение useMatch Возвращает совпадающие данные для маршрута по указанному пути относительно текущего местоположения useMatches Возвращает текущие совпадения маршрута на странице. Это полезно для создания абстракций в родительских лэйаутах, чтобы получить доступ к данным дочерних маршрутов
6.8. Составляющие библиотеки  357 Хук Описание useNavigate Возвращает функцию, которая позволяет осуществлять переходы программно на другой путь. Например: const navigate = useNavigate(); <Button onClick={() => { navigate("/wood-interiors"); } /> useNavigation Позволяет все, что необходимо знать о переходах по страницам, чтобы при ожидании результатов во время перехода создать его индикаторы или оптимистичный UI при мутации данных useNavigationType Возвращает текущий тип перехода или то, как пользователь пришел на текущую страницу — через pop, push или replace истории стека useOutlet Возвращает элемент для дочернего маршрута на текущем уровне иерархии. Используется внутренне в компоненте <Outlet>, чтобы отрендерить дочерние маршруты useOutletContext Часто родительские маршруты управляют состоянием других значений, которые вы хотите разделять с дочерними маршрутами. Если пожелаете, вы можете создавать собственные провайдеры контекста, но это наиболее часто встречающаяся ситуация, встроенная в <Outlet>. function Parent() { const [count, setCount] = React.useState(0); return <Outlet context={[count, setCount]} />; } useParams Возвращает объект пар «ключ — значение» динамических параметров из текущего URL, где были совпадения с <Route path>. Дочерние маршруты наследуют все параметры от родительских useResolvedPath Сравнивает pathname в предоставленном значении To с текущим местоположением. Полезен, когда строятся ссылки из относительных значений useRevalidator Позволяет провести повторную валидацию данных по каким-либо причинам. React Router автоматически проводит повторную валидацию данных — после того как действие (action) было вызвано. Но вы можете захотеть провести повторную валидацию по другим причинам, например при возвращении фокуса на окно useRouteError Внутри errorElement возвращает что-либо, что было возвращено как исключение из действия (action), загрузчика (loader) или рендеринга (rendering) useRouteLoaderData Делает данные доступными в любом текущем отрендеренном маршруте, где-либо в дереве. Это полезно для глубоко вложенных компонентов в дереве, которым нужны данные из маршрутов, расположенных гораздо выше, а также родительских маршрутов, нуждающихся в данных дочерних маршрутов и расположенных глубже в дереве useRoutes Функциональный эквивалент <Routes>, но использует JavaScript-объекты вместо элементов <Route>, чтобы определять ваши маршруты. Эти объекты имеют те же свойства, что и обычные <Route>-элементы, но им не требуется JSX. Возвращаемое значение хука или React; элемент, который можно использовать, чтобы отрендерить, или null — если нет никакого совпадения Продолжение 
358  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL Таблица 6.5 (продолжение) Хук Описание useSearchParams Используется, чтобы читать и изменять строку запроса в URL для текущего местоположения. Подобно хуку useState, хук useSearchParams возвращает массив двух значений, параметры поиска текущего местоположения и функцию, которая может использоваться, чтобы их обновить. Подобно хуку useState, setSearchParams (эта функция возвращается из хука useSearchParams для изменения параметров) также поддерживает функциональное обновление. Поэтому вы можете предоставить функцию, которая примет поисковые параметры и вернет обновленные значения useSearchParams (RN) Для React Native. Используется, чтобы читать и изменять строку запроса в URL для текущего местоположения useSubmit Императивная версия компонента <Form>, которая позволяет отправлять форму вместо пользователя. import { useSubmit, Form } from "react-router-dom"; function SearchField() { let submit = useSubmit(); return ( <Form onChange={(event) => { submit(event.currentTarget); }} > <input type="text" name="search" /> <button type="submit">Search</button> </Form> ); } 6.8.3. Компоненты Подраздел «Компоненты» состоит из нескольких частей (табл. 6.6–6.8). Рекомендуется составить собственные примеры использования каждого компонента. Действуя таким образом, вы получите полное представление об их функциональности. Таблица 6.6. Компоненты-маршрутизаторы Компонент Описание <BrowserRouter> Сохраняет текущее местоположение в адресной строке браузера, используя чистый URL, и осуществляет переходы, используя встроенный стек истории браузера <HashRouter> Предназначен для использования в браузерах, где URL не может быть отправлен на сервер по разным причинам. Это иногда происходит в некоторых разделяемых сценариях хостинга, где у вас нет полного контроля над сервером. В таких
6.8. Составляющие библиотеки  359 Компонент Описание ситуациях <HashRouter> делает возможным хранение текущего местоположения в части URL, именуемой хешем (участок, идущий после #). Изменения данного участка никогда не отправляются на сервер <MemoryRouter> Хранит местоположения внутри себя в массиве. Этот маршрутизатор не связан с внешними источниками наподобие истории стека в браузере, что делает его идеальным для ситуаций, когда вам необходимо иметь полный контроль над историей стека (например, при тестировании) <NativeRoter> Рекомендуемый интерфейс для запуска React Router в приложениях React Native <Router> Низкоуровневый интерфейс, который разделяется всеми компонентами-маршрутизаторами (наподобие <BrowserRouter> и <StaticRouter>). В терминах React является провайдером контекста, который применяет сведения маршрутизации к оставшемуся приложению <StaticRouter> Используется, чтобы рендерить React Router приложение в Node. Предоставляет текущее местоположение через свойство location Таблица 6.7. Компоненты рендеринга маршрутизаторов данных Компонент Описание <RouterProvider> Все объекты роутеров данных должны быть отправлены в этот компонент, чтобы рендерить ваше приложение и позволять использовать данные всему остальному приложению <StaticRouterProvider> Принимает в свойство router значение, возвращаемое функцией-маршрутизатором createStaticRouter(), в свойство context — от createStaticHandler(), а также рендерит ваше приложение на сервере (например, Node или другая среда исполнения JavaScript) Таблица 6.8. Компоненты для разных целей библиотеки React Router Компонент Описание <Await> Используется, чтобы отрендерить задержанные значения с автоматической обработкой ошибок <Form> Компонент, являющийся оберткой над тегом form HTML. Эмулирует браузер для маршрутизации на клиентской стороне и мутации данных <Link> Позволяет осуществить пользователю переход на другую страницу, щелкая на нем или нажимая на него. В React-Router-Dom компонент <Link> рендерится в доступный <a> элемент с действительным атрибутом href, указывающим на ресурс, с которым он связан. Это значит, что вещи, подобные щелчку правой кнопкой мыши на <Link>, работают так, как вы ожидаете. Можно использовать <Link reloadDocument>, чтобы отбросить маршрутизацию на клиентской стороне и позволить браузеру обрабатывать переход обычным образом, будто это тег <a href> Продолжение 
360  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL Таблица 6.8 (продолжение) Компонент Описание <NavLink> Особый тип компонента <Link>, который активен, находится в ожидании или уже перешел. Это полезно для создания стилей разделов меню, где нужно выделить текущий раздел <Navigate> Изменяет текущее местоположение, когда отрендерен. Это компонент-обертка над useNavigate, принимает все те же аргументы в качестве свойств <Outlet> Должен быть использован в элементах родительских маршрутов, чтобы отрендерить их элементы дочерних маршрутов. Это позволяет вкладывать UI, чтобы отображать, когда дочерние маршруты рендерятся. Если родительский маршрут в точности совпадает, он будет рендерить дочерний маршрут index, а если дочерний маршрут index отсутствует, то ничего <Route> Соединяет сегменты URL с компонентами, загрузкой данных и изменением данных. Через вложение маршрутов сложные лэйауты приложения и зависимости данных становятся простыми <Routes> Отрендеренные где-либо в приложении, <Routes> будут сопоставляться со множеством дочерних маршрутов из текущего местоположения <ScrollRestoration> Эмулирует восстановление прокрутки браузера при изменении местоположения — после того как загрузчики (loader) закончили, чтобы гарантировать, что позиция скролла восстановлена в правильное место даже в разных доменах 6.9. Что изучить самостоятельно Как вы уже успели убедиться, в библиотеке довольно много разных составляющих. Начинать главу о React Router нужно было с основной идеи. Представляется, что я достаточно наглядно ее показал. Затем были описаны главные составляющие, с помощью которых вы можете создавать большинство приложений. Охватить все не получилось, но в этом нет необходимости. В данном разделе я описал то, что, на мой взгляд, нужно в первую очередь изучить самостоятельно, — это может вам пригодиться при разработке. А если понадобится расширить свои знания, вы сможете прочитать раздел 6.8 и определить, что еще полезно изучить. Данный раздел, как и предыдущий, структурирован по типам составляющих, а последний подраздел — это глоссарий от создателей библиотеки. Говоря «изучить самостоятельно», я имею в виду, что: y вы поймете из прочитанного, для чего предназначен определенный элемент библиотеки. Назначение здесь описано, а расширенную информацию вы можете получить на официальном сайте React Router; y вы будете составлять примеры кода, где задействуете данный элемент, чтобы увидеть, как он функционирует. В табл. 6.9–6.12 приводятся составляющие, которые вам нужно изучить самостоятельно.
6.9. Что изучить самостоятельно  361 Таблица 6.9. Функции Функция Описание createSearchParams Обертка над new URLSearchParams, которая добавляет поддержку для использования объекта со значениями массива redirect Раз в загрузчиках и действиях можно возвращать значения или выбрасывать исключения, там же можно использовать функцию redirect, чтобы перенаправлять на другой маршрут json Позволяет сократить выражение до используемого в загрузчике выражения: const loader = async () => { const data = getSomeData(); return json(data); }; Таблица 6.10. Маршрутизаторы Маршрутизатор Описание createMemoryRouter Создает маршрутизатор, использующий память (как если бы вы хранили обычный объект), и управляет собственным стеком истории вместо того, чтобы использовать стек истории браузера. Предназначен для тестирования или для такого инструмента разработки компонентов, как StoryBook createHashRouter Создает маршрутизатор для использования с частью URL, называемой хешем (#), чтобы управлять URL приложения. В остальном работает как createBrowserRouter Таблица 6.11. Хуки Хук Описание useFetchers Возвращает массив всех фетчеров (fetchers), которые выполняются без их свойств load, submit или Form. Применяется в так называемом оптимистичном UI useNavigate Возвращает функцию, которая позволяет осуществлять переходы программно на другой путь useNavigation Позволяет все, что необходимо знать о переходах по страницам, чтобы при ожидании результатов во время перехода создать индикаторы перехода или оптимистичный UI при мутации данных useRouteLoaderData Делает данные доступными в любом текущем отрендеренном маршруте, где-либо в дереве. Это полезно для глубоко вложенных компонентов в дереве, которым нужны данные из маршрутов, расположенных гораздо выше, а также родительских маршрутов, нуждающихся в данных дочерних маршрутов, расположенных глубже в дереве useSubmit Императивная версия компонента <Form>, которая позволяет отправлять форму вместо пользователя
362  Глава 6 Управление рендерингом компонентов с помощью изменяющегося URL Таблица 6.12. Компоненты Компонент Описание <Await> Используется, чтобы отрендерить задержанные значения с автоматической обработкой ошибок <NavLink> Особый тип компонента <Link>, который активен, находится в ожидании или уже перешел. Это полезно для создания стилей разделов меню, где нужно выделить текущий раздел <Navigate> Изменяет текущее местоположение, когда отрендерен. Это компонент-обертка над useNavigate, принимает те же аргументы в качестве свойств <ScrollRestoration> Эмулирует восстановление прокрутки браузера при изменении местоположения — после того как загрузчики (loader) закончили, чтобы гарантировать, что позиция скролла восстановлена в правильное место даже в разных доменах 6.9.4. Определения терминов, используемых в React Router URL — URL в адресной строке. Большинство людей используют термины «маршрут» (rout) и URL как взаимозаменяемые, но URL не является маршрутом в React Router. URL — это просто URL. Location (местоположение) — специальный объект в React Router, который основан на встроенном в браузер объекте window.location. Он знает, «где сейчас находится пользователь». Location в основном является объектом, предоставляющим URL, но содержит чуть больше информации, чем window.location. Location State (состояние местоположения) — значение, связанное с местоположением (Location), которое не кодируется в URL. Подобно хешу (в URL) или параметрам поиска (данные, кодируемые в URL), но хранится невидимо в памяти браузера. History Stack (стек истории) — по мере того как пользователь переходит с одной страницы приложения на другую (имеется в виду изменение части URL в адресной строке и вследствие этого перерисовка интерфейса), браузер сохраняет данные о каждом местоположении в стеке. Если вы нажмете кнопку Назад браузера, то увидите прямо там «стек истории». Client Side Routing (CSR) (маршрутизация на клиентской стороне) — простой HTML-документ, который может содержать ссылки на другие документы и браузер, сам обрабатывает стек истории. CSR (МКС) позволяет разработчику изменять стек истории браузера без необходимости создавать запросы к документам (имеется в виду HTML-документам) на сервер. History (история) — объект, позволяющий React Router подписаться на изменения в URL как на предоставление API, чтобы управлять стеком истории браузера программно. History Action (действие истории) — одно из действий pop, push или replace. Пользователи могут возвращаться в URL по одной из трех этих причин. Действие push происходит, когда запись добавляет в историю стека (обычно это щелчок на ссылке, или программист указал в программе перейти по другому пути в URL).
6.9. Что изучить самостоятельно  363 Действие replace подобно замене новой записи на существующую в стеке. Наконец, pop происходит, когда пользователь нажимает кнопку Назад или Вперед в браузере. Segment (сегмент) — часть URL или шаблон пути, символы между символами типа /. Например, /books/9786010835146 содержит два сегмента. Path Pattern (шаблон пути) — выглядит подобно URL, но может содержать специальные символы для поиска совпадения URL маршрута, подобно динамическим сегментам (/books/:bookId) или сегментам-звездочкам (/books/*). Это не URL, а шаблоны, с которыми React Router будет искать совпадения. Dynamic Segment (динамический сегмент) — сегмент шаблона пути, который является динамическим и может совпадать с каким-либо значением в сегменте. Например, шаблон /books/:bookId будет совпадать с URL наподобие /books/9786010835146. URL Params (параметры URL) — разобранные значения из URL, которые совпадают с динамическим сегментом. Router (маршрутизатор) — содержащий состояние, верхнеуровневый компонент, который делает все компоненты и хуки работающими. Route Config (конфигурация маршрута) — дерево объектов маршрутов, которые будут отранжированы и для которых будет искаться совпадение (включая вложенные) с текущим местоположением, чтобы создать ветку совпадений маршрута. Route (маршрут) — объект или элемент маршрута (Route Element) обычно такой формы: { path, element } или <Route path element>. Свойство path — шаблон пути. Когда он совпадает с текущим URL, элемент (element) будет отрендерен. Route Element (элемент маршрута), или <Route>. Свойства элемента читаются, чтобы создать маршрут внутри <Routes>, иначе ничего не делает. Nested Routes (вложенные маршруты) — поскольку маршруты могут иметь дочерние маршруты, каждый маршрут определяет часть URL сквозь сегменты. Один URL может совпадать со множеством маршрутов во вложенную ветку дерева. Это позволяет автоматически вкладывать верстку (layout) через компонент Outlet, относительные ссылки и др. Relative links (относительные ссылки) — ссылки (links), которые не начинаются с / и будут наследоваться от ближайшего маршрута, где они рендерятся. Это дает возможность легко погружаться вглубь URL без необходимости знать и строить весь путь. Match (совпадение) — объект, содержащий информацию, когда маршрут совпадает с URL, подобно параметрам URL и имени пути, который совпадает. Matches (совпадения) — массив маршрутов (или веток конфигурации маршрутов), который сравнивается с текущим местоположением. Эта структура позволяет использовать вложенные маршруты. Parent Route (родительский маршрут) — маршрут с дочерними маршрутами. Outlet (выход) — компонент, который рендерит следующее совпадение во множестве совпадений. Index Route (индексный маршрут) — дочерний маршрут без свойства path, который рендерится в родительском компоненте Outlet в родительском URL. Layout Route (маршрут верстки) — родительский маршрут без свойства path, который используется исключительно для группирования дочерних маршрутов внутри определенной верстки.
Глава 7 ФАЙЛЫ ПРИЛОЖЕНИЯ 7.1. Как организовать файлы приложения Действие по размещению компонентов в файлах — всегда некая система действий разработчика, основанная на привычках, привитых в какой-либо команде разработки. Обычно разработчики организуют компоненты в файлы так, чтобы впоследствии их было легко найти. То есть простая ориентация в файлах приложения является главным ключом к успеху. Ведь приложение, точнее, их подавляющее большинство постоянно развивается, обогащаясь новыми составляющими. В этом разделе я предлагаю рассмотреть достаточно интересную систему расположения файлов приложения. Она основана на двух простых принципах, которые легко понять и использовать, а также на том, что структура расположения файлов не обязана повторять структуру связей компонентов. 7.1.1. Цель Какова главная цель такой системы расположения элементов приложения в файлах? Главная цель — скорость поиска существующих элементов приложения среди файлов. Это нужно в таких случаях, как: y разработка нового; y доработка существующего. Мы живем в физическом мире, и файлы — его часть. Поэтому важно уметь ориентироваться среди файлов, чтобы находить код и понимать, куда добавлять новое. 7.1.2. Элементы приложения, размещаемые в файлах Под элементами подразумевается код в виде текста или бинарный файл, с которыми вы имеете дело. Это может быть: y функция (разные виды); y хук; y компонент; y тест; y CSS-стиль; y изображение; y статически определяемая структура данных.
7.1. Как организовать файлы приложения  365 Это то, что вы сами создаете или приносите в виде физического файла, но не импортируете из библиотеки. Импортируемое из библиотек вы используете в каком-либо месте, например в функции, хуке, компоненте, тесте. Что касается изображения, то элемент svg может быть и отдельным файлом изображения, и компонентом. В случае с CSS-стилями вы можете сохранять их в отдельный, самый обычный файл с расширением *.css. При этом использовать чистый CSS или CSS Modules. Либо использовать библиотеку styled-components (CSS in JS) и внедрять стили в компоненты, применяя их впоследствии как компоненты. А как же конфиги инструментов, которые помогают создавать приложение, например для webpack, jest, eslint и др.? Мы рассматриваем только файлы кода, составляющего приложение, те, что будут работать в браузере. С конфигами и другими вспомогательными файлами трудностей нет, думать над ними особо не нужно — они располагаются в главной директории проекта. 7.1.3. Принцип 1: роли компонентов — главный критерий их распределения по папкам Весь код приложения состоит из типов элементов, перечисленных ранее. Но что является базовым или определяющим? Самый важный — компонент. Ведь с помощью компонентов мы задаем структуру будущего приложения. Все прочие составляющие импортируются и используются в компоненте. Компоненты, как известно, связаны между собой — так получается структура приложения. Ранее, в разделе 5.19, я представил идею ролей компонентов. Компоненты приложения выстраиваются в иерархию, и роли, которые они играют, отличаются в зависимости от уровня нахождения компонента. Это хорошо видно на рис. 7.1. Рис. 7.1. Иерархия компонентов приложения и роли
366  Глава 7 Файлы приложения Не все роли должны присутствовать. Например, при использовании библиотеки React-Router-Dom нет необходимости в компонентах уровня приложения. Главную роль корневого компонента в данном случае выполняет провайдер, который осуществляет рендеринг графа (иногда дерева) компонентов на определенный путь. Теперь — главное. Поскольку структура компонентов образует иерархию и мы можем разделить их по ролям, а компонентов одной роли, как правило, несколько, самый первый уровень группировки файлов — группировка компонентов по их ролям. На рис. 7.1 почти все роли указаны справа, только роль «Приложение» — слева. В папке src для каждой роли компонентов нужно создать отдельную папку. Если у вас будет приложение, состоящее из компонентов как на рис. 7.1, то оно получит в src следующие папки: layout, pages, business, components. Есть еще пара важных моментов. Обычно компонент приложения состоит всего из одного файла, что можно видеть при установке React. Это компонент App.js, который лежит в корне папки src. Компоненты «Провайдер» тоже всегда состоят из одного компонента (как правило, но бывает и несколько), и если это провайдер уровня приложения, то он определен в библиотеке, используется на уровне приложения и импортируется или в файл App.js, или в index.js всего приложения. Папки с именами ролей компонентов всегда располагаются в папке src. Если компонентов определенной роли нет, то нет и соответствующей папки. Однако они всегда располагаются в src. 7.1.4. Принцип 2: группировка и глубина размещения остальных элементов в папках зависит от количества ролей компонентов, в которых они задействуются Все остальные элементы приложения импортируются в компоненты. На рис. 7.1 показан граф компонентов. Он не содержит многих деталей импорта других элементов. На самом деле они нас не интересуют и не пригодятся, а изобразив их на этом рисунке, мы бы сильно усложнили восприятие структуры компонентов. В названии принципа 2 использован термин «глубина размещения». Под этим подразумевается место в иерархии файлов, где размещается тот или иной тип элементов. Эта глубина может быть разной (см. пример на рис. 7.2). а б Рис. 7.2. Размещение папки hooks в
7.1. Как организовать файлы приложения  367 На рис. 7.2, а файловая структура содержит только папки с компонентами. На рис. 7.2, б в папке бизнес-компонентов есть папка с другими элементами — хуками. Однако на рис. 7.2, в мы видим эту папку на одном уровне с папками для компонентов. Так где она должна находиться? Это и определяет принцип 2. В данном случае нам нужно расположить хук (-и). Вполне разумно класть их в отдельную папку, например, с именем hooks либо в отдельный файл с именем hook.js или названный по имени самого хука, допустим useLocalStorage.js, если хук один. На рис. 7.2, б и в это папка hooks. Представим, что в приложении вы сохраняете данные из бизнес-компонентов в локальном хранилище браузера. В этом случае такой конкретный элемент, как хук useLocalStorage, а не хуки вообще или множество хуков на конкретную тему, задействуется в React-компоненте одной роли, а именно в бизнес-компоненте. Поэтому вполне разумно размещать хук в папке тех компонентов, где он используется. Это и реализовано на рис. 7.2, б. Однако может возникнуть необходимость использовать хук useLocalStorage и в другом месте, допустим в компонентах страниц (папка pages). Получается, один и тот же элемент используется в компонентах с разными ролями. В этом случае такой элемент располагается в папке src, что и продемонстрировано на рис. 7.2, в. Папка hooks расположена в папке src. 7.1.5. Чтение Представьте себе, что вы открываете файлы приложения, видите папки как на рис. 7.2, а и понимаете, что все другие типы элементов, скорее всего, используются исключительно в компонентах с одной ролью. Это говорит и о том, что проект еще в очень слабо развитом состоянии. Теперь представьте ситуацию, когда вы открываете файлы приложения и видите папки как на рис. 7.2, в. Здесь тоже сразу видно, что хуки, которые там находятся, используются в компонентах с разными ролями. Еще раз обращаю внимание на то, что в нужном месте располагается именно тот элемент, который импортируется в компонент (-ы). Допустим, есть два однотипных элемента, например хуки. Один из них используется в компонентах с разными ролями, как в примере выше, а другой, например хук useFetchProducts, — только в бизнес-компонентах. Соответственно, и располагаться они должны в разных местах, что показано на рис. 7.3. Помимо хуков, у вас могут использоваться другие типы элементов, например функции или изображения. Для них действуют такие же правила, как только что рассмотренные для хуков. Рис. 7.3. Хуки, расположенные в разных местах
368  Глава 7 Файлы приложения 7.1.6. Количество элементов в файле и условие, уточняющее принцип 2 Сколько элементов приложения располагать в одном файле? Лично я придерживаюсь правила «один основной элемент в одном файле». Под основным понимается компонент. Он может задействовать функции или хуки, которые необходимо где-то разместить. В таком случае удобно помещать их в файлы рядом с компонентом, что соответствует принципу 2. Функций или хуков может быть несколько. Они не являются основными структурными элементами приложения, а используются в компонентах. Если исключительно в одном компоненте, то их можно располагать в файлах по несколько штук, например, функции — в functions.js, хуки — в hooks.js. Кстати, некоторые разработчики называют файл с функциями utils.js. Это дело вкуса. Приведенное правило позволит вам с ходу определять, что в файле, например, functions.js, располагающемся в одной папке с компонентом, есть функции, которые используются только в этом компоненте. А зайдя в папку, например, business и увидев папку hooks, вы поймете, что в ней лежит хук (или хуки), который используется в нескольких компонентах данной роли (бизнес-компонент). Если одна функция или один хук используется для нескольких компонентов, пусть и одной роли, то их лучше разместить в отдельном файле с именем функции или хука. 7.1.7. Файл index.js Обычно в папках располагают файл index.js, который экспортирует элементы из данной папки. Это полезно тем, что при импорте можно указывать не название конкретного файла, а имя папки. Например: import {SomeComponent} from "./components"; вместо: import {SomeComponent} from "./components/SomeComponent"; 7.1.8. Компоненты, используемые только в одном месте Доработки компонентов приводят к тому, что приходится разделять один компонент на два и более. Второй или энный выделенный компонент при этом чаще всего используется только в связке с тем компонентом, из которого был выделен. Располагать такие компоненты нужно рядом с компонентом, из которого они выделены. Обычно выделение общего компонента, который будет использоваться в нескольких местах, осуществляется при рефакторинге. Его можно положить в папку components. Описанный подход согласуется с принципом 2.
7.1. Как организовать файлы приложения  369 7.1.9. Конкретное назначение функциональности Главный строительный элемент приложения — компонент. При этом бывает и так, что некоторая функциональность является значимой. Например, библиотека Redux — это набор функций. Кроме того, вы не можете разработать свою функциональность для работы с узконаправленной областью. Допустим, вы реализуете функциональность посредника. Она будет использоваться не в одном месте, поэтому вполне разумно размещать ее в папке, названной по имени функциональности, как, например, на рис. 7.4. 7.1.10. Рост приложения Рис. 7.4. Функциональность, Приложение всегда развивается и растет. Его рост — по расположенная в специально сути, добавление новых компонентов в дерево компоненотведенной для нее папке тов и добавление кода в уже существующие компоненты. Ориентироваться в иерархической структуре, каковой является граф компонентов, всегда непросто. Поэтому чем значительнее глубина вложенности файлов в папках, тем больше времени придется тратить на поиски. Конечно, то же самое можно сказать и о компонентах — чем их больше в приложении, тем выше временные затраты. Однако компоненты имеют определенную роль, а распределение по ролям упрощает ориентацию в них. Расти до бесконечности приложение не будет, как и бесконечно уходить в глубину. Я имею в виду уровни иерархии. Большинство приложений будут иметь примерно столько же уровней иерархии, как на рис. 7.1. Однако кто-то может захотеть ввести дополнительные уровни. Поэтому нет смысла располагать компоненты таким образом, чтобы файлы и папки повторяли их иерархию. Это приведет к путанице. Иерархия компонентов приложения размещается на одном уровне в виде папок, что действительно упрощает поиск компонентов, принадлежащих разным уровням. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 7.1. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 7.1. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 7.1 и ознакомьтесь с моим решением.
Глава 8 РАЗРАБОТКА ФРОНТЕНД-ПРИЛОЖЕНИЯ 8.1. Проектирование 3RApp Название 3RApp представляет собой сокращение от названий трех библиотек, используемых в разработке фронтенд-приложений, — React, React Router, Redux — с присоединением слова App. По-английски «приложение» звучит как application, и это слово принято сокращать до app. Таким образом, если все объединить, получается 3RApp. Набор библиотек, систем и платформ, на которых строится приложение, принято называть стеком. Например, есть известные JavaScript-стеки MEAN1 и MERN2, а 3R — это клиентский стек. В заголовок вынесено «Проектирование 3RApp». Имеется в виду, что вы проектируете клиентское фронтенд-приложение App с применением библиотек, входящих в стек 3R. На этом все. Процедура проектирования, предлагаемая в данной книге, основана на визуальном способе задания структуры компонентов будущего 3RApp-приложения. Проходя ее последовательно, шаг за шагом, и при этом используя наглядность, вы получаете полную картину. Процедура ведет по ключевым моментам будущего приложения, благодаря чему на этапе написания кода будут реже возникать ситуации, когда вдруг выясняется, что нужно переписать структуру компонентов или что-то полностью изменить. Имея перед глазами спроектированную согласно процедуре схему, писать по ней код гораздо легче и быстрее. Еще важно то, что, если в момент проектирования вас что-то не устраивает, вы можете это переосмыслить и сразу поменять, не изменяя код. 8.1.1. Основное выполняемое действие. Что принять во внимание Поскольку мы проектируем приложение, основное действие — выделить компоненты и задать последовательность их взаимодействия. В сами компоненты не погружаемся. Это делается исключительно на этапе реализации. Важно не путать проектирование структуры приложения и его составляющих с конструированием кода компонента (-ов) в момент его (их) реализации. На этапе проектирования вы должны отталкиваться от знаний, которые мы до сих пор осваивали. Речь о библиотеках, ролях компонентов и сведений о системе. 1 2 MEAN — MongoDB, Express, Angular, Node.js. MERN — MongoDB, Express, React, Node.js.
8.1. Проектирование 3RApp  371 Приведу пример того, что не нужно делать на этапе проектирования. Допустим, вам удалось выявить, что в лэйауте всего приложения будет компонент Header. Вы сделали это мысленно и не остановились на достигнутом, чтобы записать название компонента и нанести его на граф, а продолжаете мысленную работу по разделению компонента на меньшие, пытаясь таким образом оптимизировать реализацию. Последнее отличается от выявления компонентов с другими ролями, например компонентов с ролью «Простой конечный компонент». 8.1.2. Процедура проектирования Проектирование делится на шаги, выполняемые последовательно, при этом каждый следующий шаг основывается на результатах предыдущего. Таким образом, это не просто набор действий, а именно упорядоченный последовательный процесс. Итак, вот процедура. 1. Изучить требования, аналитику и дизайн, понять цели АС (ИС)1, то есть выяснить, какая проблема у пользователя решается или что автоматизируется. 2. Определить компоненты с ролью «Страница» (разделы) веб-приложения. 3. Выделить UI-компоненты из дизайна и дополнить граф компонентов приложения компонентами с ролями «Лэйаут», «Бизнес-компонент» и «Простой конечный компонент», а по окончании дополнить компонентом с ролью «Провайдер». 4. Задать глобальное состояние приложения или его части, дополнить граф компонентов компонентом с ролью «Провайдер» и, если требуется, дополнительными компонентами, зависящими от состояния. 5. Определить, с какими внешними системами какие компоненты взаимодействуют. 6. Определить, по какому пути в URL какой компонент будет рендериться. Над фронтенд-приложением работают несколько ролей — аналитик, дизайнер и разработчик. У каждого из них свой участок и продукты, которые выдают исполняемые роли (аналитика, дизайн, проект приложения). Описанная ниже процедура относится к разработчику, однако он выполняет свою работу, опираясь на продукты аналитика и дизайнера. Мы проектируем 3RApp-приложение, а это значит, что мы используем React, React Router и ReduxToolkit. Шаг 1. Изучить требования, аналитику, дизайн и понять цели АС (ИС), то есть выяснить, какая проблема (-ы) у пользователя решается или что автоматизируется Что понадобится: y требования; y дизайн; y аналитика. 1 АС (ИС) — сокращение словосочетаний «автоматизированная система» («информационная система»). В данном случае они являются синонимами.
372  Глава 8 Разработка фронтенд-приложения На выходе: осознанная цель (-и) проектируемого фронтенд-приложения для АС (ИС). Что сделать в шаге Нужно изучить материалы и пропустить их через себя, чтобы они стали «вашими». Уяснить цели, ради которых создается информационная система, а также выяснить ряд ключевых моментов, опираясь на которые вы будете выполнять шаги. Пример Предположим, вы начали новый проект и необходимо получить требуемые материалы. Их может быть много, и желательно (но не обязательно) изучить все. Дизайном вы будете пользоваться в течение всего проектирования, в том числе на этапе реализации. Цель можно уяснить из аналитики и требований. На что обращать внимание, кроме целей, вы поймете, ознакомившись с дальнейшими шагами процедуры. Ключевыми материалами являются цель, дизайн, пути пользователя (обычно изображаются дизайнером в виде стрелок от страницы к странице), конечные точки rest-api (обычно их описывает аналитик) и другие системы, с которыми будет взаимодействовать фронтенд-приложение, структуры данных. Ознакомившись с документами о пока не реализованном продукте или не реа­ лизованной фиче1, на следующем шаге вы приступите к противоположному действию — начнете генерировать то, что будет применяться на этапе реализации в качестве руководства. Это то, что вы записываете и зарисовываете себе и для себя (граф компонентов приложения, таблицы, описания), чтобы использовать на этапе реализации и воплотить в коде. Переходим к следующему шагу. Шаг 2. Определить компоненты с ролью «Страница» (разделы) веб-приложения Что понадобится: дизайн. На выходе: список страниц, граф страниц. Что сделать в шаге Просмотреть дизайн и определить на его основе, какие страницы появятся в будущем фронтенд-приложении. Пример При использовании информационной системы через браузер, пользователь мыслит о ней как о наборе разделов, в которых он может получить необходимую информацию или выполнить какое-то действие. Такие разделы и есть страницы. В дизайне их, как правило, показывает дизайнер. Кроме того, обычно он предусмат­ривает и страницы ошибок — при недоступности разных сторонних авто­матизированных систем или сервисов самой АС. Для каждого такого случая нужна страница. При этом страница сообщения об ошибке не обязательно явная. Это может быть страница с какой-либо функциональностью и с сообщением 1 Фича — некоторая отдельно взятая функциональность в программе или продукте. Может быть крупной или не очень. Добавление комментария к товару и редактирование его заголовка — это примеры крупных фич. Крупные они потому, что данные требуют сохранения, а значит, для них нужно создать эндпоинты, поддерживающие метод patch или put.
8.1. Проектирование 3RApp  373 о каких-то неполадках. Но бывает и так, что информационная система немасштабируемая и если и возникает ошибка на сервере, то сервис становится недоступным. Подобную ситуацию тоже нужно предусмотреть в дизайне — в виде отдельной страницы. Однако тема ассортимента потенциальных ситуаций не является для нас главной в данный момент — такие ситуации предусматривает системный аналитик, а дизайнер реализует в дизайне. Рассмотрим наглядный пример дизайна сервиса проведения собеседований. Допустим, дизайнер показывает дизайн, в котором в отдельные страницы выделены разделы, где пользователь будет осуществлять конкретные действия (рис. 8.1). Рис. 8.1. Дизайн страниц (условно) сервиса проведения собеседований На рис. 8.1 дизайн страниц показан схематично, но для понимания этого достаточно. Изображенные страницы и есть то, что вам нужно выделить для себя. Простое очевидное действие. Начинаете составлять граф приложения, затем придумываете названия компонентам страниц и заносите их в таблицу (табл. 8.1). И граф, и таблица дополняются на следующем шаге (рис. 8.2). Таблица 8.1. Список компонентов с ролью «Страница» для пояснения цифр в вершинах графа MainPage ShedulePage CandidatePage TestPage QuestionPage 1 2 3 4 5 StakeholderPage InterviewPage DashboardPage ProfilePage 6 7 8 9
374  Глава 8 Разработка фронтенд-приложения Рис. 8.2. Пример графа компонентов с ролью «Страница» для дизайна сервиса проведения собеседований Сперва может показаться непривычным начинать проектирование не с корневого компонента, а со страниц. Но впоследствии вы увидите, что мы продумываем глобальное состояние, используем React Router и компоненты с ролью «Провайдер», которые обычно располагают над всем приложением. А еще исходя из дизайна у нас может появиться отдельный компонент с ролью «Лэйаут». Обратите внимание, что в названии каждого компонента присутствует слово Page, указывающее на его роль. В каком месте ставить слово Page — в начале на­ именования компонента (слово-приставка) или в конце (окончание) — и какое число (единственное или множественное) применять к имени раздела, решать вам. Слова-окончания для именования компонентов разных ролей На самом деле, отражая в имени компонента принадлежность к роли, вы облегчаете поддержку приложения. Видя имя, вы сразу понимаете, какую роль в приложении выполняет компонент. Приведу слова-окончания, которые можно прибавлять в зависимости от роли. Провайдер — Provider. Пример: StoreProvider (или ProviderStore, если больше нравятся не слова-окончания, а приставки). Шаблон верстки — Layout. Лэйаутов может быть несколько. Если он один, то достаточно Layout, а если несколько, то нужно прибавить к слову Layout направленность на поддерево приложения, для которого оно будет выступать лэйаутом, например SheduleLayout. Приложение — App. Компонент приложения всегда один. По крайней мере, мне никогда не требовалось более одного. Но если вдруг кому-то понадобится, то к имени App нужно прибавить направленность предметной области приложения. Страница — Page. Пример: CandidatePage. Бизнес-компонент и простой конечный компонент не содержат приставок. Однако их легко отличать и от остальных компонентов, содержащих конкретные слова-окончания или приставки, и между собой. Простой конечный компонент — это некий простой элемент интерфейса. Например, Header, Button. Бизнес-компонент представляет собой некую бизнес-сущность. Например, ProductCatalog, IPAddressTable, Description. Обычно бизнес-компонент именуется с помощью двух и более слов, но иногда состоит из одного. И даже если вы столкнулись с последним вариантом, это все равно имя бизнес-сущности, а не элемента интерфейса. Например, Description или Product. Сравните эти имена с Header и Button. Предпочтение по именованию может быть отличным от того, что описано здесь. Никаких проблем: именуйте так, как сложилось в вашей команде. На мой взгляд, имя компонента — это идентификатор в системе под названием «приложение», а не художественная литература. И если в именовании присутствует какая-либо схема, то она улучшает понимание.
8.1. Проектирование 3RApp  375 Больше всего в приложении будет компонентов с ролью «Бизнес-компонент». Они характеризуются тем, что редко переиспользуются. Компонентов с ролью «Простой конечный компонент» может быть не так много, но переиспользуются они часто. Чаще всего простые конечные компоненты берутся из дизайн-систем. Вывод. Получается, что приложение — набор страниц, содержащих UI и связанных между собой. Каждая страница обладает конкретной функциональностью, доступной только на ней. С точки зрения React-разработчика и библиотеки React она представляет собой компонент уровня страницы. Переходим к следующему шагу. Шаг 3. Выделить UI-компоненты из дизайна и дополнить граф компонентов приложения компонентами с ролями «Лэйаут», «Бизнес-компонент» и «Простой конечный компонент», а по окончании дополнить компонентом с ролью «Провайдер» Что понадобится: y дизайн; y граф страниц; y таблица со страницами. На выходе: выявленные UI-компоненты всех ролей, граф компонентов приложения и таблица с компонентами ролей «Страница» и «Бизнес-компонент». Что сделать в шаге 1. Выявить общие компоненты для разных страниц. 2. Выделить компоненты, присутствующие исключительно на одной конкретной странице. 3. Дописать компоненты с ролями «Бизнес-компоненты» в таблицу к компонентам «Страницы». 4. Составить граф компонентов всего приложения, дополнив недостающими. Основной артефакт здесь дизайн. Мы выявляем разные компоненты. Полное разделение конкретного компонента на более мелкие не требуется — обычно это нужно только при реализации бизнес-компонентов. Поэтапное составление графа заключается в поэтапном выявлении в дизайне компонентов, принадлежащих к тем или иным ролям, а также определении количества мест, где они используются. Компоненты с ролью «Бизнес-компонент» тоже заносятся в таблицу с компонентами — в тех колонках, где указаны страницы, к которым они относятся. Компоненты с другими ролями трудно разместить в таблице и указать связь «родитель — дочерний», а для страниц и бизнес-компонентов это то, что нужно. Обязательно ли создавать таблицу с наименованиями страниц и бизнес-компонентов? Да, такой подход может показаться излишним, но хорошо, когда наименования компонентов где-то приведены в удобном виде. Если вам больше нравится не заносить имена в таблицу, а, скажем, наносить на граф или вообще поместить в другое место, то почему бы и нет.
376  Глава 8 Разработка фронтенд-приложения Как и в какой последовательности выделять компоненты из дизайна. Мы знаем, что компоненты бывают разных ролей. При этом UI-компонентами являются лишь обладатели ролей «Лэйаут», «Страница», «Бизнес-компонент» и «Простой конечный компонент». Страницы найдены и внесены в граф и таблицу. Остается выявить компоненты трех типов. Начинать нужно с обладателей роли «Лэйаут». Компонент с ролью «Лэйаут» применяется для создания шаблона, которому будут соответствовать все страницы приложения. Значит, смотрим, какие компоненты будущего лэйаута могут присутствовать на каждой странице, и все это будет в общем Layout, состоящем из более мелких компонентов или полностью реализованном в одном компоненте. Следующие по очереди — компоненты с ролью «Бизнес-компонент». Это основные действующие компоненты, то, ради чего пользователь использует сервис или приложение. Обычно они или обращаются за данными к серверу, или принимают данные пропсами от других компонентов. Можно сказать, что приложение существует ради этих компонентов. Их главное назначение — отобразить пользователю данные, ради которых он использует приложение. Выделение таких компонентов происходит следующим образом: рассматриваем каждую страницу, мысленно выкидывая из нее визуальные компоненты, состоящие из одного элемента, и те, которые входят в лэйаут. Бизнес-компонентов на одной странице может быть несколько — как правило, так и есть. Выявить их легко, поскольку все, что отображает данные, которые зависят от определенных параметров, и является бизнес-компонентом. Последними идут в работу компоненты с ролью «Простой конечный компонент». Они выявляются так же легко, как простые, единичные элементы интерфейса. Эти компоненты входят в компоненты других типов, причем совершенно разных (тип «Страница», «Бизнес-компонент», «Лэйаут»). Простой конечный компонент наиболее переиспользуемый тип компонента. Библиотека React Router и граф компонентов. В разделе 6.2, на рис. 6.2 и 6.3, были показаны графы того, как React Router рендерит компоненты в зависимости от пути и альтернативные компоненты. В этом разделе мы начнем составлять структуру приложения. Кое-каким техническим нюансом придется пренебречь, но сама структура будет правильной. Нюанс здесь следующий. Компонент RouterProvider принимает объект конфигурации в качестве значения свойства router. Этот объект, как вы помните из многочисленных примеров главы 6, импортирует React-компоненты. Получается, что на графе мы должны изобразить этот объект конфигурации, но делать этого не будем, так как наносим исключительно компоненты. Пример Допустим, у нас есть дизайн (рис. 8.3). Определяем компонент с ролью «Лэйаут». Глядя на дизайн, легко выделить общую верхнюю, нижнюю и левую части. Это будет компонент Layout, состоящий из трех дочерних компонентов — Header, Footer, Menu, как на рис. 8.4. Его следует нанести на граф, но в таблицу вписывать не нужно (в таблицу вносим только бизнес-компоненты, а страницы уже есть) (рис. 8.5).
8.1. Проектирование 3RApp  377 Рис. 8.3. Дизайн страниц «Расписание» и «Кандидаты» Рис. 8.4. Компонент Layout, выделенный из страниц Рис. 8.5. Граф компонентов с выделенными компонентами Layout, Menu, Header, Footer Обратите внимание, что меньшие компоненты, составляющие Layout, именуются простыми словами, но не бизнес-сущностями и не элементами интерфейса. Header, Menu и Footer в прямом смысле не элементы интерфейса, так как они являются
378  Глава 8 Разработка фронтенд-приложения составными частями (блоками, а на самом деле компонентами интерфейса) и могут быть разделены на элементарные составляющие. Определяем компоненты с ролью «Бизнес-компонент». Начинаем со страницы, расположенной слева (см. рис. 8.3). Главными действующими компонентами здесь являются расписание и детали расписания при выборе какого-либо времени. Поэтому выделяются два компонента, как показано на рис. 8.6 и 8.7. Рис. 8.6. Бизнес-компонент Shedule Рис. 8.7. Бизнес-компонент SheduleItemDetails Теперь нужно нанести на граф эти два компонента, как показано на рис. 8.8. Рис. 8.8. Граф компонентов для страницы ShedulePage Затем бизнес-компоненты необходимо внести в таблицу (табл. 8.2). Таблица 8.2. Бизнес-компоненты для страницы SgedulePage MainPage ShedulePage CandidatePage TestPage QuestionPage 1 2 3 4 5 Shedule SheduleItemDetails
8.1. Проектирование 3RApp  379 Аналогичные действия произведем и для страницы справа. Здесь мы видим резюме конкретного кандидата. Судя по ссылке «весь список кандидатов», существует некий большой список (мы видим лишь часть дизайна, а в реальности страница CandidatePage содержала бы его). Поскольку дизайнер предусмотрел всего одну страницу для списка и резюме конкретного кандидата, то последнее открывается как бы над перечнем всех других резюме, заслоняя их. Поэтому нужен компонент для данного резюме — CVCandidate1. Это не модальное окно, блокирующее остальные элементы и фокусирующее внимание пользователя строго на своих данных. В резюме кандидата присутствуют бизнес-компоненты с детальной информацией о кандидате и заинтересованных в нем заказчиках, которых может быть несколько. Поскольку мы смотрим лишь пример, остальные детали нам недоступны. Выявленные компоненты показаны на рис. 8.9 и 8.10. Рис. 8.9. Бизнес-компонент CVCandidatePersonalData Рис. 8.10. Бизнес-компонент CVStakeholderDetails Эти компоненты необходимо внести в таблицу (табл. 8.3). Таблица 8.3. Бизнес-компоненты для страницы CandidatePage MainPage ShedulePage CandidatePage TestPage QuestionPage 1 2 3 4 5 Shedule CVCandidate SheduleItemDetails CVCandidatePersonalData CVStakeholderDetails 1 CVCandidate — название компонента, состоящее из CV и Candidate. Candidate переводится как «кандидат», а сокращение CV означает Curriculum Vitae и переводится как «жизнеописание», «резюме» или «биография».
380  Глава 8 Разработка фронтенд-приложения А также нанести на граф компонентов приложения (рис. 8.11). Рис. 8.11. Граф компонентов для страницы CandidatePage С оставшимися страницами поступаем аналогичным образом. Определяем компоненты с ролью «Простой конечный компонент». Главный отличительный признак простого конечного компонента — то, что он является обычным UI-элементом интерфейса и переиспользуется в компонентах разного уровня (простых конечных компонентах, страницах, лэйауте, бизнес-компонентах). Второй отличительный признак — простота устройства, третий — то, что он не выполняет никаких запросов к данным, а четвертый — никак не связан с бизнес-действиями, но может быть составляющим элементом бизнес-компонента. Таковыми на двух страницах рис. 8.3 являются компоненты, показанные на рис. 8.12. Рис. 8.12. Простые конечные компоненты Как правило, простые конечные компоненты именуются одним словом, означающим элемент интерфейса. Первый компонент — это Header (заголовок «Расписание»). Он пересекается с именем дочернего компонента Layout . Поэтому последний можно переименовать в LayoutHeader, а простой конечный компонент оставить Header.
8.1. Проектирование 3RApp  381 На самом деле на двух страницах сервиса используется одновременно два заголовка. Второй более светлый и с меньшим размером шрифта. Но это тот же компонент Header, который должен быть настраиваемым под такие возможности. Следующий компонент — Tabs (элементы переключения «день», «неделя», «месяц» и «квартал»). Далее — Button (кнопка Позвонить). Наконец, последний — Link (ссылка Весь список кандидатов). Здесь сразу возникает вопрос об имени, ведь в библиотеке React Router уже есть компонент с таким именем. Лучше, чтобы имена не пересекались, иначе потом вы рискуете запутаться. Поэтому данный компонент нужно именовать двумя словами, например AnchorTag. Простые конечные компоненты можно нанести на граф, но вписывать их в таблицу не надо (рис. 8.13). Рис. 8.13. Граф приложения с простыми конечными компонентами Аналогичное действие по выделению простых конечных компонентов проделываем и для других страниц. Определяем компонент с ролью «Провайдер». Компонент с ролью «Провайдер» не является UI-компонентом, определяется в React и используется для работы с контекстом (см. раздел 5.11), другими словами — для передачи данных своему поддереву (или подграфу). На этапе проектирования нет необходимости определять все провайдеры в структуре. Вам нужно добавить лишь тот, что предоставляет используемая библиотека. Мы используем две библиотеки, которые определяют компоненты с ролью «Провайдер», их нужно включать в дерево компонентов. Есть и другие библиотеки,
382  Глава 8 Разработка фронтенд-приложения также используют провайдеры, но они не влияют на структуру, как провайдер из библиотек React Router и Redux Toolkit. Мы еще не определяли глобальное состояние, но сделаем это на следующем шаге. А сейчас нам нужно указать на графе компонентов приложения компонент «Провайдер» из библиотеки React Router (рис. 8.14). Рис. 8.14. Граф приложения с компонентом RouterProvider Составляя граф, вы, скорее всего, будете делать уточнения, а не выполните все сразу начисто. Например, упустите из виду компоненты, а потом обнаружите пробел. Это обычный рабочий момент. Переходим к следующему шагу. Шаг 4. Задать глобальное состояние приложения или его части, дополнить дерево компонентов компонентом с ролью «Провайдер» и, если требуется, дополнительными компонентами, зависящими от состояния Что понадобится: y граф; y дизайн; y аналитика или требования заказчиков; y таблица с компонентами страниц и бизнес-компонентов.
8.1. Проектирование 3RApp  383 На выходе: y таблица глобального состояния приложения с детальным указанием свойств; y граф компонентов приложения, дополненный минимум одним компонентом с ролью «Провайдер», если у приложения будет глобальное состояние, и/или компонентами, зависящими от состояния. Что делать в шаге Определить, что будет храниться в глобальном состоянии приложения. Задать имена свойств стора ReduxToolkit. Дополнить структуру приложения минимум одним компонентом с ролью «Провайдер», а также теми компонентами, которые могут понадобиться для реализации функциональности, зависящей от определенного состояния. Процедура Глобальное состояние может быть нескольких видов: 1) доступное для всех разделов; 2) доступное для одного конкретного раздела. Нужно посмотреть на дизайн и прочитать аналитику. Глядя на дизайн, определить, какие данные должны быть на каждой странице. Такими данными может быть аутентифицирован пользователь, корзина интернет-магазина. Затем придумать наименование свойства стора ReduxToolkit и просто его выписать. Детально продумывать структуру не обязательно. Затем нужно пройти каждую страницу дизайна и посмотреть, что она должна сохранять, чтобы при повторном возвращении пользователя на нее восстанавливалось какое-либо состояние. Это может быть описано и в аналитике. В роли данных могут выступать состояния фильтров таблиц и табов, порядок сортировки списка, выбранный элемент списка и его детали. Отмечая компоненты, восстанавливающие состояние, надо придумать ему название, которое будет в сторе, и выписать его. Пример На дизайне рис. 8.3 видно, что пользователь аутентифицировался. Значит, это можно хранить в состоянии. Придумываем имя свойству (табл. 8.4). Чаще всего выбирают auth булева типа. Это простое свойство. Далее смотрим на дизайн. Больше для всех страниц ничего не нужно сохранять. Таблица 8.4. Свойства состояния стора ReduxToolkit Свойство Что сохраняется isAuthenticated Признак аутентифицированности Рассмотрим компонент ShedulePage. Предположим, аналитик или заказчик попросили сохранять состояние табов, то есть выбранный период (день, неделя и т. д.),
384  Глава 8 Разработка фронтенд-приложения а также выбранный интервал. Поскольку это конкретная страница, имя свойства может быть таким, как показано в табл. 8.5. Таблица 8.5. Свойства состояния стора Redux Toolkit Свойство Что сохраняется isAuthenticated Признак аутентифицированности shedulePage: { tab } Состояние табов shedulePage: { selectedID } Состояние выбранного интервала Аналогичным образом нужно пройти по всем требованиям от заказчика и оставшимся страницам и заполнить таблицу. Теперь посмотрим, накладывает ли ограничение на поддеревья (подграфы) приложения какое-либо состояние. На самом деле накладывает, и это состояние isAuthenticated. Вы не станете показывать все приложение, которое мы создавали до сих пор, неаутентифицированному пользователю. Поэтому нужно разместить в графе компонентов компонент, отвечающий за аутентификацию, — назовем его Authentication (рис. 8.15). Рис. 8.15. Граф приложения с компонентом Authentication По своей роли компонент Authentication — лэйаут. Он немного отличается от просто вывода и содержит условные выражения, которые вроде бы говорят, что это бизнес-компонент. Однако бизнес-компонент всегда является дочерним компонентом страницы и не является страницей. Значит, это лэйаут. Лэйаут не обязан быть «тупым» и сделанным раз и навсегда. Он у нас условный. Добавим на граф компонент StoreProvider от библиотеки Redux Toolkit и получим полный граф приложения (рис. 8.16).
8.1. Проектирование 3RApp  385 Рис. 8.16. Полный граф приложения с компонентом Provider Переходим к следующему шагу. Шаг 5. Определить, с какими внешними системами какие компоненты взаимодействуют Что понадобится: y граф компонентов приложения, включающий все выявленные компоненты; y дизайн. На выходе: схема или таблица (как удобнее) с указанными источниками данных (Rest-сервисы) и компонентами, которые они потребляют. Что делать в шаге Определить, с какими Rest-сервисами должны взаимодействовать конкретные компоненты, чтобы у них были нужные для пользователя данные, и указать их на схеме или в таблице. Обычно эндпоинты подготавливает аналитик. Но даже если их нет (допустим, еще не успели сделать или вообще не запланировали), вы сможете определить, какие данные вам нужны, и вместо конкретного эндпоинта просто указать, чего ожидаете, вплоть до составления структуры данных. Последнее — пример того, когда фронтенд-разработчик помогает создать API, вопреки расхожему мнению, что это делает только аналитик или бэкенд-разработчик.
386  Глава 8 Разработка фронтенд-приложения Пример Начнем с состояния аутентифицированности пользователя. Обратите внимание, что форма логина и пароля расположена в компоненте MainPage и запросы происходят из него. Впоследствии, при ответе на запрос, меняется состояние стора, компонент Authentication управляет подграфом и, если пользователь аутентифицировался, рендерит дочерний компонент справа (правый подграф). Поэтому занесем в табл. 8.6 эндпоинт на который из MainPage делается запрос для аутентификации. Таблица 8.6. Эндпоинты и компоненты, в которых необходимы запросы к ним Компонент Эндпоинт MainPage POST /api/v1/login Все Rest-сервисы проектируются от общего к частному. То есть чем больше сегментов в пути, тем более детальную информацию предоставляет ресурс. Чаще всего получают список, а затем элемент списка. Другие распространенные случаи — иерархия и просто список. Посмотрим на рис. 8.3. В компоненте страницы ShedulePage есть компонент Shedule , и для него необходимо получить список временных интервалов на период, указанный в табах. Одни временные интервалы будут свободны, другие заняты. При выборе одного такого элемента осуществляется GET-запрос для получения детальной информации по данному интервалу (если он занят) или POST-запрос (если заполняется свободный интервал). Для нашего сервиса проведения собеседований это могло бы выглядеть так (табл. 8.7). Таблица 8.7. Эндпоинты и компоненты, в которых необходимы запросы к ним Компонент Эндпоинт ShedulePage GET /api/v1/shedule/{period} period = day | week | month | quarter SheduleItemDetails GET /api/v1/shedule/{ID} SheduleItemDetails POST /api/v1/shedule/create CandidatePage GET /api/v1/candidates CVCandidate GET /api/v1/candidates/{ID} Или другой пример. Допустим, вы используете библиотеку RTKQuery, где эндпоинты определяете в одном месте, а именно — в слайсах. Данные потребляют компоненты, поэтому если вы заполняете таблицу, то выглядеть она будет точно так же: напротив эндпоинта — компонент. Если у вас есть желание, то можете детальнее заполнять таблицу. Например, дополнительно указать параметры или полезную нагрузку запроса. Но, как правило, достаточно смаппить1 эндпоинты на компоненты и указать метод. 1 Смаппить (от англ. map) — одно из значений «отобразить». Это слово используют разработчики, имея в виду отображение эндпоинта на компонент.
8.1. Проектирование 3RApp  387 Шаг 6. Определить, по какому пути в URL какой компонент будет рендериться Что понадобится: y дизайн; y таблица компонентов; y граф компонентов приложения; y аналитика. На выходе: схема или таблица (как удобнее) с путями в URL, связанные с компонентами, которые будут рендериться в случае совпадения. Что делать в шаге Определить, для каких компонентов нужны статические, а для каких — динамические сегменты в URL. Занести пути и компоненты в таблицу. Для лучшего восприятия нужно понимать два свойства объекта конфигурации. Первое свойство: один компонент — один уникальный путь в URL. Второе свойство связано с присутствием нескольких сегментов в пути. Например, у вас есть путь из нескольких сегментов /path/abc. Допустим, необходимо, чтобы был путь /path/abc/def. Здесь возможны два варианта. Первый — если по пути /path/ abc/def должен отображаться компонент, не являющийся дочерним компонентом, который рендерится по пути /path/abc. Второй — если по пути /path/abc/def должен отображаться компонент, который будет являться дочерним для компонента по пути /path/abc. Схематично это показано на рис. 8.17, а дополнительные сведения — в табл. 8.8. Рис. 8.17. Как должен отрендериться компонент с несколькими сегментами в пути Пути необходимо определять по дизайну. Начать лучше со статических, не содержащих динамических сегментов. Выпишите их в таблицу. Затем найдите страницы, которые меняются динамически, и тоже занесите их в таблицу. При этом пользуйтесь вторым свойством. В основном задействуются пути на компоненты с ролью «Страница». Однако возможен и маппинг на «Бизнес-компоненты» — если в дизайне предусмотрено, допустим, или модальное окно, или выезжающая панель с данными, которые являются динамическими, а уникальный (или не обязательно уникальный) идентификатор данных размещен в URL. Когда вы приступите к реализации, объект конфигурации маршрутизатора будете создавать, основываясь на информации, сформированной во время проектирования. Собственно, сейчас вы его и продумываете, а на этапе реализации достаточно просто выразить эту таблицу на языке программирования. Частично сведения для объекта конфигурации собраны на шаге 3, где был выделен компонент с ролью «Лэйаут». Здесь — информация по страницам и другим компонентам.
388  Глава 8 Разработка фронтенд-приложения Правильно составить соответствие компонента пути поможет подсказка в виде табл. 8.8. Таблица 8.8. Места, где может рендериться компонент Что Родитель и дочерний Два на одном уровне и без дополнительных сегментов Два на одном уровне и второй с дополнительным сегментом Компоненты Родительский (Outlet) Одноуровневый Одноуровневый Дочерний (children) Одноуровневый Одноуровневый Пути в URL /parent /path1 /path1 /parent/child /path2 /path1/path2 { { { Пояснение По пути /parent рендерится только Parent, или, если задан компонент с index, то и он на месте Outlet. По пути /parent/child рендерится Child внутри Parent на месте Outlet По пути /path1 рендерится только PathOne. По пути /path2 рендерится только PathTwo По пути /path1 рендерится только PathOne. По пути /path1/path2 рендерится только PathTwo Место рендеринга PathOne на всю страницу или внутри лэйаута. PathTwo на всю страницу или внутри лэйаута PathOne на всю страницу или внутри лэйаута. PathTwo на всю страницу или внутри лэйаута routes.js } path: '/parent', element: <Parent />, children: [{ path: 'child', element: <Child /> }] Родитель на всю страницу или внутри лэйаута. Дочерний только внутри родителя на месте Outlet path: '/path1', element: <PathOne />, }, { path: '/path2', element: <PathTwo />, } Пример Начнем со статических страниц (табл. 8.9). Таблица 8.9. Статические пути в URL и рендерируемые компоненты Компонент Путь в URL MainPage / ShedulePage /shedule CandidatePage /candidate TestPage /test QuestionPage /question path: '/path1', element: <PathOne />, }, { path: '/path1/path2', element: <PathTwo />, }
8.1. Проектирование 3RApp  389 Компонент Путь в URL StakeholderPage /stakeholder InterviewPage /interview DashboardPage /dashboard ProfilePage /profile Теперь смотрим на дизайн, граф или аналитику и определяем, будет ли реализация включать в себя динамические компоненты и как их рендерить. Если не рендерить динамические компоненты с помощью динамических путей, это придется делать с помощью таких способов, как условный рендеринг, назначение стилей для скрытия/отображения элементов в DOM (табл. 8.10). Таблица 8.10. Динамические пути в URL и рендерируемые компоненты Компонент Путь в URL SheduleItemDetails /shedule/:sheduleId CVCandidate /candidate/:candidateCVId Компонент SheduleItemDetails будет рендериться как дочерний компонент, если существует расписание с таким ID. В противном случае рендерится компонент по умолчанию, а именно форма создания расписания. Компонент CVCandidate будет рендериться так же, как и дочерний компонент, при этом выглядеть как модальное окно. Достигнуть этого можно стилями CSS. Итак, процедура проектирования окончена, можно приступать к реализации. 8.1.3. Структура приложения Из раздела 5.19 вы знаете, что, помимо компонентов, существуют другие типы элементов, которые принимают участие в приложении. Это обычные функции, хуки, обработчики событий, функции высшего порядка и компоненты высшего порядка (High Order Component). Кроме того, в работе приложения принимают участие структуры данных и простые переменные. Однако структура приложения строится исключительно на компонентах. Это хорошо видно на рис. 8.16. Вы скажете: «Ну, это очевидно. Ведь мы только что сами его создали. Что в этом такого? Зачем вообще обращать на это внимание?» Структура выстраивается исключительно из компонентов, так как мы строим UI страниц, состоящих из отдельных UI-элементов (JSX), возвращаемых компонентами. И компоненты, чтобы сформировать страницу, должны выстроиться в иерархию. А страница включает в себя множество компонентов. Поэтому именно компонент играет в нашем приложении ключевую роль. Если бы у нас были другие цели — не UI, а, например, вычисления с иерархией, то вместо компонентов у нас были бы другие элементы, например обычные функции.
390  Глава 8 Разработка фронтенд-приложения Имея опыт разработки на основе React и глядя на свой код, вы увидите, что ни вызовы функций, ни вызовы хуков, ни вызовы обработчиков событий не выстроены в иерархию. Максимум — несколько вызовов функций. Тогда как дерево компонентов может быть в высоту шесть, семь и более уровней, а в ширину — достаточно разветвленным. По этой причине проектируется структура компонентов приложения. Другие составляющие второстепенны, их в структуру при проектировании не включают. Однако, как и компонентам, им уделяется внимание на этапе реализации. Пройти мимо них вы точно не сможете. 8.1.4. Качество исполнения схем Качество исполнения неважно, кроме случаев, когда это предусмотрено в вашей организации. Рисовать можно как угодно: на бумаге или на доске. Либо делать фотографии. Еще можно использовать разные мелкие предметы, которые будут символизировать вершины и ребра. Правда, при таком подходе нужно подписывать вершины, используя бумажные или иные ярлыки, чтобы были понятны отношения компонентов и сервисов с фронтенд-приложением. Наконец, можно использовать графические программы и так облегчить себе работу. 8.1.5. Если изменять последовательность шагов процедуры или игнорировать проектирование вовсе и сразу начинать с написания кода Процедуру проектирования необходимо выполнять в описанной последовательности. Это очевидно, потому что, не поняв цели проекта, вы не сможете создать что-то полезное. Попробуйте поменять местами шаги 3 и 5 — сразу растеряетесь. Все волшебство — именно так можно описать процедуру проектирования, когда она не внедрена, — заключается в том, что вы вносите порядок. Это делается физически: сначала проектируете «на бумаге» — достаете свои мысли из разума и перекладываете на бумажный носитель или в графический редактор; затем приступаете к реализации. Отсутствие хаоса всегда сказывается положительно. Приступив к реализации, вы смотрите на то, что спроектировали. При этом во время разработки не создаете хаос из мыслей, а думаете лишь о том, как лучше реализовать конкретный компонент. В вашем разуме в этот момент не мелькают «подлые» мысли типа: «А может, лучше связать с другой функцией? И надо ли мне хранить здесь данные?» Понимаете?! Вы не отвлекаетесь на сомнения, просто спокойно реализуете намеченное. Проектировать — значит наметить будущую реализацию. Когда вы перенесли свои мысли на бумагу/в графический редактор, при реализации уже гарантированно в них не запутаетесь, наоборот — увидите недочеты и сможете их подправить. Времени на изменения на шаге реализации нужно гораздо больше, чем на этапе проектирования. Ведь в момент реализации требуется удалить часть связанного кода с другим кодом и внести необходимые изменения, чтобы все работало как
8.1. Проектирование 3RApp  391 прежде. Бывает жалко расставаться с тем, что уже реализовано. Вы погружаетесь в раздумья и откладываете изменение на какое-то время. Эта работа, которую часто называют глаголом «выпилить», может вести к багам. А на этапе проектирования еще нет ни строчки кода — вы просто переосмысливаете места хранения данных, источники данных и целевые компоненты, куда эти данные должны быть записаны или переданы. В момент реализации придется сделать тройную работу — написать код, переосмыслить и переписать код. Как правило, это сопровождается ощущением запутанности или измотанности, хотя вы и килограмма веса не подняли. Обычно такое действие называют двойной работой (термин из технологии управления персоналом), но по факту она тройная. Еще существует большая ловушка — игнорирование проектирования. Я бы предостерег вас от этого, и вот почему. Когда в уме сложилось полное понимание, из каких элементов будет состоять приложение и как будут взаимодействовать компоненты с данными, это одно. Чтобы достичь такого состояния, нужно не один раз создать приложение, подобное тому, которое вы собираетесь реализовать. И совершенно другое дело, когда вы не представляете, из чего должно состоять приложение, как будут двигаться и преобразовываться данные. Если начнете не с проектирования, а с команды в терминале npx create-react-app my-new-app и продолжите без проектирования, то с большой вероятностью несколько раз перепишете целые участки кода и в вашем приложении будет трудно разобраться другому человеку. Плюс, скорее всего, проект не минует проблему с именованием компонентов, переменных и функций. Очевидно, что когда вы чего-то не понимаете, то не можете объяснить, как это работает. А раз не можете объяснить, как что-то устроено или работает, — значит, не понимаете устройство или принцип действия. И если в такой ситуации разработчик начинает писать код, вместо того чтобы проектировать, понадеявшись на авось — что решение придет само, то это просто способ справиться с непониманием. Своеобразный выход из трудного положения. Со временем к такому подходу можно привыкнуть и писать приложения, не задумываясь об их устройстве. Мелькают мысли в голове типа: «Обычно все само складывается, в процессе...» Это может войти в привычку и применяться на протяжении долгого времени. А «ответственность» за структуру будет перекладываться исключительно на библиотеки. Плюс данный подход не способствует развеиванию мифов: «Зачем заново придумывать велосипед (колесо)?», «Надо искать готовое решение». Ничего плохого в поиске готовых решений нет, но заменять собственное размышление над решением на механическое действие по его поиску просто потому, что так рекомендуют делать из каждого утюга, навязчиво выдавая это за единственно правильное действие, в корне неверно. Если вы не хотите, чтобы ваши способности свелись к чисто механическим действиям, думайте над решением. Не следуйте навязываемой моде, имейте свое мнение. Должен быть разумный баланс между поиском готовых решений и собственным размышлением. Если вы никогда не пытались проектировать, предлагаю вам изменить подход и действовать по процедуре. В любом случае стоит попробовать. Я изложил свое мнение, но никого не заставляю ему следовать.
392  Глава 8 Разработка фронтенд-приложения 8.1.6. Дополнительный стимул, или Как не делать ненужную работу Напоследок стоит отметить один момент. Занимаясь процедурой проектирования для приложения, выпишите каждую его сторону — это будет храниться в файлах или на бумаге (кому как удобнее). Ваша жизнь не состоит из одного приложения, а ваше внимание то и дело переключается с одной области на другую. Представьте себе такую ситуацию. Допустим, вы не используете процедуру проектирования, но придумали структуру компонентов приложения и вполне ею удовлетворены. Компоненты не единственная составляющая приложения, однако вы решили ничего не записывать, а запомнить. Потом, вероятно, что-то реализовали в коде. Затем уходите на выходные и возвращаетесь к приложению в понедельник. Куда вы посмотрите в первую очередь? Конечно, в код приложения, так как проектирование не делали. Вы смотрите код приложения и начинаете потихоньку вспоминать структуру. Хорошо, если вспомнили — отчетливо, все и сразу. Но далеко не всегда так. Скорее всего, понадобится посмотреть еще раз исходные данные, чтобы вспомнить решение, при этом оно не включает другие стороны проектирования. К тому же структура компонентов иерархическая, и чтобы восстановить иерархию, надо прямо смотреть код компонентов, а также какие компоненты и в какой последовательности осуществляют импорты. Поэтому, прежде чем вернуться к тому, что вы уже сделали, придется предпринять серьезные усилия. По сути, вы выполняете дополнительную работу, хотя могли все иметь перед глазами, и было бы достаточно взглянуть на написанное и изображенное, чтобы приступить к реализации. Конечно, при условии, что процедура проектирования пройдена. Ее смысл еще и в значительной экономии времени на шаге реализации, и в том, что все хранится в структурированном виде, и это записано. А значит, не нужно все придумывать заново каждый раз, возвращаясь к работе над приложением. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 8.1. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 8.1. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 8.1 и ознакомьтесь с моим решением. 8.2. Реализация 3RApp. Настройки В разделе 8.1 мы рассмотрели процедуру проектирования и получили граф компонентов, таблицу компонентов, таблицу со свойствами состояния, а также таблицу маппинга эндпоинтов и компонентов. В данном разделе на их основе мы изучим, какие действия нужно осуществлять для реализации приложения. Несмотря на то что граф компонентов довольно громоздкий, они однотипные и не вызовут трудностей. Здесь вы узнаете все шаги по реализации 3RApp.
8.2. Реализация 3RApp. Настройки  393 8.2.1. Настройки Вам необходимо иметь рабочий API с эндпоинтами, а также установить клиентские библиотеки React, React Router и Redux Toolkit. Рабочий API с эндпоинтами может быть готовым сервисом, например реализованным API, где есть все необходимые эндпоинты (ваши бэкенд-разработчики его уже создали), либо сторонним сервисом, который позволяет создавать эндпоинты и ответы в виде нужных структур данных. Кроме того, вы можете сами реализовать локальный сервер, например используя Node.js и Express, с нужными эндпоинтами и данными. Такой API нужен для тестирования реализации. Следующий шаг — установка библиотек. Например, для установки React используют create-react-app1 — специальную утилиту (бинарный npm-пакет), запускаемую через npx2, который и выполняет установку, и поддерживает параметр --template redux. Он, в свою очередь, позволяет установить одновременно и библиотеку React, и библиотеку Redux Toolkit3. Итак, вводите команду в терминале (листинг 8.1). Листинг 8.1. Установка React и Redux Toolkit одной командой npx create-react-app your-project-name --template redux Название листинга — чистой воды лукавство, потому что, кроме библиотек React и Redux Toolkit (и, разумеется, React-Redux), вы устанавливаете сервер разработки webpack-dev-server, сборщик WebPack в преднастроенном состоянии и библиотеку Jest для тестирования JavaScript. Конечно, инициализируется репозиторий Git, к тому же первый коммит, куда включены все базовые файлы вашего приложения, сделан за вас. Плюс заполненный файл package.json, содержащий в том числе раздел scripts4, команды которого вы можете задействовать сразу после установки, и многое другое. Но кое-что все-таки нужно добавить. Если вы используете собственный сервер с эндпоинтами, который должен работать локально, то надо добавить прокси. Сервер разработки WebPack запускает ваши приложения на localhost с портом начиная с 3000 и более (если вы запустили одно приложение и оно заняло порт 3000, то при запуске еще одного появится уведомление, что на таком порте уже запущено приложение, а также вопрос, запускать ли приложение на порте 3001). Локальный сервер API можно запустить на порте 4000. Если сделать это на порте 4000, а приложение — на порте 3000, то при запросах из приложения к серверу API с явно указанным портом 4000 браузер будет блокировать подобные запросы. На такой случай create-react-app подготовил функциональность, которую можно настроить прямо через package.json. Для этого в файле package.json нужно указать свойство proxy5, как на рис. 8.18. 1 2 3 4 5 https://create-react-app.dev https://www.npmjs.com/package/npx https://www.npmjs.com/package/cra-template-redux Описание команд раздела scripts (рекомендуется прочитать про команду eject) находится здесь: https://create-react-app.dev/docs/available-scripts/ https://create-react-app.dev/docs/proxying-api-requests-in-development/
394  Глава 8 Разработка фронтенд-приложения Рис. 8.18. Свойство proxy в package.json Поскольку у нас 3RApp, в приложение следует добавить библиотеку React Router (листинг 8.2). Листинг 8.2. Установка React Router npm i react-router-dom 8.2.2. Подготовка файлов приложения перед написанием кода В связи с тем что в 3RApp для основных функций приложения мы используем аж три библиотеки, некоторые компоненты и функции в подавляющем большинстве случаев одинаковые для многих приложений. Для них даже не нужно придумывать решение — оно уже существует. Это компонент App.js, функции создания стора и код файла index.js, сформированный при установке библиотек, где производится внедрение React-компонентов в DOM через библиотеку ReactDOM. А вот над бизнес-компонентами придется изрядно попотеть, чтобы реализовать идеи заказчика, потому как это отличительная особенность вашего приложения. Если посмотреть на граф приложения, который мы спроектировали в разделе 8.1 (см. рис. 8.16), то там есть два провайдера: один — для стора ReduxToolkit, другой — для маршрутизации React Router. Когда задействуется провайдер из библиотеки RouterProvider, необходимость в компоненте App.js, (App.ts) отпадает, поэтому надо удалить некоторые файлы из числа установленных по умолчанию через CRA. Среди них App.css, App.js, App.test.js, index.css, logo.svg. В итоге у вас получится список файлов приложения как на рис. 8.19. Как видно на рисунке, в папке src располагаются две папки: app и features. Обе предназначены для библиотеки Redux Toolkit. По-моему, две папки сразу — это перебор. Вполне достаточно одной, я обычно именую ее store. В папке app есть всего один файл — store.js. Его надо перенести в папку features, которую
8.2. Реализация 3RApp. Настройки  395 переименовать в store, а сам файл store.js переименовать в index.js. Разумеется, нужно исправить импорты. Таким образом, файловая система приложения будет выглядеть как на рис. 8.20. Рис. 8.19. Файлы приложения после удаления тех из них, которые не используются Рис. 8.20. Файлы приложения после переноса и переименования Разумеется, нужно исправить импорты в файле src/index.js, и у вас получится код как в листинге 8.3. Листинг 8.3. Код файла src/index.js после удаления неактуальных импортов import import import import import React from 'react'; { createRoot } from 'react-dom/client'; { Provider as StoreProvider } from 'react-redux'; { store } from './store'; reportWebVitals from './reportWebVitals'; const container = document.getElementById('root'); const root = createRoot(container); root.render( <React.StrictMode> <StoreProvider store={store}> {/* здесь будет еще компонент */} </StoreProvider> </React.StrictMode> ); reportWebVitals(); Файл reportWebVitals.js пока оставляем, решение можно принять после знакомства со следующим подразделом. Файл setupTest.js нужно оставить, если вы предполагаете использовать дополнительные функции совпадения утверждений в тестах jest. В самом файле есть ссылка на документацию. В противном случае — удалить.
396  Глава 8 Разработка фронтенд-приложения 8.2.3. Аналитика В файле index.js все еще есть вызов функции reportWebVitals — это видно в листинге 8.3. Данная функция получает показатели таких метрик, как CLS, FID, FCP, LCP, TTFB. Более того, данные этих метрик вы можете отправлять или в свою систему аналитики, или в сторонние, такие как «СберВизор»1, «Яндекс.Метрика»2 или Google Analytics3. Если есть необходимость собирать или анализировать указанные метрики, то вы можете пойти двумя путями. Первый — просматривать показатели в консоли браузера, второй — отправлять показатели метрик в систему аналитики. Библиотека web-vitals4 представляет собой набор функций, производящих измерения разных показателей, связанных с производительностью приложения, в котором эти функции исполняются. В том числе используется WebAPI PerformanceObserver. Рассмотрим метрики подробно, чтобы понимать, зачем они могут понадобиться. CLS5 (Cumulative Layout Shift) — измеряет совокупную оценку всех неожиданных сдвигов макета, которые появляются между моментом, когда страница начинает загружаться, и моментом, когда состояние жизненного цикла страницы6 изменяется на «скрытая». Этот показатель измеряет моменты, когда у пользователя визуально сдвигается экран и он, допустим, хотел нажать на одну кнопку, но за счет сдвинутого местоположения нажал в другое место. В данный показатель входят все подобные сдвиги — от начала загрузки до момента, когда страница перешла в состояние «скрытая» (рис. 8.21). Рис. 8.21. Показатели метрики Cumulative Layout Shift, CLS FID7 (First Input Delay) — измеряет время от момента, когда пользователь впервые начал взаимодействовать со страницей (например, щелкнул на ссылке или нажал кнопку), до момента, когда браузер в действительности был способен начать обрабатывать обработчик события в ответ на это взаимодействие. В следующих релизах данная метрика будет исключена, на замену ей придет другая — метрика INP. 1 2 3 4 5 6 7 https://developers.sber.ru/docs/ru/sber-api/api-cases/SberVisor/obshchie-svedeniya. https://metrika.yandex.ru. https://analytics.google.com/analytics/web/provision/#/provision. Web-vitals — библиотека для сбора метрик, созданная Google LLC, https://www.npmjs.com/ package/web-vitals. https://web.dev/articles/cls. Жизненный цикл страницы https://developer.chrome.com/docs/web-platform/page-lifecycle-api. https://web.dev/articles/fid.
8.2. Реализация 3RApp. Настройки  397 INP1 (Interaction to Next Paint) — оценивает общую отзывчивость страницы на взаимодействия с пользователем путем наблюдения за задержкой всех взаимодействий с кликами (касаниями на мобильных устройствах) и с клавиатурой, которые происходят на протяжении всего времени посещения страницы пользователем. Таким образом, с помощью этой метрики можно измерить скорость отклика (рекомендую ознакомиться со страницей этой метрики — адрес дан в сноске) (рис. 8.22). Рис. 8.22. Показатели метрики Interaction to Next Paint, INP FCP2 (First Contentful Paint) — измеряет время с момента первого перехода пользователя на страницу до момента отображения какой-либо части содержимого страницы на экране. Для этой метрики «контент» относится к тексту, изображениям (включая фоновые изображения), элементам <svg> или небелым элементам <canvas> (рис. 8.23). Рис. 8.23. Показатели метрики First Contentful Paint, FCP LCP3 (Largest Contentful Paint) — сообщает о времени загрузки наибольшего изображения или текстового блока в область видимости относительно момента времени, когда пользователь впервые зашел на страницу (рис. 8.24). Рис. 8.24. Показатели метрики Largest Contentful Paint, LCP TTFB4 (Time to First Bite) — измеряет время между запросом ресурса и моментом, когда первый байт ответа начинает возвращаться. Это помогает определить, когда веб-сервер слишком медленно реагирует на запросы. В случае запросов на навигацию, то есть запросов на HTML-документ, он предшествует всем другим значимым показателям производительности загрузки (рис. 8.25). 1 2 3 4 https://web.dev/articles/inp. https://web.dev/articles/fcp. https://web.dev/articles/lcp. https://web.dev/articles/ttfb.
398  Глава 8 Разработка фронтенд-приложения Рис. 8.25. Показатели метрики Time To First Bite, TTFB Если хотите использовать данные метрики, то вам необходимо реализовать функцию, пример которой дан в листинге 8.4. По мере получения значения каждой метрики библиотека web-vitals вызывает соответствующую функцию. В ней нужно реализовать отправку на сервер аналитики. Пример листинга 8.4 взят с официального сайта CRA, из раздела «Измерение производительности»1. Листинг 8.4. Пример реализации колбэка для reportWebVitals function sendToAnalytics(metric) { const body = JSON.stringify(metric); const url = 'https://example.com/analytics'; } // Use `navigator.sendBeacon()` // if available, falling back to `fetch()` if (navigator.sendBeacon) { navigator.sendBeacon(url, body); } else { fetch(url, { body, method: 'POST', keepalive: true }); } reportWebVitals(sendToAnalytics); При отсутствии необходимости собирать аналитику по вышеуказанным показателям следует удалить файл src/reportWebVitals.js, из файла src/index.js — импорт и вызов функции reportWebVitals, а также удалить зависимость — пакет web-vitals с помощью команды npm2 (листинг 8.5). Листинг 8.5. Удаление зависимости web-vitals npm uninstall web-vitals 8.2.4. Выбор подхода использования CSS CSS — вторая по значимости технология для фронтенд-приложения. Есть три способа ее внедрения в компоненты: y способ по умолчанию; y CSS Modules; y CSS-in-JS. 1 2 CRA-измерение производительности: https://create-react-app.dev/docs/measuring-performance/. Подробности о команде npm uninstall: https://docs.npmjs.com/cli/v10/commands/npmuninstall.
8.2. Реализация 3RApp. Настройки  399 Способ по умолчанию доступен сразу после установки приложения командой npx create-react-app. Допустим, вы только что установили приложение, а значит, у вас есть файлы App.js и App.css (листинг 8.6). Листинг 8.6. Файлы CSS и JS только что установленного приложения // App.css .App { text-align: center; } // App.js import './App.css'; function App() { return ( <div className="App"> {/**другой JSX компонента */} </div> ); } Смотрим автоматически созданные стили и компоненты. Если бы вы применяли этот способ к новым файлам, то потребовалось бы сделать следующее. Сначала — создать CSS-файл. Обычно его называют по имени компонента, для которого он создается. Затем в этом файле составить CSS-правило. Селектор может быть любым допустимым селектором CSS, например классом, идентификатором или именем тега. Можно также использовать наследование. В листинге использован селектор класса. Далее нужно импортировать весь CSS-файл в компонент. Теперь селекторы класса можно назначать элементам, как показано в листинге 8.6. Однако другие виды селекторов, не включающие класс, назначать элементам не нужно, как и при обычном использовании CSS в HTML — достаточно импортировать CSS-файл в компонент. Плюсом этого подхода является простота использования, минусом — необходимость следить за конфликтом имен классов или селекторов. Так, если в приложении есть два класса с одинаковыми именами, то применяться будет только одно правило — для класса. Подход CSS Modules1 идентичен и тоже устанавливается по умолчанию, но, в отличие от предыдущего способа, требует четырех изменений. 1. Файл CSS должен содержать в имени module.css. 2. Имена классов должны быть записаны в компонент без тире. 3. Импортируется не CSS-модуль, а идентификатор по умолчанию, являющийся объектом, который содержит имена классов. 4. Класс нужно применять как свойство объекта, импортированного из CSSмодуля. 1 https://create-react-app.dev/docs/adding-a-css-modules-stylesheet.
400  Глава 8 Разработка фронтенд-приложения Рассмотрим пример. Допустим, вы установили приложение и решили перевести страницу на CSS Modules. Значит, нужно переименовать CSS-файл для компонента App.js из App.css в App.module.css. Затем объявить CSS-правила, импортировать объект классов в компонент и применить нужные классы в свойстве className, как показано в листинге 8.7. Листинг 8.7. Использование CSS Modules в только что установленном приложении /*App.module.css*/ div p nav strong { color: palegoldenrod; } .App { text-align: center; } .AppHeader { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } /*другие классы и стили*/ // App.js import logo from './logo.svg'; import styles from './App.module.css'; function App() { return ( <div className={styles.App}> <p> <nav> <strong>Welcome to React</strong> </nav> </p> <header className={styles.AppHeader}> {/*Другой JSX*/} </div> ); } Как видно из листинга 8.7, все классы объявлены без тире. CSS-файл импортируется в компонент с помощью дефолтного импорта. Теперь классы доступны в качестве свойства объекта styles, что и видно далее, где классы назначаются элементам как свойства объекта. Может возникнуть вопрос: «Зачем дополнительные ухищрения для того, что и так есть?» Дело в том, что CSS Modules решают важную проблему, которой
8.2. Реализация 3RApp. Настройки  401 CSS обладает по умолчанию, — конфликт имен классов. Посмотрите на снимок экрана на рис. 8.26. Рис. 8.26. Имена классов CSS Modules Как видите, к именам классов добавляются наборы символов, которые гарантированно предотвратят совпадение имен классов. Другим важным дополнением является возможность осуществлять композицию стилей. Как ей воспользоваться, сказано в документации. Последний подход, который нужно обсудить, — это CSS-in-JS. Он реализуется с помощью сторонних библиотек, например styled-components. CSS-in-JS позволяет встроить CSS в компонент. Рассмотрим пример использования данного подхода. Допустим, вы установили новое приложение и дополнительно поставили библиотеку styled-components. Затем решили переводить на нее компоненты и начали с элемента header. Его стилевое правило содержит несколько объявлений стилей (листинг 8.8). Листинг 8.8. Код компонентов Header, App при использовании CSS-in-JS // Header.js import styled from'styled-components'; export const Header = styled.header` background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; `;
402  Глава 8 Разработка фронтенд-приложения // App.js import logo from './logo.svg'; import './App.css'; import { Header } from './Header'; function App() { return ( <div className="App"> <Header> <img src={logo} className="App-logo" alt="logo" /> {/*другой JSX */} </Header> </div> ); } Для использования CSS-in-JS в данном случае потребовалось создать еще один компонент Header, который представлен в листинге 8.8 и компоненте App. Он уже используется не как элемент с присвоенным CSS-классом, а как компонент. Кроме того, из файла App.css было удалено стилевое правило для класса App-header, а все объявления стилей помещены в компонент Header как CSS-in-JS. Преимущества использования данной библиотеки на этом не заканчиваются. Она позволяет избежать конфликта имен классов (рис. 8.27). Рис. 8.27. Имя класса в CSS-in-JS Завершая обзор настроек, нужно сказать, что при любом из рассмотренных способов вы можете стилизовать элементы, используя свойство style.
8.3. Реализация 3RApp. Написание кода  403 Какой именно подход выбрать для CSS, каждая команда/разработчик решают сами. Стоит заметить, что эти подходы допустимо сочетать. Кроме того, если вы предпочитаете использовать SASS, то и его можно сочетать с приведенными подходами. 8.3. Реализация 3RApp. Написание кода 8.3.1. Что подразумевается под реализацией При проектировании мы концентрируемся на структуре компонентов и не уделяем внимания таким составляющим приложения, как функции, хуки, обработчики событий, функции высшего порядка, компоненты высшего порядка, а также структуры данных и переменные. Но мы занимаемся ими на этапе реализации. Как было указано в подразделе 8.1.2, структура приложения строится на компонентах. Остальные составляющие ее не создают, а импортируются в компоненты. Реализуя приложение, мы одни компоненты импортируем в другие, те, в свою очередь, — в следующие и т. д. Каждый компонент может импортировать вспомогательные элементы, такие как функции, структуры данных, хуки и т. д. В разделе 3.6 было сказано, что реализация — это непосредственное написание кода элементов системы на языке программирования и создание связей между ними. Поскольку главным элементом структуры является компонент, а сама она строится на их основе, то ведущая задача при реализации — написать код компонентов, связывая их между собой, и написать код других элементов, связывая их с компонентами. В том же разделе сказано, что приложение нужно реализовывать сверху вниз. 8.3.2. Очередность реализации компонентов При реализации 3RApp нужно начинать с корня графа и реализовывать все компоненты каждого уровня до компонентов уровня страницы. Начиная с уровня страниц, необходимо реализовывать все компоненты каждой отдельной страницы. Как правило, начинают с самых важных страниц или с самой первой, стартовой страницы. Чаще всего важными страницами являются те, что на графе имеют большее количество связей. Как только закончена первая страница, приступают к следующей и так до конца. Это касается порядка реализации структуры. А в какой момент нужно реализовывать хуки, функции и тому подобное и связывать их с компонентами? В момент реализации React-компонента, где такой элемент используется. Такие компоненты, как StoreProvider и RouterProvider, поставляются нам из библиотек в готовом виде, поэтому наша единственная задача как разработчиков — реализовать слайсы для стора и маршруты для React Router. Подобные действия могут быть затратными по времени. Поэтому если вы хотите быстро получить результат, то определяйте сами, в каком объеме в какой момент внесете доработки
404  Глава 8 Разработка фронтенд-приложения во второстепенные элементы. При этом структура компонентов должна быть реализована последовательно, согласно вышеприведенному порядку. Рассмотрим два наглядных примера, показанных на рис. 8.28 и 8.29, где одни составляющие созданы по порядку, а другие — в последовательности, противоположной рекомендуемой. Рис. 8.28. Реализация составляющих приложения по установленному порядку Рис. 8.29. Реализация составляющих приложения не по установленному порядку Сравните составляющие в первом и втором случаях. В первом видна структура приложения, во втором есть набор элементов, которые нужно связывать между собой, но для этого еще не все составляющие в наличии. Реализация по указанному порядку важна тем, что, реализовав какой-либо компонент любого уровня, вы, в принципе, можете сразу смотреть результат в браузере. В противном случае этого сделать нельзя и приходится тратить больше времени на реализацию. Думаю, очевидно, в какой последовательности следует реализовывать приложение. Спроектированный граф компонентов приложения послужит вам наглядной картой последовательности действий при реализации компонентов. 8.3.3. Пример очередности реализации компонентов Чтобы без всяких сомнений понимать, что подразумевается под порядком реализации, рассмотрим пример с участием спроектированной ранее структуры компонентов приложения. Взглянем еще раз на рис. 8.16, а затем — на рис. 8.28. Первым React-компонентом является StoreProvider. Следовательно, начинаем с него. Этот компонент предоставляет сама библиотека, и, более того, раз мы создали приложение с помощью шаблона CRA (см. листинг 8.1), то этот компонент уже присутствует в нашем коде (см. листинг 8.3). Без объекта конфигурации стора (свойство store компонента провайдера) данный компонент бесполезен и будет
8.3. Реализация 3RApp. Написание кода  405 вызывать ошибку. Однако создание из шаблона тут вновь сделало свою работу — нам предоставлен слайс для счетчика. Принимая решение, можно прямо сейчас сделать объект конфигурации стора и протестировать или отложить это на более поздний срок, чтобы тестировать с помощью UI. Следующий компонент — RouterProvider, у которого в свойстве router тоже должен быть реализован объект конфигурации. Сам компонент предоставляется библиотекой, а объект конфигурации нужно реализовать самостоятельно. На данном этапе у вас нет компонентов, которые должны быть импортированы в объект конфигурации, поэтому достаточно вместо компонентов указать JSX. По мере реализации компонентов, которые должны присутствовать в объекте конфигурации, вы будете заменять JSX на реальный компонент. Однако наметить объект конфигурации на основе структуры компонентов нужно сейчас. Следующий компонент в дереве — Authentication. Его работа зависит от свойства isAuthenticated, располагающегося в сторе. Если ранее не был реализован слайс с запросами через RTK Query, то сейчас это можно сделать либо поставить значение true, означающее, что пользователь аутентифицирован, или false для противоположного случая. Далее реализовать сам компонент и в объект конфигурации маршрутизатора добавить Authentication как родительский компонент поддерева компонентов. После реализации компонента Authentication нужно реализовывать его дочерние компоненты, которых у него два. Начать можно с любого. Например, первым реализовать Main, затем — Layout. И Main, и Layout надо импортировать в объект конфигурации маршрутизатора и заменить ранее помещенный JSX. Теперь нужно реализовать все дочерние компоненты Layout. Если опираться на рис. 8.28 (сокращенный вариант), то их у него пять, а если на рис. 8.16 — одиннадцать. Начать имеет смысл с UI дочерних компонентов, не являющихся страницами, чтобы был готов полный лэйаут. Итак, надо реализовать компоненты LayoutHeader, Footer и Menu, которые не присутствуют в объекте конфигурации маршрутизатора. Теперь переходим к реализации страниц. Начать можно с любой. Допустим, это ShedulePage. Как правило, для страниц достаточно создать простой компонент. Например, вы реализовали другие необходимые функциональные элементы, входящие в компонент страницы. Что дальше? Дальше можно задать шаблонный код других компонентов страниц и вернуться к реализации компонентов страниц, начиная с самой первой, или не реализовывать другие компоненты страниц, а сразу перейти к реализации дочерних компонентов конкретной страницы. У нас это ShedulePage. У ShedulePage — два дочерних компонента: Shedule и SheduleItemDetails. Реализовав их, переходим на следующий уровень каждого только что реализованного компонента. Для компонента Shedule не указаны дочерние компоненты, а у SheduleItemDetails их целых три: Tabs, Header и Button. Завершив их реализацию, переходим к реализации другого компонента страницы — CVCandidate. И продолжаем вышеописанную процедуру, идя вглубь. Начиная со второй страницы часть компонентов будет переиспользована, так как простые конечные компоненты используются во многих местах.
406  Глава 8 Разработка фронтенд-приложения 8.3.4. Объект конфигурации стора ReduxToolkit Основная работа выполнена, осталось воплотить ее в коде. Нужно взять таблицу состояний, сформированную на этапе проектирования, и на ее основе создать слайсы. Сколько свойств в таблице (строки), столько будет и слайсов. 8.3.5. Объект конфигурации маршрутизатора React Router Объект конфигурации маршрутизатора может быть линейным и иерархическим (объект маршрута, в свойстве children которого определяются другие маршруты). На этапе проектирования вы все продумали. Составили таблицу с компонентами, которые должны рендериться при определенном URL в адресной строке, — как для статических, так и для динамических сегментов. Теперь нужно занести это в объект конфигурации. Чтобы на всякий случай перепроверить себя, держите под рукой граф приложения; таблицу путей в URL, связанных с компонентами, и дизайн. Объект конфигурации реализуется в коде за три действия. Первое действие — задать главный лэйаут. Начинаем с графа компонентов приложения. Определите для него маршрут с путем /, потом задайте массив в свойстве children объекта маршрута лэйаута. Могут быть и дополнительные лэйауты, ниже по графу компонентов приложения. Второе действие — задать статические маршруты. Самый первый компонент, если он включен в объект конфигурации (он может быть и не включен, для примера см. листинги 8.11–8.22, компонент MainPage) и, как лэйаут, отображается по пути /, то его необходимо определить самым первым компонентом в children. У него будет отсутствовать свойство path, зато будет присутствовать свойство и значение index: true. Затем нужно определить другие объекты маршрутов для компонентов с ролью «Страница». Если у страницы должны быть динамические маршруты, то объявляйте массив children и задавайте объекты маршрутов для динамических сегментов. Это третье действие. Оно может выполняться как совместно со вторым, так и отдельно. Все зависит от отношений компонентов, которые должны рендериться на динамические сегменты (см. дополнительную информацию в подразделе 8.1.2, в шаге 6). После создания объекта конфигурации не нужно внедрять компонент Outlet в родительские компоненты, если у вас есть компоненты, которые должны рендериться в родительском. Это будет сделано на этапе реализации компонентов. Вы не пропустите данный шаг. 8.3.6. Составляющие, необходимые для реализации компонента В данном подразделе приводятся составляющие компонента, необходимые для его правильной реализации. Эти предварительные сведения помогут лучше понять и использовать порядок реализации компонента, описываемый в следующем подразделе. При реализации нужно исходить из того, что компонент — главная составляющая структуры, остальные элементы второстепенны и будут созданы вами при необходимости. Второстепенные они не потому, что неважны, а потому, что
8.3. Реализация 3RApp. Написание кода  407 не создают структуру всего приложения. Зато второстепенные элементы создают функциональность, необходимую в компоненте, или определяют данные. Посмотрите внимательно на рис. 8.30, где указаны компоненты и все второстепенные элементы, которые могут войти в состав компонента. Сам компонент может войти в состав другого компонента или быть передан в НОС (High Order Component), чтобы впоследствии использоваться в другом компоненте. На рисунке в теле компонента показаны хуки из React, обработчики событий, локальные константы, структуры данных и вывод. Подразумевается, что они определяются в теле компонента. Обработчики событий в большинстве случаев изменяют состояние при возникновении событий и в теле обработчика событий ссылаются на одну или несколько функций, изменяющих какую-либо переменную состояния, определенных в теле компонента. По этой причине обработчик события не вынести за пределы компонента. То же касается реализации функций обратного вызова некоторых хуков, таких как useEffect и часто — useMemo, useCallback. Рис. 8.30. Компонент и вторичные элементы Такой хук, как useState, инициализирует в теле компонента переменную состояния и функцию изменения состояния переменной. Конечно, то, что выводит компонент в качестве UI, определяется в теле компонента. Другие элементы могут быть определены за его пределами и импортированы в него для использования. Что войдет в состав будущего компонента, зависит от источников данных, которые он должен отобразить в UI; от роли компонента в обмене данными с другими компонентами (описано в подразделе 5.19.5, а сами роли упоминаются в табл. 8.12); от предполагаемого поведения и от того, как реализуется UI компонента. Для уточнения деталей смотрите табл. 8.11. Напомню: поведение — это переход из одного состояния в другое.
408  Глава 8 Разработка фронтенд-приложения Таблица 8.11. Исходные данные, определяющие состав кода React-компонента Роли компонентов Обмен данными между компонентами Источники данных Функциональность компонента Провайдер Родитель — дочерний Пропсы Функция Лэйаут Дочерний — родитель API Хук Приложение Предок — потомок Переменная состояния Обработчик события Страница Потомок — предок Контекст Бизнес-компонент Компоненты из разных поддеревьев Стор Простой конечный компонент Константа Статическая структура данных Локальные хранилища Данные из объектов WebAPI На рис. 8.31 показано, что происходит с компонентом во время работы приложения. Рис. 8.31. Компонент во время работы приложения По-другому можно сказать, что это этапы жизни компонента во время работы приложения, и они определяют поведение, которое нужно реализовать в компоненте, чтобы он выполнял нужную вам функциональность (табл. 8.12). Таблица 8.12. Что происходит с компонентом во время работы приложения Этап Что происходит 1 Монтирование в DOM 2 Инициализация переменных состояния, обращение к источникам данных, передача данных другим компонентам, рендер, вызов колбэка в useEffect 3 События UI, времени или сети; изменение состояния, запуск компонента для перерендера, обращение ко всем источникам данных и/или к состоянию, передача данных другим компонентам, рендер, вызов колбэка в useEffect 4 Размонтирование или закрытие приложения. Если размонтирование, то вызов функций, возвращаемых из колбэка useEffect Третий этап является повторяемым. В данном случае поведение — это события, которые происходят, когда компонент монтирован в DOM (события, для которых зарегистрированы обработчики, подлежащие запуску, если те происходят). Чтобы от
8.3. Реализация 3RApp. Написание кода  409 события был толк, оно должно изменять состояние или обращаться к API (WebAPI), что в итоге приведет к изменению состояния или к побочному эффекту. Обращение к API не обязательно должно осуществляться напрямую. Например, для запроса данных с сервера нужно отправить экшен в стор, чтобы RTK Query сделал запрос. Отправлять экшен, естественно, надо из обработчика события. 8.3.7. Исходные данные в отношении поведения, реализуемого в компоненте Как уже было сказано, поведение — это переход от одного состояния к другому. Во фронтенд-приложении, реализуемом для браузера, есть два принципиально разных состояния: состояние React-приложения и состояние объектов window, включая сам window. Первое состояние — это обобщенное название состояний в компонентах и состояния, хранимого в сторе. Второе — значение свойств объектов в window, а также свойств самого объекта window. Взаимодействие с WebAPI позволяет изменять состояние свойств таких объектов. Изменение состояния React-компонента ведет к перерендеру. Изменение состояния объекта window не ведет к перерендеру React-компонента, в котором был вызван WebAPI, но ведет к так называемому побочному эффекту. По этой причине, если React-приложение должно обновляться в ответ на изменение состояния какого-либо объекта window, вы должны самостоятельно это реализовать в коде. Рассмотрим примеры. Допустим, пользователь щелкает на кнопке, где зарегистрирован обработчик события «Клик». Последний изменяет какую-то одну переменную состояния. После изменения состояния произойдет перерендеринг и пользователю будет показано новое значение измененной переменной, или, другими словами, новое состояние приложения. Это очевидный пример. Другой пример, когда есть изменение состояния window, но нет связи с React. Так, вы можете вызвать window.history.back() — имитировать нажатие кнопки Назад в браузере. Состояние изменилось, но с состоянием React-приложения, которое задаете вы, оно не связано. Здесь меняется состояние объекта window.history. Таким образом мы получаем побочный эффект от работы WebAPI. Еще один распространенный пример изменения состояния, не связанного с React-компонентом, — присвоение в хуке useEffect свойству window.document.title какой-либо строки. Изменяется состояние объекта document, но не переменных состояния в компоненте. Побочный эффект можно рассматривать как поведение, ведущее к изменению состояния объектов window и к осуществлению какого-то действия, но не связанного с React-компонентом и его состоянием. Наконец, допустим, есть связь. При вызове fetch в метод then обычно мы передаем функцию, меняющую переменную состояния. А для записи в localStorage нам необходимо самостоятельно сделать вызов функции, меняющей переменную состояния после того, как вызван метод localStorage.setItem(…), потому что в API localStorage не предусмотрена передача колбэков. На самом деле нужно правильно взглянуть на WebAPI. Все, что выводит компонент, все элементы JSX создаются с помощью WebAPI, и взаимодействие пользователя с интерфейсом возможно именно благодаря ему. Щелчок на кнопке, по сути, является изменением состояния DOM-элемента, то есть объекта, находящегося в window. А вызов обработчика события — это функциональность WebAPI (document).
410  Глава 8 Разработка фронтенд-приложения Вы можете возразить и сказать, что в React можно назначить событие и передать этому свойству обработчик события. Однако это обычный синтаксический сахар1 — такой же, как JSX. Ведь браузер не понимает JSX. На самом деле функционал вызова обработчиков событий в React — просто встроенный в ReactDOM вызов WebAPI anyDOMElement.addEventListener('eventName', eventHandler). Но, так как встраивается обработчик на элементе в JSX, на это не обращаешь внимания. Но это очень удобная возможность. Поскольку нам нужно, чтобы изменилось состояние React-приложения, мы в коде связываем изменение состояния объекта window с состоянием компонента. В функции обратного вызова, именуемого «обработчик», вызываем функцию, изменяющую значение переменной состояния, — это влечет за собой перерендер. Таким образом, первый важный вывод: если нужно, чтобы React-приложение отслеживало изменение состояния объектов в window, его следует связать с переменными состояния компонента. Отсюда же вытекает и второй важный вывод: изначально действие и изменение состояния происходит в объектах в window, и если вы связали с такими объектами изменение состояния React-приложения, то оно тоже изменится. Что означает такая связь? Что или явно было задействовано WebAPI и затем вызвана функция, изменяющая переменную состояния, или задействовано WebAPI, которому передали колбэк, где вызвана функция, изменяющая состояние (листинги 8.9, 8.10). Вы это делаете постоянно, и листинг 8.9 — тому подтверждение. Листинг 8.9. Передача колбэка при использовании WebAPI function App() { const [data, setData] = useState([]); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); fetch('/api/data').then(res => res.json()).then(res => { setData(res); setLoading(false); }).catch(err => { setError(err.message); setLoading(false); }); }, []); if (error) { 1 Синтаксический сахар — это введение в язык программирования нового, более удобного синтаксиса, без добавления новых возможностей. Типичный пример синтаксического сахара для JavaScript — асинхронная функция async/await, которая является сахаром для промисов. В библиотеке React синтаксический сахар — это JSX, свойства обработчиков события и их вызов и другие.
8.3. Реализация 3RApp. Написание кода  411 } } return <p>{error}</p>; return ( <div> <header> `Добро пожаловать ` <Nickname /> </header> <main> {loading ? 'Загрузка...' : data.map(item => <Item key={item.id} title={item.title} />)} </main> </div> ); Листинг 8.10. Явное связывание изменения состояния WebAPI с состоянием React-компонента export const Nickname = ({ newNickname }) => { const [nickname, setNickname] = useState(''); useEffect(() => { if (newNickname) { localStorage.setItem('nickname', newNickname); setNickname(newNickname); } const nickname = localStorage.getItem('nickname'); if (nickname) { setNickname(nickname); } }, [newNickname]); if (nickname) { return nickname; } } return null; Такая связь задается в двух местах — в обработчике события и в хуке useEffect. Кроме того, важно понимать, в какие моменты она может вызываться. Таких моментов три: y вызов обработчика события на события в DOM или WebAPI; y истечение определенного промежутка времени; y вызов колбэка после рендера компонента в хуке useEffect. Почему мест два, а моментов вызова три? Для работы с временем мы задействуем setTimeout и setInterval, а они вызываются в хуке useEffect. При использовании сторонних библиотек или кастомных хуков вы можете задействовать и эти источники. Да, «под капотом» они используют useEffect для обращения к WebAPI,
412  Глава 8 Разработка фронтенд-приложения но вы работаете с ними как с готовыми элементами в теле компонента. Исходя из этого, в коде компонента определить связь изменения состояния WebAPI и Reactприложения можно в четырех местах: y y y y обработчик события; setTimeout (setInterval); useEffect; кастомный хук или хук из библиотеки. Делая вывод из этого подраздела, замечу, что вы можете задействовать WebAPI либо только для получения побочного эффекта, либо для получения побочного эффекта и изменения состояния React-приложения. Такой взгляд на состояния и побочный эффект позволит более правильно управлять ими в коде (табл. 8.13). Таблица 8.13. Исходные данные, определяющие переход из одного состояния в другое Возможное изменение состояния Место расположения кода, изменяющего состояние Побочный эффект Обработчик события Побочный эффект и изменение состояния React-приложения setTimeout, setInterval useEffect Кастомный хук или хук из библиотеки 8.3.8. Решение, которое нужно придумать для реализации компонента Аналитика всегда описывает требуемый конечный результат и ресурсы — API и структуры данных, а относительно взаимодействия пользователя с интерфейсом всегда касается конкретных элементов (кнопки, поля ввода и др.). При этом аналитика никогда не говорит, как вы должны написать код компонента. Вам необходимо придумать, каковы источники данных, какие из них будут сохраняться в состоянии, а также поведение, чтобы изменять состояние и осуще­ ствлять обмен данными одних компонентов с другими. Вы придумываете, с помощью каких переменных, структур данных, хуков, функций, WebAPI и с учетом порядка инициализации, выполнения это будет выражено в коде компонента. Обобщая, можно сказать, что придумывание решения — это разработка последовательности обработки данных для их вывода в UI с учетом имеющихся ограничений. Ограничением выступают области видимости, контексты, последовательности рендеров и вызовов, условные инструкции, данные и их структуры, состояния, возможности хуков, возможности WebAPI, время, а также сочетание ограничений. Это и есть задача фронтенд-разработчика при реализации. 8.3.9. Реализация компонента В данном подразделе я предлагаю настоящий конструктор для создания Reactкомпонента. Его можно использовать, в том числе чтобы автоматизировать создание
8.3. Реализация 3RApp. Написание кода  413 фронтенд-приложений с помощью кодогенератора, работающего на входящих параметрах. Вам как разработчику конструктор поможет в создании разных компонентов. Он используется в качестве проверочного списка действий, которые нужно выполнить, чтобы получить код компонента. Проверочный список состоит из этапов, а каждый этап — из шагов. 1. Использование проверочного списка. Когда вы создаете компонент, независимо от роли, нужно идти по этапам. Прочитав название этапа, вы определяете необходимость действия, описанного в его наименовании, в вашем компоненте. Если необходимости нет, то пропускаете все шаги данного этапа и переходите к следующему. Если она есть, переходите к шагам. Каждый шаг сформулирован в виде вопроса. Читаете вопрос, и если он неактуален для вашего компонента, то вы переходите к следующему. Если актуален, то читаете, что надо сделать, и выполняете. Когда вы проходите по шагам этапа, требуемая функциональность может быть достигнута ранее, нежели будут пройдены все указанные шаги. В этом случае оставляйте оставшиеся шаги и переходите к следующему этапу. Такой подход позволит быстро пройти длинный список и быстро получить код компонента. 2. Случаи использования проверочного списка. Когда возникают трудности при создании решения или компонент чересчур большой и вы не знаете, с чего начать, список придется кстати. Его также можно использовать частично, а именно конкретный этап. Ведь проверочный список записан, он есть, и в него можно, так сказать, подсматривать. 3. Общее действие для каждого шага. Существует так называемый JSDoc @TODO. Когда вы не можете в данный момент реализовать полностью то, что требуется, обязательно оставляйте @TODO и указывайте, что и когда нужно сделать, чтобы он был удален. Обычно разные IDE напоминают вам при коммите, что есть @TODO. 4. Забегание вперед или необходимость вернуться назад. Предложенная последовательность — это предполагаемая последовательность шагов. Будут возникать ситуации, когда вам придется выполнить что-то указанное на более позднем этапе или шаге. Или ситуации, когда вы пропустили шаг, но потом осознали решение по-новому и вам нужно вернуться, чтобы сделать или повторить какойто шаг. Поэтому лучше ознакомиться со всем списком вопросов и действий. Данный проверочный список исчерпывающий. Вы привыкли к своему порядку реализации компонента, так же как я, автор этого списка. Поэтому просьба, особенно на первое время: когда вы дошли до какого-то шага, где предписано выполнить определенные шаги, не выполняйте больше, чем сказано. Если то, что вы хотите выполнить сразу после этого шага, еще не затрагивалось в списке, оно наверняка появится на более позднем шаге. Просто немного изменяется последовательность. Чтобы не загромождать проверочный список кодом, после каждого шага приводится указание на листинг, в котором он реализован и уже рассмотрен в книге, чтобы дать более широкое представление о том, какой результат должен быть получен от шага. Кроме того, дается ссылка на раздел или подраздел, где описывается процедура создания какого-либо элемента (функции, хука и т. п.).
414  Глава 8 Разработка фронтенд-приложения Этап 1. Определяем данные, необходимые компоненту для их вывода в UI1 Проходя шаги данного этапа, держите под рукой дизайн и таблицу маппинга компонентов на эндпоинты. С их помощью вы будете делать вывод о полноте задействованных данных. После выполнения каждого шага этапа нужно совершить следующие действия: y свериться с дизайном и определить, все ли данные определены для данного компонента; y если в таблице маппинга компонентов на эндпоинты присутствует данный компонент, то уточнить, все ли данные определены для данного компонента. Если все, то сразу переходите к следующему этапу. В противном случае продолжайте двигаться по шагам. Компонент, который вы реализуете, всегда нужно рассматривать как дочерний. Ведь если посмотреть на любой компонент в структуре приложения, то отсутствие родительского есть только в одном случае — когда этот компонент является корнем приложения. 1. Какие данные передает родитель вашему компоненту? Что сделать? В параметрах компонента указать идентификаторы пропсов для данных. 2. Присутствует ли данный компонент в таблице маппинга компонентов на эндпоинты? Что сделать? Использовать один из трех вариантов: 1) React Router; 2) RTK Query; 3) fetch и useEffect. Если вы хотите использовать возможности ReactRouter, тогда ваш реализу­ емый компонент должен: 1) или быть указан в объекте конфигурации маршрутизатора ReactRouter; 2) или быть дочерним компонентом компонента, указанного в объекте ReactRouter; 3) или быть потомком компонента, указанного в объекте ReactRouter. Вам нужно реализовывать для пункта 1, так как для пунктов 2 и 3 вы уже реализовали данный шаг. В объекте конфигурации нужно в маршруте для реализуемого компонента в свойстве loader объявить и реализовать функцию, от­куда сделать запрос к данным на эндпоинт, который есть в таблице маппинга компонентов на эндпоинты. В компонент, нужно импортировать хук useLoaderData, разместить вверху компонента и вызвать. Возвращенное значение присвоить переменной. Либо можно импортировать из слайса RTK Query хук для асинхронного получения данных (разумеется, слайс должен быть реализован). Либо определить в хуке useEffect получение данных с помощью fetch или WebSocket. Если используется хук из RTK Query, то в теле компонента нужно вызвать такой хук, чтобы получить данные. 1 UI (от англ. user interface — «интерфейс пользователя») — это элементы на веб-странице и их внешний вид, с которыми взаимодействует пользователь.
8.3. Реализация 3RApp. Написание кода  415 3. 4. 5. 6. Если данные получаются через fetch или WebSocket, то необходимо допол­ нительно определить три переменных состояния и функции их изменения с помощью хука useState или useReducer. Первая переменная — для данных, вторая — в случае возврата ошибки из запроса и третья — для указания со­ стояния загрузки. В случае хука RTK Query эти переменные будут возвра­ щены его вызовом хука. Теперь нужно перед вызовом функции fetch сделать вызов функции, изменяющей переменную состояния загрузки данных, для указания ее начала. После получения данных функции, изменяющей пере­ менную их состояния, нужно передать сами данные, а в случае возврата ошибки — вызвать функцию для изменения переменной состояния ошибки. После чего вновь вызвать функцию для изменения состояния загрузки. Если вы задействовали fetch, возможно, у вас реализован кастомный хук. Тогда импортируйте кастомный хук и совершайте действия для его внедрения. Листинги: loader (6.10), fetch (8.9–8.10), RTK Query (8.23–8.26). Описания: useEffect (подраздел 5.7.2), RTK Query (раздел 4.9), useState (под­ раздел 5.6.4), useReducer (раздел 5.14), кастомные хуки (подраздел 5.7.14). Должна (-ы) ли быть определена (-ы) переменная (-ые) состояния? Что сделать? Если переменных состояния немного, то задействовать хук useState. Если же их большое количество, то удобнее использовать useReducer. Определить переменные состояния и функции, их изменяющие. Листинги: useState (8.9–8.10), useReducer (5.64). Описания: useState (подраздел 5.6.4), useReducer (раздел 5.14). Должна (-ы) ли быть определена (-ы) переменная (-ые) состояния, при изменении которой (-ых) не происходит перерендер? Что сделать? Объявить идентификаторы, задействовав хук useRef и передав в вызов начальное значение. Идентификатор — это объект, содержащий свойство current, изменение которого не ведет к перерендеру, но мемоизирует значение между рендерами. Листинги: useRef (5.66). Описания: useRef (раздел 5.15). Присутствует ли в текущем поддереве предок (речь о способе обмена данными «предок — потомок», означающем, что компонент с такой ролью — провайдер), по отношению к которому реализуемый компонент является потомком, то есть должен ли он получить данные из контекста? Что сделать? Импортировать хук useContext и объект контекста. В теле компонента вызвать хук и передать ему аргументом объект контекста. Присвоить возвращаемое значение константе. Листинги: «предок — потомок» (5.89–5.93), useContext (5.47). Описания: «предок — потомок» (подраздел 5.19.5), useContext (раздел 5.11). Должен ли компонент получить данные из стора? Что сделать? Импортировать хук useSelector из библиотеки React Redux и селектор из слайса. Селектор передать в хук и определить константу для значения, возвращаемого из стора. Листинги: селектор (4.13), useSelector (5.89).
416  Глава 8 Разработка фронтенд-приложения 7. 8. 9. 10. Описания: селектор (подразделы 4.4.5, 4.8.7), useSelector (подраздел 5.18.3). Должен ли данный компонент определить константу (-ы) или импортировать ее (их)? Что сделать? Определить константу (-ы) или импортировать глобальную (-ые) константу (-ы) приложения. Описания: подраздел 5.5.3. Должен ли данный компонент импортировать статическую структуру данных? Что сделать? Импортировать статическую структуру данных или объект конфигурации. Листинги: использование статической структуры данных (6.1, 8.31). Должны ли данные быть извлечены из локальных хранилищ? Что сделать? Если вы используете библиотеку, реализующую работу с одним из локальных хранилищ, импортируйте хук из нее. В теле компонента сделайте вызов хука и присвойте возвращаемое значение константе. Если вы самостоятельно реализуете работу с хранилищем, то импортируйте хуки useEffect и useState. В теле компонента объявите переменную состояния и функцию, ее изменяющую, а также реализуйте вызов хука useEffect и колбэк. В теле колбэка обратитесь к нужному локальному хранилищу, а полученные данные передайте в вызов функции, изменяющей переменную состояния. Если у вас есть готовый хук для работы с локальным хранилищем, использованный гдето ранее, используйте его. В любом случае реализованную самостоятельно функциональность впоследствии вы сможете перенести в кастомный хук. Листинги: localStorage (8.9, 8.10). Описания: useState (подраздел 5.6.4), useEffect (подраздел 5.7.2), кастомные хуки (подраздел 5.7.14). Данные должны быть извлечены из объектов window с помощью WebAPI или с помощью готовых специализированных хуков библиотек? Что сделать? Действия в данной ситуации аналогичны действиям, описанным на предыдущем шаге, с той разницей, что объектом WebAPI выступают не локальные хранилища, а другие объекты, например Location, History, Geolocation и др. Такая библиотека, как React Router, содержит большое количество хуков, извлекающих данные из таких объектов. Листинги: компонент Result (6.19), компонент SheduleItemDetails (8.11–8.22). Описания: React Router (раздел 6.8, подраздел 6.8.2). Этап 2. Предварительная обработка данных Если данные необходимо предобработать, например обогатить сведениями или преобразовать структуру, то понадобятся функции. При этом генерация данных также может быть отнесена к предобработке, под которой понимается предварительная обработка перед их выводом в DOM. Проходя шаг данного этапа, держите под рукой дизайн. С его помощью вы будете делать вывод о правильности примененных преобразований к данным.
8.3. Реализация 3RApp. Написание кода  417 11. Должны ли быть данные предобработаны? Что сделать? Взять готовую или при необходимости реализовать и разместить за пределами компонента функцию, отвечающую за преобразование данных. Затем импортировать ее и разместить вызов в теле компонента. Листинги: useMemo (5.73). Описания: подраздел 5.19.6. Этап 3. Определяем UI и то, как данные будут выводиться в UI Смысл данного этапа в объединении JSX, представляющего HTML-теги, и данных, которые определены на первом этапе. Вам также нужно реализовать внешний вид компонента в соответствии с дизайном. Источников данных может быть несколько. Некоторые из них пребывают более чем в одном состоянии. Например, при исполь­ зовании запросов к API вы можете получить ошибку в качестве ответа. На данном этапе вы или работаете с компонентами из дизайн-систем, или при­ бегаете к самостоятельной реализации с помощью CSS. Возможно и то и другое. Ваш компонент указан в родительском (или в объекте конфигурации маршрутизатора ReactRouter) в качестве комментария с @TODO. Им­портируйте текущий компонент в родительский (или в объекте конфигурации маршрутизатора), чтобы при изменении оформления сразу видеть результаты в браузере. По этой причине дизайн нужно всегда держать под рукой для отслеживания шагов данного этапа. С его помощью вы будете делать вывод о правильности расположения и внешнего вида данных, а также JSX, выводи­мого компонентом. После выполнения каждого шага данного этапа нужно сверяться с дизайном и уточнять, соответствует ли внешний вид компонента требованиям. Здесь можно принять решение о разделении компонента на большее количество частей, нежели предполагалось изначально. В любом случае вам придется создать до­ полнительный компонент, если полученные данные являются списком или деревом. Если вы определили, что компонент выглядит так, как должен выглядеть, и вы­ водит корректные данные, то сразу переходите к следующему этапу. В противном случае продолжайте двигаться по шагам. Общее действие для каждого шага. Если предполагается использовать CSS, то: 1) проверить импортирован ли уже такой файл и если да, то реализовать требуемые правила, 2) если нет, необходимо создать CSS-файл, задать требуемые правила и импортировать его в компонент. 12. Предполагается ли вывод исключительно JSX без композиции с дочерними компонентами? Что сделать? Выполнить общее действие. При необходимости — импортиро­ вать в компонент компоненты из дизайн-системы. Составить JSX при помощи импортированного компонента (ов) или самостоятельно реализовать структуру на основе тегов (React-элементов). Если нужно стилизовать элементы CSS-классами, укажите имена клас­са (-ов) в свойстве className нужного элемента. Если вы предпочитаете инлайн-стили, то укажите их в свойстве style. Если компонент предполагает вывод данных, то разместите переменные, содержащие данные, в нужных местах JSX, обрамляя их фигурными скобками {variable} или передайте пропсами в компоненты дизайн-системы.
418  Глава 8 Разработка фронтенд-приложения 13. Предполагает ли компонент композицию с другими дочерними компонентами (помимо компонентов дизайн-системы или UI-библиотеки)? Что сделать? Выполнить общее действие. Импортировать другие реализо­ ванные вами компоненты и при необходимости компоненты из дизайн-системы. Составить JSX при помощи импортированного (ых) компонента (ов) и при необходимости дополнительно реализовать вывод при помощи тегов (React-элементов). Если дочерние компоненты еще не реализованы, то сделать заглушки блоч­ными или строчными элементами и разместить @TODO с указанием имени компонента, после реализации которого заглушку нужно заменить на JSX компонента. В компоненты (в том числе и не реализованные, указанные в виде комментария) передать данные в пропсы. Затем при необходи­ мости в свойстве className нужного элемента задать класс стиля. Если вы предпочитаете инлайн-стили, то укажите их в свойстве style. Если компонент предполагает вывод данных не через дочерние компоненты, то разместите переменные, содержащие данные, в нужных местах JSX, обрамляя их фигурными скобками {variable} или передайте пропсами в компоненты дизайн-системы. Описания: композиция (раздел 5.4). 14. Является ли данный компонент родительским сегментом пути для дочернего сегмента в настройках объекта конфигурации маршрутизатора React Router? Что сделать? Импортировать в компонент из библиотеки React Router компонент Outlet и разместить в нужном месте в JSX. Листинги: 6.2, 6.7. Описания: Outlet (подраздел 5.4.3). 15. Предполагается ли получение данных от пользователя в приложение при помощи полей ввода, находящихся в UI? Что сделать? Импортировать компонент, который представляет JSX, содержа­ щий элемент input (или любой другой элемент формы) с нужным типом (текст, числа, телефонный номер и др.). Иначе — включить в JSX элемент input (или любой другой элемент формы) нужного типа. В обоих случаях и компонент, и элемент должны быть управляемыми. Для этого переменную состояния, объявленную на шаге 3, поместить в свойство value поля input или в импорти­рованный компонент. Если вы используете библиотеку для работы с формами, необходимо импортировать из нее компонент, представляю­щий форму в целом и элементы формы. Некоторые библиотеки предоставляют хуки для работы с вводимыми в формы данными. Их нужно задействовать.. Листинги: 5.49, 5.51, 6.17, 6.18. Описания: формы (раздел 5.12), Form React Router (раздел 6.6). 16. Предполагается ли отображать данные с помощью списка? Что сделать? Выполнить общее действие. Импортировать компонент, отвечающий за рендер одного элемента списка, или импортировать компонент из дизайн-системы. В JSX с помощью метода массивов map реализовать колбэк, возвращающий импортированный компонент для одного элемента списка, в который передать выводимые данные. Если в данных существует уникальный ID передать его в свойство key или предусмотреть, чтобы этому свойству было присвоено уникальное значение. Затем при необходимости
8.3. Реализация 3RApp. Написание кода  419 в свойстве className нужного элемента задать класс стиля. Если вы предпочитаете инлайн-стили, то укажите их в свойстве style. Предложение. Обычно, когда вы только начинаете реализовывать компонент, у вас нет компонента, который рендерит один элемент списка. Но его будущий JSX можно сразу реализовать в текущем месте и впоследствии вынести в отдельный компонент. Листинги: список (5.31). Описания: map (подраздел 5.8.3). 17. Предполагается ли вывод иерархической структуры типа «дерево» в виде списка? Что сделать? Импортировать компонент, который представляет один узел дерева. Обычно иерархическая структура данных «дерево» — это объект, представляющий узел, у которого есть свойство children, чьим значением является массив аналогичных узлов и свойство value. Для вывода такой структуры компонент, например NodeTree, должен принимать в качестве свойства узел (объект). В JSX нужно выводить свойство value. Если у узла не пустой массив свойства children, то вывести список через метод map, где колбэк метода возвращает компонент NodeTree, которому передается key, и сам узел. Листинги: 8.31. Описания: map (подраздел 5.8.3). 18. Выводимые данные получены из стороннего API? Что сделать? В шаге 2 были определены переменные состояния в случае возврата ошибки и состояния загрузки, при получении данных через fetch в useEffect. Если вы используете хук RTK Query, то он также вернет аналогичные переменные состояния. В теле компонента, перед основным JSX, написать инструкцию с условием, что если переменная состояния ошибки не равна null, то нужно вывести в инструкции return, соответствующий JSX. Чуть ниже определить инструкцию с условием, что если переменная состояния загрузки указывает на загрузку данных, то в инструкции return вывести JSX прелоадера. Далее без всяких условий должна следовать инструкция return с основным JSX компонента. Листинги: 8.9, компонент SubcategoryPage (8.23–8.26). Описания: RTK Query (раздел 4.9), useState (подраздел 5.6.4), useEffect (подраздел 5.7.2). 19. Вам нужно, чтобы компонент делал вывод по условию? Что сделать? Условный рендеринг. Если вы хотите задать в JSX условие, то задействуйте тернарный оператор (?:). Если вы хотите использовать условную инструкцию if, то вам нужно написать несколько выводов, содержащих инструкции return. Листинги: компонент Authentication (8.11–8.22). Этап 4. Определяем поведение, ведущее к изменению состояния Определяем поведение, необходимое компоненту и наступающее или при возникновении события, или по прошествии времени или сразу после рендера компонента в DOM. Вам может понадобится или работа только с WebAPI, или работа
420  Глава 8 Разработка фронтенд-приложения с переменной состояния, или же и то и другое. Если нужно обратиться к WebAPI, то реализуем это: 1) в обработчике события, 2) в функциях setTimeout и setInterval, 3) в хуке useEffect, 4) в кастомном хуке или в хуке из библиотеки. При необходимости связываем изменение состояния после обращения к WebAPI с состоянием React-компонента. Если нужно изменить переменную состояния, то обращаемся к изменяющей ее функции все в тех же четырех местах. Во время прохождения данного этапа держите под рукой требования и дизайн. С помощью дизайна вы определяете места возникновения и тип события. С помощью требований — сверяетесь с тем, что описано в них, с тем, что вы получите в результате использования WebAPI. 20. Возникает необходимость обратиться к WebAPI только ради побочного эффекта? Что сделать? У вас есть четыре варианта. • Если нужно сразу после рендера. Если вы самостоятельно реализуете работу с WebAPI, то импортировать хук useEffect. В его вызове нужно определить колбэк, в теле которого обратиться к нужному объекту WebAPI. Возможно, придется задать условие выполнения. Данный колбэк может быть вынесен за пределы компонента, если в условии не содержится обращения к его идентификаторам, а только к WebAPI.  Если нужно при возникновении события. Определите элемент и событие, при возникновении которого на этом элементе должен быть запущен обработчик события. В теле компонента или за его пределами обозначьте функцию (обработчик события), в теле которой обратитесь к нужному WebAPI. Дайте этой функции имя, начинающееся с handle, и прибавьте наименование события. В JSX-элементе, где оно должно возникать, укажите свойство, содержащее название события, и добавьте приставку on. Передайте ссылку на обработчик события в данном свойстве.  Если нужно по времени. Импортируйте хук useEffect и в вызове колбэка вызовите функцию setTimeout или setInterval. Первым параметром в нее нужно передать колбэк, вторым — время в миллисекундах, через которое он будет вызван. Если колбэк содержит только обращение к WebAPI, его можно определить за пределами компонента. Далее напишите выражение return и в качестве значения верните функцию, в теле которой вызовите функцию clearTimeout или clearInterval. Ее аргументами должны быть идентификаторы таймера или интервала.  Если необходимо использовать хук (кастомный или из библиотеки), который реализует нужную вам функциональность (работает с определенным WebAPI), то импортируйте его в компонент. Если при вызове хук должен вернуть какой-либо метод, то надо присвоить его идентификатору, чтобы впоследствии вызвать. Выражение вызова возвращенного из хука метода использовать или в обработчике события, или в хуке useEffect либо setTimeout, setInterval. Если же такой хук ничего не возвращает, то его нужно вызвать в теле компонента и, скорее всего, передать конкретные параметры. Листинги: 2.7, 2.13, 2.25, 6.2.
8.3. Реализация 3RApp. Написание кода  421 Описания: браузер (раздел 2.3), DOM (раздел 2.5), события (раздел 2.6), эффекты (раздел 5.7), подраздел 8.3.4. 21. Возникает необходимость и обратиться к WebAPI, и изменить переменную состояния? Общее действие. Для всех вариантов шага 20 после обращения к WebAPI может возникнуть необходимость изменить значение переменной состояния, определенной в компоненте, с помощью вызова изменяющей ее функции (определена на шаге 3) или отправить экшен в стор. Для отправки экшена в стор в компонент надо импортировать хук useDispatch из библиотеки React Redux. Затем в теле компонента вызвать хук useDispatch и возвращаемое значение присвоить какому-либо идентификатору (обычно dispatch). Также в компонент необходимо импортировать action creator из слайса. Вызов action creator с переданными ему аргументами (обычно новое состояние) следует помещать в вызов функции dispatch. Значение нового состояния на этом шаге в большинстве случаев получается из обращения к WebAPI. Что сделать? У вас есть четыре варианта.  Если нужно сразу после рендера. Если вы самостоятельно реализуете работу с WebAPI, то импортировать хук useEffect. В его вызове нужно определить колбэк. Вероятно, придется задать условие выполнения. В теле колбэка обратитесь к нужному объекту WebAPI, затем выполните общее действие (указано в начале).  Если нужно при возникновении события. Определите элемент и событие, при возникновении которого на данном элементе должен быть запущен обработчик события. В теле компонента определите обработчик события, в теле которого обратитесь к нужному WebAPI, а затем произведите общее действие (указано вначале). Дайте имя этой функции, начинающееся с handle, и прибавьте наименование события. В JSX-элементе, где оно должно возникать, укажите название свойства, содержащее название события, и добавьте приставку on. Передайте ссылку на обработчик события в данном свойстве.  Если нужно по времени. Импортируйте хук useEffect и в вызове колбэка вызовите функцию setTimeout или setInterval. Первым параметром в нее нужно передать колбэк, вторым — время в миллисекундах, через которое он будет вызван. В его теле обратитесь к нужному WebAPI, затем произведите общее действие (указано вначале). Далее напишите выражение return и в качестве значения верните функцию, в теле которой вызовите функцию clearTimeout или clearInterval. Ее аргументами должны быть идентификаторы таймера или интервала.  Если нужно использовать хук (кастомный или из библиотеки), который реализует нужную вам функциональность (работает с определенным WebAPI), то такой хук должен принимать функцию, при вызове которой ей в аргументе должно быть передано какое-либо значение, полученное от WebAPI. Иначе он бессмысленен для данной ситуации. Если хук подходит, то импортируйте его в компонент. Реализуйте функцию обратного
422  Глава 8 Разработка фронтенд-приложения вызова в теле компонента и внутри этого колбэка выполните общее действие (указано в начале). Передайте в вызов хука этот колбэк. Может быть и так, что вызов хука не принимает аргументы, но возвращает объект с методами или просто функцию. Тогда следует присвоить возвращаемое значение идентификатору. Выражение вызова метода или функции нужно использовать или в обработчике события, или в хуке useEffect либо в setTimeout, setInterval. Листинги: 5.23, 8.9, 8.10. Описания: браузер (раздел 2.3), DOM (раздел 2.5), события (раздел 2.6), useEffect (подраздел 5.7.2), useState (подраздел 5.6.4), useDispatch (подраздел 5.18.3), компонент ShedulePage (разделы 8.11–8.22). 22. Возникает необходимость отправить мутирующий запрос? Что сделать? У вас есть три варианта.  Отправка при задействовании WebAPI. Определите обработчик события handleSubmit, в котором получите данные формы и с помощью fetch отправьте одним из методов: POST, PATCH или PUT. При этом установите требуемые заголовки. Обработчик события передайте ссылкой в форму, в свойство onSubmit.  Отправка при задействовании RTK Query. Создайте мутирующий эндпоинт в нужном слайсе. Экспортируйте автоматически сгенерированный хук из слайса и импортируйте в компонент, где есть форма, данные которой вы будете отправлять. Вызовите импортированный хук — он вернет массив с функцией и объектом. Определите обработчик события handleSubmit, где вызовите полученную из хука функцию и передайте аргументом данные формы. Они будут являться body в запросе на сервер. Обработчик события передайте ссылкой в форму, в свойство onSubmit.  Отправка с использованием React Router. В объекте конфигурации маршрутизатора, в маршруте, где у вас рендерится компонент с формой, добавьте свойство action. Определите для него функцию, в теле которой нужно сформировать тело запроса, заголовки и метод. При этом данные в функцию будут переданы через параметр request. В компоненте, который привязан к маршруту и для которого вы определяете action, форма должна быть сформирована с помощью компонента Form из библиотеки React Router, где свойствами указываются URL для отправки (action) и метод отправки (method). Листинги: React Router (6.18), компонент SubcategoryPage (8.23–8.26). Описания: RTK Query (раздел 4.9), React Router (раздел 6.6). 23. Возникает необходимость изменить состояние переменной? Что сделать? У вас есть два варианта.  Если нужно при возникновении события. Определите элемент и событие, при возникновении которого на данном элементе должен быть запущен обработчик события. В теле компонента обозначьте обработчик события, в теле которого вызовите функцию изменения переменной
8.3. Реализация 3RApp. Написание кода  423 состояния, определенную на шаге 3. Дайте обработчику имя, начинающееся с handle, и прибавьте наименование события. В JSX-элементе, где оно должно возникать, укажите свойство, содержащее название события, и добавьте приставку on. Передайте ссылку на обработчик события в данном свойстве.  Если нужно по времени. Импортируйте хук useEffect и в вызове колбэка вызовите функцию setTimeout или setInterval. Первым параметром в нее нужно передать колбэк, вторым — время в миллисекундах, через которое он будет вызван. Если колбэк содержит только обращение к WebAPI, то его можно определить за пределами компонента. Далее напишите выражение return и в качестве значения верните функцию, в теле которой вызовите функцию clearTimeout или clearInterval. Ее аргументами должны быть идентификаторы таймера или интервала. Листинги: 8.9, 8.10. Описания: события (раздел 2.6), useEffect (подраздел 5.7.2), useState (подраздел 5.6.4), обработка событий (раздел 5.9). 24. Возложил ли родительский компонент или компонент-предок на данный компонент необходимость отправки ему каких-либо данных? Что сделать? Определите, на какое событие производится отправка данных. При необходимости может изменяться состояние. Объявите соответствующий обработчик события. Если родитель возлагает на данный компонент передачу данных, то в пропсах объявите идентификатор с именем того события, во время которого будет вызвана родительская функция (например, onClick, onMouseOver). В родительском компоненте, в выводе (выражение return) в JSX компонента, который сейчас реализуется, добавьте свойство onClick и передайте туда функцию, у которой параметром выступают данные, передаваемые от дочернего компонента. Вызов пропса onClick поместите в обработчик события и аргументом передайте или объект события, или какие-либо данные (например, полученные запросом), или новое значение состояния (если это подразумевается). Если на данный компонент предок возлагает передачу данных, то импортируйте в компонент хук useContext и импортируйте соответствующий объект контекста. Из этого контекста извлеките функцию и поместите ее вызов в обработчик события. Аргументом передайте или объект события, или какие-либо данные (например, полученные запросом), или новое значение состояния (если это подразумевается). Листинги: 5.89–5.93. Описания: подраздел 5.19.5. 25. Компонент должен возложить изменение состояния на дочерний компонент или на потомков? Что сделать? Определите обертывающую функцию, в теле которой должна вызываться функция, изменяющая состояние. Обертывающая функция может принимать аргумент, какие-либо данные и передавать их в качестве нового состояния в функцию, изменяющую состояние. Передавать в обертывающую
424  Глава 8 Разработка фронтенд-приложения функцию новые данные будет или дочерний компонент, или любой из потомков. Если обработку события необходимо возложить на дочерний компонент, то обертывающую функцию достаточно указать в качестве значения свойства данного компонента. Если обработку события нужно возложить на компонент-потомок, то необходимо создать объект контекста с помощью функции createContext из React. Затем импортировать объект контекста в текущий компонент, в выражении return указать объект контекста и через точку получить доступ к свойству Provider. В значении value передать объект со свойством, значением которому передать ссылку на обертывающую функцию. Листинги: 5.89–5.93. Описания: подраздел 5.19.5. Этап 5. Определяем обмен данными 26. Должен ли текущий компонент передавать данные потомкам? Что сделать? Создайте объект контекста с помощью функции createContext из React. Затем импортируйте объект контекста в текущий компонент, в выражении return укажите объект контекста и через точку получите доступ к свойству Provider. В значении value передайте объект со свойством, значением которому передайте значение переменной состояния, определенной на шаге 3 или 4. Листинги: 5.89–5.93. Описания: подраздел 5.19.5. 27. Должен ли текущий компонент передавать данные в стор? Что сделать? Импортируйте хук useDispatch, action creator. В теле компонента вызовите хук useDispatch и присвойте возвращаемой функции имя (обычно dispatch). Определите, значение какой переменной состояния необходимо отправлять в стор Redux Toolkit. Реализуйте обработчик события, в теле которого поместите вызов функции dispatch. Аргументом передайте вызов функции action creator и сообщите этой функции значение переменной состояния, определенной на шаге 3 или 4. Листинги: 5.89–5.93. Описания: useDispatch (подраздел 5.18.3), подраздел 5.19.5. Этап 6. Оптимизация 28. Нужно оптимизировать данные? Что сделать? Импортируйте хук useMemo из React. Листинги: useMemo (5.73), useCallback (5.76). Описания: useMemo (подраздел 5.16.1), useCallback (подраздел 5.16.3) 29. Нужно оптимизировать перерендеры поддерева? Что сделать? Импортируйте HOC memo из React. Листинги: 5.60. Описания: HOC memo (подраздел 5.13.3).
8.3. Реализация 3RApp. Написание кода  425 Этап 7. Другие действия Это этап заключительных действий. 30. Компонент входит в объект конфигурации маршрутизатора или указан в родительских компонентах с помощью @TODO? Что сделать? Если входит в объект конфигурации, то импортируйте реализованный компонент в объект конфигурации маршрутизатора и замените JSX из элементов на JSX компонента. Если указан в родительском компоненте с помощью @TODO, то импортируйте его в компонент, замените в месте, где есть @TODO для этого компонента на JSX, представляющий собой компонент, и при необходимости передайте пропсы. 31. Необходимо прокомментировать определенные переменные или выражения? Что сделать? Напишите комментарий над переменной или выражением. 32. Вы использовали временные переменные, оставляли @TODO и делали выводы в консоль? Что сделать? Просмотрите, не остались ли неиспользуемые идентификаторы и выводы в консоль. Если остались, удалите их. Затем снова пробегитесь глазами по @TODO. Если они нужны — оставьте, в противном случае удалите. Если вам по-прежнему требуются временные переменные (например, заглушки) вместо реальных данных, то оставьте над ними @TODO с пометкой, что это следует удалить после какого-то события в изменении кода приложения или в сторонних системах. 8.3.10. Хуки В табл. 8.14 приведены чаще всего используемые хуки, в зависимости от роли компонента. Таблица 8.14. Хуки, которые чаще всего используются, в зависимости от роли Хук Провайдер Верстка Приложение Страница Бизнес Компонент useState + – + + + + useReducer + – + + + – useRef – – – – + + useEffect + + + + + + useContext + – – – + – Если вы задаетесь вопросом, как насчет остальных хуков, типа useCallback и useMemo, то знайте: они влияют на оптимизацию производительности, а не задают основную функциональность. Про хуки React можно прочитать в разделах и подразделах: useState (подраздел 5.6.4), useEffect (подраздел 5.7.2), useContext (раздел 5.11), useReducer (раздел 5.14), useRef (подраздел 5.15.1), useMemo (подраздел 5.16.1), useCallback
426  Глава 8 Разработка фронтенд-приложения (подраздел 5.16.3), кастомные хуки (подраздел 5.17.4), useSelector, useDispatch (подраздел 5.18.3), хуки библиотеки React Router (подраздел 6.8.2). Важный вывод про хук useEffect — он используется для разных целей. В одном случае это извлечение данных через WebAPI, в другом — обращение к WebAPI для осуществления действия. Поэтому ваш колбэк может быть переплетением выражений, которые друг с другом не связаны. При этом они могут и, как правило, содержат условия, необходимые при использовании инструкции if. В случае дальнейшего развития компонента такой код создает сложности для понимания, поэтому применение процедуры в некоторой степени упрощает содержимое этого хука. 8.3.11. Время, затрачиваемое на реализацию Если еще раз взглянуть на подразделы 8.3.7 и 8.3.8, то становится очевидным, что основная часть времени затрачивается на реализацию. При этом здесь всего лишь описаны подходы к реализации при задействовании библиотек стека 3RApp — и не более. В реальных проектах библиотек гораздо больше, и они используются практически для всех случаев. Их особенно много для WebAPI, в том числе в виде набора хуков. Любая библиотека требует знакомства с ней. А это ваше время, которое складывается с тем временем, которое вы тратите на написание кода. Кроме того, прежде чем писать код компонента, нужно сесть и подумать, как все реализовывать, то есть подумать над решением. Это время тоже плюсуется с временными затратами на написание кода. К тому же часто вы будете не писать код, а знакомиться с уже имеющимся, даже если создали его сами. И это время складывается с тем, за которое вы пишете код реализации. Также вам необходимо написать тесты на функции, хуки и компоненты. Плюс, когда пишете код, вам нужно его проверять, чтобы убедиться, что приложение работает так, как было задумано. Кто-то пишет и часто проверяет, кто-то сначала напишет весь компонент или функцию и только потом проверяет. Таким образом, время на реализацию складывается из пяти этапов: чтения, знакомства с новыми библиотеками и их опробования, составления решения, написания тестов, написания кода и его тестирования. Этап знакомства с новой библиотекой есть не всегда, поэтому основных этапов четыре. Известно, что существует два подхода к разработке: y планирование решения на основе знания и написание кода по придуманному решению (пусть даже в уме); y метод проб и ошибок. Проверочный список дает возможность действовать согласно первому способу. Второй обычно затратен по времени и не всегда приводит к нужным оптимальным результатам. Он используется, когда нет полного знания, как действовать и с помощью чего составлять решение. Приводимая в подразделе 8.3.8 процедура, по моему замыслу, должна ускорить и упростить получение кода компонента. Скорее всего, первое время она не даст плюс в скорости, но определенно упорядочит вашу работу по реализации компонентов. Потому что понимание приводит к упорядочению действий,
8.4. Пример реализации компонентов с помощью конструктора  427 незнание — к необходимости постоянно что-то придумывать, а придумывание — это затраты времени. 8.3.12. Семантический HTML В компонентах нужно использовать семантический HTML и активно применять такие CSS-разделы, как flex, grid. Уверен, вы не раз слышали, что существует некий семантический HTML. Что здесь подразумевается? Некий специфичный вид HTML? Вовсе нет — самый обычный. Тогда почему его обозначают дополнительным словом, ведь и так все ясно? Дело вот в чем. Вы, конечно, можете обходиться двумя видами HTML-элементов — блочными (например, div) и строчными (например, span). И во всем приложении, не заморачиваясь по поводу других тегов, использовать весьма ограниченный их набор. Выглядеть все будет одинаково. Однако в HTML существует много других тегов, которые имеют целевое назначение и, по сути, тоже делятся на блочные и строчные. Тут появляется необходимость описать HTML как семантический. Под HTML мы периодически понимаем все теги, использованные в приложении. А семантика, если не вдаваться в подробности, означает смысл. Любой тег имеет семантику. Например, тег footer очевидно предназначен для создания футера приложения. Встретив его, вы сразу поймете, с чем имеете дело и где располагается этот HTML. Или, например, теги nav, section. Наверное, не придет на ум использовать nav в качестве тега, содержащего текстовое описание товара. В стандарте HTML1 теги поделены на четкие группы. Если их периодически просматривать, то путаница, что использовать, постепенно уйдет. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 8.3. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 8.3. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 8.3 и ознакомьтесь с моим решением. 8.4. Пример реализации компонентов с помощью конструктора В данном разделе приведен пример создания нескольких компонентов согласно процедуре, описанной в подразделе 8.3.8. Она применена к компонентам разных ролей. Реализовываться будут компоненты приложения, спроектированного в разделе 8.1. Конечный результат представлен в репозитории кода, в папках 8.11–8.22. Этап настроек выполнен и здесь не рассматривается. 1 По адресу https://html.spec.whatwg.org/#semantics-2 см. раздел 4.
428  Глава 8 Разработка фронтенд-приложения Для демонстрации процедуры построения компонентов я проведу вас по всем этапам и по каждому реализуемому компоненту. Во всех реализациях далее используется: y дизайн, показанный на рис. 8.3; y граф, показанный на рис. 8.15; y табл. 8.1 и 8.2 с маппингом эндпоинтов на компоненты. Кроме того, нужно оставить примечание по поводу компонента Layout на этом графе. Компоненты с ролью страницы рендерятся в нем через специальный компонент Outlet из библиотеки React Router. На графе они выглядят как импортированные в него в качестве дочерних компонентов. Все так, но делается это через указанный посредник, который я не считаю нужным изображать на графе. Обращаю также внимание на указание, когда использовать проверочный список. В том же подразделе 8.3.8 сказано, что процедура является проверочным списком и используется, когда: y компонент слишком большой и вы не знаете, с чего начать; y возникают трудности в создании решения; y нужно подсмотреть действия по конкретному этапу. Мы представим (притворимся), что у нас второй случай — трудности в создании решения. Всего в проверочном списке семь этапов. 1. Определение данных, нужных компоненту для их вывода в UI. 2. Предварительная обработка данных. 3. Определение UI и того, как данные будут в него выводиться. 4. Определение поведения, ведущего к изменению состояния. 5. Определение обмена данными. 6. Оптимизация. 7. Другие действия. Семь названных этапов содержат 32 действия. Обязательным является только этап 3. При этом всем шагам предшествует создание или придумывание решения. Это не действие по написанию кода, поэтому оно не включено в проверочный список. Однако это действие имеет ту же важность, что и каждый этап. Иногда оно очевидно, иногда — нет. Каждый разработчик осуществляет его по-своему. Важное замечание относительно примеров кода. Код в репозитории расположен в папках 8.11–8.22 и находится в состоянии, в котором мы его описываем в этой книге. 8.4.1. Layout Этап 1. Определение данных, нужных компоненту для их вывода в UI. Принятие решения. Данные не требуются. Действие. Переходим к этапу 3. Этап 3. Определение UI и того, как данные будут в него выводиться. Принятие решения. Этап требуется.
8.4. Пример реализации компонентов с помощью конструктора  429 Действие. Знакомимся с общими действиями для каждого шага. Проходим по шагам этапа. y Предполагается ли вывод исключительно UI без данных? Нет. Хотя у компонента нет своего вывода, он задействует другие компоненты. y Предполагает ли компонент композицию с другими компонентами для вывода данных (помимо компонентов дизайн-системы или UI-библиотеки)? Да. На данный момент такие компоненты еще не реализованы, поскольку являются дочерними. Листинг 8.11. src/layout/Layout.js import {Fragment} from'react'; // import {Menu} from './Menu'; // import {Footer} from './Footer'; // import {Header} from './Header'; export const Layout = () => { return ( <Fragment> <header>{/* @TODO Header */}</header> <aside>{/* @TODO Menu */}</aside> <main>{/* */}</main> <footer>{/* @TODO Footer */}</footer> </Fragment> ); } y Является ли данный компонент родительским сегментом пути для дочернего сегмента в настройках объекта конфигурации маршрутизатора React Router? Листинг 8.12. src/layout/Layout.js import { Outlet } from 'react-router-dom'; // ранее реализованный код <main><Outlet /></main> // ранее реализованный код На данном этапе все, что необходимо, достигнуто. Больше никаких действий по данному компоненту не требуется. 8.4.2. ShedulePage Этап 1. Определение данных, нужных компоненту для их вывода в UI. Принятие решения. Данные требуются. Действие. Переходим к шагу 1. y Какие данные передает родитель вашему компоненту? Родитель не передает данных. y Присутствует ли данный компонент в таблице маппинга компонентов на эндпоинты?
430  Глава 8 Разработка фронтенд-приложения Листинг 8.13. src/pages/ShedulePage.js import {useEffect, useState} from'react'; export const ShedulePage = () => { const [sheduleItemList, setSheduleItemList] = useState([]); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); } useEffect(() => { setLoading(true); fetch('/api/v1/shedule/').then(res => res.json()).then(res => { setSheduleID(res[time].slotID); setSheduleItemList(res); setLoading(false); }).catch(err => { setError(err.message); setLoading(false); }); }, []); y Должна (-ы) ли быть определена (-ы) переменная (-ые) состояния? Нет. y Должна (-ы) ли быть определена (-ы) переменная (-ые) состояния, при изменении которой (-ых) не происходит перерендера? Нет. y Присутствует ли в текущем поддереве предок (речь о способе обмена данными «Предок — потомок», означающем, что компонент с такой ролью — провайдер), по отношению к которому реализуемый компонент является потомком, то есть должен получить данные из контекста? Нет. y Должен ли компонент получить данные из стора? Листинг 8.14. src/pages/ShedulePage.js import {useEffect, useState} from'react'; import { useSelector } from 'react-redux'; import { selectTab } from '../store/features/shedule'; export const ShedulePage = () => { const [sheduleItemList, setSheduleItemList] = useState([]); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const tab = useSelector(selectTab); useEffect(() => { setLoading(true); fetch('/api/v1/shedule').then(res => res.json()).then(res => { setSheduleID(res[time].slotID); // ранее реализованный код
8.4. Пример реализации компонентов с помощью конструктора  431 y Должен ли данный компонент определить константу (-ы) или импортировать ее (их)? Нет. Дальнейшие шаги не требуются. Переходим к этапу 2. Этап 2. Предварительная обработка данных. Принятие решения. Этап требуется. Действие. Определяем, что нужно предобработать. y Должны ли быть данные предобработаны? Листинг 8.15. src/pages/ShedulePage.js import import import import {useEffect, useState} from'react'; { useSelector } from 'react-redux'; { selectTab } from '../store/features/shedule'; { getPeriod } from './getPeriod'; export const ShedulePage = () => { const [sheduleItemList, setSheduleItemList] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const tab = useSelector(selectTab); const period = getPeriod(tab); useEffect(() => { setLoading(true); fetch(`/api/v1/shedule/${period}`).then(res => res.json()).then(res => { setSheduleID(res[time].slotID); // ранее реализованный код В этапе 2 всего один шаг. Переходим к этапу 3. Этап 3. Определение UI и того, как данные будут в него выводиться. Принятие решения. Этап требуется. Действие. Знакомимся с общими действиями для каждого шага. Проходим по шагам этапа. y Предполагается ли вывод исключительно UI без данных? Нет, данные нужно выводить. Сам компонент страницы выводит только компоненты. y Предполагает ли компонент композицию с другими компонентами для вывода данных (помимо компонентов дизайн-системы или UI-библиотеки)? Да, компоненты еще не реализованы. Листинг 8.16. rc/pages/ShedulePage.js import {useEffect, useState, Fragment} from'react'; // import {Shedule} from '../business'; // import {Tabs} from '../components'; // ранее реализованный код
432  Глава 8 Разработка фронтенд-приложения return ( <Fragment> <section> <div>Tabs{/* @TODO <Tabs /> */}</div> </section> <section> <div>Shedule {/* @TODO <Shedule sheduleList={sheduleItemList} /> */}</div> </section> </Fragment> ); y Является ли данный компонент родительским сегментом пути для дочернего сегмента в настройках объекта конфигурации маршрутизатора React Router? Листинг 8.17. rc/pages/ShedulePage.js // ранее реализованный код <section> <div>Shedule{/* @TODO <Shedule sheduleId={sheduleId} /> */}</div> <div><Outlet /></div> </section> // ранее реализованный код y Предполагаются ли в UI поля ввода данных от пользователя в приложение? Нет. y Предполагается ли отображать данные с помощью списка? Нет. y Предполагается ли вывод иерархической структуры типа «дерево» в виде списка? Нет. y Выводимые данные получены из стороннего API? Листинг 8.18. src/pages/ShedulePage.js // ранее реализованный код if (error) { return <div>{error}</div>; } if (loading) { return <div>Загружается...</div>; } // ранее реализованный код y Вам необходимо, чтобы компонент делал вывод по условию? Нет. Шаги этапа 3 закончены, и переходим к этапу 4.
8.4. Пример реализации компонентов с помощью конструктора  433 Этап 4. Определение поведения, ведущего к изменению состояния. Принятие решения. Этап не требуется. Действие. Переходим к этапу 5. Этап 5. Определение обмена данными. Принятие решения. Этап требуется. Действие. Знакомимся с общими действиями для каждого шага. Проходим по шагам этапа. y Должен ли текущий компонент передавать данные потомкам? Нет. y Должен ли текущий компонент передавать данные в стор? Листинг 8.19. src/pages/ShedulePage.js import import import import {useEffect, useState} from'react'; { useSelector, useDispatch } from 'react-redux'; { selectTab, changeTab } from '../store/features/shedule'; { getPeriod } from './getPeriod'; export const ShedulePage = () => { const [sheduleItemList, setSheduleItemList] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const tab = useSelector(selectTab); const period = getPeriod(tab); const handleChangeTab = (tab) => { dispatch(changeTab(tab)); }; // ранее реализованный код Шаги этапа 5 закончены, и переходим к этапу 6. Этап 6. Оптимизация. Принятие решения. Оптимизация не требуется. Действие. Переход к этапу 7: другие действия. Этап 7. Другие действия. Принятие решения. Этап требуется. Действие. Проходим шаги этапа. y Компонент входит в объект конфигурации маршрутизатора или указан в родительских компонентах с помощью @TODO? Листинг 8.20. src/routes.js import {createBrowserRouter} from 'react-router-dom'; import {Authentication} from './layout'; import { ShedulePage } from './pages';
434  Глава 8 Разработка фронтенд-приложения export const routes = createBrowserRouter([ { path: '/', element: <Authentication />, children: [{ path:'shedule', element: <ShedulePage />, children: [{ index: true, element: <div>Forma</div>, }, { path: ':sheduleId', element: <div>SheduleItemDetails</div> }] }, { path: 'candidate', element: <div>CandidatePage</div>, children: [{ path: ':candidateCVId', element: <div>CandidateDetails</div> }] }] } ]); y Вам необходимо прокомментировать определенные переменные или вы- ражения? Нет. y Вы использовали временные переменные, оставляли @TODO и делали выводы в консоль? Оставлял @TODO и решил переименовать после реализации некоторых компонентов. 8.4.3. SheduleItemDetails Этап 1. Определение данных, нужных компоненту для их вывода в UI. Принятие решения. Данные требуются. Действие. Переходим к шагу 1. y Какие данные передает родитель вашему компоненту? Нет. (Означает, что шаг пропускается.) y Присутствует ли данный компонент в таблице маппинга компонентов на эндпоинты? Листинг 8.21. src/business/SheduleItemDetails.js import {useState, useEffect} from 'react'; export const SheduleItemDetails = ({ shedule }) => { const [sheduleDetails, setSheduleDetails] = useState([]);
8.4. Пример реализации компонентов с помощью конструктора  435 const [error, setError] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); fetch(`/api/v1/shedule/${sheduleId}`) .then(res => res.json()) .then(data => { setSheduleDetails(data); setLoading(false); }) .catch(err => { setError(err.message); setLoading(false); }); }, [sheduleId]); }; y Должна (-ы) ли быть определена (-ы) переменная (-ые) состояния? Нет. Все оставшиеся шаги этапа 1 пропускаются. Этап 2. Предварительная обработка данных. Принятие решения. Этап не требуется. Действие. Переходим к этапу 3. Этап 3. Определение UI и того, как данные будут в него выводиться. Принятие решения. Этап требуется. Действие. Знакомимся с общими действиями для каждого шага. Проходим по шагам этапа. y Предполагается ли вывод исключительно UI без данных? Нет. y Предполагает ли компонент композицию с другими компонентами для вывода данных (помимо компонентов дизайн-системы или UI-библио­ теки)? Листинг 8.22. src/business/SheduleItemDetails.js // import {Header } from '../components'; // import {Button} from '../components'; import styles from './SheduleItemDetails.module.css'; // ранее реализованный код const {fio, vacancy, candidateID} = sheduleDetails; return ( <section className={styles.details}> <div> {/* @TODO <Header fio /> */} </div>
436  Глава 8 Разработка фронтенд-приложения ); <div> {/* @TODO <Header vacancy /> */} </div> <div> {/* @TODO <Button candidateID /> */} </div> </section> y Является ли данный компонент родительским сегментом пути для дочернего y y y y сегмента в настройках объекта конфигурации маршрутизатора React Router? Нет. Предполагается ли в UI поля ввода данных от пользователя в приложение? Нет. Предполагается ли отображать данные с помощью списка? Нет. Предполагается ли вывод иерархической структуры типа «дерево» в виде списка? Нет. Выводимые данные получены из стороннего API? Листинг 8.23. src/business/SheduleItemDetails.js // ранее реализованный код if (error) { return <div>{error.message}</div>; } if (loading) { return <div>Загрузка...</div>; } // ранее реализованный код y Вам необходимо, чтобы компонент делал вывод по условию? Нет, так как все необходимое уже реализовано. Этап 4. Определение поведения, ведущего к изменению состояния. Принятие решения. Этап требуется. Действие. Проходим по шагам этапа. y Возникает необходимость обратиться к WebAPI только ради побочного эффекта? Листинг 8.24. src/business/SheduleItemDetails.js // ранее реализованный код import {useCallToSomeService} from './useCallToSomeService'; // ранее реализованный код
8.4. Пример реализации компонентов с помощью конструктора  437 <div>{/* @TODO <Button candidateId={candidateId} onClick={() => callToSomeService(candidateId)}/>*/} <button onClick={() => callToSomeService(phoneNumber)}>Позвонить</button> </div> y Возникает необходимость обратиться к WebAPI и изменить переменную состояния? Нет. y Возникает необходимость отправить мутирующий запрос? Нет. y Возникает необходимость изменить состояние переменной? Нет. Все оставшиеся шаги этапа 4 «Определение поведения, ведущего к изменению состояния» пропускаются. Этап 5. Определение обмена данными. Принятие решения. Этап не требуется. Действие. Переходим к этапу 6. Этап 6. Оптимизация. Принятие решения. Этап не требуется. Действие. Переходим к этапу 7. y Компонент входит в объект конфигурации маршрутизатора или указан в родительских компонентах с помощью @TODO? Листинг 8.25. src/routes.js import { SheduleItemDetails } from './business'; // ранее реализованный код children: [{ path:'shedule', element: <ShedulePage />, children: [{ index: true, element: <div>Forma</div>, }, { path: ':sheduleId', element: <SheduleItemDetails /> }] }, { // ранее реализованный код y Вам необходимо прокомментировать определенные переменные или выражения? Нет.
438  Глава 8 Разработка фронтенд-приложения y Вы использовали временные переменные, оставляли @TODO и делали выводы в консоль? Это не нужно. 8.4.4. Заключение Создавать компонент по проверочному списку проще, хотя в какой-то степени это формализует процесс. По мере прохождения этого списка некоторые ранее пройденные шаги уточняются, вносятся дополнительные изменения. Что точно обеспечивает проверочный список, так это полноту возможных действий для реализации компонента. Еще он экономит время, так как, взявшись за список, вы идете по шагам. Без списка в каждом компоненте приходится думать, с чего начинать и что делать далее. Это занимает время, а порой приводит к откладыванию действия. Таким образом реализация растягивается на более длительный период времени. После нескольких практических использований списка к реализации компонентов вы начнете применять его с легкостью. Однако к такому подходу нужно привыкнуть, как и ко всему новому. Надеюсь, проверочный список действительно облегчит ваш труд. 8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения В разделе 8.1 описана пошаговая процедура проектирования фронтенд-приложения. А в этом разделе на примере приложения интернет-магазина мы рассмотрим применение данной процедуры и узнаем, какие действия по реализации и в какой последовательности нужно выполнять. Далее в упражнениях вы еще раз потренируете два основных действия: y процедуру проектирования; y создание приложения 3RApp на основе проектирования. И наконец, вам предстоит практическое задание по самостоятельному проектированию и реализации. Оно большое и разбито на несколько этапов. Единственное в своем роде. Можно сказать, что это ваша дипломная работа. Для разработки 3RApp есть предварительные требования. Вам необходимо: y знать, как использовать каждую библиотеку: React, React Router и Redux Toolkit. Это ключевой пункт. Вы не обязаны знать каждую из них во всех тонкостях (они немаленькие по объему); y знать и уметь применять процедуру проектирования 3RApp; y знать, как настроить проект перед разработкой; y знать все составляющие, которые задействуются в компоненте для реализации;
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  439 y поупражняться, чтобы владеть процедурой создания компонентов с при- менением проверочного списка (по желанию). Поскольку к данному моменту мы прошли все необходимое, изучать что-то дополнительно не будем — кроме применения проектирования и реализации. Знание того, как успешно применять что-то, само по себе является методом. Поэто­му данный раздел является заключительным применительно к вашим навыкам проектирования и разработки. Впереди — несколько разделов и даже целая глава. Следующий раздел посвящен использованию нейросетей для ускорения разработки, а глава 9 — совместной разработке. 8.5.1. Этап 1. Применение процедуры проектирования 3RApp-приложения Процедура включает в себя следующие шаги. 1. Изучение требований, аналитики, дизайна и понимание цели АС (ИС), то есть выяснение, какая (-ие) проблема (-ы) пользователя решается (-ются) или что автоматизируется. 2. Определение компонентов с ролью «Страница» (разделы) веб-приложения. 3. Выделение UI-компонентов из дизайна и дополнение графа компонентов приложения компонентами с ролями «Лэйаут», «Бизнес-компонент» и «Простой конечный компонент», а по окончании — дополнение компонентом с ролью «Провайдер». 4. Задание глобального состояния приложения или его части и дополнение графа компонентов приложения компонентом с ролью «Провайдер», а при необходимости — дополнительными компонентами, зависящими от состояния. 5. Определение, с какими внешними системами какие компоненты взаимодействуют. 6. Определение, по какому пути в URL какой компонент будет рендериться. Шаг 1. Изучение требований, аналитики, дизайна и понимание цели АС (ИС), то есть выяснение, какая (-ие) проблема (-ы) пользователя решается (-ются) или что автоматизируется. Этот магазин заказа товара является минимальным жизнеспособным продуктом, где покупатель может положить товар (-ы) в корзину и сделать заказ. Аутентификация не подразумевается. На этапе «Форма заказа» пользователь вводит необходимые данные. Количество товаров любое. Каталог иерархический и может быть любого размера. Верхними уровнями в каталоге являются наименования крупных категорий, например «Одежда», «Книги», «Бытовая техника» и т. д. Следующий уровень — уточняющая подкатегория, например, в одежде — «Брюки», «Обувь», в бытовой технике — «Холодильники», «Духовые шкафы». Следующий уровень — производитель или бренд. Например, для брюк это «BruKeee» (название выдумано), для духовых шкафов — «Духовенций» (название выдумано). Когда пользователь заходит в каталог, он видит товары по выбранной им категории. Эта страница работает как фильтр.
440  Глава 8 Разработка фронтенд-приложения Шаг 2. Определение компонентов с ролью «Страница» (разделы) веб-приложения. Из дизайна задаем названия для компонентов с ролью «Страница» и заносим их в таблицу (табл. 8.15, 8.16). Таблица 8.15. Выписанные компоненты с ролью «Страница», часть 1 MainPage CatalogPage SearchResultPage CategoryPage SubcategoryPage 1 2 3 4 5 Таблица 8.16. Выписанные компоненты с ролью «Страница», часть 2 ProductPage BasketPage OrderFormPage ConfirmationPage 6 7 8 9 Составляем граф страниц (рис. 8.32). Рис. 8.32. Граф страниц Шаг 3. Выделение UI-компонентов из дизайна и дополнение графа компонентов приложения компонентами с ролями «Лэйаут», «Бизнес-компонент» и «Простой конечный компонент», а по окончании — дополнение компонентом с ролью «Провайдер». Лэйаут. Смотрим PDF-файл (файл Дизайн интернет-магазина.pdf расположен в папке 8/artifacts в репозитории) на дизайн и видим, что часть страниц имеют похожий лэйаут, где есть строка поиска, корзина, верхняя часть с названием магазина и ссылками и нижняя часть с названием, ссылками на некоторые разделы, а также значками мессенджеров и социальных сетей. Другая часть страниц не имеет строки поиска и корзины, но содержит верхнюю часть и нижнюю часть. Соответственно, выделяем два лэйаута: первый будет содержать только верхнюю и нижнюю части, второй — то же самое, но дополнительно строку поиска с кнопкой плюс компонент со значком корзины и суммой в ней. Лэйаут показан на рис. 8.33. Теперь нужно решить, где в графе компонентов будут расположены лэйауты. Они состоят из компонентов, которые нужно изобразить в качестве дочерних. Итак, у нас получается следующий граф (рис. 8.34). В табл. 8.15 мы заносим только компоненты «Страница» и «Бизнес-компонент», поэтому она остается без изменений, а пояснения по наименованиям только что выделенных компонентов приведу списком: y 10 — CommonLayout; y 11 — LayoutFooter; y 12 — LayoutHeader; y 13 — SearchAndBasketLayout; y 14 — ProductSearchForm; y 15 — BasketValue.
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  441 Рис. 8.33. Один из лэйаутов Рис. 8.34. Лэйауты, составляющие их компоненты и страницы Чтобы с лэйаутами было понятнее, уточню. CommonLayout включает в себя SearchAndBasketLayout как дочерний компонент. Каждый из них задейству- ет возможности библиотеки React Router, которая предоставляет компонент Outlet для рендеринга дочерних компонентов. Компоненты страниц, показанные на рис. 8.34 (1–9), как раз и будут рендериться внутри этого компонента, как в лэйауте CommonLayout (с 7-го по 9-й), так и в SearchAndBasketLayout (с 1-го по 6-й). На графе компонентов конфигурацию маршрутизатора не изобразить, значит, нет смысла изображать и компонент Outlet. Поэтому условно показываем связь с лэйаутом. Мы еще дойдем до реализации, и этот момент станет более очевидным. Бизнес-компоненты. Рассмотрим страницу «Главная» и, отделив компоненты лэйаута, выделим бизнес-компоненты. Для этого снова смотрим дизайн. Как выделять компоненты, описано в процедуре проектирования (подраздел 8.1.2, шаг 3). На «Главной» можно выделить три бизнес-компонента — Slider, ProductList и ProductCardSquare. Их интерфейс представлен на рис. 8.35 и 8.36.
442  Глава 8 Разработка фронтенд-приложения Рис. 8.35. Бизнес-компонент Slider Рис. 8.36. Бизнес-компонент ProductCard Внесем их в таблицу (табл. 8.17). Таблица 8.17. Компоненты для страницы «Главная» MainPage CatalogPage SearchResultPage ProductPage 1 2 3 4 Slider ProductCard Теперь внесем выявленные бизнес-компоненты в граф (рис. 8.37). Рис. 8.37. Бизнес-компоненты страницы «Главная»
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  443 Под номером 16 на рис. 8.37 — Slider, 17 — ProductList. Аналогичную процедуру на основе дизайна проделываем для всех остальных страниц и заносим эти компоненты в табл. 8.18. С целью обучения и наглядности для вас подготовлен специальный PDF-файл, где все компоненты из дизайна разделены по ролям. Он расположен в той же папке (8/artifacts), что и PDF-файл с дизайном, и называется Компоненты, выделенные из дизайна и сгруппированные по ролям.pdf. Прежде чем двигаться дальше, откройте его и рассмотрите колонку с выделенными бизнескомпонентами. Таблица 8.18. Бизнес-компоненты компонентов с ролью «Страница» Компонент «Страница» Номер Компоненты MainPage 1 CatalogPage 2 ProductCategories SearchResultPage 3 Menu Sorter И др. CategoryPage 4 SubcategoryPage 5 ProductPage 6 NewMark Presentation Price И др. BasketPage 7 ProductBasket TotalBasket OrderFormPage 8 NameForm DeliveryForm ConfirmationPage 9 ProductList Slider ProductCardSquare TotalOrderForm Полный список бизнес-компонентов для страницы SearchResultPage: Menu, Sorter, ViewMode, ProductList. Полный список бизнес-компонентов для страницы CategoryPage: Menu, Product- Categories, Sorter, ViewMode, ProductList. Полный список бизнес-компонентов для страницы SubcategoryPage: Menu, FilterCheckbox, Sorter, ViewMode, ProductList. Полный список бизнес-компонентов для страницы ProductPage : NewMark , Presentation, Price, AddFavorite, ProductDetails, ProductDescription. Может возникнуть вопрос по поводу компонента ProductList. На странице MainPage товар представлен в квадратном визуальном элементе, а на странице SearchResultPage — в прямоугольном. Если посмотреть файл Компоненты, выделенные из дизайна и сгруппированные по ролям.pdf, то там есть два компонента — ProductCardSquare и ProductCardRect. Если внимательно изучить дизайн, то станет очевидно, что компонент Price используется во многих местах. Однако напрямую — только в компоненте Product. Я счел, что это бизнес-компонент и пример того бизнес-компонента, который переиспользуется во множестве мест. Наносим бизнес-компоненты на граф. Теперь граф выглядит следующим образом (рис. 8.38).
444  Глава 8 Разработка фронтенд-приложения Рис. 8.38. Граф компонентов со всеми выявленными бизнес-компонентами Наименования компонентов приведу списком (на рисунке — сверху вниз и слева направо): y 10 — CommonLayout; y 11 — LayoutFooter; y 12 — LayoutHeader; y 13 — SearchAndBasketLayout; y 14 — ProductSearchForm; y 15 — BasketValue; y 1 — MainPage; y 2 — CatalogPage; y 3 — SearchResultPage; y 4 — CategoryPage; y 5 — SubcategoryPage; y 6 — ProductPage; y 7 — BasketPage; y 8 — OrderFormPage; y 9 — ConfirmationPage; y 16 — Slider; y 17 — ProductList; y 18 — ProductCategories; y 19 — Menu; y 20 — Sorter; y 21 — ViewMode; y 22 — FilterCheckbox; y 23 — NewMark; y 24 — Presentation; y 25 — Price;
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  445 26 — AddFavorite; 27 — ProductDetails; 28 — ProductDescription; 29 — ProductBasket; 30 — TotalBasket; 31 — NewForm; 32 — DeliveryForm; 33 — TotalOrderForm; 34 — ProductCardRect; 35 — ProductCardSquare. Простой конечный компонент. Выделим заголовок, то есть компонент Header. Это стандартный простой конечный компонент (рис. 8.39). y y y y y y y y y y Рис. 8.39. Связи простого конечного компонента Header Если вы посмотрите дизайн, то обнаружите, что компонент Header с разными свойствами шрифта присутствует почти в каждом компоненте. Имеет смысл создавать некий набор из заголовка и текста, на английском обычно именуемый Typography. Как правило, такой набор, причем состоящий из малого количества компонентов, может представлять весь необходимый спектр элементов дизайна — в том, что касается текстового оформления. Другой стандартный конечный компонент — Button (рис. 8.40). Рис. 8.40. Связи простого конечного компонента Button
446  Глава 8 Разработка фронтенд-приложения Он, как и компонент Header, используется в двух типах ролей — «Страницы» и «Бизнес-компоненты» (рис. 8.41, 8.42). Рис. 8.41. Связи простого конечного компонента Breadcrumbs Рис. 8.42. Связи простого конечного компонента Text Если нанести простые конечные компоненты на граф компонентов, приведенный ранее, то получится путаница, будет трудно разобрать связи и даже номера других компонентов. Поэтому они показаны отдельно. Однако при создании вы можете использовать другую площадь для нанесения, например в графическом редакторе, — тогда все поместится. Но я могу точно сказать, что в этом нет необходимости. Провайдер. Если взглянуть в дизайне на страницу MainPage, то станет видно, что в хедере ссылка «Главная» выделена серым цветом, а ссылка «Каталог» — стандартным цветом непосещенных ссылок. Это значит, что данный компонент должен указывать текущий раздел. Самое оптимальное здесь — задействовать возможности библиотеки React Router по стилизации ссылки текущего раздела. По этой причине компонент «Лэйаут» (LayoutCommon) должен управляться библиотекой, а значит, самый общий лэйаут должен быть дочерним элементом компонента «Провайдер» (RouterProvider) из библиотеки React Router, что показано на рис. 8.43. Если бы такой необходимости не было, то можно было бы «Провайдер» поместить в «Лэйаут» как дочерний. Рис. 8.43. Граф компонентов с компонентом RouterProvider
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  447 Шаг 4. Задание глобального состояния приложения или его части и дополнение графа компонентов приложения компонентом с ролью «Провайдер», а если требуется — дополнительными компонентами, зависящими от состояния. При взгляде на дизайн становится очевидным глобальное состояние корзины. Находясь на одной странице товара и добавляя его в корзину, затем возвращаясь в каталог, переходя на другую карточку товара и добавляя товар в корзину, мы должны видеть ее актуальное состояние. Поскольку у нас первая версия, нет необходимости сохранять это состояние в базе данных на сервере — достаточно сохранять на клиенте (табл. 8.19). У вас может быть другое мнение. Таблица 8.19. Свойства состояния стора ReduxToolkit (часть 1) Свойство Что сохраняется basket : { products: {}, total: 0, count: 0 } Добавленные в корзину товары, их перечень по ID, их количество по позициям, полная стоимость товаров с учетом количества и полное количество единиц товаров Другие, с моей точки зрения, важные свойства стора: y критерий сортировки (по цене, популярности) и порядок; y режим просмотра (квадратные или прямоугольные окошки с товарами); y пагинация; y фильтрация. Свойства с 1-го по 3-е должны быть применены на страницах Search­ResultPage, CategoryPage и SubcategoryPage, а свойство 4 — только на странице SubcategoryPage. Свойства 1, 3 и 4 участвуют в запросах. Эти запросы осуществляются при задействовании RTK Query, который кэширует параметры запросов и полученные данные, и если параметры не изменились, то повторно сетевой запрос не делается. Это значит, что если, находясь в результатах поиска, мы перейдем на страницу товара, то, покинув результаты поиска, эта страница как React-компонент будет размонтирована из DOM. При возвращении назад со страницы товара страница результатов поиска как компонент вновь монтируется в DOM, и вам необходимо восстановить состояние. Передав из глобального состояния номер страницы (пагинация), критерий сортировки и порядок, вы тем самым передадите те же параметры в эндпоинт RTK Query и получите те же самые данные (не через запрос, а из кэша), что и в последний раз при переходе на страницу товара. Однако и пользователь окажется на странице, которая будет в таком же состоянии, что была до того, как он ее покинул. Добавим наши новые свойства в таблицу с состоянием (табл. 8.20). Для удобства пользователя, с моей точки зрения, данные формы в разделе «Форма заказа» нужно хранить в глобальном состоянии — все по той же причине. Если пользователь зашел на форму заказа, заполнил сведения о себе и выбрал способ доставки, потом перешел в каталог, чтобы добавить еще товар, а затем вернулся в корзину и перешел на форму заказа, то внесенные ранее данные будут
448  Глава 8 Разработка фронтенд-приложения восстановлены из глобального состояния и заполнены. Это создаст положительный опыт. Сохраним как фамилию, имя и номер телефона, так и выбранный способ доставки (табл. 8.21). Таблица 8.20. Свойства состояния стора ReduxToolkit (часть 2) Свойство Что сохраняется sorting: { search: { type: null, order: null }, category: …, subcategory: … } Сортировка для трех страниц: 1) результаты поиска; 2) категории; 3) подкатегории viewMode: { search: "square", category: "square", subcategory: "square" } Режим просмотра списка товаров pagination: { search: 0, category: 0, subcategory: 0 } Пагинация для запросов filter: { subcategory: { type: "manufacturer", value: null } } Фильтрация по производителю Таблица 8.21. Свойства состояния стора ReduxToolkit (часть 3) Свойство Что сохраняется customer: { name: null, secondName: null, phone: null } Данные покупателя, внесенные в форму заказа delivery: { service: null, cost: 0 } Данные по выбранному способу доставки Находясь на странице товара, пользователь может добавить его в избранное. Поскольку у нас первая версия, пока мы храним данные избранных товаров на клиенте. Добавим в глобальное состояние выбор пользователя (табл. 8.22). Разумеется, такие данные нужно сохранять в локальное хранилище браузера. Таблица 8.22. Свойства состояния стора ReduxToolkit (часть 4) Свойство Что сохраняется favorite: { } Данные о добавленных ID товаров в избранное. Сохраняются ID, дата и время добавления У нас получилось восемь свойств в сторе. Добавим для глобального состояния компонент «Провайдер» и нанесем его на граф компонентов. Другие компоненты, которые выводятся в зависимости от состояния, не нужны (рис. 8.44).
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  449 Рис. 8.44. Дерево компонентов с компонентом StoreProvider для Redux Toolkit Шаг 5. Определение, с какими внешними системами какие компоненты взаимодействуют. Предположим, что уже существует разработанный API. В таком случае заполнение табл. 8.23 не отнимет много времени. Совсем другая ситуация, когда его нет. Тогда вам придется сначала потратить время на разработку API и только потом приступить к маппингу компонентов на эндпоинты. Таблица 8.23. Эндпоинты и компоненты Компонент Эндпоинт Комментарий MainPage GET /api/v1/products/special Специальные предложения, доступные всем GET /api/v1/products/popular Популярные товары всего каталога GET /api/v1/products/lastordered Последние заказанные товары во всем магазине GET /api/v1/menu/categories Список категорий каталога, отображаемых в меню GET /api/v1/products/categories/ Популярные товары по категориям CatalogPage popular Продолжение 
450  Глава 8 Разработка фронтенд-приложения Таблица 8.23 (продолжение) Компонент Эндпоинт CategoryPage GET /api/v1/menu/categories GET /api/v1/menu/categories/ Список подкатегорий одной указанной категории GET /api/v1/products/categories/ Популярные товары конкретной категории, например «Бытовая техника» {categoryId}/subcategories {categoryId}/subcategories/ popular SubcategoryPage Комментарий GET /api/v1/menu/categories GET /api/v1/menu/categories/ {categoryId}/subcategories GET /api/v1/products/categories/ {categoryId}/subcategories/ {subcategoryId} Товары каталога, находящиеся в подкатегории выбранной категории. Например, подкатегория «Холодильники» в категории «Бытовая техника» ProductPage GET /api/v1/products/{productId} Все данные по конкретному товару SearchResultPage GET /api/v1/menu/categories GET /api/v1/products/search Результаты поиска среди товаров по введенному пользователем запросу DeliveryForm GET /api/v1/delivery/services Список способов передачи заказа клиенту и сведения по ним OrderFormPage POST /api/v1/orders/create Эндпоинт создания заказов Поскольку в компонентах SearchResultPage, CategoryPage и SubcategoryPage подразумевается фильтрация по цене и популярности, а в компоненте SubcategoryPage еще и по производителю, то эндпоинты с методами запросов GET должны поддерживать согласованные параметры (табл. 8.24). Например, можно использовать формат OData1. Таблица 8.24. Пути в URL и рендерируемые компоненты 1 Компонент Путь в URL CatalogPage /catalog SearchResultPage /search OData — формат параметров в виде соглашений. Ознакомиться можно здесь: https://docs. oasis-open.org/odata/odata/v4.0/os/part1-protocol/odata-v4.0-os-part1-protocol.html#_­ Toc372793691.
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  451 Компонент Путь в URL BasketPage /basket OrderFormPage /order-form ConfirmationPage /confirmation CategoryPage /catalog/:category SubcategoryPage /catalog/:category/:subcategory ProductPage /catalog/:category/:subcategory/:productId Шаг 6. Определение, по какому пути в URL какой компонент будет рендериться. Теперь мы знаем структуру компонентов приложения интернет-магазина и знаем, какое должно быть глобальное состояние, какие компоненты с какими эндпоинтами взаимодействуют и какие компоненты будут рендериться при изменении URL. Поэтому теперь переходим ко второму этапу — к реализации. 8.5.2. Этап 2. Реализация 3RApp Настройки и подготовка. Реализация начинается с подготовки, а затем происходит само написание кода компонентов и других составляющих компонентов. В качестве эндпоинтов используем собственный сервер с заглушками, который будет работать через прокси от CRA. Приложение использует ReactRouter, значит, компонента с ролью «Приложение» не будет. Поэтому при подготовке файлов удаляем компонент App.js, файл стиля и тест, а также файл для внедрения аналитики. Для CSS будем использовать CSS Modules. Реализация. При реализации держим под рукой граф компонентов, показанный на рис. 8.44, и дополнительные изображения связей простых конечных компонентов с рис. 8.39–8.42. И еще таблицу компонентов страниц и бизнес-компонентов (табл. 8.18). Для вашего удобства эти артефакты вынесены в отдельные PDF-файлы, расположенные в репозитории кода, в папке 8/artifacts. Все компоненты реализованного приложения можно посмотреть в репозитории, в папке 8/internet-shop. Там они в полностью реализованном состоянии. А в папке 8/8.26-8.29 — в том состоянии, в котором приводятся здесь в качестве листингов. В этом разделе для наглядности приводится пример только некоторых составляющих и компонентов. Они в таком состоянии, будто вы их последовательно реализуете, чтобы быстрее привыкнуть к новому подходу. Реализация осуществляется последовательно, сверху вниз, начиная с компонента родителя. В нашем случае это компонент 41 — StoreProvider. Компонент предоставляется библиотекой. Нам необходимо обеспечить реализацию слайсов и создание общего редьюсера. Вот листинг первого слайса для состояния сортировки страниц (листинг 8.26).
452  Глава 8 Разработка фронтенд-приложения Листинг 8.26. Слайс для состояния сортировки страниц SearchResultPage, CategoryPage, SubcategoryPage, src/store/sortingSlice.js import { createSlice } from '@reduxjs/toolkit'; const initialState = { search: { type: null, // Тип сортировки 'price', 'popularity', null order: null // Порядок сортировки: 'asc', 'desc', null }, // страница поиска category: { type: null, order: null }, // категория subcategory: { type: null, order: null } // подкатегория }; const sortingSlice = createSlice({ name: 'sorting', initialState, reducers: { setSortingType(state, action) { const { page, type } = action.payload; state[page].type = type; }, setSortingOrder(state, action) { const { page, order } = action.payload; state[page].order = order; }, resetSorting(state, action) { const { page } = action.payload; state[page].type = null; state[page].order = null; }, } }); export const { setSortingType, setSortingOrder, resetSorting, } = sortingSlice.actions; export const selectSortingType = (page) => (state) => state.sorting[page].type; export const selectSortingOrder = (page) => (state) => state.sorting[page].order; export default sortingSlice.reducer;
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  453 Последовательно создаем все слайсы для режима просмотра, пагинации, фильтрации по производителю, данные покупателя, выбранный способ доставки и избранное товаров. И переходим к следующему компоненту — 40 — RouterProvider, который тоже поставляется библиотекой. Нам необходимо реализовать объект конфигурации маршрутизатора (листинг 8.27). Листинг 8.27. Объект конфигурации маршрутизатора в состоянии, когда только началась реализация приложения, src/routes.js import { createBrowserRouter, Outlet } from 'react-router-dom'; export const routes = createBrowserRouter([{ path: '/', element: <div>CommonLayout <Outlet /></div>, children: [{ path: '/', element: <div>SearchAndBasketLayout<Outlet /></div>, children: [{ index: true, element: <div>MainPage</div>, }, { path: 'catalog', element: <div>CatalogPage</div>, }, { path: 'search', element: <div>SearchResultPage</div>, }, { path: '/catalog/:category', element: <div>CategoryPage</div>, }, { path: '/catalog/:category/:subcategory', element: <div>SubcategoryPage</div>, }, { path: '/catalog/:category/:subcategory/:productId', element: <div>ProductPage</div>, },], }, { path: '/basket', element: <div>BasketPage</div>, }, { path: '/order-form', element: <div>OrderFormPage</div>, }, { path: '/confirmation', element: <div>ConfirmationPage</div>, }] }]);
454  Глава 8 Разработка фронтенд-приложения Из объекта конфигурации видно, что ни один компонент еще не реализован. По мере реализации объект обновляется. Это предусмотрено в этапе «Другие действия», шаг 30, в процедуре реализации компонента. Поскольку часть кода уже реализована, можно протестировать работу. На данном этапе важно протестировать пути. Запускаем приложение командой npm start, находясь в директории, набираем в адресной строке браузера пути и отмечаем для себя, ведет ли приложение себя ожидаемым или неожидаемым образом, а именно — тот ли компонент рендерится на указанный вами путь. Разумеется, вместо сегментов-параметров нужно подставить какое-нибудь значение. Все работает, двигаемся дальше. Теперь приступаем к реализации компонента 10 — CommonLayout. В подразделе 8.4.1 мы уже рассматривали компоненты с ролью «Лэйаут» и изучать его листинг не будем. После реализации кода CommonLayout обновляем объект конфигурации и двигаемся дальше. Нужно реализовать дочерние компоненты CommonLayout — LayoutFooter, LayoutHeader и SearchAndBasketLayout. Последний, в свою очередь, является лэйаутом для компонентов уровня страницы. Помимо указанных дочерних компонентов, у CommonLayout есть и страницы. Как говорилось ранее, начинать можно с любой страницы. Обычно начинают с самой важной или с первой. Как только реализованы компоненты LayoutFooter, LayoutHeader и SearchAndBasketLayout, а последний находится в объекте конфигурации, обновим и его, затем приступаем к реализации страниц (листинг 8.28). Компоненты страниц реализуем по порядку, в такой последовательности: 1) 2) 3) 4) 5) 6) 7) 8) 9) MainPage; CatalogPage; SearchResultPage; CategoryPage; SubcategoryPage; ProductPage; BasketPage; OrderFormPage; ConfirmationPage. Листинг 8.28. SubcategoryPage, src/pages/SubcategoryPage.js import React, {Fragment} from "react"; import {useParams, useNavigate} from "react-router-dom"; import {useSelector} from 'react-redux'; import { useGetMenuCategoriesQuery, useGetMenuSubcategoriesQuery } from "../../store/menuAPI"; import {useGetProductsSubcategoriesQuery} from "../../store/productsAPI";
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  455 import { selectSortingType, selectSortingOrder } from "../../store/sortingSlice"; import { selectViewMode } from "../../store/viewModeSlice"; //import {ProductList, Menu, Sorter, ViewMode, FilterCheckbox} from './business'; //import {Header, Spinner} from './components'; import styles from "./SubcategoryPage.module.css"; // Все @TODO Удалить после реализации и импорта компонентов export const SubcategoryPage = (/** Шаг 1 не нужен */) => { const navigate = useNavigate(); // Шаг 6 const sortingType = useSelector(selectSortingType('subcategory')); const sortingOrder = useSelector(selectSortingOrder('subcategory')); const viewMode = useSelector(selectViewMode('subcategory')); // Шаг 10 const {category, subcategory} = useParams(); // Шаг 2 const { isLoading: isLoadingCategory, error: errorCategory, data: dataCategory } = useGetMenuCategoriesQuery(); const { isLoading: isLoadingSubcategory, error: errorSubcategory, data: dataSubcategory, } = useGetMenuSubcategoriesQuery(category); const { isLoading: isLoadingProducts, error: errorProducts, data: dataProducts, refetch: refetchProducts, } = useGetProductsSubcategoriesQuery({ category, subcategory }); // Шаг 20 const handleChangeSubcategory = (newSubcategory) => { navigate(`/catalog/${category}/${newSubcategory}`); } // Шаг 18 if (!isLoadingCategory || !isLoadingSubcategory || !isLoadingProducts) { return (<Fragment>"Загрузка..."{/**@TODO <Spinner /> */}</Fragment>); } const [categoryTitle, subcategoryTitle] = [ dataCategory.selected, dataSubcategory.selected]; return ( <Fragment> {/** Шаг 13 */} <aside>Menu{/**@TODO <Menu * className={styles.menu} * selectedCategory={categoryTitle.code} * selectedSubcategory={subcategoryTitle.code} * category={dataCategory} * subcategory={dataSubcategory} /> */} </aside>
456  Глава 8 Разработка фронтенд-приложения } ); <header>{subcategoryTitle.title}{/**@TODO <Header * className={styles.header} * title={subcategoryTitle.title} * />} */}</header> <section> {/**@TODO <FilterCheckbox * className={styles.filterCheckbox} * onChange={handleChangeSubcategory} * list={dataSubcategory} /> */} </section> <section> <div>{sortingType}, {sortingOrder} {/**@TODO <Sorter * className={styles.sorter} * type={sortingType} * order={sortingOrder} /> */} </div> <div>{viewMode} {/**@TODO <ViewMode * className={styles.viewMode} * mode={viewMode} /> */} </div> </section> <section> {/** Также Шаг 16 */} {/**@TODO <ProductList * className={styles.productList} * productList={dataProducts} /> */} </section> </Fragment> После реализации всех компонентов уровня страницы переходим к первому компоненту и начинаем реализацию всех компонентов вглубь (уровень — страница). Самая первая у нас — MainPage, начинаем с нее. Для данной страницы, глядя на граф компонентов и в таблицу, нужно реализовать бизнес-компоненты Slider и ProductList. У компонента ProductList тоже необходимо реализовать бизнескомпоненты ProductCardRect и ProductCardSquare. Затем переходим к простым конечным компонентам. Для Slider и ProductList реализуем простой конечный компонент Header. Затем для ProductCardRect и ProductCardSquare — простой конечный компонент Button, а для ProductCardRect — еще и Text. Далее переходим к CatalogPage, для которой нужно реализовать лишь ProductCategories. Причем компоненты Header и ProductList уже реализованы: первый достаточно просто импортировать, а второй не требует дополнительных действий (реализован со всем необходимым).
8.5. Использование процедуры проектирования 3RApp и реализация 3RApp-приложения  457 Так следует поступать с каждой страницей, для которой нужно реализовать бизнес-компоненты. Для последних необходимо реализовать или меньшие бизнескомпоненты, или простые конечные компоненты. На этом реализация заканчивается. Чем дальше вы движетесь по страницам слева направо, тем меньше требуется реализовывать и бизнес-компонентов, и простых конечных компонентов. При таком подходе, заканчивая какой-либо компонент, вы должны протестировать его работоспособность. Я рекомендую действовать по процедуре создания компонента, чтобы экономить время и не заниматься выводами в консоль. То есть сначала реализовать компонент по процедуре, а затем перейти к тестированию. Посмотрим на пример бизнес-компонента FilterCheckbox (листинг 8.29). Этот компонент является дочерним для страницы SubcategoryPage. Листинг 8.29. FilterCheckbox, src/business/FilterCheckbox.js import styles from './FilterCheckbox.module.css'; // Шаг 1 export const FilterCheckbox = ({ onChange, current, list }) => { // Шаг 16 return ( <nav> { list.map((item) => { const {id, code, title} = item; return (<span className={current === code ? styles.current : styles.alternative} key={id} onClick={() => onChange(code)}> {title} </span>); }) } </nav> ); }; Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 8.5. Выполните упражнения по порядку. y Затем откройте папку problems и в ней — подпапку 8.5. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 8.5 и ознакомьтесь с моим решением.
458  Глава 8 Разработка фронтенд-приложения 8.6. Использование нейросетей во фронтенд-разработке 8.6.1. Причина Зачем проделывать путь длиной в целую книгу, чтобы прийти к такому помощнику, как искусственный интеллект (ИИ) в виде нейросети, способный практически все сделать за нас? Я лично убежден в следующем: чтобы создавать программные системы с помощью нейросети, необходимо знать, как устроены программные системы. Вам придется задавать очень точные вопросы, а без знания устройства программных систем это невозможно. Получить готовый продукт, работающий как полагается, — тоже. Надежды на «вдруг повезет» здесь не работают. 8.6.2. Новое поколение инструментов В самом начале книги, в главе 2, я рассказал о становлении области фронтенд-разработки и на рис. 1.1 оставил вопрос на четвертой эпохе. Вы наверняка догадались, что она уже наступила. Это эпоха автоматизации выполнения задач по написанию кода. Сегодня вы можете использовать несколько разных сервисов, работающих на основе искусственного интеллекта. Я расскажу о GigaChat («ГигаЧат») и о том, как он может помочь в разработке фронтенд-приложения. Сегодня, когда рынок ИИ только начинает свой стремительный рост, работодатели не предъявляют особых требований к умению использовать его в работе. Завтра все изменится. Необходимо как можно раньше взять ИИ к себе в помощники, но не на замену. Сегодня искусственный интеллект, к сожалению, способен заменить простого кодера-джуна. Однако это прогресс с точки зрения автоматизации. 8.6.3. GigaChat GigaChat — сервис на основе искусственного интеллекта, разработанный «Сбером». Он способен вести диалог с пользователем, генерировать тексты и изображения, а также писать программный код. GigaChat использует генеративный ИИ на базе больших языковых моделей и основан на нейросетевом ансамбле NeONKA, включающем в себя модели ruGPT-3.5, Kandinsky 2.1, ruCLIP и FRED-T5. Обучение нейросетевой модели производится с помощью метода оценки. GigaChat поддерживает русский и английский языки, может использоваться для решения разных задач, в том числе для создания контента, разработки программного обеспечения и генерации изображений. 8.6.4. Как задействовать Наша задача — посмотреть, как использовать GigaChat для написания кода. Вы можете идти двумя путями: использовать сервис или установить плагин GigaCode (рис. 8.45).
8.6. Использование нейросетей во фронтенд-разработке  459 Рис. 8.45. Диалоговое окно GigaChat GigaChat располагается по адресу https://giga.chat/, плагин GigaCode — на https:// gitverse.ru. Вам нужно или зарегистрироваться, или войти по SberID. Если вы пользуетесь услугами «Сбера», то, скорее всего, он у вас уже есть. Если нет, то получать его недолго. Работать можно с помощью текста или голоса либо загружать файлы. Вот пример голосового ввода (рис. 8.46). Рис. 8.46. Ввод с помощью микрофона
460  Глава 8 Разработка фронтенд-приложения Также напомню, что в диалоговом окне есть справка, которая поможет освоиться. Установленный плагин GigaCode и открытый для работы с ним чат выглядит так, как показано на рис. 8.47. Рис. 8.47. Плагин GigaCode в диалоговом режиме С плагином GigaCode можно взаимодействовать двумя способами. Первый — диалоговый, как и у GigaChat, через браузер. У него есть специальное поле для ввода текста. Второй — в области редактирования кода, с помощью запросов или во время написания текста кода. В последнем случае GigaCode будет предлагать автодополнение. Пример автодополнения показан на рис. 8.48. О запросах в области редактирования речь пойдет далее. Рис. 8.48. Автодополнение 8.6.5. Немного про «общение» с ИИ Искусственный интеллект позволяет вести с ним диалог, будто вы общаетесь с живым человеком. Поэтому первое время, если вы еще не пользовались ИИ, будете в восторге. Однако для работы надо учитывать пару важных моментов. Посылаемые вами фразы — команды для модели, работающей на машине. Модель обучилась «понимать» входящие в нее предложения, составленные из слов, и генерирует в ответ такой набор слов, который с наибольшей вероятностью соответствует входящему запросу. То же касается изображений и музыки.
8.6. Использование нейросетей во фронтенд-разработке  461 Команды называются промптами. Промпт (prompt) — входные данные, которые используются для генерации ответа или выполнения задачи с помощью искусственного интеллекта. Промпт может быть представлен в виде текста, изображения, формулы или других типов информации. Он определяет качество генерации контента и успех решения задачи. При разработке промпта необходимо максимально точно и конкретно формулировать инструкцию и задачу, которую вы хотите решить. Чем подробнее в промпте описана задача, тем лучше результат. Мы будем использовать текстовые промпты. GigaChat является генеративным ИИ. Это тип системы ИИ, способной генерировать новые данные, такие как текст, изображения или другой контент, на основе обучающих данных. Общаясь с генеративным ИИ, иногда вы будете замечать некоторые «странности». Они называются галлюцинациями. Галлюцинация у искусственного интеллекта (ИИ) — уверенная реакция или ответ, который не подтверждается данными обучения ИИ или не имеет отношения к действительности. Я столкнулся с галлюцинациями, когда просил рекомендовать книги из определенной области. Мне выдавался некий список, я с энтузиазмом начинал искать книги в интернете, и тут — бабах! Оказывается, ни книг, ни их авторов в помине нет. Это не должно пугать, просто нужно понимать, что происходит. ИИ — не просто одноразовое общение. GigaChat может сохранять контекст беседы. Контекст — это запоминание всей информации, которую вы сообщаете GigaChat, и ее использование для последующих ответов. Итак, вы можете использовать промпт, задействовать контекст и должны следить за галлюцинациями. Вот и вся механика работы пользователя с ИИ. 8.6.6. Используем GigaChat для написания кода Теперь нам пригодятся все знания, полученные ранее. Создадим первый промпт: Напиши код React-компонента, который при первом рендере делает запрос к данным на сервер. Запрос выполняется из кастомного хука и сохраняется в переменной состояния. Листинг 8.30. Ответ GigaChat на промпт import React, { useState, useEffect } from 'react'; import axios from 'axios'; const CustomHook = () => { const [data, setData] = useState({}); // Кастомный хук для запроса данных const fetchData = async () => { try { const response = await axios.get('https://example.com/api'); setData(response.data);
462  Глава 8 Разработка фронтенд-приложения } catch (error) { console.error('Error fetching data:', error); } }; // Вызываем хук при первом рендере useEffect(() => { fetchData(); }, []); return { data }; }; export default function App() { const { data } = CustomHook(); if (!data) { return <div>Загрузка...</div>; } } return ( <div> {/* Используем данные из состояния */} <h1>{data.title}</h1> <p>{data.description}</p> </div> ); В упражнениях и практических заданиях я предлагаю вам потренироваться и создать разные виды составляющих приложения с помощью GigaChat. Код компонентов он умеет писать. Попробуем дать задание на создание структуры данных. Промпт: Создай JSON массива с пятью элементами, где каждый объект представляет собой учебный предмет в университете. Листинг 8.31. Ответ из пяти элементов на промпт [{ "name": "Математика", "credits": 5, "prerequisites": ["Алгебра", "Геометрия"] }, { }, … "name": "Физика", "credits": 4, "laboratory": true Приводить все элементы не имеет смысла. Промпт создали, но, допустим, вам маловато данных. Значит, нужно задействовать контекст.
8.6. Использование нейросетей во фронтенд-разработке  463 Новый промпт: Дополни каждый объект приведенного выше JSON тремя свойствами, описывающими учебный предмет. Листинг 8.32. Обогащенный JSON через задействование контекста { }, "name": "Математика", "credits": 5, "prerequisites": ["Алгебра", "Геометрия"], "description": "Курс математических методов...", "teacher": "Профессор Иванов", "classroom": "Аудитория 101" Обращаю внимание, что я не копировал и не вставлял в диалоговое окно предыдущий сгенерированный JSON. Кроме того, GigaChat дополнил код пояснением, какие свойства он добавил. 8.6.7. Используем GigaCode для написания кода Взаимодействовать с GigaCode в области редактирования кода можно двумя способами. Первый, как было показано ранее, — автодополнение. Второй способ — создание промпта с помощью комментария. Вы пишете промпт как комментарий и жмете Enter. Но данный способ не всегда работает, поэтому его не очень удобно задействовать. Вот пример такого промпта (рис. 8.49). Рис. 8.49. Промпт через комментарий Или другой промпт в качестве автодополнения (рис. 8.50). Рис. 8.50. Генерация через автодополнение Как прозвучало в определении промпта, его необходимо максимально точно конструировать, чтобы получить наиболее точную генерацию. Слишком общие промпты дают слишком обобщенные результаты.
464  Глава 8 Разработка фронтенд-приложения Давайте проверим этот момент. Создадим промпт и отправим его в диалоговое окно плагина. Например: Сгенерируй React-компонент, который обменивается данными с сервером по WebSocket через кастомный хук. Данные выводятся в виде таблицы. Таблица взята из библиотеки antd. В таблице есть сортировка и фильтры по каждой колонке. Всего колонок 5: город, компания, численность персонала, с какого года клиент, число серверов. Листинг 8.33. Ответ на промпт import React, { useState, useEffect } from 'react'; import { Table, Input, Button } from 'antd'; import useWebSocket from 'react-use-websocket'; const columns = [ { title: 'Город', dataIndex: 'city', sorter: (a, b) => a.city.localeCompare(b.city), filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => ( <div> <Input onChange={e => setSelectedKeys(e.target.value)} /> <Button onClick={() => confirm()} typeprimary> Применить </Button> <Button onClick={() => clearFilters()}> Сбросить </Button> </div> ), filterIcon: () => <SearchOutlined />, onFilter: (value, record) => record.city.includesvalue, }, { title: 'Компания', dataIndex: 'company', sorter: (a, b) => a.company.localeCompare(b.company), filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => ( <div> <Input onChange={e => setSelectedKeys(e.target.value)} /> <Button onClick={() => confirm()} type="primary"> Применить </Button> <Button onClick={() => clearFilters()}> Сбросить </Button> </div> ), filterIcon: () => <SearchOutlined />, onFilter: (value, record) => record.company.includes(value), },
8.6. Использование нейросетей во фронтенд-разработке  465 { }, { title: 'Численность персонала', dataIndex: 'staffCount', sorter: (a, b) => a.staff - b.staff, title: 'С какого года клиент', dataIndex: 'clientSince', sorter: (a, b) => a.clientSince - b.clientSince, }, { title: 'Число серверов', dataIndex: 'serverCount', sorter: (a, b) => a.serverCount - b.serverCount, }, ]; const WebsocketTable = () => { const [data, setData] = useState([]); const { sendMessage, lastMessage } = useWebSocket('://:8080', { onOpen: () => console.log('Connected'), onMessage: (message) => { setData([...data, JSON.parse(message.data)]); }, onError: (error) => console.log('Error:', error), }); useEffect(() => { if (lastMessage !== null) { setData([...data, JSON.parse(lastMessage.data)]); } }, [lastMessage]); return ( <Table columns={columns} dataSource={data} pagination={} rowKey="id" /> ); }; export default WebsocketTable; Признаюсь: мне пришлось исправить несколько синтаксических недочетов, но код в листинге представлен в том виде, в котором его сгенерировал GigaChat. При этом в свойство pagination ничего не было помещено. Там должен находиться объект конфигурации пагинации. Для деталей нужно посмотреть документацию к компоненту Table библиотеки antd. И данный недочет с пагинацией не является галлюцинацией.
466  Глава 8 Разработка фронтенд-приложения Вывод о том, что чем точнее промпт, тем более конкретный ответ вы получите, подтверждается нашим промптом. Совершенно нормально уточнять промпт для генерации компонентов еще больше, нежели показано в примере. Прочтите также подраздел 8.6.1. Надеюсь, данный инструмент вам поможет. Упражнения и практические задания Для данного раздела предусмотрены упражнения и практические задания. y Откройте папку drills и в ней — подпапку 8.6. Выполните упражнения по по- рядку. y Затем откройте папку problems и в ней — подпапку 8.6. Выполните практическое задание. y После выполнения практического задания откройте папку answers, в ней — подпапку 8.6 и ознакомьтесь с моим решением.
Глава 9 СОВМЕСТНАЯ РАБОТА НАД ПРИЛОЖЕНИЕМ 9.1. Как изучать существующее приложение В большинстве случаев, устроившись в новую компанию, вы будете работать с созданными приложениями. Более того, даже если вы создаете новое приложение, через мгновение оно таковым уже не является. Поэтому бо́льшую часть времени разработчики занимаются доработкой существующих приложений. Нужно ли тратить время на изучение? Если да, то с чего начать, что изучать и сколько времени на это тратить? Изучение приложения возможно в двух случаях: y вы берете задачу (баг или фичу1), создаете решение и изучаете прямо в момент создания решения; y целенаправленно изучаете составляющие для ознакомления. Объекты, которые нужно изучать, делятся на два списка. Первый — это связи, роли компонентов и подсистемы (некоторый набор связанных компонентов, выполняющих определенную роль, например авторизация), то есть все, что относится к структуре, а не к обработке данных. Второй — это функции, хуки, структуры данных, константы, переменные, используемый API и используемая функциональность в виде законченных модулей (например, библиотека Redux Toolkit), то есть все, что относится к обработке данных, а не к созданию структуры. Одновременно изучать все подряд бессмысленно. Приложение всегда большое и состоит из подсистем. Никто не будет выделять вам время специально на изучение. Поэтому ниже я приведу два метода, которые можно использовать для знакомства с приложением. Из изученных составляющих, перечисленных выше, у вас постепенно сложится полная картина. 9.1.1. Метод погружения Данный метод проводится в два этапа: 1) нахождение компонента; 2) детальное изучение элементов, входящих в компонент, которые относятся к решаемой задаче. 1 Фича (от англ. feature) — любая функциональность приложения, которая существует или может быть добавлена.
468  Глава 9 Совместная работа над приложением Метод носит такое название потому, что вы спускаетесь вниз по иерархии компонентов, а затем детально изучаете составляющие. Применяется при решении бага или создании новой фичи. Сам по себе метод очень быстрый и вполне естественный для разработчиков. Как использовать. Этап 1. Вариант первый — нахождение компонента. Нужно найти компонент, относящийся к вашей задаче. Ранее мы рассматривали создание приложения 3RApp. Чаще всего React-приложения включают в себя библиотеку React Router. Поэтому вы без труда сможете определить дерево рендера, которое относится к нужному разделу. От уровня страницы погружаетесь по ссылкам до того уровня, на котором вам необходимо внести изменения. Под выражением «по ссылкам» понимается следующее действие. Вы заходите в компонент уровня страницы. У компонента есть вывод (все, что возвращается инструкцией return из компонента, как интерфейс). Среди него присутствуют другие, дочерние компоненты (как правило, не один). Теперь вам нужно проходить по веткам и осуществлять поиск нужного компонента среди дочерних. И так уровень за уровнем. Когда нужно исправить баг, то, как правило, если приложение разрабатывается давно, это бизнес-компоненты. Или, если вам нужно внести допфункциональность, а приложение еще молодое, то, может, придется создавать в том числе простые переиспользуемые компоненты. Если вы дошли до нужного компонента, значит, уже обозрели связи и одну ветку (точнее, путь от компонента-страницы до компонента нижнего уровня) структуры какого-то конкретного раздела приложения. Вариант второй — обходной, с привлечением IDE. Если не удается найти нужный компонент указанным выше способом, то можно использовать поиск по текстам, CSS-классам или HTML-элементам с определенными атрибутами и, возможно, с конкретными значениями. Это обходной метод, с привлечением возможностей IDE, позволяющий быстро искать среди текстов в файлах. Сценарии JavaScript — текстовые файлы. При разработке некоторые тексты в сценариях помещаются как литерал, который является константным. Текст выводится как есть на экран, и по нему можно искать компонент, где эта константа расположена. Если же она вынесена, то, найдя константу, нужно понять, где она используется. Такой поиск делается в IDE по всем файлам. Точно так же, с помощью поиска, можно найти определенный CSS-класс или HTML-тег. Однако если компонент импортирован из библиотеки, то он не попадет в поле зрения поиска. Этап 2. Детальное изучение элементов функциональности, входящих в состав компонента. Составляющие, которые нужно детально изучить, — это другие компоненты, используемые в целевом компоненте, функции, хуки, обработчики событий, структуры данных, константы и переменные, а также используемый API и большие функциональные модули, то есть все, что обрабатывает данные. В зависимости от решаемой задачи вы дорабатываете или ищете, в чем причина некорректной работы, и создаете решение, одновременно изучая составляющие. В этом шаге внесение изменений переплетено с изучением, но, по сути, не относится
9.1. Как изучать существующее приложение  469 к нему. При этом можно точно сказать, что без изучения доработки невозможны, поскольку иначе, если вы не понимаете устройство и порядок работы компонента, вы не сможете правильно написать код. Процедура чтения компонента описана в подразделе 5.19.4. Изучение, то есть метод погружения, возможно только в конкретный момент, когда нужно разработать что-то новое или исправить уже существующее. После исправления вы знаете, как устроен участок вашего приложения. Вывод напрашивается сам собой: чем больше задач вы берете в работу, тем лучше узнаете приложение, тем быстрее и проще разрабатываете и исправляете. Иногда при доработке подсистемы полезно изучить ее составляющие и связи в целом, а не только на изменяемом участке. 9.1.2. Формирование структуры Понимание приложения складывается, когда у вас есть представление о структуре компонентов и их связях. При использовании этого метода вы не погружаетесь в детали реализации, такие как функции, хуки, обработчики, структуры данных, константы, переменные и API. Структуру компонентов приложения можно изучать целенаправленно, изобразив ее на бумаге или в программе, чтобы всегда иметь перед глазами. Это нужно сделать один раз. Ведь если вы пришли в существующее приложение и сами не планировали его структуру с нуля, то такое изображение полезно сделать с целью изучения (если кто-то не сделал это ранее). Вы можете подумать: «Зачем мне это делать? Приложение развивается и дорабатывается, а его структура постоянно изменяется». Верно. Но давайте взглянем на рис. 9.1 и уровни компонентов. На нем представлено приложение, где каждый раздел представляет собой некую отдельную функцию. Рис. 9.1. Структура компонентов приложения
470  Глава 9 Совместная работа над приложением Страницы или разделы задаются с самого начала и редко изменяются. Чаще всего дорабатываются компоненты, представляющие интерес для пользователя, или бизнес-компоненты. Количество последних меняется редко. Однако сами бизнес-компоненты часто изменяют свое поддерево. Простые переиспользуемые компоненты, как правило, создаются с самого начала и если изменяются, то лишь иногда. А поскольку многие из них переиспользуются в других местах, вместо того чтобы вносить изменения в существующие компоненты, обычно добавляют новые. На схеме структуры каждый компонент должен быть подписан именем компонента. На рис. 9.1 имена опущены, чтобы не загромождать обзор. Как составить схему? Что наносить и каким графическим элементом? На схему нужно нанести компоненты и связи, как показано на рис. 9.2. При этом следует подписать имена компонентов. Однако вы можете захотеть детальнее изобразить связи, а также показать наличие колбэков от родителя к дочернему компоненту. Поскольку далеко не все остальные пары компонентов «родитель — дочерний» приложения будут иметь связь с помощью колбэка, такие пары обозначаются как на рис. 9.2, посередине. А те, где родитель передает в дочерний колбэк для вызова, можно обозначить как на рис. 9.2, справа. Но это по вашему желанию. На мой взгляд, достаточно показать связь. Составив такую схему компонентов, вы будете ее Рис. 9.2. Обозначение связей использовать с разными целями: y для общения с коллегами; y при доработках, чтобы сориентироваться и взглянуть на общую картину; y при рефакторинге. Ту же самую информацию можно получить в коде. Но на подобной схеме связи представлены нагляднее. Схему вы составите один раз, потом в некоторых подде­ ревьях она разъедется. Однако если схема понадобится, например, для рефакторинга, то вы всегда сможете привести ее к текущему состоянию и сделаете это гораздо быстрее, чем если бы делали впервые. 9.1.3. Вывод Зная структуру, не обязательно изучать составляющие каждого компонента: приложения обычно включают в себя много компонентов. Их структуру и нужно изучать в первую очередь. А составляющие компонентов или большие функциональные модули — в момент решения задачи. Постепенно, решая разные задачи, вы изучите большую часть приложения. При этом, вероятно, не освоите его целиком, оставив за бортом то, с чем не сталкивались.
9.2. Код-ревью  471 Изучать конечные компоненты легко: у них нет такого же большого количества функциональности, нужной для обработки данных, как у бизнес-компонентов. Последние в приложении содержат главную функциональность, чаще всего подвергаются доработке или устранению багов. Поэтому постепенно вы столкнетесь с каждым из них. Работа разработчика тяжела для тех, кто избегает задач. Поэтому, если вы хотите расти в профессии, в том числе как руководитель, выполняйте задачи как можно скорее — тогда у вас в спринте будет время на обучение, которое можно потратить в самом спринте. И именно на обучение, а не на что-то другое. Иногда дают слишком много задач, и вы кажетесь перегруженными работой. Выход один — учиться быстрее выполнять задачи. Вы это можете! 9.2. Код-ревью 9.2.1. Что такое код-ревью Это два английских слова: code и review. Они переводятся как «проверка кода». Хотя слово review имеет другие значения, «проверка» — самое подходящее. По-русски обычно пишут код-ревью, в разработке это привычное словосочетание. Что представляет собой действие код-ревью? Это когда другой разработчик (чаще разработчики) смотрит код, который вы написали в качестве решения задачи, чтобы определить его соответствие: y поставленной задаче; y правилам, принятым в группе. При нахождении несоответствий другой разработчик оставляет комментарий и, возможно, рекомендацию по исправлению. 9.2.2. Эмоции и код-ревью Верный, то есть не вызывающий сомнений признак, что вы чего-то не знаете в проекте или технологиях, — то, что вы избегаете делать код-ревью другим разработчикам или избегаете его вообще, а также не желаете показывать свой код коллегам, чтобы они сделали вам код-ревью. Слово «избегаете» здесь можно заменить на «испытываете неуверенность». С таким чувством легко справиться. Первое действие — продолжать делать коллегам код-ревью, при необходимости — заставлять себя выполнять это действие. Если вы его избегаете, то остаетесь в состоянии незнания. А вам нужно двигаться вперед. Поэтому — делайте код-ревью. Второе, очень важное действие — продолжать изучать и практиковать темы, в которых у вас есть пробелы. Данная книга, упражнения и практические задания помогут в этом. Второе действие гораздо обширнее первого, здесь придется приложить немало усилий. При этом оба действия окупятся сторицей (в стократном размере).
472  Глава 9 Совместная работа над приложением 9.2.3. Качество код-ревью Качество написанного кода зависит от того, какой разработчик его писал. Затем он создает пул-реквест. Качество будет зависеть и от того, кто его проводит. Вы сами наверняка замечали, что один ревьюер (так называется человек, который осуще­ ствляет проверку) не обращает никакого внимания на ошибки, а другой дотошно на них указывает. Так от чего зависит качество код-ревью? Ответ на этот вопрос может показаться очевидным: «Хм, ну, разумеется, от опыта!» Отлично, но давайте разберемся, какой именно опыт и только ли он нужен для хорошего код-ревью. Формулировка «качество проводимого код-ревью зависит от знаний» слишком общая. Необходимо уточнение — каких знаний, в каком объеме, а также их достаточность. Вот шкала, которая, по моему мнению, показывает области знаний и практики, от базового уровня до желательного, которые влияют на качество код-ревью1. 1. Знание языка программирования. 2. Знание используемых в коде библиотек и фреймворков. ---------------------------------------------3. Знание запахов в коде2. 4. Знакомство с архитектурой решения. 5. Код-стайл, принятый в группе. 6. Знакомство с предметной областью. Сможете ли вы проводить код-ревью без знания предметной области? Да. Но будете обращать меньше внимания на корректность использования функций и данных, относящихся к ней. Сможете ли вы проводить код-ревью без знания стиля кода, принятого в группе? Да. Но уже не будете обращать внимания на элементы, которые пишутся не так, как принято. Сможете ли вы проводить код-ревью без знания архитектуры системы? Да. Просто не будете смотреть на разного рода IP и эндпоинты или подсистемы. Сможете ли вы проводить код-ревью без знания запахов в коде? Да. Но вы точно не обратите внимания на участки кода, содержащие код с запахом. Сможете ли вы проводить код-ревью без знания фреймворков и библиотек? Вряд ли. Наконец, сможете ли вы проводить код-ревью без знания языка программирования? Определенно нет. Итак, что эта шкала дает нам в практическом плане? Она показывает, что вам нужно изучать. 1 2 Разделительная черта ниже означает, что выше нее находится базовый минимум, который влияет на качество код-ревью. Если нет знаний выше этой линии, то такого человека нужно обучать. Код с запахом — жаргонное наименование признака, говорящего о том, что какой-то участок кода требует применения рефакторинга. Введено в лексикон Кентом Беком.
9.2. Код-ревью  473 Каждый указанный выше элемент сам по себе можно оценить по шкале, и в итоге получится общая объективная оценка, которая покажет, какой код-ревью может провести ревьюер. Однако заниматься этим точно никто не станет. А себе самому данная шкала может показать, что вам подтянуть. 9.2.4. Неочевидное становится явным Пул-реквест1 (ПР) — это место, где становится очевидным то, что не было явным при разработке. Другие участники смотрят со своей точки зрения, поэтому получается более объективная картина, нежели когда на свой код смотрит сам разработчик. 9.2.5. Код-ревью — это групповой процесс В код-ревью у вас может быть роль либо ревьюера, либо автора ПР. Определяется это тем, кто выставил ПР. Принимают участие минимум два человека — автор ПР и ревьюер, то есть процесс групповой. Ответственность за код-ревью несет группа, так как она решает, какой код должен быть в ее собственной кодовой базе (репозитории). Это значит, что если вы не делали ПР, то несете за него ответственность в том числе как участник группы. А другие члены группы несут ответственность за ваш ПР. Заявление про ответственность может немного смутить, но существует довольно простой инструмент, позволяющий приближать работу других ревьюеров и автора пул-реквеста к единообразию. Это специальный чек-лист. Как его составить, рассказано в разделе 9.4. Код-ревью имеет целью не только проверки соответствия кода установленным правилам, но и помогает: y быть в курсе изменений; y обучаться; y обучать. 9.2.6. Быть в курсе изменений Лучшего места и момента, чтобы изучать изменения, вносимые в код приложения, не найти. Когда приступаешь к изучению кода приложения, смотришь на его модули, и они предстают перед тобой в полном виде, так что приходится смотреть все подряд. Но, как правило, модули растут постепенно. Код-ревью как раз дают возможность начать с какого-то момента детальнее вникать или по-другому — быть в курсе того, каким станет новое состояние кода приложения. 1 Пул-реквест (от англ. pull-request) — запрос на внесение ваших изменений в репозиторий.
474  Глава 9 Совместная работа над приложением 9.2.7. Обучаться Все коллеги имеют разный опыт и изучали разные источники, работали над разными приложениями. Каждый может внести что-то свое так, как не делал другой. И когда коллеги выкладывают пул-реквест, код может быть несколько сложнее, чем к нему привык коллега, и тогда ему придется изучить непонятный момент. Если взглянуть на это по-другому, то появляется возможность изучить что-то новое, то, чего он ранее не видел. Это тот самый момент, который привносит пользу в обучение. 9.2.8. Обучать Обучение с помощью ПР идет двумя путями. Первый описан выше — когда более опытный коллега делает ПР, а менее опытный, глядя на код и желая его понимать, должен заглянуть в материалы по языку, подходу или принципам разработки либо выяснить, что происходит. Это обучение. Точно так же, когда разработчик делает ПР, коллеги указывают ему на некорректности в коде или на то, как можно было сделать иначе. Это второй путь обучения — оставлять комментарии, чтобы другой разработчик мог учиться подходить к написанию кода по-другому. У код-ревью есть и побочный эффект для обучения. Например, один коллега оставляет комментарий на чужой ПР. Заходит другой, смотрит на этот комментарий и примеряет его на свою работу. Со временем все коллеги становятся примерно одного уровня — если проявляют настойчивость в работе. 9.2.9. Код-ревью — это общение Код-ревью подразумевает общение, которое ведет к более высокой квалификации. Так что это подарок, и к нему надо именно так относиться. Ведь это вклад в вашу квалификацию. Все, что присуще людям при общении, присуще и при код-ревью. Как существуют отличия у разных людей, групп и компаний, так же существуют и отличия в том, на что обращают внимание в той или иной группе разработчиков. Это не значит, что не нужно привносить свои правила, но можно не игнорировать сложившуюся практику. Оставляя комментарии, коллеги-разработчики старались что-то высказать, чтобы ваш код был лучше. Подтвердите это, покажите, что видите их работу и что она для вас значима. Вполне ожидаемы нападки. Это нормальное явление. Значит, у кого-то из ваших коллег сорвана маска. Маска — некий социальный механизм, с помощью которого человек хочет вам показать, что является каким-то. Вы же подбросили ситуацию, когда он не может реагировать как обычно, находясь в маске, поскольку, видимо,
9.3. Налаживаем процедуру код-ревью в группе  475 приходится прикладывать иные, непривычные умственные усилия. В итоге проявляется истинное лицо человека, и это не обязательно плохо. Как поступить в таком случае? Надо ли как-то улаживать ситуацию? Или перестать делать то, что делали? Нет. Продолжайте работать в том же темпе — со временем ситуация изменится. И ни в коем случае не давите на это как на кнопку. То есть нельзя использовать код-ревью для контроля за коллегами или пытаться показать свое превосходство в техническом плане. Код-ревью — это и самообучение, и обучение других, и обучение вас самих. Поэтому в этой деятельности не место эгоизму или скрытым замыслам по контролю окружения. 9.3. Налаживаем процедуру код-ревью в группе Несмотря на простоту и очевидность процедуры код-ревью, эта область частенько не налажена в группах разработчиков, регулярно приводит к бардаку и отнимает много времени для выполнения простой работы по выпуску релиза в ПРОМ1. Например, возникает аврал перед выпуском релиза, потому что накопилось много пул-реквестов, которые никто не смотрел, и из-за срочности коллеги снисходительно относятся к допущенным ошибкам в коде у других разработчиков. После аврала приходится спешно чинить баги и выпускать хотфиксы2. Или сама процедура кодревью отнимает много внимания, а пул-реквест, прежде чем попасть в репозиторий, долго висит. В конечном счете еще приходится разрешать конфликты, которых вполне можно было избежать. Этот раздел построен следующим образом. Сначала объясняются составляющие процедуры код-ревью, а в конце раздела приводится пошаговая инструкция по ее внедрению в группе. 9.3.1. Код-ревью — этап процесса производства программного продукта Все этапы производства программного продукта должны быть отлажены (например, сборка DevOps). Данный этап исключением не является. У него есть составляющие, которые нужно привести в определенный порядок. Отладив его, вы будете тратить примерно одинаковое время на каждый пул-реквест. И главное — сможете контролировать этап, он перестанет занимать ваше внимание, то есть отвлекать. 1 2 Имеется в виду стенд промышленной эксплуатации информационной системы. Хотфикс — исправление в релизной ветке.
476  Глава 9 Совместная работа над приложением 9.3.2. Составляющие код-ревью Составляющие перечислены в порядке группировки. Места осуществления действий y Группа в мессенджере. y Репозиторий. Действующие роли y Автор ПР. y Ревьюер. Объекты рассмотрения в репозитории y Изменения (доработки или новые разработки). y Исправления замечаний. Сообщения в мессенджере y От автора ПР. y От ревьюера. Комментарии, вопросы или ответы в репозитории y От ревьюера — комментарии и/или вопросы. y От автора ПР — ответы. Действие в репозитории y Апрув (одобрение) от ревьюера. Здесь представлена полная картина всех составляющих, которые присутствуют в код-ревью (рис. 9.3). Рис. 9.3. Составляющие код-ревью
9.3. Налаживаем процедуру код-ревью в группе  477 9.3.3. Пример взаимодействия при код-ревью В табл. 9.1 приведен обычный порядок действий, когда взаимодействуют два человека. Таблица 9.1. Обычный порядок действий при пул-реквесте Место размещения Что размещает или что делает Кто Изменения Автор ПР Группа в мессенджере Репозиторий + + Сообщение о ПР + Смотрит ПР + Оставляет комментарий + Пишет сообщение об оставленном комментарии + Читает сообщение + Смотрит комментарий + Вносит исправления в ПР + Ревьюер Автор ПР Сообщение об изменениях + Просматривает изменения + Ставит апрув + Пишет сообщение + Подтверждает апрув сообщением Ревьюер Автор ПР 9.3.4. Группа в мессенджере Для работы над код-ревью нужно создать отдельную группу в мессенджере, который использует ваша команда. Возможно, группа у вас уже есть, поэтому вести общение по код-ревью можно и там. Почему нужна группа для общения по поводу код-ревью? Представьте, что вы не создали такую группу и пишете личные сообщения. Вам придется уведомлять о ПР каждого участника. Затем кто-то оставляет комментарии, вы вносите исправления и нужно снова уведомлять. Группа в мессенджере решает эту проблему. Хочу предостеречь по поводу количества мессенджеров и источников коммуникаций. У вас может быть и рабочая почта, и личный аккаунт в каком-либо мессенджере. Некоторые участники вашей команды могут писать во все доступные источники. Вам необходимо предотвращать такие попытки и уведомлять коллег о том, что существует одно место, куда они должны посылать сообщения о необходимости
478  Глава 9 Совместная работа над приложением сделать код-ревью. И это группа в мессенджере, потому что так все участники будут видеть сообщения. Кто-то видит первым, подхватывает инициативу и проводит код-ревью. Задача такой группы — свести к одному количество источников, где помещаются сообщения о пул-реквестах. Несколько коммуникационных источников мешают и отвлекают. 9.3.5. Время прохождения код-ревью. Где могут возникать трудности Из табл. 9.1 видно, что автор и ревьюер ведут активную переписку в группе мессенджера и взаимодействуют в репозитории. Они делают это попеременно. Действие выполняется в репозитории, далее участники группы уведомляют друг друга об этом. Поэтому важно следить за сообщениями в группе, чтобы вовремя реагировать на действия коллег в отношении код-ревью. Где могут возникнуть заторы? Автор ПР может создать ПР и никого не уведомить. Ревьюер — сделать ревью, но не уведомить об этом автора. Автор — увидеть сообщение в группе о комментарии или в самом репозитории, но отложить исправление. Или внести исправления и уведомить об этом ревьюера, который проставил апрув, но забегался и забыл сообщить об этом. Когда на любом шаге проведения код-ревью добавляется время, это всегда увеличивает длительность общей процедуры код-ревью. Чтобы искоренить заторы, важно принять несколько правил. Первое — код-ревью делается с каждым ПР. Второе — как только ПР выставлен, каждый должен проявить инициативу и начать делать код-ревью. О том, что кто-то взял в код-ревью ПР, можно уведомлять коллег в группе. Тогда, если они видят сообщение о новом ПР и на него нет никаких реакций, тот, кто первым увидел, и должен начать выполнение код-ревью. В мессенджерах есть возможность ставить эмодзи. 9.3.6. Инструмент и метод создания качественного ПР Для создания более качественных ПР группа должна создать чек-лист проверки ПР, который каждый разработчик может использовать с двумя целями: y как автор ПР и перед тем, как выставить его для обзора; y чтобы проверять по нему ПР коллег. Чек-лист должен быть коротким, чтобы им было удобно пользоваться. Как его составить, описано в следующем разделе. Как только вы решили, что код можно отправлять на ПР, сделайте следующее. y Мысленно поменяйте роль разработчика на роль ревьюера. Этот прием используют актеры, он позволяет им играть разные роли. У вас такая способность тоже есть. Если вам трудно это сделать, нужно потренироваться, используя «Упражнение по смене ролей». y Возьмите в руки чек-лист и пройдитесь по своему коду, будто вы ревьюер. Не критикуйте свой код. Если он работает, это приемлемое решение. Вам нужно лишь пройтись по чек-листу и проверить, соответствует ли ему ваш ПР. Если чек-листа нет, просто еще раз взгляните глазами ревьюера.
9.4. Как быстро стать своим в новом коллективе  479 9.3.7. Как внедрить или улучшить процедуру код-ревью 1. Станьте ответственным за данное действие. Неважно, внедряете вы его впервые или улучшаете процесс. 2. Если в мессенджере нет группы, создайте ее. Если она уже существует, то задействуйте. 3. Внедрите правило — одна группа для общения. Остальные каналы резервные и не используются, если группа доступна. 4. Создайте чек-лист для проверки ПР перед код-ревью и его проведением. Проведите собрание с коллегами, где расскажите о том, как должны обстоять дела, а именно: 1) покажите составляющие код-ревью и обязательно рис. 9.3, расскажите, как действовать согласно табл. 9.1; 2) расскажите про правило для общения по код-ревью, а именно: одна группа, никаких личных сообщений и имейлов. Запасные каналы остаются на случай, если ваш мессенджер подвергся DDoS-атаке и стал недоступен; 3) выработайте правила ведения совместной разработки. Коллеги захотят принять участие, кто-нибудь наверняка проявит инициативу. Например, это может быть помощь в разработке чек-листа или количество апрувов, после которых ПР считается принятым и его можно мержить. Или способы разрешения конфликтов при невозможности договориться, а также действий на случай, если код-ревью вызывает негативные эмоции. 5. Начинайте действовать по новой процедуре сразу после собрания. 6. Как только заметите отход от новой процедуры, уведомляйте об этом коллег, чтобы они, как и вы, направляли свое внимание на ускорение процедуры кодревью. Например, если кто-то выставил ПР и сообщил, а никто, кроме вас, не смотрит. Или если кто-то по-прежнему не использует для уведомления группу, а пишет личные сообщения или подолгу отвечает на оставленные вопросы и комментарии. Всегда, когда замечаете добавление времени на любом из шагов процедуры код-ревью, интересуйтесь у коллег, что произошло. Со временем код-ревью придет к такой скорости, что о нем даже никто думать не будет и это станет естественным процессом в вашей команде. 9.4. Как быстро стать своим в новом коллективе Когда новый сотрудник приходит в команду, он явно не знает, на что его коллеги по разработке обращают внимание, когда кто-то делает ПР. Некоторые группы составляют чек-листы того, что, по их мнению, не должно присутствовать в коде, или того, как что-то должно выглядеть. Это может быть описано в виде не чек-листа, а списка правил. Но большинство групп так не делают. И даже если таких списков нет, ничего страшного. Вы можете создать их сами. Если вы не новичок в группе, но хотели бы, чтобы такой список или правила существовали, то можете воспользоваться описанным ниже способом.
480  Глава 9 Совместная работа над приложением 9.4.1. Нужны ли написанные правила Каждая группа индивидуальна, то есть обладает особенностями. Все, что считается нормальным для одной группы, в другой может не использоваться. В этом нет ничего удивительного. Если попросить две разные группы реализовать одни и те же требования, то их коды окажутся написаны по-разному. Да, будут использованы одни подходы, но сами приложения выстроены по-другому. Индивидуальность разработчиков проявляется в том, как они именуют идентификаторы, переиспользуют функции, хуки и компоненты; группируют код в компоненты и составляют связи между ними, чтобы получить приложение. При этом они используют одни и те же способы, библиотеки, платформы, наконец, языки разработки. В группе всегда есть сложившийся индивидуальный стиль именований идентификаторов или использования определенных видов хуков для управления состоянием. Например, называть переменные так, чтобы их имена содержали имя какой-либо сущности, а имя первого параметра в колбэке методов массивов — всегда одним словом item. Для управления состоянием используется строго хук useState — потому что так привыкли, и даже если явно проще задействовать useReducer, все равно берут useState. И это не является ошибкой. Просто вещь из разряда «я художник, я так вижу», по сути — проявление индивидуальности. Нужно ли создавать в группе правила, или и так все понятно? Если вас не затрудняет то, что некоторые правила подразумеваются, хоть и не описаны, и все с этим согласны, почему нет? Может быть и другое. Например, в группе появляются новые коллеги и вам нужно им объяснять, на что обращать внимание или почему вы именуете идентификаторы так, как у вас сложилось, и часто используете группировку JSX определенным образом. В этом случае проще где-то описать, на что обращать внимание и как действовать. Приложение начинают разрабатывать сегодня, и через пару лет, когда появятся новые библиотеки, его код будет выглядеть неким легаси, но выполнять работу. Поэтому доработки должны производиться с учетом используемой библиотеки или стиля, которого придерживались разработчики. Иначе существует риск, что придет новый разработчик и станет тратить время на перевод кода на новую модную библиотеку. Если смысл перевода близок к нулю, то лучше бы новому сотруднику изучить то, как нужно дорабатывать текущее приложение с учетом используемых библиотек и их версий. В последнем случае очень сильно помогут описанные правила. 9.4.2. Правила крупных компаний Ваш подход не должен соответствовать подходу к стилю написания кода крупного игрока по нескольким причинам. Главная среди них — в больших компаниях, где более тысячи разработчиков фронтенда, работа над конкретным приложением ведется небольшой группой из трех-пяти человек. Состав групп примерно одинаковый, просто у крупных игроков намного большее число приложений, над которыми нужно работать. По этой причине там требуется большое количество разработчиков одного направления. И так же, как в других организациях, здесь формируются правила. Скорее всего, их создавала не сама группа, а отдельная
9.4. Как быстро стать своим в новом коллективе  481 должность, например, технический писатель, который сделал правила максимально соответствующими идеальным представлениям о том, каким должен выглядеть профессионально написанный код. Если вы получили такие правила и находите их подходящими, то вполне возможно применять их, равняясь на большую компанию. Но это необязательное условие. Вполне можно написать правила и составить чек-листы самостоятельно. 9.4.3. Новые договоренности Уровень мастерства разработчиков постоянно улучшается. Скорость у всех разная и зависит от многих факторов. Наиболее значимый вклад делают несколько регулярных действий. Первое действие — знакомство с кодом приложений других разработчиков на общедоступных ресурсах (например, gitverse, github, npmjs) и с тем, как его делают другие. Это позволяет увеличить количество способов решения одних и тех же задач разработки. Второе действие — регулярная практика написания и чтения собственного кода. Когда читаешь свой код, а это происходит регулярно, начинаешь хорошо понимать, насколько он может быть ясен другим. В будущих разработках вы учитываете то, что, на ваш взгляд, можно улучшить. Третье действие — просмотр ПР других разработчиков и проведение им кодревью. Через какое-то время ведения совместной деятельности некоторые разработчики приходят к мнению, что предыдущие договоренности относительно конкретного пункта чек-листа или правил можно пересмотреть по той или иной причине. Такой пункт заменяют или вовсе удаляют. 9.4.4. Как составить чек-лист Здесь описан подход, выполняемый человеком, а не машиной. Если у вас получится использовать нейросеть для совершения этого действия, то вы сэкономите время. Хотя ручная работа тоже полезна. В группе могла быть или отсутствовать практика проведения код-ревью. Рассмотрим, как создать чек-лист, если вы пришли в группу (или давно находитесь в ней), в которой проводили код-ревью. Так как в группе проводилось код-ревью, коллеги оставляли комментарии, авторы ПР в ответ исправляли свой код и отвечали на вопросы, которые появлялись наряду с комментариями. Поэтому есть история взаимодействия за определенный период. Вот список шагов, которые нужно выполнить. 1. Выбрать ПР в состоянии merged. Необходимо зайти в репозиторий и открыть смерженные ПР за длительный период. Скажем, за полгода и, наверное, максимум — за год. Больше вряд ли имеет смысл, так как квалификация разработчиков изменяется, и всегда в лучшую сторону. Вам нужно набрать достаточный объем информации.
482  Глава 9 Совместная работа над приложением Любой репозиторий, представляя список ПР, в том числе показывает количество оставленных комментариев. Открывайте ПР, где есть комментарии. Начинайте с самых последних (недавних) и двигайтесь к прошлому. Можно выполнять одновременно со вторым шагом. 2. Копировать комментарии и вопросы, одновременно группируя. В этом шаге вы копируете комментарии и/или вопросы в отдельный файл, одновременно группируя. Если комментарий никуда не подходит, то создае­ те новую группу. Накопите большой объем комментариев, достаточный, с вашей точки зрения. Комментарии бывают двух видов с точки зрения возможности взять их за правило. Некоторые можно обобщать, то есть формулировать из них правило, другие — нет. Пример комментария, когда можно сформулировать вывод: «...можно избавиться от лишних переменных...» Такой вывод применим и в других проектах. Другой случай, более частный, относящийся чисто к коду вашего проекта: «…это дублирование функциональности, так как у нас уже есть специальная функция isNotFalsy, нужно использовать ее». Такая функция написана чисто в коде вашего приложения. Это тоже можно взять за правило, правда, надо правильно обобщить. Пример, когда комментарий нельзя взять за правило: «Предлагаю разделить компонент на две части...» Такой комментарий больше касается начального проектирования компонента или ситуации, когда компонент долго дорабатывался и только сейчас один из коллег заметил, что его действительно можно разделить на два. Вывод типа «лучше проектировать» не имеет смысла, разве что при составлении курса повышения квалификации коллег, когда вам нужно найти, в чем у них есть трудности, чтобы сделать на этом упор. Лучшее проектирование компонента приходит с практикой (если данному моменту специально не обучали). Как следствие, при доработке разработчику становится очевидно, нужно ли разделять компонент. 3. Сделать вывод. В этом шаге вы делаете вывод из групп комментариев и при необходимости перегруппируете комментарии. Возьмите одну любую группу и оцените все входящие в нее комментарии — на предмет того, все ли подходит группе. Если есть что-то неподходящее, то или переместите в другую группу, или создайте новую. Допустим, много комментариев «...дать более описательное имя переменной...» и комментарий «...можно опустить переменную, если вернуть из функции выражение вычисления как результат функции». Разобравшись с подходящими комментариями, вы делаете вывод, то есть формулируете вывод из комментариев группы. Например, давать переменным описательные имена. Затем берете следующую группу и поступаете аналогичным образом. Так поступаете со всеми группами. Если в группе всего один комментарий, то никаких проблем — это тоже группа. Если комментарий можно обобщить, тоже нормально.
9.4. Как быстро стать своим в новом коллективе  483 Замечания относительно именования идентификаторов, скорее всего, будут встречаться чаще. 4. Превратить список в чек-лист. Пройдите по сделанным вами выводам и оформите эти пункты в виде чек-листа. Это не значит просто перенести выводы предыдущего шага в отдельный список. Поскольку вы создаете чек-лист, необходимо внести пояснения к пунктам. Например, у вас есть пункт «Использовать enum вместо магических чисел». Ниже вы даете пояснение, что если задействуете числа из предметной области, то брать их нужно из переменной типа enum. А если для определенного момента такой не существует, то создать ее. Или «Использовать деструктуризацию параметра». Пояснение: при использовании нескольких свойств объекта в коде функции деструктурировать объект, чтобы по несколько раз не дублировать его идентификатор. Пройдите все выводы до конца и создавайте каждый пункт чек-листа с пояснениями. 5. Предоставить чек-лист на рассмотрение коллегам и при необходимости внести коррективы или попросить коллег сделать это. Если вы составите проверочный список, который можно использовать перед каждым ПР, то заслужите уважение за проделанную работу. Подобные списки многие хотят составить, но «руки не доходят». Причина — неизвестно, на что реально обратить внимание и будет ли это приемлемо. Однако такой источник, на основе которого я предлагаю вам составить чек-лист, отражает реальные взгляды группы на подход к тому, как писать код. Ведь это ее участники оставляли комментарии. Значит, они думают, что так правильно! Реальная ценность этого в том, что вы работаете с настоящей, неподдельной и не высказанной каким-то авторитетом мыслью или указанием начальника. Здесь вы работаете с тем, что пишут коллеги. 9.4.5. Как составить чек-лист, если у группы нет практики код-ревью Нужно найти open source — проект, естественно, на русском языке (если читаете по-английски, то и на английском) и желательно живой. Такие проекты есть в репозиториях GitVerse и GitHub. Список выполняемых действий точно такой же, как для собственного репозитория. Начинайте изучать комментарии. Соберите статистические сведения по типам замечаний и оформите их в виде проверочного списка. Затем предоставьте его коллегам на рассмотрение. Добейтесь от них согласия, при необходимости — поправьте список и начинайте смело делать код-ревью. Это все.
ГЛОССАРИЙ Глоссарий — это маленький словарь, в котором слова приводятся в тех значениях, в которых используются в тексте книги, а не во всех возможных значениях. Слова идут в следующем порядке: сначала русские, затем — английские. А Анонимная функция — функция, у которой отсутствует имя, но присутствует тело и блок параметров. Функцию без имени выделяют отдельно из-за определения самого понятия «функция», которое говорит, что это именованный блок инструкций. И действительно, в теле содержится некий набор инструкций, которые программист заранее предписывает к выполнению. С целью не переписывать то же самое по несколько раз и была придумана функция. Чтобы выполнить тот же блок инструкций еще раз, нужно просто обратиться к имени и вызвать этот блок с помощью вызова (...). Поскольку у анонимной функции нет имени, использоваться она может только в месте определения. Артефакт — результат, который был получен определенной ролью в качестве итога работы над задачей. Артефактом работы аналитика будет описание процессов. Артефактом работы архитектора — диаграммы. Артефактом работы разработчика — файлы с кодом. Артефактом работы руководителя может быть план или тот же артефакт, полученный сотрудником, который находится в подчинении у данного руководителя. Аутентификация — процесс, позволяющий установить, что пользователь, который пытается получить доступ к определенной информации, действительно тот, за кого себя выдает. В Валидировать — действие по слову «валидация», что означает процесс проверки введенных данных на соответствие определенным правилам, условиям и/или ограничениям. Виртуальный DOM — структура данных в виде дерева, содержащая точно такие же связи между узлами, как те, что должны быть в реальном DOM в самом браузере. Каждый узел в виртуальном DOM тоже содержит данные, которые должны быть использованы в узлах реального DOM. Надо специально указать, что реальный DOM, который строит браузер, содержит намного больше свойств в каждом узле, чем виртуальный. Главные данные одного узла виртуального DOM — связи с другими узлами (отношения «родитель — дочерний»), чьи данные должны быть отображены в виде текста на странице, и данные о стилях.
Глоссарий  485 Высший порядок — словосочетание используется не само по себе, перед ним всегда есть слово, например компонент высшего порядка или функция высшего порядка. Пришло из области функционального программирования (см. Функцио­ нальное программирование). Это значит, что функция принимает некоторый аргумент, являющийся функцией или компонентом (по сути, это тоже функция, но ранее в React использовались классовые компоненты), который их возвращает. Возвращаемые функция или компонент наделяются некоторой дополнительной функциональностью, в том числе может присутствовать замыкание (см. Замыкание) на область видимости. Вычисление — это обработка числовой информации ручным или машинным способом; действия над числами для нахождения искомого. Вы часто встречаете это слово, и его используют для описания действия над выражениями в языках программирования, причем для любых типов данных, например const str = someStr.concat(otherStr), а также для описания последовательности действий в программе, например if (value) {…} else { for (let i = 0; …}. Еще есть значение, связанное с догадками, но это относится к человеку, а не к компьютеру. В английском языке почти идентичные определения. Так почему значения в разговорном языке касаются чисел, а мы используем это слово и для порядка действий, и для нахождения итогового значения, например строки? Возможно, потому, что компьютер хранит данные в виде чисел, а информатика берет свое начало в математике. Ну а математика имеет дело с числами и операциями над ними. Д Девайс — физическое устройство, позволяющее выполнять определенную функцию. В данном случае будет воспроизводить изображения, вероятно, даже имитировать тактильные ощущения. Декоратор — паттерн, который существует в объектно-ориентированном программировании и работа которого заключается в том, что он добавляет объекту новую функциональность. Может применяться и с функциями. Дерево — 1) граф без циклов, где количество ребер на единицу меньше количества вершин, при этом одна вершина выделена и называется корнем; 2) структура данных, где каждый элемент, называемый узлом или вершиной, хранит данные и содержит ссылку на другой (-ие) элемент (-ы) той же структуры. В дереве есть специально выделенная вершина, которая называется корнем. Вершина, не имеющая ссылок на другие элементы, называется листом. В программе дерево может быть реализовано разными способами: при задействовании объектов ({}), массивов ([]) и структур Set, Map. Деструктуризация — специальное действие по получению значения переменной, которая расположена в структуре данных. Для его извлечения используется способ доступа не с помощью точки, как в объектах, и не с помощью индекса, как в массивах, а с помощью указания устройства структуры данных — вплоть до расположения нужного значения, которому присваивается имя. Это имя становится идентификатором в той области видимости, где производится деструктуризация.
486  Глоссарий Дизайн-система — обширный набор UI-компонентов, объединенных единым стилевым оформлением, обладающих необходимой функциональностью, предоставляющих интерфейс и готовых к использованию. Создается как для UI-библиотек, так и для базовых технологий. Примером может служить известная Material UI от Google для React и Bootstrap для CSS и JavaScript. З Зависимость — модуль, обычно называемый пакетом, содержащий выражения на языках JavaScript и/или TypeScript, импортируемый для использования в вашей программе. Замыкание — некоторый способ, заключающийся в том, что функция высшего порядка (см. Функция высшего порядка) возвращает функцию, которая может обращаться к объекту вызова функции высшего порядка и которая не уничтожается сборщиком мусора, пока существует возвращенная функция. Таким образом можно организовать сохранение значений между вызовами возвращенной функции. Запомненный образец кодирования — придуманное описание модели разработки, когда разработчик запомнил, как должен выглядеть код, который работает, но не понимает некоторых моментов в нем и поэтому не может изменять его по собственному желанию, из-за чего предпочитает оставлять его всегда таким, каким он был при знакомстве. Данный подход ведет к тому, что частенько применяется «копипаст» готовых и чужих решений. Это на самом деле простое непонимание, которое легко исправить путем взаимодействия с областью, в которой плохо разбираешься. Слово «взаимодействие» выделено, потому что, когда у разработчика есть непонятная область, он старается ее избегать. Ключ к пониманию — взаимодействие. Ключ к непониманию — избегание взаимодействия. И Иерархия — расположение частей или элементов целого в порядке от высшего к низшему или от низшего к высшему. Из коробки — имеется в виду функциональность, поставляемая какой-либо библиотекой, которая может быть использована сразу или с дополнительными настройками и которую не нужно разрабатывать самостоятельно. Импрувмент (от англ. improvement — «улучшение») — предложения, поступающие от пользователей системы, по улучшению какой-либо функциональности или добавления совершенно новой функциональности. Они имеют низкий приоритет, так как поступили не от заказчика. От заказчика поступают требования. Искусственный интеллект — синтетическая система, демонстрирующая «умное» поведение. (Определение взято из книги: Харбанс Р. Грокаем алгоритмы искусcтвенного интеллекта. — СПб.: Питер, 2023). К Клиент-серверное приложение — распределенное приложение, основанное на модели вычислений, где клиент запрашивает услуги у другой сущности — сервера. В типичном для бизнес-систем клиент-серверном приложении клиент выполняется на персональном компьютере, а расположенный на удаленной, более производительной машине сервер предоставляет ему услуги по доступу к хранящимся
Глоссарий  487 на сервере данным. Клиентская часть приложения обычно оптимизируется для взаимодействия с пользователем, в то время как серверная предоставляет функциональность, совместно используемую многими пользователями. Код-ревью (от англ. code review) — процедура проверки другими участниками команды пул-реквеста, созданного автором. Код-ревью можно проводить, только если ваш код проверяет другой. В код-ревью участвуют две роли — автор пул-реквеста и ревьюер. Ревьюер (от англ. review) означает «проверять». После проведения процедуры код-ревью код становится таковым, каковым участники команды считают его приемлемым для слияния с основным кодом и отправки в промышленную эксплуатацию. Код с запахом — жаргонное наименование признака, говорящего о том, что какой-то участок кода требует применения рефакторинга. Введено в лексикон Кентом Беком. Код-стайл (от англ. code style) — имеется в виду руководство по стилевому оформлению кода приложения, а именно компонентов, функций, кастомных хуков, CSS-стилей, HTML-тегов, JSX, правила именования, форматирование отступов, использование библиотек, определенные способы использования операторов и инструкций языков программирования и т. п. Контекст в React — любые данные, которые доступны целому поддереву компонентов. Чтобы они стали именоваться контекстом, используется специальная функция из React createContext и специальный компонент Provider, являющийся свойством созданного объекта контекста. Композиция — способ связи двух объектов, когда один компонент управляет жизненным циклом другого компонента. Если один компонент использует другой, то, как только первый прекратит свое существование, второй сделает то же самое вместе с ним. Конфигурация — набор программных установок (например, значений переменных, управляющих последовательностей), задающих состав системы или приложения, функциональные возможности и/или режимы функционирования программы. Это также фактически установленный состав системы. Координация — это когда устраняются разногласия или противоречия (если они есть) и устанавливаются соответствия между действиями участников выполнения работ. Примером может быть последовательность работ. Если не готов дизайн, то сначала дизайнер его создает. Тем временем фронтенд-разработчик пишет юниттесты или выполняет технический долг. Кортеж в программировании — это компактная структура данных, чаще всего неизменяемая, содержащая упорядоченную коллекцию объектов. Примерами кортежей служат: 1) координаты точки на карте (широта, долгота); 2) адрес (страна, город, улица, дом); 3) информация о книге (название, автор, издательство, год выпуска). Кросс-браузерный — имеется в виду способность работать в браузерах разных производителей или в разных версиях одного производителя. Л Лендинг (другое название — «одностраничник») — наименование вида вебстраницы, на которую зазывают пользователя и с которой пользователю больше
488  Глоссарий никуда не нужно переходить, так как он может здесь осуществить заказ товара и/или получить всю необходимую информацию. Лэйаут (от англ. layout — «макет», «расположение», «размещение») — в вебразработке повторяемый набор компонентов от страницы к странице, от раздела к разделу, который составляет основу внешнего вида приложения. Повторяемый набор компонентов — такие компоненты, которые не изменяются от раздела к разделу. Как правило, это хедер, футер, компонент разделов и общий компонент, внутри которого располагаются все другие компоненты. Лэйаут может быть и меньшего размера, чем все приложение. Например, когда существует ряд повторяемых компонентов для одного раздела. Лэйаут определяется дизайном. У этого слова есть и другие значения. Например, в дизайне и типографике так называют сам процесс расположения элементов. М Макет — в том числе слово из полиграфии, означающее эталон для верстки. В веб используется в аналогичном значении, но применительно к веб-страницам. Мемоизация — способ оптимизации, применяемый преимущественно для ускорения выполнения программ (за счет дополнительных затрат памяти). Предусматривает запоминание результатов вычисления функций, чтобы при последующем вызове функции при тех же входных данных можно было не повторять вычисления, а сразу выбирать результаты. Вместо слова «мемоизация» можно использовать выражение «запоминание (сохранение) результатов». Модуль — в программировании оформленный специальным образом, функционально законченный и самостоятельный (в том числе по отношению к компиляции или загрузке) блок кода, взаимодействие с которым осуществляется через его внешний интерфейс. Раздельная трансляция (separate compilation) модулей важна при создании больших систем. Кроме того, каждый отдельный модуль может повторно использоваться в других проектах. Разбиение программы на модули, модульность (modularity), существенно облегчает ее понимание, разработку, документирование, отладку, модификацию и сопровождение. В зависимости от контекста синонимами этого термина являются unit, programm unit и package. Мутировать — применительно к разработке означает видоизменение структуры данных или переменной путем создания так называемого побочного эффекта (см. Побочный эффект), в противоположность созданию измененной копии, как делает чистая функция. Например, изменение свойства таким образом — const obj = { prop: 5 }; obj.prop++ — называется «мутировать свойство». Н Нативный (от англ. native) — составляющее исходное вещество или источник. (Определение взято из Merriam Webster Dictionary.) Конечно, у этого слова есть другие значения. На русский переводится как «родной», «собственный». В сфере разработки это слово употребляют применительно к встроенным в язык средствам, классам и их методам. Нормализация — процесс структурирования данных с использованием ссылочной модели данных. Нотация — система условных письменных обозначений, принятая в какой-либо области.
Глоссарий  489 О Облачный сервис — сервис, предоставляемый в веб. Используя его, пользователь может заказать такие элементы инфраструктуры, как процессор, память, жесткий диск (HDD, SSD), сети. Такой способ использования называется IaaS — Infrastructure as a Service (инфраструктура как сервис). Возможность заказывать железо с определенными параметрами предоставляется благодаря виртуализации. При использовании IaaS вы самостоятельно далее устанавливаете программное обеспечение вплоть до операционной системы. Однако если вы желаете только настраивать определенные параметры уже установленной операционной системы и базы данных и хотите лишь загрузить созданный вами код и запустить его, то это также возможно. В этом случае вы не будете управлять параметрами железа, но сможете настраивать параметры операционной системы (управлять ею) и настраивать базу данных или другое программное обеспечение и, конечно, сможете запускать свой код. Такой подход называется PaaS — Platform as a Service (платформа как сервис). И последний случай, когда вы не управляете ни железом, ни платформой, а только пользуетесь каким-либо сервисом через веб-интерфейс либо через его API. Это SaaS — Software as a Service (программное обеспечение как сервис). Объект, подобный массиву — обычный объект JavaScript, который содержит свойство length и ключи в виде цифр. Для работы в прототипе есть методы, имена и функция, аналогичные тем, что присутствуют в прототипе массивов. Таковым является объект с интерфейсом NodeList. Подробно ознакомиться можно по адресу https://developer.mozilla.org/en-US/docs/Web/API/NodeList. П Параметр — переменная (-ые), не имеющая (-ие) конкретного (-ых) значения (-ий) в момент объявления функции. Параметры перечисляются через запятую в специально отведенном для них разделе параметров функции, которые приводятся в круглых скобках. В теле функции они используются как переменные. Впоследствии, при вызове функции, ей передаются аргументы, которые и становятся значениями параметров. Очевидно, что значение параметра определяется в момент вызова функции и от вызова к вызову может принимать любое допустимое значение. Плагин (от англ. plugin) — небольшое программное обеспечение, которое подключается к более крупной программе. Слово plugin присутствует в самом языке и означает что-то, что можно подключить к какой-либо электрической схеме. Платформа — программа, такая как браузер или Node.js, исполняемая в операционной системе и предоставляющая API к определенной функциональности, например к запросам по сети, или к взаимодействию с файловой системой. В качестве входных данных принимает текст программы, написанный на языке JavaScript, поэтому API доступен через глобальные идентификаторы прямо в тексте программы. Например, запросы по сети можно осуществлять через идентификатор fetch, а получать информацию о текущем местоположении объекта document — через глобальный идентификатор window, свойство location. Плотность потока — показатель, означающий количество каких-либо единиц в единицу времени. Например, если говорят «плотность потока железнодорожных вагонов в сутки», то имеют в виду количество вагонов, проходящих через какой-то участок, например через железнодорожную станцию, допустим, за 24 часа. Период
490  Глоссарий времени может быть и часом, и неделей. Данное описание подходит к слову «плотность», у нас еще есть «поток». Поток — это непрерывное движение чего-либо. Побочный эффект — под этим термином понимают несколько различных изменений, которые происходят при работе кода программы. Это функция, которая при вызове изменяет глобальную переменную. Вызов платформенного API, который приводит к действию или изменению состояния браузера. Примерами побочных эффектов браузерного API будут использование console.log, document.title = 'Заголовок', document.body.appendChild(elem), fetch(url), localStorage.set('prop', 'value'). Попап (от англ. pop-up) — название UI-компонента, как правило входящего в дизайн системы, который при обычных условиях не виден на странице, но появляется при щелчке на каком-либо элементе и выглядит как всплывающее окошко с информацией. Такое окошко связано с элементом, на котором производится щелчок. Попап не блокирует остальной интерфейс, как делает модальное окно. Провайдер (от англ. provide — «предоставлять») — то, что делает доступным какие-либо данные целому поддереву элементов, начиная с дочерних, самого провайдера. В данном случае — контекст. Промпт — запрос к языковой модели, на который предполагается ответ, имеющий смысл для человека. Пул-реквест (от англ. pull-request) — имеется в виду запрос на внесение ваших изменений в репозиторий. Р Расширение — дополнительная программа к какой-либо другой, основной программе, дополняющая ее узкоспецилизированной функциональностью. Redux DevTools — это расширение, а браузер — его основная программа. Рендерить — в русском языке произносится так, как звучит по-английски. В переводе означает «изображать», «исполнять», «превращать», «воспроизводить». Репозиторий — место хранения кода в файлах, откуда его можно получить или где можно разместить, а также дополнить или удалить. Реставрация — восстановление в первоначальном виде чего-то испорченного или обветшалого (особенно произведений искусства). С Синтаксический сахар — введение в язык программирования нового, более удобного синтаксиса без добавления новых возможностей. Типичный пример синтаксического сахара для JavaScript — асинхронная функция async/await, которая является сахаром для промисов. В библиотеке React синтаксический сахар — это JSX, свойства обработчиков события и их вызов и др. Слайс (от англ. slice) — можно перевести как «часть», «срез», а также как «ломоть», «долька», «кусочек». Слот — свойство компонента, которое предназначено для присвоения ему другого компонента в виде JSX. Структура — то, каким образом однородные элементы объединены в более крупное сообщество. Каждый элемент может быть элементарным (например, строка или число) или какой-либо структурой, соединяющей разнородные данные. Структура всегда объединяет однородные элементы — с помощью связей или объединения по
Глоссарий  491 порядку. Никаким другим образом она не создается. С помощью связей создаются объекты и связные списки. С помощью объединения по порядку — массивы. Конечно, существуют структуры, объединяющие два базовых способа. Т Табы (от англ. tab — «вкладка», «закладка») — общепринятый способ отображения содержимого, когда его можно делить на разделы и отображать в одном и том же пространстве разное содержимое путем переключения между разделами. В таком инструменте всегда отображается какой-то один раздел, остальные скрыты. Реализован практически во всех UI-библиотеках и дизайн-системах. Техдолг — разница между заявленными в реализации требованиями и фактически созданными изменениями, которую нужно дореализовать. При этом отсутствие юнит-тестов для участков, которые должны были быть покрыты ими, но остались не покрыты, тоже образует разницу. Трафик — перемещение, поток данных в передающей среде, например поток сообщений (пакетов передаваемых данных) в Глобальной сети. Также это означает загруженность сети (по аналогии с автотранспортом на дорогах). Трафик состоит из передаваемых данных и служебной информации, необходимой для организации их прохождения. Ф Фактор — существенное обстоятельство, способствующее какому-либо процессу, явлению. Фича — некоторая отдельно взятая функциональность в программе или продукте. Может быть крупной и не очень. Примером является добавление комментария к товару (достаточно крупная фича) или редактирование заголовка комментария (небольшая фича). Функциональное программирование — это такой способ написания кода, когда используются чистые функции. Чистая функция — это функция, которая не создает побочных эффектов и при одних и тех же входных данных возвращает одни и те же выходные данные (то есть зависит от своих аргументов). Побочным эффектом может быть отправка запроса на сервер или запись в историю браузера. Но более правильно сказать, что в основном используются чистые функции. Почему в основном? Да потому, что без побочных эффектов никак не создать фронтенд-приложение. Вам нужно будет работать с платформой, чтобы пользователь мог взаимодействовать с приложением. Поэтому код часто разделяют на тот, что состоит из чистых функций, и тот, что работает с API какой-либо платформы. React является библиотекой, где компоненты являются чистыми функциями. А с платформой (API браузера) работает ReactDOM. Функция высшего порядка — функция, которая может иметь аргументы, являющиеся функциями, и обязательно возвращает функцию. Таким образом производится обогащение функциональностью уже имеющихся функций. Х Хотфикс — код, позволяющий быстро исправить обнаруженные ошибки в уже созданном релизе, который установлен на сервере промышленной эксплуатации. Если ошибка возникает на сервере ИФТ или ПСИ и исправления в код вносятся на данные серверы, то это называется исправлением бага.
492  Глоссарий Хук — специальная точка входа, присоединения; зацепка. Место в программе, куда сторонние разработчики могут подсоединить дополнительный код (обычно для расширения функциональных возможностей). Аналогичные точки могут существовать в аппаратуре, однако там это более строго определено на уровне разъемов, перемычек и интерфейсов. Э Эмулировать — имитировать программными средствами работу какого-либо физического устройства, процесса, другого программного средства или действий. Эндпоинт (от англ. endpoint — «конечная точка») — ресурс в интернете, доступный по протоколу HTTP, обладающий такими отличительными свойствами, как хост, порт, путь, параметры, метод. API в интернете, работающие по протоколу HTTP, создаются с помощью эндпоинтов. Эндпоинты необходимы для работы с данными, а именно для выполнения всех ключевых операций (чтение, изменение, удаление). Энтерпрайз (от англ. enterprise) — подразумевается написание приложений для организаций, которые оказывают услуги широким слоям населения, а также то, что уровень предприятий использует надежные решения. Надежным можно считать решение, которое имело опыт эксплуатации и протестировано в разных ситуациях, имеет стабильную версию. Эпоха — продолжительный период времени, имеющий какие-либо характерные особенности. Эффект — какое-либо действие, производимое компьютером, помимо вычисления значений, в ответ на вызов методов или функций. B Blink — наименование механизма (движок) рендеринга (вывода содержимого на экран), с помощью которого строится DOM, внедряется V8 и запускается JavaScript, запрашиваются ресурсы из сетевого стека, вычисляются стили. C CRUD — сокращение из первых букв слов Create, Read, Update, Delete — создание, чтение, обновление, удаление. Это четыре базовые функции по управлению данными. G GigaChat — диалоговая ИИ-модель, которая отвечает на вопросы, сочиняет тексты, пишет код и рисует картинки. Говорит на русском языке и понимает английский. GigaCode — позволяет генерировать код на разных языках с применением ИИ, выполнять автозаполнение и создавать запросы. GoF (Gang of Four) — «банда четырех». Так называли себя четыре автора 23 самых известных паттернов проектирования. Можно сказать, что они стали
Глоссарий  493 родоначальниками самого явления — паттерна для IT. Авторы выявили три области применения: структуру, создание объектов и взаимодействие объектов. GPT (Generative Pretrained Transformer) — генеративный, предварительно обученный трансформер. Трансформер — это нейронная сеть, которая обрабатывает всю последовательность текста сразу, выявляя взаимосвязи между словами, а не обрабатывая текст последовательно, слово за словом. Нейронная сеть, по сути, модель, которая, по мнению исследователей моделей и мира, лежит в основе человеческого разума (речь о мыслительной деятельности, которая будто бы происходит в мозгу, состоящем из нейронов) и которую можно перенести в сферу обработки данных. Таким образом, создается впечатление, что программа, которая использует нейронную сеть, обладает знаниями и способна к разумным ответам. Сеть — это элементы, содержащие связи с другими элементами, по которым могут передаваться данные. H Hack — 1) быстрое решение трудной программной задачи, умный прием программирования; 2) быстро написать (сленг. «слепить», «сварганить») программу, обычно в ущерб качеству. HOC (High Order Component) — функция, которая в качестве параметра принимает React-компонент и возвращает другой компонент, декорированный какой-либо функциональностью или содержащий какое-либо состояние. Выступает декоратором. С помощью НОС можно создавать компоненты с предзаготовленной функциональностью, при этом сами компоненты, передаваемые в качестве аргумента, могут отличаться друг от друга. Название High Order Component переводится как «компонент высшего порядка» (см. Высший порядок). I IDE (Integrated Development Environment) — в дословном переводе «интегрированная среда разработки» (ИСР). Имеется в виду программа, в которой есть текстовый редактор кода, функция подсветки синтаксиса многих языков программирования, интегрированный терминал для взаимодействия с операционной системой. А также встроенный графический клиент для системы контроля версий, например Git, Mercurial. Коммерческие версии IDE, кроме прочего, включают в себя меню для работы с компиляторами и системами сборки проектов, а также отладчик (debugger). По сути, IDE объединяет множество разных отдельных программ, требуемых на разных этапах создания вашей программы, над которой ведется работа и которая называется проектом. При взаимодействии с IDE вы переходите от этапа к этапу, практически не замечая, что на самом деле взаимодействуете с разными программами. Среди разработчиков сокращение ИСР не используется, вместо него в ходу английская аббревиатура IDE (по-русски «идэе»). R RFC (Request for Comments) — собрание документов, в которых описываются спецификации разных технологий интернета. Например, протокол RFC 9112 описывает HTTP 1/1, RFC 9113 HTPP 2, RFC 9114 HTTP 3.
494  Глоссарий S Snapshot (пер. с англ. snapshot — «моментальный снимок», «мгновенный снимок») — совокупность значений переменных, где содержится состояние в конкретный момент времени работы фронтенд-приложения, до следующего перерендера (или после предыдущего). В IT используется не только применительно к библио­ текам для работы с фронтендом, но и по отношению к базам данных, файловым системам. Кроме того, этим словом обозначают копию экрана. Но фактически — моментальную копию видеопамяти компьютера (редко ОЗУ), получаемую с помощью специальной программы. SPA (Single Page Application) — приложение, исключающее загрузку страниц, которая осуществляется при переходах по ссылке. SPA один раз загружается в брау­зер, в дальнейшем взаимодействие пользователя обрабатывается так, что, хоть и создается видимость смены вида страниц, это происходит без загрузки новой HTML-страницы. T Thunk — заимствованное слово из области программирования систем. Означает подпрограмму, используемую для внедрения вычисления в другую подпрограмму. В основном применяется для задержки вычисления до тех пор, пока не потребуется его результат, или для вставки операций в начале либо в конце другой подпрограммы. Есть много других вариантов применения в генерации кода компилятором и модульном программировании. W W3C (World Wide Web Concorcium). Слово «консорциум» означает объединение компаний и частных лиц на основе общего соглашения для осуществления какого-либо мероприятия. Консорциум W3C — международное сообщество, где члены-организации, постоянные сотрудники и другие люди, участвующие в деятельности, совместно разрабатывают стандарты веба. Директор консорциума — Тим Бернерс Ли.
Антон Кузьмин Разработка фронтенд-приложений Руководитель дивизиона Руководитель проекта Ведущий редактор Литературный редактор Художественный редактор Корректоры Верстка Ю. Сергиенко Н. Михеева Н. Гринчик В. Гуляева В. Мостипан Е. Павлович, Н. Терех Г. Блинов Изготовлено в России. Изготовитель: ООО «Прогресс книга». Место нахождения и фактический адрес: 194044, Россия, г. Санкт-Петербург, Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373. Дата изготовления: 08.2025. Наименование: книжная продукция. Срок годности: не ограничен. Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12 — Книги печатные профессиональные, технические и научные. Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01. Подписано в печать 09.07.25. Формат 70×100/16. Бумага офсетная. Усл. п. л. 39,990. Тираж 700. Заказ 0000.
Анхель Сола Орбайсета СОЗДАНИЕ ФРОНТЕНД-ФРЕЙМВОРКА С НУЛЯ Мы используем фронтенд-фреймворки каждый день, но вряд ли действительно понимаем, что происходит там, под капотом. Создание своего фреймворка — отличный способ узнать, как он работает с DOM, генерирует представления страниц, передает данные между компонентами и взаимодействует с операционной системой. Шаг за шагом мы будем создавать собственный фреймворк на JavaScript и опубликуем его как npm-пакет. Пора разобраться, как работает современный фреймворк, научиться управлять состоянием компонентов и их жизненным циклом, работать с виртуальным DOM и использовать алгоритмы согласования для эффективного обновления HTML. Все ключевые концепции изложены простым языком и сопровождаются схемами. Многие концепции заимствованы из React, Svelte, Angular и других известных инструментов. КУПИТЬ